@zenalexa/unicli 0.219.0 → 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 +9 -9
- package/README.md +55 -249
- package/README.zh-CN.md +55 -249
- 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/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/manifest-compact.txt +10 -10
- package/dist/manifest-search.json +1 -1
- package/dist/manifest.json +5130 -112
- 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 +4 -3
- 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/aibase/news.test.ts +42 -0
- package/src/adapters/aibase/news.ts +118 -0
- package/src/adapters/arxiv/papers.test.ts +59 -0
- package/src/adapters/arxiv/papers.ts +226 -0
- package/src/adapters/bbc/topic.test.ts +52 -0
- package/src/adapters/bbc/topic.ts +149 -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/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/product.test.ts +111 -0
- package/src/adapters/coupang/product.ts +256 -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/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/dianping/shop.test.ts +134 -0
- package/src/adapters/dianping/shop.ts +261 -0
- package/src/adapters/dockerhub/registry.test.ts +97 -0
- package/src/adapters/dockerhub/registry.ts +223 -0
- package/src/adapters/endoflife/product.test.ts +50 -0
- package/src/adapters/endoflife/product.ts +128 -0
- package/src/adapters/facebook/marketplace-extra.test.ts +132 -0
- package/src/adapters/facebook/marketplace-extra.ts +213 -0
- package/src/adapters/flathub/apps.test.ts +85 -0
- package/src/adapters/flathub/apps.ts +254 -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/homebrew/packages.test.ts +109 -0
- package/src/adapters/homebrew/packages.ts +304 -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/lichess/players.test.ts +99 -0
- package/src/adapters/lichess/players.ts +277 -0
- package/src/adapters/lobsters/read-domain.test.ts +121 -0
- package/src/adapters/lobsters/read-domain.ts +400 -0
- package/src/adapters/maven/artifact.test.ts +67 -0
- package/src/adapters/maven/artifact.ts +155 -0
- package/src/adapters/mdn/search.test.ts +39 -0
- package/src/adapters/mdn/search.ts +133 -0
- package/src/adapters/medium/tag.test.ts +64 -0
- package/src/adapters/medium/tag.ts +164 -0
- package/src/adapters/npm/package.test.ts +53 -0
- package/src/adapters/npm/package.ts +177 -0
- package/src/adapters/nuget/package.test.ts +102 -0
- package/src/adapters/nuget/package.ts +193 -0
- package/src/adapters/nvd/cve.test.ts +66 -0
- package/src/adapters/nvd/cve.ts +182 -0
- package/src/adapters/oeis/sequences.test.ts +71 -0
- package/src/adapters/oeis/sequences.ts +234 -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/osv/security.test.ts +91 -0
- package/src/adapters/osv/security.ts +298 -0
- package/src/adapters/packagist/package.test.ts +62 -0
- package/src/adapters/packagist/package.ts +146 -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/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/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/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/stackoverflow/questions.test.ts +207 -0
- package/src/adapters/stackoverflow/questions.ts +765 -0
- package/src/adapters/steam/app.test.ts +68 -0
- package/src/adapters/steam/app.ts +218 -0
- package/src/adapters/tiktok/creator-videos.test.ts +158 -0
- package/src/adapters/tiktok/creator-videos.ts +370 -0
- package/src/adapters/tvmaze/shows.test.ts +93 -0
- package/src/adapters/tvmaze/shows.ts +271 -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/weibo/favorites-publish.test.ts +177 -0
- package/src/adapters/weibo/favorites-publish.ts +426 -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/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/user-helpers.ts +5 -2
- 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/zlibrary/web.test.ts +104 -0
- package/src/adapters/zlibrary/web.ts +192 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owner src/adapters/indeed/jobs.ts
|
|
3
|
+
* @does Register agent-facing Indeed search and job-detail browser commands.
|
|
4
|
+
* @needs Logged-in or challenge-cleared indeed.com browser session and stable rendered job DOM.
|
|
5
|
+
* @feeds surface coverage ledger and job-search research workflows.
|
|
6
|
+
* @breaks Indeed Cloudflare challenge changes, search-card DOM drift, or job-detail selector changes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cli, Strategy } from "../../registry.js";
|
|
10
|
+
import type { IPage } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
const INDEED_ORIGIN = "https://www.indeed.com";
|
|
13
|
+
const JK_PATTERN = /^[a-f0-9]{16}$/;
|
|
14
|
+
const FROMAGE_VALUES = new Set(["1", "3", "7", "14"]);
|
|
15
|
+
const SORT_VALUES = new Set(["relevance", "date"]);
|
|
16
|
+
|
|
17
|
+
export const INDEED_SEARCH_COLUMNS = [
|
|
18
|
+
"rank",
|
|
19
|
+
"id",
|
|
20
|
+
"title",
|
|
21
|
+
"company",
|
|
22
|
+
"location",
|
|
23
|
+
"salary",
|
|
24
|
+
"tags",
|
|
25
|
+
"url",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const INDEED_JOB_COLUMNS = [
|
|
29
|
+
"id",
|
|
30
|
+
"title",
|
|
31
|
+
"company",
|
|
32
|
+
"location",
|
|
33
|
+
"salary",
|
|
34
|
+
"job_type",
|
|
35
|
+
"description",
|
|
36
|
+
"url",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
interface IndeedSearchCard {
|
|
40
|
+
jk?: unknown;
|
|
41
|
+
title?: unknown;
|
|
42
|
+
company?: unknown;
|
|
43
|
+
location?: unknown;
|
|
44
|
+
salary?: unknown;
|
|
45
|
+
tags?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface IndeedSearchResult {
|
|
49
|
+
cards?: unknown;
|
|
50
|
+
challenge?: unknown;
|
|
51
|
+
ready?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface IndeedJobResult {
|
|
55
|
+
ready?: unknown;
|
|
56
|
+
challenge?: unknown;
|
|
57
|
+
notFound?: unknown;
|
|
58
|
+
title?: unknown;
|
|
59
|
+
company?: unknown;
|
|
60
|
+
location?: unknown;
|
|
61
|
+
salary?: unknown;
|
|
62
|
+
jobType?: unknown;
|
|
63
|
+
description?: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function coerceIndeedInt(value: unknown): number {
|
|
67
|
+
if (value === undefined || value === null || value === "") return Number.NaN;
|
|
68
|
+
const numberValue = typeof value === "number" ? value : Number(value);
|
|
69
|
+
return Number.isFinite(numberValue) && Number.isInteger(numberValue)
|
|
70
|
+
? numberValue
|
|
71
|
+
: Number.NaN;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function requireIndeedBoundedInt(
|
|
75
|
+
value: unknown,
|
|
76
|
+
defaultValue: number,
|
|
77
|
+
maxValue: number,
|
|
78
|
+
label: string,
|
|
79
|
+
): number {
|
|
80
|
+
const numberValue = coerceIndeedInt(value ?? defaultValue);
|
|
81
|
+
if (!Number.isInteger(numberValue) || numberValue <= 0) {
|
|
82
|
+
throw new Error(`indeed ${label} must be a positive integer.`);
|
|
83
|
+
}
|
|
84
|
+
if (numberValue > maxValue) {
|
|
85
|
+
throw new Error(`indeed ${label} must be <= ${maxValue}.`);
|
|
86
|
+
}
|
|
87
|
+
return numberValue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function requireIndeedNonNegativeInt(
|
|
91
|
+
value: unknown,
|
|
92
|
+
defaultValue: number,
|
|
93
|
+
label: string,
|
|
94
|
+
): number {
|
|
95
|
+
const numberValue = coerceIndeedInt(value ?? defaultValue);
|
|
96
|
+
if (!Number.isInteger(numberValue) || numberValue < 0) {
|
|
97
|
+
throw new Error(`indeed ${label} must be a non-negative integer.`);
|
|
98
|
+
}
|
|
99
|
+
return numberValue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function requireIndeedJobKey(value: unknown): string {
|
|
103
|
+
const id = String(value ?? "")
|
|
104
|
+
.trim()
|
|
105
|
+
.toLowerCase();
|
|
106
|
+
if (!id) throw new Error("indeed job id is required.");
|
|
107
|
+
if (!JK_PATTERN.test(id)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`indeed job id "${String(value)}" is not a valid jk; expected 16-char hex.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return id;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function requireIndeedQuery(value: unknown): string {
|
|
116
|
+
const query = String(value ?? "").trim();
|
|
117
|
+
if (!query) throw new Error("indeed query cannot be empty.");
|
|
118
|
+
return query;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function requireIndeedFromage(value: unknown): string {
|
|
122
|
+
if (value === undefined || value === null || value === "") return "";
|
|
123
|
+
const fromage = String(value).trim();
|
|
124
|
+
if (!FROMAGE_VALUES.has(fromage)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`indeed fromage must be one of 1/3/7/14 days, got "${String(value)}".`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return fromage;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function requireIndeedSort(value: unknown): string {
|
|
133
|
+
const sort = String(value ?? "relevance")
|
|
134
|
+
.trim()
|
|
135
|
+
.toLowerCase();
|
|
136
|
+
if (!SORT_VALUES.has(sort)) {
|
|
137
|
+
throw new Error(`indeed sort must be "relevance" or "date".`);
|
|
138
|
+
}
|
|
139
|
+
return sort;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildIndeedSearchUrl(config: {
|
|
143
|
+
query: string;
|
|
144
|
+
location: string;
|
|
145
|
+
fromage: string;
|
|
146
|
+
sort: string;
|
|
147
|
+
start: number;
|
|
148
|
+
}): string {
|
|
149
|
+
const params = new URLSearchParams();
|
|
150
|
+
params.set("q", config.query);
|
|
151
|
+
if (config.location) params.set("l", config.location);
|
|
152
|
+
if (config.fromage) params.set("fromage", config.fromage);
|
|
153
|
+
if (config.sort && config.sort !== "relevance") {
|
|
154
|
+
params.set("sort", config.sort);
|
|
155
|
+
}
|
|
156
|
+
if (config.start > 0) params.set("start", String(config.start));
|
|
157
|
+
return `${INDEED_ORIGIN}/jobs?${params.toString()}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function buildIndeedJobUrl(jk: string): string {
|
|
161
|
+
return `${INDEED_ORIGIN}/viewjob?jk=${jk}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function dedupeIndeedTags(tags: unknown, salary: string): string {
|
|
165
|
+
if (!Array.isArray(tags)) return "";
|
|
166
|
+
const seen: string[] = [];
|
|
167
|
+
for (const tag of tags) {
|
|
168
|
+
const text = String(tag ?? "").trim();
|
|
169
|
+
if (!text || text === salary || seen.includes(text)) continue;
|
|
170
|
+
seen.push(text);
|
|
171
|
+
}
|
|
172
|
+
return seen.join(" · ");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function indeedSearchCardToRow(
|
|
176
|
+
card: IndeedSearchCard,
|
|
177
|
+
rank: number,
|
|
178
|
+
): Record<string, unknown> {
|
|
179
|
+
const jk = String(card?.jk ?? "").trim();
|
|
180
|
+
const salary = String(card?.salary ?? "").trim();
|
|
181
|
+
return {
|
|
182
|
+
rank,
|
|
183
|
+
id: jk,
|
|
184
|
+
title: String(card?.title ?? "")
|
|
185
|
+
.replace(/\s+/g, " ")
|
|
186
|
+
.trim(),
|
|
187
|
+
company: String(card?.company ?? "")
|
|
188
|
+
.replace(/\s+/g, " ")
|
|
189
|
+
.trim(),
|
|
190
|
+
location: String(card?.location ?? "")
|
|
191
|
+
.replace(/\s+/g, " ")
|
|
192
|
+
.trim(),
|
|
193
|
+
salary,
|
|
194
|
+
tags: dedupeIndeedTags(card?.tags, salary),
|
|
195
|
+
url: jk ? buildIndeedJobUrl(jk) : "",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function buildIndeedSearchExtractScript(): string {
|
|
200
|
+
return `(async () => {
|
|
201
|
+
const hasResults = () => Boolean(document.querySelector('.job_seen_beacon'));
|
|
202
|
+
const hasEmptyState = () => {
|
|
203
|
+
const text = document.body?.innerText || '';
|
|
204
|
+
return Boolean(document.querySelector('[data-testid="searchCountPages"], [data-testid="searchCount"], [data-testid="noResultsMessage"], [data-testid="empty-serp-result"]'))
|
|
205
|
+
|| /did not match any jobs|no jobs found|0 jobs/i.test(text);
|
|
206
|
+
};
|
|
207
|
+
let ready = hasResults() || hasEmptyState();
|
|
208
|
+
for (let index = 0; index < 30; index += 1) {
|
|
209
|
+
if (ready) break;
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
211
|
+
ready = hasResults() || hasEmptyState();
|
|
212
|
+
}
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
const cards = [];
|
|
215
|
+
for (const block of document.querySelectorAll('.job_seen_beacon')) {
|
|
216
|
+
const titleAnchor = block.querySelector('h2.jobTitle a, [class*="jcs-JobTitle"]');
|
|
217
|
+
const jk = titleAnchor?.getAttribute('data-jk') || '';
|
|
218
|
+
if (!jk || seen.has(jk)) continue;
|
|
219
|
+
seen.add(jk);
|
|
220
|
+
const tags = Array.from(block.querySelectorAll('.metadataContainer li span'))
|
|
221
|
+
.map((element) => (element.textContent || '').trim())
|
|
222
|
+
.filter(Boolean);
|
|
223
|
+
cards.push({
|
|
224
|
+
jk,
|
|
225
|
+
title: block.querySelector('h2.jobTitle span')?.textContent?.trim() || '',
|
|
226
|
+
company: block.querySelector('[data-testid="company-name"]')?.textContent?.trim() || '',
|
|
227
|
+
location: block.querySelector('[data-testid="text-location"]')?.textContent?.trim() || '',
|
|
228
|
+
salary: block.querySelector('.salary-snippet-container span')?.textContent?.trim() || '',
|
|
229
|
+
tags,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const challenge = (document.title || '').includes('Just a moment') || Boolean(document.querySelector('[id^="cf-"]'));
|
|
233
|
+
return { cards, challenge, ready };
|
|
234
|
+
})()`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function buildIndeedJobExtractScript(): string {
|
|
238
|
+
return `(async () => {
|
|
239
|
+
let ready = Boolean(document.querySelector('#jobDescriptionText, h1, [data-testid="error-page"]'));
|
|
240
|
+
for (let index = 0; index < 30; index += 1) {
|
|
241
|
+
if (ready) break;
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
243
|
+
ready = Boolean(document.querySelector('#jobDescriptionText, h1, [data-testid="error-page"]'));
|
|
244
|
+
}
|
|
245
|
+
const challenge = (document.title || '').includes('Just a moment') || Boolean(document.querySelector('[id^="cf-"]'));
|
|
246
|
+
const headline = document.querySelector('h1')?.textContent || '';
|
|
247
|
+
const notFound = Boolean(document.querySelector('[data-testid="error-page"]')) || /Page Not Found|not found/i.test(headline);
|
|
248
|
+
const title = document.querySelector('h1')?.textContent?.trim() || '';
|
|
249
|
+
const company = document.querySelector('[data-testid="inlineHeader-companyName"] a, [data-testid="inlineHeader-companyName"], [data-company-name="true"]')?.textContent?.trim() || '';
|
|
250
|
+
const location = document.querySelector('[data-testid="jobsearch-JobInfoHeader-companyLocation"] div, [data-testid="inlineHeader-companyLocation"]')?.textContent?.trim() || '';
|
|
251
|
+
const salary = document.querySelector('[id*="salaryInfoAndJobType"] span, [data-testid="job-salary"]')?.textContent?.trim() || '';
|
|
252
|
+
const jobType = Array.from(document.querySelectorAll('[id*="salaryInfoAndJobType"] span, [data-testid="job-type"]'))
|
|
253
|
+
.map((element) => (element.textContent || '').trim())
|
|
254
|
+
.filter((text) => text && text !== salary)
|
|
255
|
+
.join(', ');
|
|
256
|
+
const description = document.querySelector('#jobDescriptionText')?.innerText?.trim() || '';
|
|
257
|
+
return { ready, challenge, notFound, title, company, location, salary, jobType, description };
|
|
258
|
+
})()`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
cli({
|
|
262
|
+
site: "indeed",
|
|
263
|
+
name: "search",
|
|
264
|
+
description: "Search Indeed jobs through the rendered browser DOM",
|
|
265
|
+
domain: "www.indeed.com",
|
|
266
|
+
strategy: Strategy.COOKIE,
|
|
267
|
+
browser: true,
|
|
268
|
+
args: [
|
|
269
|
+
{ name: "query", type: "str", required: true, positional: true },
|
|
270
|
+
{ name: "location", type: "str", default: "" },
|
|
271
|
+
{ name: "fromage", type: "str", default: "" },
|
|
272
|
+
{ name: "sort", type: "str", default: "relevance" },
|
|
273
|
+
{ name: "start", type: "int", default: 0 },
|
|
274
|
+
{ name: "limit", type: "int", default: 15 },
|
|
275
|
+
],
|
|
276
|
+
columns: INDEED_SEARCH_COLUMNS,
|
|
277
|
+
func: async (page, kwargs) => {
|
|
278
|
+
const p = page as IPage;
|
|
279
|
+
const query = requireIndeedQuery(kwargs.query);
|
|
280
|
+
const location = String(kwargs.location ?? "").trim();
|
|
281
|
+
const fromage = requireIndeedFromage(kwargs.fromage);
|
|
282
|
+
const sort = requireIndeedSort(kwargs.sort);
|
|
283
|
+
const start = requireIndeedNonNegativeInt(kwargs.start, 0, "start");
|
|
284
|
+
const limit = requireIndeedBoundedInt(kwargs.limit, 15, 25, "limit");
|
|
285
|
+
const url = buildIndeedSearchUrl({
|
|
286
|
+
query,
|
|
287
|
+
location,
|
|
288
|
+
fromage,
|
|
289
|
+
sort,
|
|
290
|
+
start,
|
|
291
|
+
});
|
|
292
|
+
await p.goto(url);
|
|
293
|
+
await p.wait(4);
|
|
294
|
+
const result = (await p.evaluate(
|
|
295
|
+
buildIndeedSearchExtractScript(),
|
|
296
|
+
)) as IndeedSearchResult;
|
|
297
|
+
if (result?.challenge) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
"Indeed served a Cloudflare challenge page. Open indeed.com in the connected browser and clear the challenge, then retry.",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (!result?.ready) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"Indeed search page did not expose result or empty-state markers before timeout.",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const cards = Array.isArray(result?.cards) ? result.cards : [];
|
|
308
|
+
if (cards.length === 0) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`No Indeed jobs matched "${query}"${location ? ` in ${location}` : ""}.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return cards
|
|
314
|
+
.slice(0, limit)
|
|
315
|
+
.map((card, index) =>
|
|
316
|
+
indeedSearchCardToRow(card as IndeedSearchCard, start + index + 1),
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
cli({
|
|
322
|
+
site: "indeed",
|
|
323
|
+
name: "job",
|
|
324
|
+
description: "Read an Indeed job posting by 16-character jk job key",
|
|
325
|
+
domain: "www.indeed.com",
|
|
326
|
+
strategy: Strategy.COOKIE,
|
|
327
|
+
browser: true,
|
|
328
|
+
args: [{ name: "id", type: "str", required: true, positional: true }],
|
|
329
|
+
columns: INDEED_JOB_COLUMNS,
|
|
330
|
+
func: async (page, kwargs) => {
|
|
331
|
+
const p = page as IPage;
|
|
332
|
+
const jk = requireIndeedJobKey(kwargs.id);
|
|
333
|
+
const url = buildIndeedJobUrl(jk);
|
|
334
|
+
await p.goto(url);
|
|
335
|
+
await p.wait(4);
|
|
336
|
+
const detail = (await p.evaluate(
|
|
337
|
+
buildIndeedJobExtractScript(),
|
|
338
|
+
)) as IndeedJobResult;
|
|
339
|
+
if (detail?.challenge) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
"Indeed served a Cloudflare challenge page. Open indeed.com in the connected browser and clear the challenge, then retry.",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (!detail?.ready) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
"Indeed job page did not expose detail or error markers before timeout.",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (detail?.notFound || (!detail?.title && !detail?.description)) {
|
|
350
|
+
throw new Error(`No Indeed job posting found for jk "${jk}".`);
|
|
351
|
+
}
|
|
352
|
+
return [
|
|
353
|
+
{
|
|
354
|
+
id: jk,
|
|
355
|
+
title: String(detail.title ?? "")
|
|
356
|
+
.replace(/\s+/g, " ")
|
|
357
|
+
.trim(),
|
|
358
|
+
company: String(detail.company ?? "")
|
|
359
|
+
.replace(/\s+/g, " ")
|
|
360
|
+
.trim(),
|
|
361
|
+
location: String(detail.location ?? "")
|
|
362
|
+
.replace(/\s+/g, " ")
|
|
363
|
+
.trim(),
|
|
364
|
+
salary: String(detail.salary ?? "")
|
|
365
|
+
.replace(/\s+/g, " ")
|
|
366
|
+
.trim(),
|
|
367
|
+
job_type: String(detail.jobType ?? "")
|
|
368
|
+
.replace(/\s+/g, " ")
|
|
369
|
+
.trim(),
|
|
370
|
+
description: String(detail.description ?? ""),
|
|
371
|
+
url,
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
},
|
|
375
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { resolveCommand } from "../../registry.js";
|
|
3
|
+
import { requireInstagramCollectionInput } from "./collections.js";
|
|
4
|
+
|
|
5
|
+
function pageMock(evaluateResults: unknown[]) {
|
|
6
|
+
return {
|
|
7
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
evaluate: vi.fn(async () => evaluateResults.shift()),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("instagram collection agent-facing commands", () => {
|
|
13
|
+
it("validates collection inputs before navigation", async () => {
|
|
14
|
+
expect(requireInstagramCollectionInput(" Research ", "name")).toBe(
|
|
15
|
+
"Research",
|
|
16
|
+
);
|
|
17
|
+
expect(() => requireInstagramCollectionInput("", "name")).toThrow(
|
|
18
|
+
"cannot be empty",
|
|
19
|
+
);
|
|
20
|
+
const command = resolveCommand("instagram", "collection-create")?.command;
|
|
21
|
+
const page = pageMock([]);
|
|
22
|
+
await expect(command!.func!(page, { name: "" })).rejects.toThrow(
|
|
23
|
+
"cannot be empty",
|
|
24
|
+
);
|
|
25
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("creates Instagram saved-post collections", async () => {
|
|
29
|
+
const command = resolveCommand("instagram", "collection-create")?.command;
|
|
30
|
+
const page = pageMock([
|
|
31
|
+
{
|
|
32
|
+
ok: true,
|
|
33
|
+
row: {
|
|
34
|
+
status: "Created",
|
|
35
|
+
collectionId: "123456",
|
|
36
|
+
collectionName: "Research",
|
|
37
|
+
mediaCount: 0,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
await expect(command!.func!(page, { name: "Research" })).resolves.toEqual([
|
|
42
|
+
{
|
|
43
|
+
status: "Created",
|
|
44
|
+
collectionId: "123456",
|
|
45
|
+
collectionName: "Research",
|
|
46
|
+
mediaCount: 0,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
expect(page.goto).toHaveBeenCalledWith("https://www.instagram.com");
|
|
50
|
+
expect(String(page.evaluate.mock.calls[0][0])).toContain(
|
|
51
|
+
"/api/v1/collections/create/",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("deletes Instagram saved-post collections", async () => {
|
|
56
|
+
const command = resolveCommand("instagram", "collection-delete")?.command;
|
|
57
|
+
const page = pageMock([
|
|
58
|
+
{
|
|
59
|
+
ok: true,
|
|
60
|
+
row: {
|
|
61
|
+
status: "Deleted",
|
|
62
|
+
collectionId: "123456",
|
|
63
|
+
collectionName: "Research",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
await expect(command!.func!(page, { target: "Research" })).resolves.toEqual(
|
|
68
|
+
[
|
|
69
|
+
{
|
|
70
|
+
status: "Deleted",
|
|
71
|
+
collectionId: "123456",
|
|
72
|
+
collectionName: "Research",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
);
|
|
76
|
+
expect(page.goto).toHaveBeenCalledWith("https://www.instagram.com");
|
|
77
|
+
expect(String(page.evaluate.mock.calls[0][0])).toContain(
|
|
78
|
+
"/api/v1/collections/list/",
|
|
79
|
+
);
|
|
80
|
+
expect(String(page.evaluate.mock.calls[0][0])).toContain(
|
|
81
|
+
"/api/v1/collections/",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("propagates Instagram collection API failures", async () => {
|
|
86
|
+
const command = resolveCommand("instagram", "collection-delete")?.command;
|
|
87
|
+
const page = pageMock([
|
|
88
|
+
{ ok: false, error: "Collection not found: Research" },
|
|
89
|
+
]);
|
|
90
|
+
await expect(command!.func!(page, { target: "Research" })).rejects.toThrow(
|
|
91
|
+
"Collection not found",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owner src/adapters/instagram/collections.ts
|
|
3
|
+
* @does Register agent-facing Instagram saved collection create and delete commands.
|
|
4
|
+
* @needs Logged-in www.instagram.com browser session with csrftoken cookie.
|
|
5
|
+
* @feeds surface coverage ledger and saved-post collection management workflows.
|
|
6
|
+
* @breaks Instagram private collection API changes, CSRF cookie changes, or collection list schema drift.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cli, Strategy } from "../../registry.js";
|
|
10
|
+
import type { IPage } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
interface InstagramCollectionResult {
|
|
13
|
+
ok?: unknown;
|
|
14
|
+
error?: unknown;
|
|
15
|
+
row?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface InstagramCollectionRow {
|
|
19
|
+
status?: unknown;
|
|
20
|
+
collectionId?: unknown;
|
|
21
|
+
collectionName?: unknown;
|
|
22
|
+
mediaCount?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function requireInstagramCollectionInput(
|
|
26
|
+
value: unknown,
|
|
27
|
+
label: string,
|
|
28
|
+
): string {
|
|
29
|
+
const text = String(value ?? "").trim();
|
|
30
|
+
if (!text) throw new Error(`Instagram collection ${label} cannot be empty.`);
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildCreateCollectionScript(name: string): string {
|
|
35
|
+
return `(async () => {
|
|
36
|
+
const collectionName = ${JSON.stringify(name)};
|
|
37
|
+
const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';
|
|
38
|
+
if (!csrf) return { ok: false, error: 'csrftoken cookie missing; log in to Instagram and retry.' };
|
|
39
|
+
const form = new FormData();
|
|
40
|
+
form.append('name', collectionName);
|
|
41
|
+
form.append('module_name', 'collection_create');
|
|
42
|
+
const response = await fetch('https://www.instagram.com/api/v1/collections/create/', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
credentials: 'include',
|
|
45
|
+
headers: {
|
|
46
|
+
'X-IG-App-ID': '936619743392459',
|
|
47
|
+
'X-CSRFToken': csrf,
|
|
48
|
+
},
|
|
49
|
+
body: form,
|
|
50
|
+
});
|
|
51
|
+
const text = await response.text();
|
|
52
|
+
let data = {};
|
|
53
|
+
try { data = text ? JSON.parse(text) : {}; } catch {}
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
return { ok: false, error: 'Failed to create collection: HTTP ' + response.status + (text ? ' - ' + text.slice(0, 200) : '') };
|
|
56
|
+
}
|
|
57
|
+
if (data?.status && data.status !== 'ok') {
|
|
58
|
+
return { ok: false, error: 'Instagram returned non-ok status: ' + JSON.stringify(data).slice(0, 300) };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
row: {
|
|
63
|
+
status: 'Created',
|
|
64
|
+
collectionId: String(data?.collection_id ?? ''),
|
|
65
|
+
collectionName: String(data?.collection_name ?? collectionName),
|
|
66
|
+
mediaCount: data?.collection_media_count ?? 0,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
})()`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildDeleteCollectionScript(target: string): string {
|
|
73
|
+
return `(async () => {
|
|
74
|
+
const raw = ${JSON.stringify(target)};
|
|
75
|
+
const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';
|
|
76
|
+
if (!csrf) return { ok: false, error: 'csrftoken cookie missing; log in to Instagram and retry.' };
|
|
77
|
+
const headers = { 'X-IG-App-ID': '936619743392459' };
|
|
78
|
+
const listResponse = await fetch('https://www.instagram.com/api/v1/collections/list/?collection_types=%5B%22MEDIA%22%5D', {
|
|
79
|
+
credentials: 'include',
|
|
80
|
+
headers,
|
|
81
|
+
});
|
|
82
|
+
if (!listResponse.ok) {
|
|
83
|
+
return { ok: false, error: 'Failed to list collections: HTTP ' + listResponse.status + '; log in to Instagram and retry.' };
|
|
84
|
+
}
|
|
85
|
+
const listData = await listResponse.json();
|
|
86
|
+
const collections = Array.isArray(listData?.items) ? listData.items : [];
|
|
87
|
+
const isNumericId = /^\\d{6,}$/.test(raw);
|
|
88
|
+
let id = '';
|
|
89
|
+
let resolvedName = '';
|
|
90
|
+
if (isNumericId) {
|
|
91
|
+
const hit = collections.find((collection) => String(collection?.collection_id) === raw);
|
|
92
|
+
if (!hit) return { ok: false, error: 'Collection id not found in your account: ' + raw };
|
|
93
|
+
id = String(hit.collection_id);
|
|
94
|
+
resolvedName = String(hit.collection_name || '');
|
|
95
|
+
} else {
|
|
96
|
+
const wanted = raw.toLowerCase();
|
|
97
|
+
const matches = collections.filter((collection) => String(collection?.collection_name || '').trim().toLowerCase() === wanted);
|
|
98
|
+
if (matches.length === 0) {
|
|
99
|
+
const names = collections.map((collection) => collection?.collection_name).filter(Boolean);
|
|
100
|
+
return { ok: false, error: 'Collection not found: ' + raw + '. Available: ' + (names.length ? names.join(', ') : '(none)') };
|
|
101
|
+
}
|
|
102
|
+
if (matches.length > 1) {
|
|
103
|
+
const ids = matches.map((collection) => collection.collection_id).join(', ');
|
|
104
|
+
return { ok: false, error: 'Multiple collections share the name "' + raw + '" (ids: ' + ids + '). Pass the numeric collection_id explicitly.' };
|
|
105
|
+
}
|
|
106
|
+
id = String(matches[0].collection_id);
|
|
107
|
+
resolvedName = String(matches[0].collection_name || raw);
|
|
108
|
+
}
|
|
109
|
+
const form = new FormData();
|
|
110
|
+
form.append('module_name', 'collection_settings');
|
|
111
|
+
const response = await fetch('https://www.instagram.com/api/v1/collections/' + encodeURIComponent(id) + '/delete/', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
credentials: 'include',
|
|
114
|
+
headers: { ...headers, 'X-CSRFToken': csrf },
|
|
115
|
+
body: form,
|
|
116
|
+
});
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
let data = {};
|
|
119
|
+
try { data = text ? JSON.parse(text) : {}; } catch {}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
return { ok: false, error: 'Failed to delete collection: HTTP ' + response.status + (text ? ' - ' + text.slice(0, 200) : '') };
|
|
122
|
+
}
|
|
123
|
+
if (data?.status && data.status !== 'ok') {
|
|
124
|
+
return { ok: false, error: 'Instagram returned non-ok status: ' + JSON.stringify(data).slice(0, 300) };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
row: {
|
|
129
|
+
status: 'Deleted',
|
|
130
|
+
collectionId: id,
|
|
131
|
+
collectionName: resolvedName,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
})()`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function mapCollectionRow(row: unknown): InstagramCollectionRow {
|
|
138
|
+
const value =
|
|
139
|
+
row && typeof row === "object" ? (row as InstagramCollectionRow) : {};
|
|
140
|
+
return {
|
|
141
|
+
status: String(value.status ?? ""),
|
|
142
|
+
collectionId: String(value.collectionId ?? ""),
|
|
143
|
+
collectionName: String(value.collectionName ?? ""),
|
|
144
|
+
mediaCount:
|
|
145
|
+
value.mediaCount === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: Number(value.mediaCount) || 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
cli({
|
|
152
|
+
site: "instagram",
|
|
153
|
+
name: "collection-create",
|
|
154
|
+
description: "Create a new Instagram saved-posts collection",
|
|
155
|
+
domain: "www.instagram.com",
|
|
156
|
+
strategy: Strategy.COOKIE,
|
|
157
|
+
browser: true,
|
|
158
|
+
args: [{ name: "name", type: "str", required: true, positional: true }],
|
|
159
|
+
columns: ["status", "collectionId", "collectionName", "mediaCount"],
|
|
160
|
+
func: async (page, kwargs) => {
|
|
161
|
+
const name = requireInstagramCollectionInput(kwargs.name, "name");
|
|
162
|
+
const p = page as IPage;
|
|
163
|
+
await p.goto("https://www.instagram.com");
|
|
164
|
+
const result = (await p.evaluate(
|
|
165
|
+
buildCreateCollectionScript(name),
|
|
166
|
+
)) as InstagramCollectionResult;
|
|
167
|
+
if (!result?.ok) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
String(result?.error || "Instagram collection create failed."),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return [mapCollectionRow(result.row)];
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
cli({
|
|
177
|
+
site: "instagram",
|
|
178
|
+
name: "collection-delete",
|
|
179
|
+
description: "Delete an Instagram saved-posts collection by name or id",
|
|
180
|
+
domain: "www.instagram.com",
|
|
181
|
+
strategy: Strategy.COOKIE,
|
|
182
|
+
browser: true,
|
|
183
|
+
args: [{ name: "target", type: "str", required: true, positional: true }],
|
|
184
|
+
columns: ["status", "collectionId", "collectionName"],
|
|
185
|
+
func: async (page, kwargs) => {
|
|
186
|
+
const target = requireInstagramCollectionInput(kwargs.target, "target");
|
|
187
|
+
const p = page as IPage;
|
|
188
|
+
await p.goto("https://www.instagram.com");
|
|
189
|
+
const result = (await p.evaluate(
|
|
190
|
+
buildDeleteCollectionScript(target),
|
|
191
|
+
)) as InstagramCollectionResult;
|
|
192
|
+
if (!result?.ok) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
String(result?.error || "Instagram collection delete failed."),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const row = mapCollectionRow(result.row);
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
status: row.status,
|
|
201
|
+
collectionId: row.collectionId,
|
|
202
|
+
collectionName: row.collectionName,
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
},
|
|
206
|
+
});
|