@zenalexa/unicli 0.218.1 → 0.220.0
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/AGENTS.md +42 -2
- package/README.md +169 -95
- package/README.zh-CN.md +169 -95
- package/bin/unicli-mcp +14 -0
- package/dist/adapters/1point3acres/forum.d.ts +58 -0
- package/dist/adapters/1point3acres/forum.d.ts.map +1 -0
- package/dist/adapters/1point3acres/forum.js +708 -0
- package/dist/adapters/1point3acres/forum.js.map +1 -0
- package/dist/adapters/aibase/news.d.ts +21 -0
- package/dist/adapters/aibase/news.d.ts.map +1 -0
- package/dist/adapters/aibase/news.js +96 -0
- package/dist/adapters/aibase/news.js.map +1 -0
- package/dist/adapters/arxiv/papers.d.ts +27 -0
- package/dist/adapters/arxiv/papers.d.ts.map +1 -0
- package/dist/adapters/arxiv/papers.js +193 -0
- package/dist/adapters/arxiv/papers.js.map +1 -0
- package/dist/adapters/bbc/topic.d.ts +24 -0
- package/dist/adapters/bbc/topic.d.ts.map +1 -0
- package/dist/adapters/bbc/topic.js +122 -0
- package/dist/adapters/bbc/topic.js.map +1 -0
- package/dist/adapters/chatgpt/web.d.ts +24 -0
- package/dist/adapters/chatgpt/web.d.ts.map +1 -0
- package/dist/adapters/chatgpt/web.js +242 -0
- package/dist/adapters/chatgpt/web.js.map +1 -0
- package/dist/adapters/claude/web.d.ts +24 -0
- package/dist/adapters/claude/web.d.ts.map +1 -0
- package/dist/adapters/claude/web.js +575 -0
- package/dist/adapters/claude/web.js.map +1 -0
- package/dist/adapters/codex/projects.d.ts +27 -0
- package/dist/adapters/codex/projects.d.ts.map +1 -0
- package/dist/adapters/codex/projects.js +147 -0
- package/dist/adapters/codex/projects.js.map +1 -0
- package/dist/adapters/coingecko/markets.d.ts +19 -0
- package/dist/adapters/coingecko/markets.d.ts.map +1 -0
- package/dist/adapters/coingecko/markets.js +474 -0
- package/dist/adapters/coingecko/markets.js.map +1 -0
- package/dist/adapters/coupang/product.d.ts +27 -0
- package/dist/adapters/coupang/product.d.ts.map +1 -0
- package/dist/adapters/coupang/product.js +211 -0
- package/dist/adapters/coupang/product.js.map +1 -0
- package/dist/adapters/crates/registry.d.ts +44 -0
- package/dist/adapters/crates/registry.d.ts.map +1 -0
- package/dist/adapters/crates/registry.js +186 -0
- package/dist/adapters/crates/registry.js.map +1 -0
- package/dist/adapters/ctrip/travel.d.ts +83 -0
- package/dist/adapters/ctrip/travel.d.ts.map +1 -0
- package/dist/adapters/ctrip/travel.js +630 -0
- package/dist/adapters/ctrip/travel.js.map +1 -0
- package/dist/adapters/dblp/publications.d.ts +41 -0
- package/dist/adapters/dblp/publications.d.ts.map +1 -0
- package/dist/adapters/dblp/publications.js +409 -0
- package/dist/adapters/dblp/publications.js.map +1 -0
- package/dist/adapters/deepseek/web.d.ts +1 -1
- package/dist/adapters/deepseek/web.d.ts.map +1 -1
- package/dist/adapters/deepseek/web.js +66 -1
- package/dist/adapters/deepseek/web.js.map +1 -1
- package/dist/adapters/defillama/protocols.d.ts +13 -0
- package/dist/adapters/defillama/protocols.d.ts.map +1 -0
- package/dist/adapters/defillama/protocols.js +218 -0
- package/dist/adapters/defillama/protocols.js.map +1 -0
- package/dist/adapters/devto/read.d.ts +26 -0
- package/dist/adapters/devto/read.d.ts.map +1 -0
- package/dist/adapters/devto/read.js +110 -0
- package/dist/adapters/devto/read.js.map +1 -0
- package/dist/adapters/dianping/shop.d.ts +38 -0
- package/dist/adapters/dianping/shop.d.ts.map +1 -0
- package/dist/adapters/dianping/shop.js +194 -0
- package/dist/adapters/dianping/shop.js.map +1 -0
- package/dist/adapters/dockerhub/registry.d.ts +36 -0
- package/dist/adapters/dockerhub/registry.d.ts.map +1 -0
- package/dist/adapters/dockerhub/registry.js +172 -0
- package/dist/adapters/dockerhub/registry.js.map +1 -0
- package/dist/adapters/endoflife/product.d.ts +11 -0
- package/dist/adapters/endoflife/product.d.ts.map +1 -0
- package/dist/adapters/endoflife/product.js +113 -0
- package/dist/adapters/endoflife/product.js.map +1 -0
- package/dist/adapters/facebook/marketplace-extra.d.ts +9 -0
- package/dist/adapters/facebook/marketplace-extra.d.ts.map +1 -0
- package/dist/adapters/facebook/marketplace-extra.js +170 -0
- package/dist/adapters/facebook/marketplace-extra.js.map +1 -0
- package/dist/adapters/flathub/apps.d.ts +17 -0
- package/dist/adapters/flathub/apps.d.ts.map +1 -0
- package/dist/adapters/flathub/apps.js +220 -0
- package/dist/adapters/flathub/apps.js.map +1 -0
- package/dist/adapters/goproxy/module.d.ts +24 -0
- package/dist/adapters/goproxy/module.d.ts.map +1 -0
- package/dist/adapters/goproxy/module.js +221 -0
- package/dist/adapters/goproxy/module.js.map +1 -0
- package/dist/adapters/grok/web.d.ts +29 -0
- package/dist/adapters/grok/web.d.ts.map +1 -0
- package/dist/adapters/grok/web.js +553 -0
- package/dist/adapters/grok/web.js.map +1 -0
- package/dist/adapters/hackernews/read.d.ts +31 -0
- package/dist/adapters/hackernews/read.d.ts.map +1 -0
- package/dist/adapters/hackernews/read.js +201 -0
- package/dist/adapters/hackernews/read.js.map +1 -0
- package/dist/adapters/hf/paper.d.ts +22 -0
- package/dist/adapters/hf/paper.d.ts.map +1 -0
- package/dist/adapters/hf/paper.js +112 -0
- package/dist/adapters/hf/paper.js.map +1 -0
- package/dist/adapters/homebrew/packages.d.ts +52 -0
- package/dist/adapters/homebrew/packages.d.ts.map +1 -0
- package/dist/adapters/homebrew/packages.js +240 -0
- package/dist/adapters/homebrew/packages.js.map +1 -0
- package/dist/adapters/indeed/jobs.d.ts +38 -0
- package/dist/adapters/indeed/jobs.d.ts.map +1 -0
- package/dist/adapters/indeed/jobs.js +300 -0
- package/dist/adapters/indeed/jobs.js.map +1 -0
- package/dist/adapters/instagram/collections.d.ts +9 -0
- package/dist/adapters/instagram/collections.d.ts.map +1 -0
- package/dist/adapters/instagram/collections.js +174 -0
- package/dist/adapters/instagram/collections.js.map +1 -0
- package/dist/adapters/lichess/players.d.ts +46 -0
- package/dist/adapters/lichess/players.d.ts.map +1 -0
- package/dist/adapters/lichess/players.js +221 -0
- package/dist/adapters/lichess/players.js.map +1 -0
- package/dist/adapters/lobsters/read-domain.d.ts +35 -0
- package/dist/adapters/lobsters/read-domain.d.ts.map +1 -0
- package/dist/adapters/lobsters/read-domain.js +306 -0
- package/dist/adapters/lobsters/read-domain.js.map +1 -0
- package/dist/adapters/maven/artifact.d.ts +30 -0
- package/dist/adapters/maven/artifact.d.ts.map +1 -0
- package/dist/adapters/maven/artifact.js +121 -0
- package/dist/adapters/maven/artifact.js.map +1 -0
- package/dist/adapters/mdn/search.d.ts +11 -0
- package/dist/adapters/mdn/search.d.ts.map +1 -0
- package/dist/adapters/mdn/search.js +115 -0
- package/dist/adapters/mdn/search.js.map +1 -0
- package/dist/adapters/medium/tag.d.ts +15 -0
- package/dist/adapters/medium/tag.d.ts.map +1 -0
- package/dist/adapters/medium/tag.js +148 -0
- package/dist/adapters/medium/tag.js.map +1 -0
- package/dist/adapters/npm/package.d.ts +32 -0
- package/dist/adapters/npm/package.d.ts.map +1 -0
- package/dist/adapters/npm/package.js +141 -0
- package/dist/adapters/npm/package.js.map +1 -0
- package/dist/adapters/nuget/package.d.ts +34 -0
- package/dist/adapters/nuget/package.d.ts.map +1 -0
- package/dist/adapters/nuget/package.js +135 -0
- package/dist/adapters/nuget/package.js.map +1 -0
- package/dist/adapters/nvd/cve.d.ts +42 -0
- package/dist/adapters/nvd/cve.d.ts.map +1 -0
- package/dist/adapters/nvd/cve.js +132 -0
- package/dist/adapters/nvd/cve.js.map +1 -0
- package/dist/adapters/oeis/sequences.d.ts +14 -0
- package/dist/adapters/oeis/sequences.d.ts.map +1 -0
- package/dist/adapters/oeis/sequences.js +219 -0
- package/dist/adapters/oeis/sequences.js.map +1 -0
- package/dist/adapters/openalex/works.d.ts +43 -0
- package/dist/adapters/openalex/works.d.ts.map +1 -0
- package/dist/adapters/openalex/works.js +267 -0
- package/dist/adapters/openalex/works.js.map +1 -0
- package/dist/adapters/openfda/records.d.ts +18 -0
- package/dist/adapters/openfda/records.d.ts.map +1 -0
- package/dist/adapters/openfda/records.js +209 -0
- package/dist/adapters/openfda/records.js.map +1 -0
- package/dist/adapters/openreview/papers.d.ts +34 -0
- package/dist/adapters/openreview/papers.d.ts.map +1 -0
- package/dist/adapters/openreview/papers.js +463 -0
- package/dist/adapters/openreview/papers.js.map +1 -0
- package/dist/adapters/osv/security.d.ts +36 -0
- package/dist/adapters/osv/security.d.ts.map +1 -0
- package/dist/adapters/osv/security.js +247 -0
- package/dist/adapters/osv/security.js.map +1 -0
- package/dist/adapters/packagist/package.d.ts +31 -0
- package/dist/adapters/packagist/package.d.ts.map +1 -0
- package/dist/adapters/packagist/package.js +108 -0
- package/dist/adapters/packagist/package.js.map +1 -0
- package/dist/adapters/pubmed/articles.d.ts +31 -0
- package/dist/adapters/pubmed/articles.d.ts.map +1 -0
- package/dist/adapters/pubmed/articles.js +385 -0
- package/dist/adapters/pubmed/articles.js.map +1 -0
- package/dist/adapters/pypi/package.d.ts +38 -0
- package/dist/adapters/pypi/package.d.ts.map +1 -0
- package/dist/adapters/pypi/package.js +235 -0
- package/dist/adapters/pypi/package.js.map +1 -0
- package/dist/adapters/qwen/web.d.ts +26 -0
- package/dist/adapters/qwen/web.d.ts.map +1 -0
- package/dist/adapters/qwen/web.js +672 -0
- package/dist/adapters/qwen/web.js.map +1 -0
- package/dist/adapters/reddit/account.d.ts +12 -0
- package/dist/adapters/reddit/account.d.ts.map +1 -0
- package/dist/adapters/reddit/account.js +409 -0
- package/dist/adapters/reddit/account.js.map +1 -0
- package/dist/adapters/rednote/web.d.ts +30 -0
- package/dist/adapters/rednote/web.d.ts.map +1 -0
- package/dist/adapters/rednote/web.js +858 -0
- package/dist/adapters/rednote/web.js.map +1 -0
- package/dist/adapters/rest-countries/countries.d.ts +14 -0
- package/dist/adapters/rest-countries/countries.d.ts.map +1 -0
- package/dist/adapters/rest-countries/countries.js +231 -0
- package/dist/adapters/rest-countries/countries.js.map +1 -0
- package/dist/adapters/reuters/article-detail.d.ts +37 -0
- package/dist/adapters/reuters/article-detail.d.ts.map +1 -0
- package/dist/adapters/reuters/article-detail.js +139 -0
- package/dist/adapters/reuters/article-detail.js.map +1 -0
- package/dist/adapters/rfc/rfc.d.ts +11 -0
- package/dist/adapters/rfc/rfc.d.ts.map +1 -0
- package/dist/adapters/rfc/rfc.js +121 -0
- package/dist/adapters/rfc/rfc.js.map +1 -0
- package/dist/adapters/rubygems/gem.d.ts +26 -0
- package/dist/adapters/rubygems/gem.d.ts.map +1 -0
- package/dist/adapters/rubygems/gem.js +96 -0
- package/dist/adapters/rubygems/gem.js.map +1 -0
- package/dist/adapters/stackoverflow/questions.d.ts +79 -0
- package/dist/adapters/stackoverflow/questions.d.ts.map +1 -0
- package/dist/adapters/stackoverflow/questions.js +504 -0
- package/dist/adapters/stackoverflow/questions.js.map +1 -0
- package/dist/adapters/steam/app.d.ts +39 -0
- package/dist/adapters/steam/app.d.ts.map +1 -0
- package/dist/adapters/steam/app.js +165 -0
- package/dist/adapters/steam/app.js.map +1 -0
- package/dist/adapters/tiktok/creator-videos.d.ts +52 -0
- package/dist/adapters/tiktok/creator-videos.d.ts.map +1 -0
- package/dist/adapters/tiktok/creator-videos.js +267 -0
- package/dist/adapters/tiktok/creator-videos.js.map +1 -0
- package/dist/adapters/tvmaze/shows.d.ts +13 -0
- package/dist/adapters/tvmaze/shows.d.ts.map +1 -0
- package/dist/adapters/tvmaze/shows.js +240 -0
- package/dist/adapters/tvmaze/shows.js.map +1 -0
- package/dist/adapters/twitter/bookmark-folders.d.ts +33 -0
- package/dist/adapters/twitter/bookmark-folders.d.ts.map +1 -0
- package/dist/adapters/twitter/bookmark-folders.js +290 -0
- package/dist/adapters/twitter/bookmark-folders.js.map +1 -0
- package/dist/adapters/twitter/quote.d.ts +18 -0
- package/dist/adapters/twitter/quote.d.ts.map +1 -0
- package/dist/adapters/twitter/quote.js +285 -0
- package/dist/adapters/twitter/quote.js.map +1 -0
- package/dist/adapters/twitter/tweet-actions.d.ts +12 -0
- package/dist/adapters/twitter/tweet-actions.d.ts.map +1 -0
- package/dist/adapters/twitter/tweet-actions.js +145 -0
- package/dist/adapters/twitter/tweet-actions.js.map +1 -0
- package/dist/adapters/twitter/tweet-url.d.ts +15 -0
- package/dist/adapters/twitter/tweet-url.d.ts.map +1 -0
- package/dist/adapters/twitter/tweet-url.js +57 -0
- package/dist/adapters/twitter/tweet-url.js.map +1 -0
- package/dist/adapters/uisdc/news.d.ts +22 -0
- package/dist/adapters/uisdc/news.d.ts.map +1 -0
- package/dist/adapters/uisdc/news.js +91 -0
- package/dist/adapters/uisdc/news.js.map +1 -0
- package/dist/adapters/weibo/favorites-publish.d.ts +28 -0
- package/dist/adapters/weibo/favorites-publish.d.ts.map +1 -0
- package/dist/adapters/weibo/favorites-publish.js +356 -0
- package/dist/adapters/weibo/favorites-publish.js.map +1 -0
- package/dist/adapters/wikidata/entities.d.ts +15 -0
- package/dist/adapters/wikidata/entities.d.ts.map +1 -0
- package/dist/adapters/wikidata/entities.js +219 -0
- package/dist/adapters/wikidata/entities.js.map +1 -0
- package/dist/adapters/wikipedia/page.d.ts +21 -0
- package/dist/adapters/wikipedia/page.d.ts.map +1 -0
- package/dist/adapters/wikipedia/page.js +116 -0
- package/dist/adapters/wikipedia/page.js.map +1 -0
- package/dist/adapters/wttr/weather.d.ts +12 -0
- package/dist/adapters/wttr/weather.d.ts.map +1 -0
- package/dist/adapters/wttr/weather.js +207 -0
- package/dist/adapters/wttr/weather.js.map +1 -0
- package/dist/adapters/xianyu/publish.d.ts +31 -0
- package/dist/adapters/xianyu/publish.d.ts.map +1 -0
- package/dist/adapters/xianyu/publish.js +349 -0
- package/dist/adapters/xianyu/publish.js.map +1 -0
- package/dist/adapters/xiaohongshu/user-helpers.d.ts +2 -2
- package/dist/adapters/xiaohongshu/user-helpers.d.ts.map +1 -1
- package/dist/adapters/xiaohongshu/user-helpers.js +5 -4
- package/dist/adapters/xiaohongshu/user-helpers.js.map +1 -1
- package/dist/adapters/yuanbao/web.d.ts +27 -0
- package/dist/adapters/yuanbao/web.d.ts.map +1 -0
- package/dist/adapters/yuanbao/web.js +365 -0
- package/dist/adapters/yuanbao/web.js.map +1 -0
- package/dist/adapters/zhihu/collection.d.ts +33 -0
- package/dist/adapters/zhihu/collection.d.ts.map +1 -0
- package/dist/adapters/zhihu/collection.js +185 -0
- package/dist/adapters/zhihu/collection.js.map +1 -0
- package/dist/adapters/zlibrary/web.d.ts +19 -0
- package/dist/adapters/zlibrary/web.d.ts.map +1 -0
- package/dist/adapters/zlibrary/web.js +153 -0
- package/dist/adapters/zlibrary/web.js.map +1 -0
- package/dist/browser/daemon-client.js +2 -2
- package/dist/browser/daemon-client.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/adapter-authoring.d.ts +28 -0
- package/dist/commands/adapter-authoring.d.ts.map +1 -0
- package/dist/commands/adapter-authoring.js +231 -0
- package/dist/commands/adapter-authoring.js.map +1 -0
- package/dist/commands/browser/actions.d.ts +13 -0
- package/dist/commands/browser/actions.d.ts.map +1 -0
- package/dist/commands/{browser-operator.js → browser/actions.js} +19 -12
- package/dist/commands/browser/actions.js.map +1 -0
- package/dist/commands/browser/adapter.d.ts +10 -0
- package/dist/commands/browser/adapter.d.ts.map +1 -0
- package/dist/commands/{browser-adapter-authoring.js → browser/adapter.js} +14 -7
- package/dist/commands/browser/adapter.js.map +1 -0
- package/dist/commands/browser/authoring.d.ts +10 -0
- package/dist/commands/browser/authoring.d.ts.map +1 -0
- package/dist/commands/{browser-authoring-operator.js → browser/authoring.js} +12 -5
- package/dist/commands/browser/authoring.js.map +1 -0
- package/dist/commands/browser/index.d.ts +10 -0
- package/dist/commands/browser/index.d.ts.map +1 -0
- package/dist/commands/{browser.js → browser/index.js} +13 -14
- package/dist/commands/browser/index.js.map +1 -0
- package/dist/commands/{browser-operator-runtime.d.ts → browser/runtime.d.ts} +9 -2
- package/dist/commands/browser/runtime.d.ts.map +1 -0
- package/dist/commands/{browser-operator-runtime.js → browser/runtime.js} +16 -9
- package/dist/commands/browser/runtime.js.map +1 -0
- package/dist/commands/explore.d.ts +5 -5
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +7 -97
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/generate.d.ts +5 -5
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +8 -179
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/operate.js +1 -1
- package/dist/commands/operate.js.map +1 -1
- package/dist/commands/synthesize.d.ts +5 -5
- package/dist/commands/synthesize.d.ts.map +1 -1
- package/dist/commands/synthesize.js +8 -161
- package/dist/commands/synthesize.js.map +1 -1
- package/dist/discovery/aliases.d.ts +5 -10
- package/dist/discovery/aliases.d.ts.map +1 -1
- package/dist/discovery/aliases.js +10 -10
- package/dist/discovery/aliases.js.map +1 -1
- package/dist/discovery/macos-dynamic.d.ts.map +1 -1
- package/dist/discovery/macos-dynamic.js +17 -3
- package/dist/discovery/macos-dynamic.js.map +1 -1
- package/dist/engine/browser/action-evidence.js +1 -1
- package/dist/engine/browser/action-evidence.js.map +1 -1
- package/dist/engine/steps/map.d.ts +7 -0
- package/dist/engine/steps/map.d.ts.map +1 -1
- package/dist/engine/steps/map.js +16 -2
- package/dist/engine/steps/map.js.map +1 -1
- package/dist/manifest-compact.txt +10 -10
- package/dist/manifest-search.json +1 -1
- package/dist/manifest.json +5437 -247
- package/dist/transport/refs.d.ts +5 -3
- package/dist/transport/refs.d.ts.map +1 -1
- package/dist/transport/refs.js +8 -1
- package/dist/transport/refs.js.map +1 -1
- package/dist/transport/sidecar-binary.d.ts +7 -0
- package/dist/transport/sidecar-binary.d.ts.map +1 -1
- package/dist/transport/sidecar-binary.js +28 -8
- package/dist/transport/sidecar-binary.js.map +1 -1
- package/package.json +8 -6
- package/server.json +3 -3
- package/skills/unicli/SKILL.md +1 -1
- package/skills/unicli-claude-code/SKILL.md +1 -1
- package/skills/unicli-hermes/SKILL.md +1 -1
- package/src/adapters/1point3acres/forum.test.ts +300 -0
- package/src/adapters/1point3acres/forum.ts +852 -0
- package/src/adapters/36kr/hot.yaml +1 -0
- package/src/adapters/36kr/search.yaml +1 -0
- package/src/adapters/_archived/README.md +12 -0
- package/src/adapters/{apple-music → _archived/apple-music}/rate-album.yaml +5 -12
- package/src/adapters/_archived/archive.json +44 -0
- package/src/adapters/{az → _archived/az}/account.yaml +5 -1
- package/src/adapters/{ctrip → _archived/ctrip}/hot.yaml +5 -1
- package/src/adapters/{ctrip → _archived/ctrip}/search.yaml +6 -2
- package/src/adapters/{gcloud → _archived/gcloud}/projects.yaml +5 -1
- package/src/adapters/aibase/news.test.ts +42 -0
- package/src/adapters/aibase/news.ts +118 -0
- package/src/adapters/amazon/new-releases.yaml +1 -0
- package/src/adapters/amazon/search.yaml +1 -0
- package/src/adapters/apple-podcasts/search.yaml +1 -0
- package/src/adapters/apple-podcasts/top.yaml +1 -0
- package/src/adapters/arxiv/papers.test.ts +59 -0
- package/src/adapters/arxiv/papers.ts +226 -0
- package/src/adapters/arxiv/search.yaml +1 -0
- package/src/adapters/arxiv/trending.yaml +1 -0
- package/src/adapters/baidu/hot.yaml +1 -0
- package/src/adapters/baidu/search.yaml +1 -0
- package/src/adapters/bbc/top.yaml +1 -0
- package/src/adapters/bbc/topic.test.ts +52 -0
- package/src/adapters/bbc/topic.ts +149 -0
- package/src/adapters/bilibili/feed.yaml +1 -0
- package/src/adapters/bilibili/hot.yaml +1 -0
- package/src/adapters/bilibili/trending.yaml +1 -0
- package/src/adapters/binance/hot.yaml +1 -0
- package/src/adapters/binance/top.yaml +1 -0
- package/src/adapters/bluesky/search.yaml +1 -0
- package/src/adapters/bluesky/trending.yaml +1 -0
- package/src/adapters/chatgpt/web.test.ts +121 -0
- package/src/adapters/chatgpt/web.ts +286 -0
- package/src/adapters/claude/web.test.ts +206 -0
- package/src/adapters/claude/web.ts +684 -0
- package/src/adapters/cnki/search.yaml +1 -0
- package/src/adapters/cnn/top.yaml +1 -0
- package/src/adapters/codex/projects.test.ts +77 -0
- package/src/adapters/codex/projects.ts +178 -0
- package/src/adapters/coingecko/markets.test.ts +156 -0
- package/src/adapters/coingecko/markets.ts +574 -0
- package/src/adapters/coupang/hot.yaml +1 -0
- package/src/adapters/coupang/product.test.ts +111 -0
- package/src/adapters/coupang/product.ts +256 -0
- package/src/adapters/coupang/search.yaml +1 -0
- package/src/adapters/crates/registry.test.ts +89 -0
- package/src/adapters/crates/registry.ts +247 -0
- package/src/adapters/ctrip/travel.test.ts +359 -0
- package/src/adapters/ctrip/travel.ts +792 -0
- package/src/adapters/dangdang/hot.yaml +1 -0
- package/src/adapters/dangdang/search.yaml +1 -0
- package/src/adapters/dblp/publications.test.ts +123 -0
- package/src/adapters/dblp/publications.ts +494 -0
- package/src/adapters/deepseek/web.test.ts +69 -0
- package/src/adapters/deepseek/web.ts +78 -1
- package/src/adapters/defillama/protocols.test.ts +75 -0
- package/src/adapters/defillama/protocols.ts +253 -0
- package/src/adapters/devto/read.test.ts +49 -0
- package/src/adapters/devto/read.ts +145 -0
- package/src/adapters/devto/search.yaml +1 -0
- package/src/adapters/devto/top.yaml +1 -0
- package/src/adapters/dianping/hot.yaml +1 -0
- package/src/adapters/dianping/search.yaml +1 -0
- package/src/adapters/dianping/shop.test.ts +134 -0
- package/src/adapters/dianping/shop.ts +261 -0
- package/src/adapters/dictionary/search.yaml +1 -0
- package/src/adapters/dockerhub/registry.test.ts +97 -0
- package/src/adapters/dockerhub/registry.ts +223 -0
- package/src/adapters/douban/new-movies.yaml +1 -0
- package/src/adapters/douban/search.yaml +1 -0
- package/src/adapters/doubao/new.yaml +1 -0
- package/src/adapters/douyu/hot.yaml +1 -0
- package/src/adapters/douyu/search.yaml +1 -0
- package/src/adapters/eastmoney/hot.yaml +1 -0
- package/src/adapters/eastmoney/search.yaml +1 -0
- package/src/adapters/ele/hot.yaml +1 -0
- package/src/adapters/ele/search.yaml +1 -0
- package/src/adapters/endoflife/product.test.ts +50 -0
- package/src/adapters/endoflife/product.ts +128 -0
- package/src/adapters/exchangerate/list.yaml +1 -0
- package/src/adapters/facebook/feed.yaml +1 -0
- package/src/adapters/facebook/marketplace-extra.test.ts +132 -0
- package/src/adapters/facebook/marketplace-extra.ts +213 -0
- package/src/adapters/facebook/search.yaml +1 -0
- package/src/adapters/flathub/apps.test.ts +85 -0
- package/src/adapters/flathub/apps.ts +254 -0
- package/src/adapters/futu/hot.yaml +1 -0
- package/src/adapters/gemini/deep-research-result.yaml +1 -0
- package/src/adapters/gemini/new.yaml +1 -0
- package/src/adapters/gh/search-repos.yaml +1 -0
- package/src/adapters/gitee/search.yaml +1 -0
- package/src/adapters/gitee/trending.yaml +1 -0
- package/src/adapters/gitlab/search.yaml +1 -0
- package/src/adapters/gitlab/trending.yaml +1 -0
- package/src/adapters/google/search.yaml +1 -0
- package/src/adapters/goproxy/module.test.ts +72 -0
- package/src/adapters/goproxy/module.ts +258 -0
- package/src/adapters/grok/web.test.ts +181 -0
- package/src/adapters/grok/web.ts +640 -0
- package/src/adapters/hackernews/read.test.ts +68 -0
- package/src/adapters/hackernews/read.ts +265 -0
- package/src/adapters/hf/paper.test.ts +48 -0
- package/src/adapters/hf/paper.ts +138 -0
- package/src/adapters/hf/top.yaml +1 -0
- package/src/adapters/homebrew/packages.test.ts +109 -0
- package/src/adapters/homebrew/packages.ts +304 -0
- package/src/adapters/huggingface-papers/search.yaml +1 -0
- package/src/adapters/imdb/search.yaml +1 -0
- package/src/adapters/imdb/top.yaml +1 -0
- package/src/adapters/imdb/trending.yaml +1 -0
- package/src/adapters/imessage/recent.yaml +1 -0
- package/src/adapters/imessage/search.yaml +1 -0
- package/src/adapters/indeed/jobs.test.ts +230 -0
- package/src/adapters/indeed/jobs.ts +375 -0
- package/src/adapters/instagram/collections.test.ts +94 -0
- package/src/adapters/instagram/collections.ts +206 -0
- package/src/adapters/instagram/search.yaml +1 -0
- package/src/adapters/itch-io/popular.yaml +1 -0
- package/src/adapters/itch-io/search.yaml +1 -0
- package/src/adapters/itch-io/top.yaml +1 -0
- package/src/adapters/ithome/hot.yaml +1 -0
- package/src/adapters/jianyu/search.yaml +1 -0
- package/src/adapters/jike/feed.yaml +1 -0
- package/src/adapters/jike/search.yaml +1 -0
- package/src/adapters/juejin/hot.yaml +1 -0
- package/src/adapters/juejin/search.yaml +1 -0
- package/src/adapters/kuaishou/hot.yaml +1 -0
- package/src/adapters/kuaishou/search.yaml +1 -0
- package/src/adapters/leetcode/discuss-search.yaml +1 -0
- package/src/adapters/lichess/players.test.ts +99 -0
- package/src/adapters/lichess/players.ts +277 -0
- package/src/adapters/linear/issue-list.yaml +1 -0
- package/src/adapters/linkedin/search.yaml +1 -0
- package/src/adapters/linkedin/timeline.yaml +1 -0
- package/src/adapters/linux-do/feed.yaml +1 -0
- package/src/adapters/linux-do/hot.yaml +1 -0
- package/src/adapters/lobsters/hot.yaml +1 -0
- package/src/adapters/lobsters/read-domain.test.ts +121 -0
- package/src/adapters/lobsters/read-domain.ts +400 -0
- package/src/adapters/lobsters/search.yaml +1 -0
- package/src/adapters/macos/apps-list.yaml +1 -0
- package/src/adapters/macos/calendar-list.yaml +1 -0
- package/src/adapters/macos/contacts-search.yaml +1 -0
- package/src/adapters/macos/notes-list.yaml +1 -0
- package/src/adapters/macos/notes-search.yaml +1 -0
- package/src/adapters/macos/photos-search.yaml +1 -0
- package/src/adapters/macos/reminders-list.yaml +1 -0
- package/src/adapters/macos/shortcuts-list.yaml +1 -0
- package/src/adapters/maimai/search.yaml +1 -0
- package/src/adapters/maoyan/hot.yaml +1 -0
- package/src/adapters/maoyan/search.yaml +1 -0
- package/src/adapters/mastodon/search.yaml +1 -0
- package/src/adapters/mastodon/timeline.yaml +1 -0
- package/src/adapters/mastodon/trending.yaml +1 -0
- package/src/adapters/maven/artifact.test.ts +67 -0
- package/src/adapters/maven/artifact.ts +155 -0
- package/src/adapters/maven/info.yaml +46 -0
- package/src/adapters/maven/search.yaml +44 -0
- package/src/adapters/mdn/search.test.ts +39 -0
- package/src/adapters/mdn/search.ts +133 -0
- package/src/adapters/medium/feed.yaml +1 -0
- package/src/adapters/medium/search.yaml +1 -0
- package/src/adapters/medium/tag.test.ts +64 -0
- package/src/adapters/medium/tag.ts +164 -0
- package/src/adapters/medium/trending.yaml +1 -0
- package/src/adapters/meituan/search.yaml +1 -0
- package/src/adapters/mubu/list.yaml +1 -0
- package/src/adapters/mubu/search.yaml +1 -0
- package/src/adapters/netease-music/hot.yaml +1 -0
- package/src/adapters/netease-music/search.yaml +1 -0
- package/src/adapters/netease-music/top.yaml +1 -0
- package/src/adapters/notion/search.yaml +1 -0
- package/src/adapters/npm/package.test.ts +53 -0
- package/src/adapters/npm/package.ts +177 -0
- package/src/adapters/npm-trends/trending.yaml +1 -0
- package/src/adapters/nuget/info.yaml +41 -0
- package/src/adapters/nuget/package.test.ts +102 -0
- package/src/adapters/nuget/package.ts +193 -0
- package/src/adapters/nuget/search.yaml +43 -0
- package/src/adapters/nvd/cve.test.ts +66 -0
- package/src/adapters/nvd/cve.ts +182 -0
- package/src/adapters/nytimes/search.yaml +1 -0
- package/src/adapters/nytimes/top.yaml +1 -0
- package/src/adapters/obs/stream-start.yaml +1 -0
- package/src/adapters/obs/stream-stop.yaml +1 -0
- package/src/adapters/obsidian/search.yaml +1 -0
- package/src/adapters/oeis/sequences.test.ts +71 -0
- package/src/adapters/oeis/sequences.ts +234 -0
- package/src/adapters/ollama/list.yaml +1 -0
- package/src/adapters/openalex/works.test.ts +99 -0
- package/src/adapters/openalex/works.ts +319 -0
- package/src/adapters/openfda/records.test.ts +90 -0
- package/src/adapters/openfda/records.ts +239 -0
- package/src/adapters/openreview/papers.test.ts +139 -0
- package/src/adapters/openreview/papers.ts +560 -0
- package/src/adapters/openrouter/search.yaml +1 -0
- package/src/adapters/osv/security.test.ts +91 -0
- package/src/adapters/osv/security.ts +298 -0
- package/src/adapters/packagist/info.yaml +34 -0
- package/src/adapters/packagist/package.test.ts +62 -0
- package/src/adapters/packagist/package.ts +146 -0
- package/src/adapters/packagist/search.yaml +41 -0
- package/src/adapters/pexels/search.yaml +1 -0
- package/src/adapters/pinduoduo/hot.yaml +1 -0
- package/src/adapters/pinduoduo/search.yaml +1 -0
- package/src/adapters/powerpoint/list.yaml +1 -0
- package/src/adapters/producthunt/browse.yaml +1 -0
- package/src/adapters/producthunt/hot.yaml +1 -0
- package/src/adapters/producthunt/search.yaml +1 -0
- package/src/adapters/pub-dev/info.yaml +34 -0
- package/src/adapters/pub-dev/search.yaml +43 -0
- package/src/adapters/pubmed/articles.test.ts +96 -0
- package/src/adapters/pubmed/articles.ts +497 -0
- package/src/adapters/pypi/package.test.ts +131 -0
- package/src/adapters/pypi/package.ts +297 -0
- package/src/adapters/quark/search.yaml +1 -0
- package/src/adapters/qwen/web.test.ts +176 -0
- package/src/adapters/qwen/web.ts +758 -0
- package/src/adapters/reddit/account.test.ts +56 -0
- package/src/adapters/reddit/account.ts +493 -0
- package/src/adapters/rednote/web.test.ts +354 -0
- package/src/adapters/rednote/web.ts +968 -0
- package/src/adapters/replicate/search.yaml +1 -0
- package/src/adapters/replicate/trending.yaml +1 -0
- package/src/adapters/rest-countries/countries.test.ts +80 -0
- package/src/adapters/rest-countries/countries.ts +271 -0
- package/src/adapters/reuters/article-detail.test.ts +65 -0
- package/src/adapters/reuters/article-detail.ts +186 -0
- package/src/adapters/reuters/search.yaml +1 -0
- package/src/adapters/reuters/top.yaml +1 -0
- package/src/adapters/rfc/rfc.test.ts +37 -0
- package/src/adapters/rfc/rfc.ts +133 -0
- package/src/adapters/rubygems/gem.test.ts +43 -0
- package/src/adapters/rubygems/gem.ts +126 -0
- package/src/adapters/rubygems/info.yaml +27 -0
- package/src/adapters/rubygems/search.yaml +39 -0
- package/src/adapters/sinablog/hot.yaml +1 -0
- package/src/adapters/sinablog/search.yaml +1 -0
- package/src/adapters/slack/search.yaml +1 -0
- package/src/adapters/smzdm/hot.yaml +1 -0
- package/src/adapters/smzdm/search.yaml +1 -0
- package/src/adapters/spotify/search.yaml +1 -0
- package/src/adapters/spotify/top-tracks.yaml +1 -0
- package/src/adapters/sspai/hot.yaml +1 -0
- package/src/adapters/stackoverflow/hot.yaml +1 -0
- package/src/adapters/stackoverflow/questions.test.ts +207 -0
- package/src/adapters/stackoverflow/questions.ts +765 -0
- package/src/adapters/stackoverflow/search.yaml +1 -0
- package/src/adapters/steam/app.test.ts +68 -0
- package/src/adapters/steam/app.ts +218 -0
- package/src/adapters/steam/new-releases.yaml +1 -0
- package/src/adapters/steam/search.yaml +1 -0
- package/src/adapters/steam/top-sellers.yaml +1 -0
- package/src/adapters/substack/feed.yaml +1 -0
- package/src/adapters/substack/search.yaml +1 -0
- package/src/adapters/substack/trending.yaml +1 -0
- package/src/adapters/taobao/hot.yaml +1 -0
- package/src/adapters/taobao/search.yaml +1 -0
- package/src/adapters/techcrunch/search.yaml +1 -0
- package/src/adapters/theverge/search.yaml +1 -0
- package/src/adapters/threads/hot.yaml +1 -0
- package/src/adapters/threads/search.yaml +1 -0
- package/src/adapters/tiktok/creator-videos.test.ts +158 -0
- package/src/adapters/tiktok/creator-videos.ts +370 -0
- package/src/adapters/tiktok/search.yaml +1 -0
- package/src/adapters/tiktok/trending.yaml +1 -0
- package/src/adapters/toutiao/hot.yaml +1 -0
- package/src/adapters/toutiao/search.yaml +1 -0
- package/src/adapters/tvmaze/shows.test.ts +93 -0
- package/src/adapters/tvmaze/shows.ts +271 -0
- package/src/adapters/twitch/search.yaml +1 -0
- package/src/adapters/twitch/top.yaml +1 -0
- package/src/adapters/twitter/bookmark-folders.test.ts +164 -0
- package/src/adapters/twitter/bookmark-folders.ts +366 -0
- package/src/adapters/twitter/quote.test.ts +157 -0
- package/src/adapters/twitter/quote.ts +332 -0
- package/src/adapters/twitter/tweet-actions.test.ts +51 -0
- package/src/adapters/twitter/tweet-actions.ts +187 -0
- package/src/adapters/twitter/tweet-url.ts +65 -0
- package/src/adapters/uisdc/news.test.ts +46 -0
- package/src/adapters/uisdc/news.ts +111 -0
- package/src/adapters/unsplash/search.yaml +1 -0
- package/src/adapters/v2ex/hot.yaml +1 -0
- package/src/adapters/v2ex/search.yaml +1 -0
- package/src/adapters/vercel/list.yaml +1 -0
- package/src/adapters/wechat-channels/hot.yaml +1 -0
- package/src/adapters/wechat-channels/search.yaml +1 -0
- package/src/adapters/weibo/favorites-publish.test.ts +177 -0
- package/src/adapters/weibo/favorites-publish.ts +426 -0
- package/src/adapters/weibo/feed.yaml +1 -0
- package/src/adapters/weibo/hot.yaml +1 -0
- package/src/adapters/weibo/search.yaml +1 -0
- package/src/adapters/weibo/timeline.yaml +1 -0
- package/src/adapters/weibo/trending.yaml +1 -0
- package/src/adapters/weixin/hot.yaml +1 -0
- package/src/adapters/weixin/search.yaml +1 -0
- package/src/adapters/weread/search.yaml +1 -0
- package/src/adapters/wikidata/entities.test.ts +103 -0
- package/src/adapters/wikidata/entities.ts +253 -0
- package/src/adapters/wikipedia/page.test.ts +49 -0
- package/src/adapters/wikipedia/page.ts +158 -0
- package/src/adapters/wikipedia/search.yaml +1 -0
- package/src/adapters/wikipedia/trending.yaml +1 -0
- package/src/adapters/wrangler/list.yaml +1 -0
- package/src/adapters/wttr/weather.test.ts +99 -0
- package/src/adapters/wttr/weather.ts +239 -0
- package/src/adapters/xianyu/publish.test.ts +210 -0
- package/src/adapters/xianyu/publish.ts +420 -0
- package/src/adapters/xiaohongshu/feed.yaml +1 -0
- package/src/adapters/xiaohongshu/hot.yaml +1 -0
- package/src/adapters/xiaohongshu/trending.yaml +1 -0
- package/src/adapters/xiaohongshu/user-helpers.ts +5 -2
- package/src/adapters/xueqiu/feed.yaml +1 -0
- package/src/adapters/xueqiu/hot-stock.yaml +1 -0
- package/src/adapters/xueqiu/hot.yaml +1 -0
- package/src/adapters/xueqiu/search.yaml +1 -0
- package/src/adapters/yahoo-finance/search.yaml +1 -0
- package/src/adapters/yahoo-finance/trending.yaml +1 -0
- package/src/adapters/youtube/trending.yaml +1 -0
- package/src/adapters/yuanbao/new.yaml +1 -0
- package/src/adapters/yuanbao/web.test.ts +144 -0
- package/src/adapters/yuanbao/web.ts +435 -0
- package/src/adapters/zhihu/collection.test.ts +96 -0
- package/src/adapters/zhihu/collection.ts +239 -0
- package/src/adapters/zhihu/feed.yaml +1 -0
- package/src/adapters/zhihu/hot.yaml +1 -0
- package/src/adapters/zhihu/search.yaml +1 -0
- package/src/adapters/zhihu/trending.yaml +1 -0
- package/src/adapters/zlibrary/web.test.ts +104 -0
- package/src/adapters/zlibrary/web.ts +192 -0
- package/src/adapters/zotero/search.yaml +1 -0
- package/src/adapters/zsxq/search.yaml +1 -0
- package/dist/commands/browser-adapter-authoring.d.ts +0 -3
- package/dist/commands/browser-adapter-authoring.d.ts.map +0 -1
- package/dist/commands/browser-adapter-authoring.js.map +0 -1
- package/dist/commands/browser-authoring-operator.d.ts +0 -3
- package/dist/commands/browser-authoring-operator.d.ts.map +0 -1
- package/dist/commands/browser-authoring-operator.js.map +0 -1
- package/dist/commands/browser-operator-runtime.d.ts.map +0 -1
- package/dist/commands/browser-operator-runtime.js.map +0 -1
- package/dist/commands/browser-operator.d.ts +0 -6
- package/dist/commands/browser-operator.d.ts.map +0 -1
- package/dist/commands/browser-operator.js.map +0 -1
- package/dist/commands/browser.d.ts +0 -11
- package/dist/commands/browser.d.ts.map +0 -1
- package/dist/commands/browser.js.map +0 -1
- package/dist/mcp/sse-transport.d.ts +0 -34
- package/dist/mcp/sse-transport.d.ts.map +0 -1
- package/dist/mcp/sse-transport.js +0 -182
- package/dist/mcp/sse-transport.js.map +0 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owner src/adapters/twitter/quote.ts
|
|
3
|
+
* @does Register agent-facing Twitter quote-tweet composer automation implemented with site-specific safety checks.
|
|
4
|
+
* @needs Logged-in x.com browser session, exact tweet URL scoping, optional image upload bridge.
|
|
5
|
+
* @feeds surface coverage ledger and Twitter quote workflows with attachment guardrails.
|
|
6
|
+
* @breaks X composer DOM drift or unverified quote-card rendering can post the wrong content.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdtempSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
statSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { homedir, tmpdir } from "node:os";
|
|
18
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
19
|
+
import { cli, Strategy } from "../../registry.js";
|
|
20
|
+
import type { AdapterArg } from "../../types.js";
|
|
21
|
+
import {
|
|
22
|
+
buildTwitterArticleScopeSource,
|
|
23
|
+
parseTwitterTweetUrl,
|
|
24
|
+
} from "./tweet-url.js";
|
|
25
|
+
|
|
26
|
+
const COMPOSER_FILE_INPUT_SELECTOR =
|
|
27
|
+
'input[type="file"][data-testid="fileInput"]';
|
|
28
|
+
const SUPPORTED_IMAGE_EXTENSIONS = new Set([
|
|
29
|
+
".jpg",
|
|
30
|
+
".jpeg",
|
|
31
|
+
".png",
|
|
32
|
+
".gif",
|
|
33
|
+
".webp",
|
|
34
|
+
]);
|
|
35
|
+
const MAX_IMAGE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
36
|
+
const CONTENT_TYPE_TO_EXTENSION: Record<string, string> = {
|
|
37
|
+
"image/jpeg": ".jpg",
|
|
38
|
+
"image/jpg": ".jpg",
|
|
39
|
+
"image/png": ".png",
|
|
40
|
+
"image/gif": ".gif",
|
|
41
|
+
"image/webp": ".webp",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
interface QuoteBrowserPage {
|
|
45
|
+
goto: (url: string, opts?: Record<string, unknown>) => Promise<unknown>;
|
|
46
|
+
wait: (args: unknown) => Promise<unknown>;
|
|
47
|
+
evaluate: (script: string) => Promise<unknown>;
|
|
48
|
+
setFileInput?: (selector: string, files: string[]) => Promise<unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DownloadedImage {
|
|
52
|
+
absPath: string;
|
|
53
|
+
cleanupDir: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildQuoteComposerUrl(value: unknown): string {
|
|
57
|
+
const parsed = parseTwitterTweetUrl(value);
|
|
58
|
+
return `https://x.com/compose/post?url=${encodeURIComponent(parsed.url)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeHomePath(value: string): string {
|
|
62
|
+
return value.replace(/^~(?=$|\/)/, homedir());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveTwitterImagePath(value: unknown): string {
|
|
66
|
+
const raw = String(value ?? "").trim();
|
|
67
|
+
if (!raw) throw new Error("twitter quote image path cannot be empty.");
|
|
68
|
+
const absPath = resolve(normalizeHomePath(raw));
|
|
69
|
+
if (!existsSync(absPath))
|
|
70
|
+
throw new Error(`Image file not found: ${absPath}.`);
|
|
71
|
+
const ext = extname(absPath).toLowerCase();
|
|
72
|
+
if (!SUPPORTED_IMAGE_EXTENSIONS.has(ext)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Unsupported image format "${ext}". Supported: jpg, jpeg, png, gif, webp.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const size = statSync(absPath).size;
|
|
78
|
+
if (size > MAX_IMAGE_SIZE_BYTES) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Image too large: ${(size / 1024 / 1024).toFixed(1)} MB (max 20 MB).`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return absPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveRemoteImageExtension(
|
|
87
|
+
url: string,
|
|
88
|
+
contentType: string | null,
|
|
89
|
+
): string {
|
|
90
|
+
const normalizedContentType = String(contentType ?? "")
|
|
91
|
+
.split(";")[0]
|
|
92
|
+
.trim()
|
|
93
|
+
.toLowerCase();
|
|
94
|
+
const fromContentType = CONTENT_TYPE_TO_EXTENSION[normalizedContentType];
|
|
95
|
+
if (fromContentType) return fromContentType;
|
|
96
|
+
const fromPath = extname(new URL(url).pathname).toLowerCase();
|
|
97
|
+
if (SUPPORTED_IMAGE_EXTENSIONS.has(fromPath)) return fromPath;
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Unsupported remote image format "${normalizedContentType || "unknown"}". Supported: jpg, jpeg, png, gif, webp.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function downloadTwitterImage(
|
|
104
|
+
value: unknown,
|
|
105
|
+
): Promise<DownloadedImage> {
|
|
106
|
+
const raw = String(value ?? "").trim();
|
|
107
|
+
let parsed: URL;
|
|
108
|
+
try {
|
|
109
|
+
parsed = new URL(raw);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error(`Invalid image URL: ${raw}.`);
|
|
112
|
+
}
|
|
113
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
114
|
+
throw new Error(`Unsupported image URL protocol: ${parsed.protocol}.`);
|
|
115
|
+
}
|
|
116
|
+
const response = await fetch(parsed.toString());
|
|
117
|
+
if (!response.ok)
|
|
118
|
+
throw new Error(`Image download failed: HTTP ${response.status}.`);
|
|
119
|
+
const contentLength = Number(response.headers.get("content-length") || "0");
|
|
120
|
+
if (contentLength > MAX_IMAGE_SIZE_BYTES) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Image too large: ${(contentLength / 1024 / 1024).toFixed(1)} MB (max 20 MB).`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const ext = resolveRemoteImageExtension(
|
|
126
|
+
parsed.toString(),
|
|
127
|
+
response.headers.get("content-type"),
|
|
128
|
+
);
|
|
129
|
+
const cleanupDir = mkdtempSync(join(tmpdir(), "unicli-twitter-quote-"));
|
|
130
|
+
const absPath = join(cleanupDir, `image${ext}`);
|
|
131
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
132
|
+
if (buffer.byteLength > MAX_IMAGE_SIZE_BYTES) {
|
|
133
|
+
rmSync(cleanupDir, { recursive: true, force: true });
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Image too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB (max 20 MB).`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
writeFileSync(absPath, buffer);
|
|
139
|
+
return { absPath, cleanupDir };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function mimeTypeForImage(absPath: string): string {
|
|
143
|
+
const ext = extname(absPath).toLowerCase();
|
|
144
|
+
if (ext === ".png") return "image/png";
|
|
145
|
+
if (ext === ".gif") return "image/gif";
|
|
146
|
+
if (ext === ".webp") return "image/webp";
|
|
147
|
+
return "image/jpeg";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function attachComposerImage(
|
|
151
|
+
page: QuoteBrowserPage,
|
|
152
|
+
absImagePath: string,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
let uploaded = false;
|
|
155
|
+
if (page.setFileInput) {
|
|
156
|
+
try {
|
|
157
|
+
await page.setFileInput(COMPOSER_FILE_INPUT_SELECTOR, [absImagePath]);
|
|
158
|
+
uploaded = true;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
if (!/unknown action|not supported/i.test(message)) {
|
|
162
|
+
throw new Error(`Image upload failed: ${message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!uploaded) {
|
|
167
|
+
const base64 = readFileSync(absImagePath).toString("base64");
|
|
168
|
+
const upload = (await page.evaluate(`
|
|
169
|
+
(() => {
|
|
170
|
+
const input = document.querySelector(${JSON.stringify(COMPOSER_FILE_INPUT_SELECTOR)});
|
|
171
|
+
if (!input) return { ok: false, error: 'No file input found on page' };
|
|
172
|
+
const binary = atob(${JSON.stringify(base64)});
|
|
173
|
+
const bytes = new Uint8Array(binary.length);
|
|
174
|
+
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
175
|
+
const transfer = new DataTransfer();
|
|
176
|
+
const blob = new Blob([bytes], { type: ${JSON.stringify(mimeTypeForImage(absImagePath))} });
|
|
177
|
+
transfer.items.add(new File([blob], ${JSON.stringify(basename(absImagePath))}, { type: ${JSON.stringify(mimeTypeForImage(absImagePath))} }));
|
|
178
|
+
Object.defineProperty(input, 'files', { value: transfer.files, writable: false });
|
|
179
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
180
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
181
|
+
return { ok: true };
|
|
182
|
+
})()
|
|
183
|
+
`)) as { ok?: boolean; error?: string };
|
|
184
|
+
if (!upload?.ok) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Image upload failed: ${upload?.error ?? "unknown error"}.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await page.wait(2);
|
|
191
|
+
const uploadState = (await page.evaluate(`
|
|
192
|
+
(() => {
|
|
193
|
+
const previewCount = document.querySelectorAll(
|
|
194
|
+
'[data-testid="attachments"] img, [data-testid="attachments"] video, [data-testid="tweetPhoto"]'
|
|
195
|
+
).length;
|
|
196
|
+
const hasMedia = previewCount > 0
|
|
197
|
+
|| !!document.querySelector('[data-testid="attachments"]')
|
|
198
|
+
|| !!Array.from(document.querySelectorAll('button,[role="button"]')).find((el) =>
|
|
199
|
+
/remove media|remove image|remove/i.test((el.getAttribute('aria-label') || '') + ' ' + (el.textContent || ''))
|
|
200
|
+
);
|
|
201
|
+
return { ok: hasMedia, previewCount };
|
|
202
|
+
})()
|
|
203
|
+
`)) as { ok?: boolean; previewCount?: number };
|
|
204
|
+
if (!uploadState?.ok)
|
|
205
|
+
throw new Error("Image upload failed: preview did not appear.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function buildQuoteSubmitScript(text: string, tweetId: string): string {
|
|
209
|
+
return `(async () => {
|
|
210
|
+
try {
|
|
211
|
+
${buildTwitterArticleScopeSource(tweetId)}
|
|
212
|
+
const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
|
|
213
|
+
const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
|
|
214
|
+
const box = boxes.find(visible) || boxes[0];
|
|
215
|
+
if (!box) return { ok: false, message: 'Could not find the quote composer text area. Are you logged in?' };
|
|
216
|
+
box.focus();
|
|
217
|
+
const textToInsert = ${JSON.stringify(text)};
|
|
218
|
+
if (!document.execCommand('insertText', false, textToInsert)) {
|
|
219
|
+
const transfer = new DataTransfer();
|
|
220
|
+
transfer.setData('text/plain', textToInsert);
|
|
221
|
+
box.dispatchEvent(new ClipboardEvent('paste', { clipboardData: transfer, bubbles: true, cancelable: true }));
|
|
222
|
+
}
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
224
|
+
let hasQuoteCard = false;
|
|
225
|
+
for (let i = 0; i < 20; i += 1) {
|
|
226
|
+
hasQuoteCard = __twHasLinkToTarget(document);
|
|
227
|
+
if (hasQuoteCard) break;
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
229
|
+
}
|
|
230
|
+
if (!hasQuoteCard) return { ok: false, message: 'Quote target did not render in the composer. The source tweet may be deleted or restricted.' };
|
|
231
|
+
const buttons = Array.from(document.querySelectorAll('[data-testid="tweetButton"], [data-testid="tweetButtonInline"]'));
|
|
232
|
+
const button = buttons.find((el) => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true');
|
|
233
|
+
if (!button) return { ok: false, message: 'Tweet button is disabled or not found.' };
|
|
234
|
+
button.click();
|
|
235
|
+
const normalize = (value) => String(value || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim();
|
|
236
|
+
const expectedText = normalize(textToInsert);
|
|
237
|
+
for (let i = 0; i < 30; i += 1) {
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
239
|
+
const alerts = Array.from(document.querySelectorAll('[role="alert"], [data-testid="toast"]')).filter(visible);
|
|
240
|
+
const success = alerts.find((el) => /sent|posted|your post was sent|your tweet was sent/i.test(el.textContent || ''));
|
|
241
|
+
if (success) return { ok: true, message: 'Quote tweet posted successfully.' };
|
|
242
|
+
const failure = alerts.find((el) => /failed|error|try again|not sent|could not/i.test(el.textContent || ''));
|
|
243
|
+
if (failure) return { ok: false, message: (failure.textContent || 'Quote tweet failed to post.').trim() };
|
|
244
|
+
const visibleBoxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]')).filter(visible);
|
|
245
|
+
const composerStillHasText = visibleBoxes.some((node) => normalize(node.innerText || node.textContent || '').includes(expectedText));
|
|
246
|
+
if (!composerStillHasText) return { ok: true, message: 'Quote tweet posted successfully.' };
|
|
247
|
+
}
|
|
248
|
+
return { ok: false, message: 'Quote tweet submission did not complete before timeout.' };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return { ok: false, message: String(error?.message || error) };
|
|
251
|
+
}
|
|
252
|
+
})()`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const quoteArgs: AdapterArg[] = [
|
|
256
|
+
{
|
|
257
|
+
name: "url",
|
|
258
|
+
type: "str",
|
|
259
|
+
required: true,
|
|
260
|
+
positional: true,
|
|
261
|
+
description: "Tweet URL to quote",
|
|
262
|
+
format: "uri",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "text",
|
|
266
|
+
type: "str",
|
|
267
|
+
required: true,
|
|
268
|
+
positional: true,
|
|
269
|
+
description: "Quote text",
|
|
270
|
+
},
|
|
271
|
+
{ name: "image", type: "str", description: "Optional local image path" },
|
|
272
|
+
{ name: "image-url", type: "str", description: "Optional remote image URL" },
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
cli({
|
|
276
|
+
site: "twitter",
|
|
277
|
+
name: "quote",
|
|
278
|
+
description: "Quote-tweet a specific tweet with text and optional image",
|
|
279
|
+
domain: "x.com",
|
|
280
|
+
strategy: Strategy.UI,
|
|
281
|
+
browser: true,
|
|
282
|
+
args: quoteArgs,
|
|
283
|
+
columns: ["status", "message", "text"],
|
|
284
|
+
func: async (page, kwargs) => {
|
|
285
|
+
if (!page || typeof page !== "object") {
|
|
286
|
+
throw new Error("Browser session required for twitter quote.");
|
|
287
|
+
}
|
|
288
|
+
if (kwargs.image && kwargs["image-url"]) {
|
|
289
|
+
throw new Error("Use either --image or --image-url, not both.");
|
|
290
|
+
}
|
|
291
|
+
const browserPage = page as QuoteBrowserPage;
|
|
292
|
+
const target = parseTwitterTweetUrl(kwargs.url);
|
|
293
|
+
const text = String(kwargs.text ?? "").trim();
|
|
294
|
+
if (!text) throw new Error("twitter quote text cannot be empty.");
|
|
295
|
+
let localImagePath: string | undefined;
|
|
296
|
+
let cleanupDir: string | undefined;
|
|
297
|
+
try {
|
|
298
|
+
if (kwargs.image) {
|
|
299
|
+
localImagePath = resolveTwitterImagePath(kwargs.image);
|
|
300
|
+
} else if (kwargs["image-url"]) {
|
|
301
|
+
const downloaded = await downloadTwitterImage(kwargs["image-url"]);
|
|
302
|
+
localImagePath = downloaded.absPath;
|
|
303
|
+
cleanupDir = downloaded.cleanupDir;
|
|
304
|
+
}
|
|
305
|
+
await browserPage.goto(buildQuoteComposerUrl(target.url), {
|
|
306
|
+
waitUntil: "load",
|
|
307
|
+
settleMs: 2500,
|
|
308
|
+
});
|
|
309
|
+
await browserPage.wait({
|
|
310
|
+
selector: '[data-testid="tweetTextarea_0"]',
|
|
311
|
+
timeout: 15,
|
|
312
|
+
});
|
|
313
|
+
if (localImagePath)
|
|
314
|
+
await attachComposerImage(browserPage, localImagePath);
|
|
315
|
+
const result = (await browserPage.evaluate(
|
|
316
|
+
buildQuoteSubmitScript(text, target.id),
|
|
317
|
+
)) as { ok?: boolean; message?: string };
|
|
318
|
+
if (result.ok) await browserPage.wait(3);
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
status: result.ok ? "success" : "failed",
|
|
322
|
+
message: result.message || "",
|
|
323
|
+
text,
|
|
324
|
+
...(kwargs.image ? { image: kwargs.image } : {}),
|
|
325
|
+
...(kwargs["image-url"] ? { "image-url": kwargs["image-url"] } : {}),
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
} finally {
|
|
329
|
+
if (cleanupDir) rmSync(cleanupDir, { recursive: true, force: true });
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildTweetToggleScript,
|
|
4
|
+
buildTwitterArticleScopeSource,
|
|
5
|
+
parseTwitterTweetUrl,
|
|
6
|
+
} from "./tweet-actions.js";
|
|
7
|
+
|
|
8
|
+
describe("twitter agent-facing tweet actions", () => {
|
|
9
|
+
it("validates full tweet URLs and extracts ids", () => {
|
|
10
|
+
expect(parseTwitterTweetUrl("https://x.com/jack/status/20")).toEqual({
|
|
11
|
+
id: "20",
|
|
12
|
+
url: "https://x.com/jack/status/20",
|
|
13
|
+
});
|
|
14
|
+
expect(
|
|
15
|
+
parseTwitterTweetUrl("https://mobile.twitter.com/i/status/123"),
|
|
16
|
+
).toEqual({
|
|
17
|
+
id: "123",
|
|
18
|
+
url: "https://mobile.twitter.com/i/status/123",
|
|
19
|
+
});
|
|
20
|
+
expect(() =>
|
|
21
|
+
parseTwitterTweetUrl("https://example.com/jack/status/20"),
|
|
22
|
+
).toThrow("host");
|
|
23
|
+
expect(() => parseTwitterTweetUrl("https://x.com/jack")).toThrow(
|
|
24
|
+
"extract tweet ID",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("builds exact tweet-scoped article lookup", () => {
|
|
29
|
+
const source = buildTwitterArticleScopeSource("123");
|
|
30
|
+
expect(source).toContain('const tweetId = "123"');
|
|
31
|
+
expect(source).toContain("findTargetArticle");
|
|
32
|
+
expect(source).toContain("__twGetStatusIdFromHref(link.href) === tweetId");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("builds unlike script with idempotent not-liked path", () => {
|
|
36
|
+
const script = buildTweetToggleScript("unlike", "123");
|
|
37
|
+
expect(script).toContain('[data-testid="unlike"]');
|
|
38
|
+
expect(script).toContain('[data-testid="like"]');
|
|
39
|
+
expect(script).toContain("Tweet is not liked (already unliked).");
|
|
40
|
+
expect(script).toContain("Tweet successfully unliked.");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("builds retweet and unretweet scripts with confirmation selectors", () => {
|
|
44
|
+
expect(buildTweetToggleScript("retweet", "123")).toContain(
|
|
45
|
+
'[data-testid="retweetConfirm"]',
|
|
46
|
+
);
|
|
47
|
+
expect(buildTweetToggleScript("unretweet", "123")).toContain(
|
|
48
|
+
'[data-testid="unretweetConfirm"]',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owner src/adapters/twitter/tweet-actions.ts
|
|
3
|
+
* @does Register agent-facing Twitter unlike, retweet, and unretweet URL actions.
|
|
4
|
+
* @needs Browser session on x.com, exact tweet URL parsing, scoped article action buttons.
|
|
5
|
+
* @feeds surface coverage ledger, Twitter write-action workflows, safe tweet-level UI actions.
|
|
6
|
+
* @breaks X DOM test-id drift or weak tweet scoping can act on the wrong tweet.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cli, Strategy } from "../../registry.js";
|
|
10
|
+
import type { AdapterArg } from "../../types.js";
|
|
11
|
+
import {
|
|
12
|
+
buildTwitterArticleScopeSource,
|
|
13
|
+
parseTwitterTweetUrl,
|
|
14
|
+
} from "./tweet-url.js";
|
|
15
|
+
|
|
16
|
+
export { buildTwitterArticleScopeSource, parseTwitterTweetUrl };
|
|
17
|
+
|
|
18
|
+
type ToggleAction = "unlike" | "retweet" | "unretweet";
|
|
19
|
+
|
|
20
|
+
const ACTIONS: Record<
|
|
21
|
+
ToggleAction,
|
|
22
|
+
{
|
|
23
|
+
activeTestId: string;
|
|
24
|
+
inactiveTestId: string;
|
|
25
|
+
confirmTestId: string;
|
|
26
|
+
alreadyMessage: string;
|
|
27
|
+
successMessage: string;
|
|
28
|
+
missingMessage: string;
|
|
29
|
+
mismatchMessage: string;
|
|
30
|
+
needsConfirm: boolean;
|
|
31
|
+
}
|
|
32
|
+
> = {
|
|
33
|
+
unlike: {
|
|
34
|
+
activeTestId: "unlike",
|
|
35
|
+
inactiveTestId: "like",
|
|
36
|
+
confirmTestId: "",
|
|
37
|
+
alreadyMessage: "Tweet is not liked (already unliked).",
|
|
38
|
+
successMessage: "Tweet successfully unliked.",
|
|
39
|
+
missingMessage:
|
|
40
|
+
"Could not find the Unlike button on this tweet. Are you logged in?",
|
|
41
|
+
mismatchMessage:
|
|
42
|
+
"Unlike action was initiated but UI did not update as expected.",
|
|
43
|
+
needsConfirm: false,
|
|
44
|
+
},
|
|
45
|
+
retweet: {
|
|
46
|
+
activeTestId: "retweet",
|
|
47
|
+
inactiveTestId: "unretweet",
|
|
48
|
+
confirmTestId: "retweetConfirm",
|
|
49
|
+
alreadyMessage: "Tweet is already retweeted.",
|
|
50
|
+
successMessage: "Tweet successfully retweeted.",
|
|
51
|
+
missingMessage:
|
|
52
|
+
"Could not find the Retweet button on this tweet. Are you logged in?",
|
|
53
|
+
mismatchMessage:
|
|
54
|
+
"Retweet action was initiated but UI did not update as expected.",
|
|
55
|
+
needsConfirm: true,
|
|
56
|
+
},
|
|
57
|
+
unretweet: {
|
|
58
|
+
activeTestId: "unretweet",
|
|
59
|
+
inactiveTestId: "retweet",
|
|
60
|
+
confirmTestId: "unretweetConfirm",
|
|
61
|
+
alreadyMessage: "Tweet is not retweeted (already removed).",
|
|
62
|
+
successMessage: "Tweet successfully unretweeted.",
|
|
63
|
+
missingMessage:
|
|
64
|
+
"Could not find the Unretweet button on this tweet. Are you logged in?",
|
|
65
|
+
mismatchMessage:
|
|
66
|
+
"Unretweet action was initiated but UI did not update as expected.",
|
|
67
|
+
needsConfirm: true,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function buildTweetToggleScript(
|
|
72
|
+
action: ToggleAction,
|
|
73
|
+
tweetId: string,
|
|
74
|
+
): string {
|
|
75
|
+
const config = ACTIONS[action];
|
|
76
|
+
return `(async () => {
|
|
77
|
+
try {
|
|
78
|
+
${buildTwitterArticleScopeSource(tweetId)}
|
|
79
|
+
let targetArticle = null;
|
|
80
|
+
let activeButton = null;
|
|
81
|
+
let inactiveButton = null;
|
|
82
|
+
for (let i = 0; i < 20; i++) {
|
|
83
|
+
targetArticle = findTargetArticle();
|
|
84
|
+
activeButton = targetArticle?.querySelector('[data-testid="${config.activeTestId}"]') || null;
|
|
85
|
+
inactiveButton = targetArticle?.querySelector('[data-testid="${config.inactiveTestId}"]') || null;
|
|
86
|
+
if (activeButton || inactiveButton) break;
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
88
|
+
}
|
|
89
|
+
if (inactiveButton) return { ok: true, message: ${JSON.stringify(config.alreadyMessage)} };
|
|
90
|
+
if (!activeButton) return { ok: false, message: ${JSON.stringify(config.missingMessage)} };
|
|
91
|
+
activeButton.click();
|
|
92
|
+
${
|
|
93
|
+
config.needsConfirm
|
|
94
|
+
? `
|
|
95
|
+
let confirmButton = null;
|
|
96
|
+
for (let i = 0; i < 20; i++) {
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
98
|
+
confirmButton = document.querySelector('[data-testid="${config.confirmTestId}"]');
|
|
99
|
+
if (confirmButton) break;
|
|
100
|
+
}
|
|
101
|
+
if (!confirmButton) return { ok: false, message: 'Confirmation menu item did not appear.' };
|
|
102
|
+
confirmButton.click();
|
|
103
|
+
`
|
|
104
|
+
: ""
|
|
105
|
+
}
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
107
|
+
const verifyArticle = findTargetArticle() || targetArticle;
|
|
108
|
+
const verifyButton = verifyArticle?.querySelector('[data-testid="${config.inactiveTestId}"]');
|
|
109
|
+
if (verifyButton) return { ok: true, message: ${JSON.stringify(config.successMessage)} };
|
|
110
|
+
return { ok: false, message: ${JSON.stringify(config.mismatchMessage)} };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { ok: false, message: String(error?.message || error) };
|
|
113
|
+
}
|
|
114
|
+
})()`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function tweetActionFunc(action: ToggleAction) {
|
|
118
|
+
return async (page: unknown, kwargs: Record<string, unknown>) => {
|
|
119
|
+
if (!page || typeof page !== "object") {
|
|
120
|
+
throw new Error(`Browser session required for twitter ${action}.`);
|
|
121
|
+
}
|
|
122
|
+
const target = parseTwitterTweetUrl(kwargs.url);
|
|
123
|
+
const browserPage = page as {
|
|
124
|
+
goto: (url: string) => Promise<unknown>;
|
|
125
|
+
wait: (args: unknown) => Promise<unknown>;
|
|
126
|
+
evaluate: (script: string) => Promise<{ ok?: boolean; message?: string }>;
|
|
127
|
+
};
|
|
128
|
+
await browserPage.goto(target.url);
|
|
129
|
+
await browserPage.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
130
|
+
const result = await browserPage.evaluate(
|
|
131
|
+
buildTweetToggleScript(action, target.id),
|
|
132
|
+
);
|
|
133
|
+
if (result.ok) await browserPage.wait(2);
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
status: result.ok ? "success" : "failed",
|
|
137
|
+
message: result.message || "",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tweetUrlArg: AdapterArg[] = [
|
|
144
|
+
{
|
|
145
|
+
name: "url",
|
|
146
|
+
type: "str",
|
|
147
|
+
required: true,
|
|
148
|
+
positional: true,
|
|
149
|
+
description: "Tweet URL",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
cli({
|
|
154
|
+
site: "twitter",
|
|
155
|
+
name: "unlike",
|
|
156
|
+
description: "Twitter unlike a specific tweet",
|
|
157
|
+
domain: "x.com",
|
|
158
|
+
strategy: Strategy.UI,
|
|
159
|
+
browser: true,
|
|
160
|
+
args: tweetUrlArg,
|
|
161
|
+
columns: ["status", "message"],
|
|
162
|
+
func: tweetActionFunc("unlike"),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
cli({
|
|
166
|
+
site: "twitter",
|
|
167
|
+
name: "retweet",
|
|
168
|
+
description: "Twitter retweet a specific tweet",
|
|
169
|
+
domain: "x.com",
|
|
170
|
+
strategy: Strategy.UI,
|
|
171
|
+
browser: true,
|
|
172
|
+
args: tweetUrlArg,
|
|
173
|
+
columns: ["status", "message"],
|
|
174
|
+
func: tweetActionFunc("retweet"),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
cli({
|
|
178
|
+
site: "twitter",
|
|
179
|
+
name: "unretweet",
|
|
180
|
+
description: "Twitter unretweet a specific tweet",
|
|
181
|
+
domain: "x.com",
|
|
182
|
+
strategy: Strategy.UI,
|
|
183
|
+
browser: true,
|
|
184
|
+
args: tweetUrlArg,
|
|
185
|
+
columns: ["status", "message"],
|
|
186
|
+
func: tweetActionFunc("unretweet"),
|
|
187
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owner src/adapters/twitter/tweet-url.ts
|
|
3
|
+
* @does Provide exact Twitter/X tweet URL parsing and DOM target scoping helpers implemented with site-specific safety checks.
|
|
4
|
+
* @needs Full https Twitter/X status URLs and browser DOM roots containing status links.
|
|
5
|
+
* @feeds Twitter write-action adapters and agent-facing quote/retweet safety checks.
|
|
6
|
+
* @breaks Twitter URL shape drift or substring matching can target the wrong tweet.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TWEET_PATH_PATTERN = /^\/(?:[^/]+|i)\/status\/(\d+)\/?$/;
|
|
10
|
+
const TWITTER_HOSTS = new Set(["x.com", "twitter.com"]);
|
|
11
|
+
|
|
12
|
+
interface ParsedTweetUrl {
|
|
13
|
+
id: string;
|
|
14
|
+
url: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isTwitterHost(hostname: string): boolean {
|
|
18
|
+
return (
|
|
19
|
+
TWITTER_HOSTS.has(hostname) ||
|
|
20
|
+
hostname.endsWith(".x.com") ||
|
|
21
|
+
hostname.endsWith(".twitter.com")
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseTwitterTweetUrl(value: unknown): ParsedTweetUrl {
|
|
26
|
+
const raw = String(value ?? "").trim();
|
|
27
|
+
if (!raw) throw new Error("twitter tweet URL cannot be empty.");
|
|
28
|
+
let parsed: URL;
|
|
29
|
+
try {
|
|
30
|
+
parsed = new URL(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error(`Invalid tweet URL: ${raw}.`);
|
|
33
|
+
}
|
|
34
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
35
|
+
if (parsed.protocol !== "https:" || !isTwitterHost(hostname)) {
|
|
36
|
+
throw new Error(`Invalid tweet URL host: ${raw}.`);
|
|
37
|
+
}
|
|
38
|
+
const match = parsed.pathname.match(TWEET_PATH_PATTERN);
|
|
39
|
+
if (!match?.[1])
|
|
40
|
+
throw new Error(`Could not extract tweet ID from URL: ${raw}.`);
|
|
41
|
+
return { id: match[1], url: parsed.toString() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildTwitterArticleScopeSource(tweetId: string): string {
|
|
45
|
+
return `
|
|
46
|
+
const tweetId = ${JSON.stringify(tweetId)};
|
|
47
|
+
const __twTweetPathRe = /^\\/(?:[^/]+|i)\\/status\\/(\\d+)\\/?$/;
|
|
48
|
+
const __twIsTwitterHost = (hostname) => hostname === 'x.com'
|
|
49
|
+
|| hostname === 'twitter.com'
|
|
50
|
+
|| hostname.endsWith('.x.com')
|
|
51
|
+
|| hostname.endsWith('.twitter.com');
|
|
52
|
+
const __twGetStatusIdFromHref = (href) => {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(href, window.location.origin);
|
|
55
|
+
if (parsed.protocol !== 'https:' || !__twIsTwitterHost(parsed.hostname.toLowerCase())) return null;
|
|
56
|
+
return parsed.pathname.match(__twTweetPathRe)?.[1] || null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const __twHasLinkToTarget = (root) => Array.from(root.querySelectorAll('a[href*="/status/"]'))
|
|
62
|
+
.some((link) => __twGetStatusIdFromHref(link.href) === tweetId);
|
|
63
|
+
const findTargetArticle = () => Array.from(document.querySelectorAll('article')).find(__twHasLinkToTarget);
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildExtractUisdcNewsJs,
|
|
4
|
+
mapUisdcNewsPayload,
|
|
5
|
+
requireNewsLimit,
|
|
6
|
+
} from "./news.js";
|
|
7
|
+
|
|
8
|
+
describe("uisdc agent-facing news command", () => {
|
|
9
|
+
it("validates limits and exposes selector extraction code", () => {
|
|
10
|
+
expect(requireNewsLimit(undefined)).toBe(20);
|
|
11
|
+
expect(requireNewsLimit("50")).toBe(50);
|
|
12
|
+
expect(() => requireNewsLimit("51")).toThrow("limit must be");
|
|
13
|
+
expect(buildExtractUisdcNewsJs()).toContain(".dubao-item");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("maps extracted rows and rejects selector drift", () => {
|
|
17
|
+
expect(
|
|
18
|
+
mapUisdcNewsPayload(
|
|
19
|
+
{
|
|
20
|
+
ok: true,
|
|
21
|
+
rows: [
|
|
22
|
+
{
|
|
23
|
+
title: " Design news ",
|
|
24
|
+
summary: " AI update ",
|
|
25
|
+
url: " https://www.uisdc.com/news/a ",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
1,
|
|
30
|
+
),
|
|
31
|
+
).toEqual([
|
|
32
|
+
{
|
|
33
|
+
rank: 1,
|
|
34
|
+
title: "Design news",
|
|
35
|
+
summary: "AI update",
|
|
36
|
+
url: "https://www.uisdc.com/news/a",
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
expect(() =>
|
|
40
|
+
mapUisdcNewsPayload({ ok: false, reason: "selector-missing" }, 10),
|
|
41
|
+
).toThrow("selector drift");
|
|
42
|
+
expect(() => mapUisdcNewsPayload({ ok: true, rows: [] }, 10)).toThrow(
|
|
43
|
+
"no news rows",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|