@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
package/clis/chatgpt/utils.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ChatGPT web browser automation helpers
|
|
2
|
+
* ChatGPT web browser automation helpers.
|
|
3
3
|
* Cross-platform: works on Linux/macOS/Windows via OpenCLI's CDP browser automation.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { htmlToMarkdown } from '@jackwener/opencli/utils';
|
|
7
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
6
9
|
export const CHATGPT_DOMAIN = 'chatgpt.com';
|
|
7
10
|
export const CHATGPT_URL = 'https://chatgpt.com';
|
|
8
11
|
|
|
@@ -11,8 +14,14 @@ const COMPOSER_SELECTORS = [
|
|
|
11
14
|
'[aria-label="Chat with ChatGPT"]',
|
|
12
15
|
'[placeholder="Ask anything"]',
|
|
13
16
|
'#prompt-textarea',
|
|
17
|
+
'[data-testid="prompt-textarea"]',
|
|
18
|
+
'[contenteditable="true"][role="textbox"]',
|
|
19
|
+
];
|
|
20
|
+
const SEND_BUTTON_LABELS = [
|
|
21
|
+
'Send prompt',
|
|
22
|
+
'Send message',
|
|
23
|
+
'Send',
|
|
14
24
|
];
|
|
15
|
-
const SEND_BTN_SELECTOR = 'button[aria-label="Send prompt"]';
|
|
16
25
|
|
|
17
26
|
function isSameChatGPTConversation(currentUrl, expectedUrl) {
|
|
18
27
|
if (!currentUrl || !expectedUrl) return false;
|
|
@@ -58,6 +67,116 @@ function buildComposerLocatorScript() {
|
|
|
58
67
|
`;
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
export function normalizeBooleanFlag(value, fallback = false) {
|
|
71
|
+
if (typeof value === 'boolean') return value;
|
|
72
|
+
if (value == null || value === '') return fallback;
|
|
73
|
+
const normalized = String(value).trim().toLowerCase();
|
|
74
|
+
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function requireNonEmptyPrompt(prompt, commandName) {
|
|
78
|
+
const text = String(prompt ?? '').trim();
|
|
79
|
+
if (!text) {
|
|
80
|
+
throw new ArgumentError(
|
|
81
|
+
`${commandName} prompt cannot be empty`,
|
|
82
|
+
`Example: opencli ${commandName} "hello"`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function requirePositiveInt(value, flagLabel, hint) {
|
|
89
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
90
|
+
throw new ArgumentError(`${flagLabel} must be a positive integer`, hint);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parseChatGPTConversationId(value) {
|
|
96
|
+
const raw = String(value ?? '').trim();
|
|
97
|
+
const match = raw.match(/(?:^|\/c\/)([A-Za-z0-9_-]{8,})(?:[/?#]|$)/);
|
|
98
|
+
if (match) return match[1];
|
|
99
|
+
if (/^[A-Za-z0-9_-]{8,}$/.test(raw)) return raw;
|
|
100
|
+
throw new ArgumentError(
|
|
101
|
+
'chatgpt detail requires a conversation id or /c/<id> URL',
|
|
102
|
+
'Example: opencli chatgpt detail 123e4567-e89b-12d3-a456-426614174000',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function currentChatGPTUrl(page) {
|
|
107
|
+
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
108
|
+
return typeof url === 'string' ? url : '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function isOnChatGPT(page) {
|
|
112
|
+
const url = await currentChatGPTUrl(page);
|
|
113
|
+
if (!url) return false;
|
|
114
|
+
try {
|
|
115
|
+
const host = new URL(url).hostname;
|
|
116
|
+
return host === CHATGPT_DOMAIN || host.endsWith(`.${CHATGPT_DOMAIN}`);
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function ensureOnChatGPT(page) {
|
|
123
|
+
if (await isOnChatGPT(page)) return false;
|
|
124
|
+
await page.goto(CHATGPT_URL, { settleMs: 2000 });
|
|
125
|
+
await page.wait(2);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function startNewChat(page) {
|
|
130
|
+
await page.goto(`${CHATGPT_URL}/new`, { settleMs: 2000 });
|
|
131
|
+
await page.wait(2);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getPageState(page) {
|
|
135
|
+
return await page.evaluate(`(() => {
|
|
136
|
+
const isVisible = (el) => {
|
|
137
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
138
|
+
const style = window.getComputedStyle(el);
|
|
139
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
140
|
+
const rect = el.getBoundingClientRect();
|
|
141
|
+
return rect.width > 0 && rect.height > 0;
|
|
142
|
+
};
|
|
143
|
+
const composerSelectors = ${JSON.stringify(COMPOSER_SELECTORS)};
|
|
144
|
+
const hasComposer = composerSelectors.some((selector) =>
|
|
145
|
+
Array.from(document.querySelectorAll(selector)).some((node) => isVisible(node))
|
|
146
|
+
);
|
|
147
|
+
const text = (document.body?.innerText || '').replace(/\\s+/g, ' ').trim();
|
|
148
|
+
const loginLink = Array.from(document.querySelectorAll('a, button')).find((node) => {
|
|
149
|
+
const label = ((node.innerText || node.textContent || '') + ' ' + (node.getAttribute('aria-label') || '')).trim().toLowerCase();
|
|
150
|
+
return isVisible(node) && /^(log in|login|sign up|sign in)$/.test(label);
|
|
151
|
+
});
|
|
152
|
+
const userMenu = document.querySelector('[data-testid="profile-button"], [aria-label*="Profile"], [aria-label*="Account"], button[id*="headlessui-menu-button"]');
|
|
153
|
+
const hasLoginGate = !!loginLink || /log in to chatgpt|sign up to chatgpt|welcome to chatgpt/i.test(text);
|
|
154
|
+
return {
|
|
155
|
+
url: window.location.href,
|
|
156
|
+
title: document.title,
|
|
157
|
+
hasComposer,
|
|
158
|
+
isLoggedIn: hasComposer || !!userMenu || !hasLoginGate,
|
|
159
|
+
hasLoginGate,
|
|
160
|
+
};
|
|
161
|
+
})()`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function ensureChatGPTLogin(page, message = 'ChatGPT requires a logged-in browser session.') {
|
|
165
|
+
const state = await getPageState(page);
|
|
166
|
+
if (!state.isLoggedIn || state.hasLoginGate) {
|
|
167
|
+
throw new AuthRequiredError(CHATGPT_DOMAIN, message);
|
|
168
|
+
}
|
|
169
|
+
return state;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function ensureChatGPTComposer(page, message = 'ChatGPT composer is not available on the current page.') {
|
|
173
|
+
const state = await ensureChatGPTLogin(page, message);
|
|
174
|
+
if (!state.hasComposer) {
|
|
175
|
+
throw new CommandExecutionError(message);
|
|
176
|
+
}
|
|
177
|
+
return state;
|
|
178
|
+
}
|
|
179
|
+
|
|
61
180
|
/**
|
|
62
181
|
* Send a message to the ChatGPT composer and submit it.
|
|
63
182
|
* Returns true if the message was sent successfully.
|
|
@@ -116,7 +235,8 @@ export async function sendChatGPTMessage(page, text) {
|
|
|
116
235
|
const sent = await page.evaluate(`
|
|
117
236
|
(() => {
|
|
118
237
|
const btns = Array.from(document.querySelectorAll('button'));
|
|
119
|
-
const
|
|
238
|
+
const labels = ${JSON.stringify(SEND_BUTTON_LABELS)};
|
|
239
|
+
const sendBtn = btns.find(b => labels.includes(b.getAttribute('aria-label') || '') && !b.disabled);
|
|
120
240
|
return { sendBtnFound: !!sendBtn };
|
|
121
241
|
})()
|
|
122
242
|
`);
|
|
@@ -127,13 +247,181 @@ export async function sendChatGPTMessage(page, text) {
|
|
|
127
247
|
|
|
128
248
|
await page.evaluate(`
|
|
129
249
|
(() => {
|
|
130
|
-
const
|
|
250
|
+
const labels = ${JSON.stringify(SEND_BUTTON_LABELS)};
|
|
251
|
+
const sendBtn = Array.from(document.querySelectorAll('button')).find(b => labels.includes(b.getAttribute('aria-label') || '') && !b.disabled);
|
|
131
252
|
if (sendBtn) sendBtn.click();
|
|
132
253
|
})()
|
|
133
254
|
`);
|
|
134
255
|
return true;
|
|
135
256
|
}
|
|
136
257
|
|
|
258
|
+
export async function getVisibleMessages(page) {
|
|
259
|
+
const result = await page.evaluate(`(() => {
|
|
260
|
+
const isVisible = (el) => {
|
|
261
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
262
|
+
const style = window.getComputedStyle(el);
|
|
263
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
264
|
+
const rect = el.getBoundingClientRect();
|
|
265
|
+
return rect.width > 0 && rect.height > 0;
|
|
266
|
+
};
|
|
267
|
+
const normalize = (value) => String(value || '').replace(/\\u00a0/g, ' ').replace(/[ \\t]+\\n/g, '\\n').replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
268
|
+
const roleOf = (node) => {
|
|
269
|
+
const attr = node.getAttribute('data-message-author-role') || node.getAttribute('data-author') || '';
|
|
270
|
+
if (/assistant/i.test(attr)) return 'Assistant';
|
|
271
|
+
if (/user/i.test(attr)) return 'User';
|
|
272
|
+
const testid = node.getAttribute('data-testid') || '';
|
|
273
|
+
if (/assistant/i.test(testid)) return 'Assistant';
|
|
274
|
+
if (/user/i.test(testid)) return 'User';
|
|
275
|
+
const label = node.getAttribute('aria-label') || '';
|
|
276
|
+
if (/assistant|chatgpt/i.test(label)) return 'Assistant';
|
|
277
|
+
if (/you|user/i.test(label)) return 'User';
|
|
278
|
+
return '';
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
let nodes = Array.from(document.querySelectorAll('[data-message-author-role], article[data-testid*="conversation-turn"]'));
|
|
282
|
+
nodes = nodes.filter((node) => node instanceof HTMLElement && isVisible(node));
|
|
283
|
+
|
|
284
|
+
const rows = [];
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
for (const node of nodes) {
|
|
287
|
+
let role = roleOf(node);
|
|
288
|
+
const roleNode = node.querySelector('[data-message-author-role], [data-author]');
|
|
289
|
+
if (!role && roleNode) role = roleOf(roleNode);
|
|
290
|
+
if (!role) continue;
|
|
291
|
+
|
|
292
|
+
const contentNode = node.querySelector('[data-message-author-role] .markdown')
|
|
293
|
+
|| node.querySelector('.markdown')
|
|
294
|
+
|| node.querySelector('[data-message-author-role]')
|
|
295
|
+
|| node;
|
|
296
|
+
const html = contentNode instanceof HTMLElement ? (contentNode.innerHTML || '') : '';
|
|
297
|
+
const text = normalize(contentNode instanceof HTMLElement ? (contentNode.innerText || contentNode.textContent || '') : '');
|
|
298
|
+
if (!text) continue;
|
|
299
|
+
const key = role + '\\n' + text;
|
|
300
|
+
if (seen.has(key)) continue;
|
|
301
|
+
seen.add(key);
|
|
302
|
+
rows.push({ role, text, html });
|
|
303
|
+
}
|
|
304
|
+
return rows;
|
|
305
|
+
})()`);
|
|
306
|
+
if (!Array.isArray(result)) return [];
|
|
307
|
+
return result.map((item, index) => ({
|
|
308
|
+
Index: index + 1,
|
|
309
|
+
Role: item?.role === 'Assistant' ? 'Assistant' : 'User',
|
|
310
|
+
Text: String(item?.text || '').trim(),
|
|
311
|
+
Html: String(item?.html || ''),
|
|
312
|
+
})).filter((item) => item.Text);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function messageHtmlToMarkdown(html) {
|
|
316
|
+
try {
|
|
317
|
+
return htmlToMarkdown(html).trim();
|
|
318
|
+
} catch {
|
|
319
|
+
return String(html || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function getBubbleCount(page) {
|
|
324
|
+
const messages = await getVisibleMessages(page);
|
|
325
|
+
return messages.length;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function waitForChatGPTResponse(page, baselineCount, prompt, timeoutSeconds) {
|
|
329
|
+
const startTime = Date.now();
|
|
330
|
+
let lastText = '';
|
|
331
|
+
let stableCount = 0;
|
|
332
|
+
|
|
333
|
+
while (Date.now() - startTime < timeoutSeconds * 1000) {
|
|
334
|
+
await page.wait(3);
|
|
335
|
+
if (await isGenerating(page)) {
|
|
336
|
+
stableCount = 0;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const messages = await getVisibleMessages(page);
|
|
341
|
+
const newMessages = messages.slice(Math.max(0, baselineCount));
|
|
342
|
+
const assistant = [...newMessages].reverse().find((m) => m.Role === 'Assistant')
|
|
343
|
+
|| [...messages].reverse().find((m) => m.Role === 'Assistant');
|
|
344
|
+
const candidate = String(assistant?.Text || '').trim();
|
|
345
|
+
if (!candidate || candidate === String(prompt || '').trim()) continue;
|
|
346
|
+
|
|
347
|
+
if (candidate === lastText) {
|
|
348
|
+
stableCount += 1;
|
|
349
|
+
if (stableCount >= 2) return candidate;
|
|
350
|
+
} else {
|
|
351
|
+
lastText = candidate;
|
|
352
|
+
stableCount = 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
throw new TimeoutError(
|
|
357
|
+
'chatgpt ask',
|
|
358
|
+
timeoutSeconds,
|
|
359
|
+
'No ChatGPT response appeared before timeout. Re-run with a higher --timeout if it is still generating.',
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function getConversationList(page) {
|
|
364
|
+
await ensureOnChatGPT(page);
|
|
365
|
+
await page.wait(2);
|
|
366
|
+
|
|
367
|
+
const openSidebar = await page.evaluate(`(() => {
|
|
368
|
+
const button = Array.from(document.querySelectorAll('button'))
|
|
369
|
+
.find((node) => /open sidebar/i.test(node.getAttribute('aria-label') || ''));
|
|
370
|
+
if (button instanceof HTMLElement) {
|
|
371
|
+
button.click();
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
})()`);
|
|
376
|
+
if (openSidebar) await page.wait(1);
|
|
377
|
+
|
|
378
|
+
let items = await extractConversationLinks(page);
|
|
379
|
+
if (!items.length) {
|
|
380
|
+
await page.goto(CHATGPT_URL, { settleMs: 2000 });
|
|
381
|
+
await page.wait(2);
|
|
382
|
+
items = await extractConversationLinks(page);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return items;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function extractConversationLinks(page) {
|
|
389
|
+
const items = await page.evaluate(`(() => {
|
|
390
|
+
const isVisible = (el) => {
|
|
391
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
392
|
+
const style = window.getComputedStyle(el);
|
|
393
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
394
|
+
const rect = el.getBoundingClientRect();
|
|
395
|
+
return rect.width > 0 && rect.height > 0;
|
|
396
|
+
};
|
|
397
|
+
const links = Array.from(document.querySelectorAll('a[href*="/c/"]'))
|
|
398
|
+
.filter((link) => link instanceof HTMLAnchorElement && isVisible(link));
|
|
399
|
+
const seen = new Set();
|
|
400
|
+
const rows = [];
|
|
401
|
+
for (const link of links) {
|
|
402
|
+
const href = link.getAttribute('href') || '';
|
|
403
|
+
const match = href.match(/\\/c\\/([^/?#]+)/);
|
|
404
|
+
if (!match || seen.has(match[1])) continue;
|
|
405
|
+
seen.add(match[1]);
|
|
406
|
+
const title = (link.innerText || link.textContent || '').replace(/\\s+/g, ' ').trim() || '(untitled)';
|
|
407
|
+
rows.push({
|
|
408
|
+
Id: match[1],
|
|
409
|
+
Title: title,
|
|
410
|
+
Url: href.startsWith('http') ? href : ('${CHATGPT_URL}' + href),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return rows;
|
|
414
|
+
})()`);
|
|
415
|
+
return Array.isArray(items)
|
|
416
|
+
? items.map((item, index) => ({
|
|
417
|
+
Index: index + 1,
|
|
418
|
+
Id: String(item?.Id || ''),
|
|
419
|
+
Title: String(item?.Title || '(untitled)').trim() || '(untitled)',
|
|
420
|
+
Url: String(item?.Url || ''),
|
|
421
|
+
})).filter((item) => item.Id)
|
|
422
|
+
: [];
|
|
423
|
+
}
|
|
424
|
+
|
|
137
425
|
/**
|
|
138
426
|
* Check if ChatGPT is still generating a response.
|
|
139
427
|
*/
|
|
@@ -244,7 +532,9 @@ export async function waitForChatGPTImages(page, beforeUrls, timeoutSeconds, con
|
|
|
244
532
|
|
|
245
533
|
export const __test__ = {
|
|
246
534
|
COMPOSER_SELECTORS,
|
|
535
|
+
SEND_BUTTON_LABELS,
|
|
247
536
|
isSameChatGPTConversation,
|
|
537
|
+
parseChatGPTConversationId,
|
|
248
538
|
};
|
|
249
539
|
|
|
250
540
|
/**
|
|
@@ -61,3 +61,16 @@ describe('chatgpt image wait contract', () => {
|
|
|
61
61
|
)).toBe(false);
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
describe('chatgpt conversation id parsing', () => {
|
|
66
|
+
it('accepts ids and chatgpt conversation URLs', () => {
|
|
67
|
+
expect(__test__.parseChatGPTConversationId('abc_123-def')).toBe('abc_123-def');
|
|
68
|
+
expect(__test__.parseChatGPTConversationId('https://chatgpt.com/c/abc_123-def?model=gpt-5')).toBe('abc_123-def');
|
|
69
|
+
expect(__test__.parseChatGPTConversationId('/c/abc_123-def')).toBe('abc_123-def');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('rejects invalid detail ids', () => {
|
|
73
|
+
expect(() => __test__.parseChatGPTConversationId('')).toThrow(/conversation id/);
|
|
74
|
+
expect(() => __test__.parseChatGPTConversationId('https://chatgpt.com/')).toThrow(/conversation id/);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/clis/chatgpt-app/ask.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
-
import { ConfigError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { ArgumentError, ConfigError } from '@jackwener/opencli/errors';
|
|
4
4
|
import { activateChatGPT, getVisibleChatMessages, selectModel, MODEL_CHOICES, isGenerating, sendPrompt } from './ax.js';
|
|
5
5
|
export const askCommand = cli({
|
|
6
6
|
site: 'chatgpt-app',
|
|
@@ -13,7 +13,7 @@ export const askCommand = cli({
|
|
|
13
13
|
args: [
|
|
14
14
|
{ name: 'text', required: true, positional: true, help: 'Prompt to send' },
|
|
15
15
|
{ name: 'model', required: false, help: 'Model/mode to use: auto, instant, thinking, 5.2-instant, 5.2-thinking', choices: MODEL_CHOICES },
|
|
16
|
-
{ name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default:
|
|
16
|
+
{ name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait for response (default: 30)', default: 30 },
|
|
17
17
|
],
|
|
18
18
|
columns: ['Role', 'Text'],
|
|
19
19
|
func: async (kwargs) => {
|
|
@@ -22,7 +22,10 @@ export const askCommand = cli({
|
|
|
22
22
|
}
|
|
23
23
|
const text = kwargs.text;
|
|
24
24
|
const model = kwargs.model;
|
|
25
|
-
const timeout =
|
|
25
|
+
const timeout = kwargs.timeout;
|
|
26
|
+
if (!Number.isInteger(timeout) || timeout < 1) {
|
|
27
|
+
throw new ArgumentError('--timeout must be a positive integer (seconds)');
|
|
28
|
+
}
|
|
26
29
|
// Switch model before sending if requested
|
|
27
30
|
if (model) {
|
|
28
31
|
activateChatGPT();
|
package/clis/chatwise/ask.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import { selectorError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { selectorError, TimeoutError } from '@jackwener/opencli/errors';
|
|
3
|
+
import {
|
|
4
|
+
buildChatwiseInjectTextJs,
|
|
5
|
+
buildChatwiseMessageCountJs,
|
|
6
|
+
buildChatwiseResponseAfterJs,
|
|
7
|
+
requirePositiveTimeout,
|
|
8
|
+
} from './utils.js';
|
|
3
9
|
export const askCommand = cli({
|
|
4
10
|
site: 'chatwise',
|
|
5
11
|
name: 'ask',
|
|
@@ -10,39 +16,16 @@ export const askCommand = cli({
|
|
|
10
16
|
browser: true,
|
|
11
17
|
args: [
|
|
12
18
|
{ name: 'text', required: true, positional: true, help: 'Prompt to send' },
|
|
13
|
-
{ name: 'timeout', required: false, help: 'Max seconds to wait (default: 30)', default:
|
|
19
|
+
{ name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait (default: 30)', default: 30 },
|
|
14
20
|
],
|
|
15
21
|
columns: ['Role', 'Text'],
|
|
16
22
|
func: async (page, kwargs) => {
|
|
17
23
|
const text = kwargs.text;
|
|
18
|
-
const timeout =
|
|
24
|
+
const timeout = requirePositiveTimeout(kwargs.timeout);
|
|
19
25
|
// Snapshot content length
|
|
20
|
-
const beforeLen = await page.evaluate(
|
|
21
|
-
(function() {
|
|
22
|
-
const msgs = document.querySelectorAll('[data-message-id], [class*="message"], [class*="bubble"]');
|
|
23
|
-
return msgs.length;
|
|
24
|
-
})()
|
|
25
|
-
`);
|
|
26
|
+
const beforeLen = await page.evaluate(buildChatwiseMessageCountJs());
|
|
26
27
|
// Send message
|
|
27
|
-
const injected = await page.evaluate(
|
|
28
|
-
(function(text) {
|
|
29
|
-
let composer = document.querySelector('textarea');
|
|
30
|
-
if (!composer) {
|
|
31
|
-
const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
|
|
32
|
-
composer = editables.length > 0 ? editables[editables.length - 1] : null;
|
|
33
|
-
}
|
|
34
|
-
if (!composer) return false;
|
|
35
|
-
composer.focus();
|
|
36
|
-
if (composer.tagName === 'TEXTAREA') {
|
|
37
|
-
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
|
|
38
|
-
setter.call(composer, text);
|
|
39
|
-
composer.dispatchEvent(new Event('input', { bubbles: true }));
|
|
40
|
-
} else {
|
|
41
|
-
document.execCommand('insertText', false, text);
|
|
42
|
-
}
|
|
43
|
-
return true;
|
|
44
|
-
})(${JSON.stringify(text)})
|
|
45
|
-
`);
|
|
28
|
+
const injected = await page.evaluate(buildChatwiseInjectTextJs(text));
|
|
46
29
|
if (!injected)
|
|
47
30
|
throw selectorError('ChatWise input element');
|
|
48
31
|
await page.wait(0.5);
|
|
@@ -53,25 +36,15 @@ export const askCommand = cli({
|
|
|
53
36
|
let response = '';
|
|
54
37
|
for (let i = 0; i < maxPolls; i++) {
|
|
55
38
|
await page.wait(pollInterval);
|
|
56
|
-
const result = await page.evaluate(
|
|
57
|
-
(function(prevLen) {
|
|
58
|
-
const msgs = document.querySelectorAll('[data-message-id], [class*="message"], [class*="bubble"]');
|
|
59
|
-
if (msgs.length <= prevLen) return null;
|
|
60
|
-
const last = msgs[msgs.length - 1];
|
|
61
|
-
const text = last.innerText || last.textContent;
|
|
62
|
-
return text ? text.trim() : null;
|
|
63
|
-
})(${beforeLen})
|
|
64
|
-
`);
|
|
39
|
+
const result = await page.evaluate(buildChatwiseResponseAfterJs(beforeLen, text));
|
|
65
40
|
if (result) {
|
|
66
|
-
|
|
67
|
-
break;
|
|
41
|
+
const next = String(result).trim();
|
|
42
|
+
if (next === response) break;
|
|
43
|
+
response = next;
|
|
68
44
|
}
|
|
69
45
|
}
|
|
70
46
|
if (!response) {
|
|
71
|
-
|
|
72
|
-
{ Role: 'User', Text: text },
|
|
73
|
-
{ Role: 'System', Text: `No response within ${timeout}s.` },
|
|
74
|
-
];
|
|
47
|
+
throw new TimeoutError('ChatWise response', timeout, 'Confirm ChatWise is done generating, then retry with a larger --timeout if needed.');
|
|
75
48
|
}
|
|
76
49
|
return [
|
|
77
50
|
{ Role: 'User', Text: text },
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ArgumentError, TimeoutError } from '@jackwener/opencli/errors';
|
|
4
|
+
import {
|
|
5
|
+
buildChatwiseInjectTextJs,
|
|
6
|
+
buildChatwiseMessageCountJs,
|
|
7
|
+
buildChatwiseResponseAfterJs,
|
|
8
|
+
requirePositiveTimeout,
|
|
9
|
+
scoreChatwiseComposerCandidate,
|
|
10
|
+
selectBestChatwiseComposer,
|
|
11
|
+
} from './utils.js';
|
|
12
|
+
import { askCommand } from './ask.js';
|
|
13
|
+
|
|
14
|
+
function candidate(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
index: 0,
|
|
17
|
+
hidden: false,
|
|
18
|
+
role: 'textbox',
|
|
19
|
+
classes: 'cm-content cm-lineWrapping',
|
|
20
|
+
editorClasses: 'cm-editor',
|
|
21
|
+
ariaLabel: '',
|
|
22
|
+
placeholder: '',
|
|
23
|
+
text: '',
|
|
24
|
+
rect: { y: 0, h: 30 },
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runBrowserScript(html, script) {
|
|
30
|
+
const dom = new JSDOM(html, { url: 'app://chatwise.local/', runScripts: 'outside-only' });
|
|
31
|
+
Object.defineProperty(dom.window.HTMLElement.prototype, 'offsetWidth', { configurable: true, get: () => 400 });
|
|
32
|
+
Object.defineProperty(dom.window.HTMLElement.prototype, 'offsetHeight', { configurable: true, get: () => 32 });
|
|
33
|
+
dom.window.HTMLElement.prototype.getClientRects = () => [{ length: 1 }];
|
|
34
|
+
dom.window.document.execCommand = () => false;
|
|
35
|
+
return { dom, result: dom.window.eval(script) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makePage(evaluateResults = []) {
|
|
39
|
+
const evaluate = vi.fn();
|
|
40
|
+
for (const result of evaluateResults) evaluate.mockResolvedValueOnce(result);
|
|
41
|
+
evaluate.mockResolvedValue(null);
|
|
42
|
+
return {
|
|
43
|
+
evaluate,
|
|
44
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('chatwise composer selection', () => {
|
|
50
|
+
it('prefers the main composer over auxiliary contenteditable editors', () => {
|
|
51
|
+
const mainComposer = candidate({
|
|
52
|
+
index: 0,
|
|
53
|
+
placeholder: 'placeholder Enter a message here, press ⏎ to send',
|
|
54
|
+
rect: { y: 860, h: 32 },
|
|
55
|
+
});
|
|
56
|
+
const optionalDescription = candidate({
|
|
57
|
+
index: 1,
|
|
58
|
+
placeholder: 'placeholder Optional description',
|
|
59
|
+
editorClasses: 'cm-editor simple-editor',
|
|
60
|
+
rect: { y: 400, h: 32 },
|
|
61
|
+
});
|
|
62
|
+
const userContext = candidate({
|
|
63
|
+
index: 2,
|
|
64
|
+
text: '# User Context Document',
|
|
65
|
+
editorClasses: 'cm-editor simple-editor',
|
|
66
|
+
rect: { y: 460, h: 1200 },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(scoreChatwiseComposerCandidate(mainComposer, 900)).toBeGreaterThan(
|
|
70
|
+
scoreChatwiseComposerCandidate(optionalDescription, 900),
|
|
71
|
+
);
|
|
72
|
+
expect(scoreChatwiseComposerCandidate(mainComposer, 900)).toBeGreaterThan(
|
|
73
|
+
scoreChatwiseComposerCandidate(userContext, 900),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(selectBestChatwiseComposer([
|
|
77
|
+
optionalDescription,
|
|
78
|
+
userContext,
|
|
79
|
+
mainComposer,
|
|
80
|
+
], 900)?.index).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects hidden or low-confidence candidates instead of injecting into the wrong editor', () => {
|
|
84
|
+
expect(selectBestChatwiseComposer([
|
|
85
|
+
candidate({
|
|
86
|
+
index: 0,
|
|
87
|
+
hidden: true,
|
|
88
|
+
placeholder: 'Enter a message here, press ⏎ to send',
|
|
89
|
+
rect: { y: 860, h: 32 },
|
|
90
|
+
}),
|
|
91
|
+
], 900)).toBeNull();
|
|
92
|
+
|
|
93
|
+
expect(selectBestChatwiseComposer([
|
|
94
|
+
candidate({
|
|
95
|
+
index: 1,
|
|
96
|
+
placeholder: 'Optional description',
|
|
97
|
+
editorClasses: 'cm-editor simple-editor',
|
|
98
|
+
rect: { y: 860, h: 32 },
|
|
99
|
+
}),
|
|
100
|
+
candidate({
|
|
101
|
+
index: 2,
|
|
102
|
+
text: '# User Context Document',
|
|
103
|
+
editorClasses: 'cm-editor simple-editor',
|
|
104
|
+
rect: { y: 870, h: 32 },
|
|
105
|
+
}),
|
|
106
|
+
], 900)).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('injects text into the scored main composer instead of the last contenteditable', () => {
|
|
110
|
+
const html = `
|
|
111
|
+
<div class="cm-editor simple-editor">
|
|
112
|
+
<div class="cm-placeholder">Optional description</div>
|
|
113
|
+
<div id="optional" contenteditable="true" role="textbox"></div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="cm-editor">
|
|
116
|
+
<div class="cm-placeholder">Enter a message here, press ⏎ to send</div>
|
|
117
|
+
<div id="main" class="cm-content" contenteditable="true" role="textbox"></div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="cm-editor simple-editor">
|
|
120
|
+
<div id="context" contenteditable="true" role="textbox"># User Context Document</div>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const { dom, result } = runBrowserScript(html, buildChatwiseInjectTextJs('hello'));
|
|
125
|
+
|
|
126
|
+
expect(result).toBe(true);
|
|
127
|
+
expect(dom.window.document.querySelector('#main')?.textContent).toBe('hello');
|
|
128
|
+
expect(dom.window.document.querySelector('#optional')?.textContent).toBe('');
|
|
129
|
+
expect(dom.window.document.querySelector('#context')?.textContent).toBe('# User Context Document');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('fails injection when only auxiliary editors are present', () => {
|
|
133
|
+
const html = `
|
|
134
|
+
<div class="cm-editor simple-editor">
|
|
135
|
+
<div class="cm-placeholder">Optional description</div>
|
|
136
|
+
<div id="optional" contenteditable="true" role="textbox"></div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="cm-editor simple-editor">
|
|
139
|
+
<div id="context" contenteditable="true" role="textbox"># User Context Document</div>
|
|
140
|
+
</div>
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const { dom, result } = runBrowserScript(html, buildChatwiseInjectTextJs('hello'));
|
|
144
|
+
|
|
145
|
+
expect(result).toBe(false);
|
|
146
|
+
expect(dom.window.document.querySelector('#optional')?.textContent).toBe('');
|
|
147
|
+
expect(dom.window.document.querySelector('#context')?.textContent).toBe('# User Context Document');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('reads only real message wrapper content after the previous count', () => {
|
|
151
|
+
const html = `
|
|
152
|
+
<div class="group/message">old message</div>
|
|
153
|
+
<div class="timestamp">12:00</div>
|
|
154
|
+
<div class="group/message">new assistant answer</div>
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
expect(runBrowserScript(html, buildChatwiseMessageCountJs()).result).toBe(2);
|
|
158
|
+
expect(runBrowserScript(html, buildChatwiseResponseAfterJs(1, 'user prompt')).result).toBe('new assistant answer');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('does not treat the user prompt wrapper as an assistant response', () => {
|
|
162
|
+
const html = `
|
|
163
|
+
<div class="group/message">old message</div>
|
|
164
|
+
<div class="group/message">user prompt</div>
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
expect(runBrowserScript(html, buildChatwiseResponseAfterJs(1, 'user prompt')).result).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('validates timeout explicitly', () => {
|
|
171
|
+
expect(requirePositiveTimeout(30)).toBe(30);
|
|
172
|
+
expect(() => requirePositiveTimeout('30')).toThrow(ArgumentError);
|
|
173
|
+
expect(() => requirePositiveTimeout(0)).toThrow(ArgumentError);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('fails fast when ask times out instead of returning a System success row', async () => {
|
|
177
|
+
const page = makePage([
|
|
178
|
+
1,
|
|
179
|
+
true,
|
|
180
|
+
null,
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
await expect(askCommand.func(page, { text: 'hello', timeout: 1 }))
|
|
184
|
+
.rejects.toBeInstanceOf(TimeoutError);
|
|
185
|
+
});
|
|
186
|
+
});
|