publishport-opencli 1.8.4-pp.1
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/LICENSE +190 -0
- package/README.md +300 -0
- package/README.zh-CN.md +341 -0
- package/cli-manifest.json +44212 -0
- package/clis/12306/auth.js +59 -0
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +166 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +424 -0
- package/clis/1688/assets.js +205 -0
- package/clis/1688/assets.test.js +39 -0
- package/clis/1688/auth.js +46 -0
- package/clis/1688/download.js +77 -0
- package/clis/1688/download.test.js +31 -0
- package/clis/1688/item.js +188 -0
- package/clis/1688/item.test.js +67 -0
- package/clis/1688/search.js +310 -0
- package/clis/1688/search.test.js +75 -0
- package/clis/1688/shared.js +557 -0
- package/clis/1688/shared.test.js +57 -0
- package/clis/1688/store.js +227 -0
- package/clis/1688/store.test.js +62 -0
- package/clis/1point3acres/auth.js +52 -0
- package/clis/1point3acres/digest.js +35 -0
- package/clis/1point3acres/forum.js +51 -0
- package/clis/1point3acres/forums.js +44 -0
- package/clis/1point3acres/hot.js +35 -0
- package/clis/1point3acres/latest.js +35 -0
- package/clis/1point3acres/notifications.js +64 -0
- package/clis/1point3acres/search.js +71 -0
- package/clis/1point3acres/thread.js +117 -0
- package/clis/1point3acres/user.js +77 -0
- package/clis/1point3acres/utils.js +247 -0
- package/clis/36kr/article.js +66 -0
- package/clis/36kr/article.test.js +46 -0
- package/clis/36kr/hot.js +87 -0
- package/clis/36kr/hot.test.js +15 -0
- package/clis/36kr/news.js +52 -0
- package/clis/36kr/news.test.js +85 -0
- package/clis/36kr/search.js +79 -0
- package/clis/51job/company.js +126 -0
- package/clis/51job/detail.js +109 -0
- package/clis/51job/hot.js +56 -0
- package/clis/51job/search.js +80 -0
- package/clis/51job/utils.js +302 -0
- package/clis/51job/utils.test.js +69 -0
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/_shared/common.js +32 -0
- package/clis/_shared/desktop-commands.js +112 -0
- package/clis/_shared/search-adapter.js +70 -0
- package/clis/_shared/site-auth.js +118 -0
- package/clis/_shared/site-auth.test.js +98 -0
- package/clis/aibase/news.js +110 -0
- package/clis/aibase/news.test.js +59 -0
- package/clis/amazon/auth.js +53 -0
- package/clis/amazon/bestsellers.js +8 -0
- package/clis/amazon/bestsellers.test.js +29 -0
- package/clis/amazon/discussion.js +123 -0
- package/clis/amazon/discussion.test.js +124 -0
- package/clis/amazon/movers-shakers.js +8 -0
- package/clis/amazon/new-releases.js +8 -0
- package/clis/amazon/offer.js +141 -0
- package/clis/amazon/offer.test.js +29 -0
- package/clis/amazon/product.js +93 -0
- package/clis/amazon/product.test.js +24 -0
- package/clis/amazon/rankings.js +227 -0
- package/clis/amazon/rankings.test.js +41 -0
- package/clis/amazon/search.js +88 -0
- package/clis/amazon/search.test.js +22 -0
- package/clis/amazon/shared.js +365 -0
- package/clis/amazon/shared.test.js +44 -0
- package/clis/antigravity/SKILL.md +38 -0
- package/clis/antigravity/_actions.js +318 -0
- package/clis/antigravity/antigravity.test.js +172 -0
- package/clis/antigravity/audit-extras.js +341 -0
- package/clis/antigravity/delete.js +60 -0
- package/clis/antigravity/dump.js +29 -0
- package/clis/antigravity/extract-code.js +33 -0
- package/clis/antigravity/history.js +26 -0
- package/clis/antigravity/mark-read.js +52 -0
- package/clis/antigravity/model.js +161 -0
- package/clis/antigravity/new.js +26 -0
- package/clis/antigravity/read.js +35 -0
- package/clis/antigravity/rename.js +33 -0
- package/clis/antigravity/send.js +36 -0
- package/clis/antigravity/serve.js +558 -0
- package/clis/antigravity/status.js +19 -0
- package/clis/antigravity/storage.js +366 -0
- package/clis/antigravity/watch.js +43 -0
- package/clis/apple-podcasts/commands.test.js +119 -0
- package/clis/apple-podcasts/episodes.js +29 -0
- package/clis/apple-podcasts/search.js +31 -0
- package/clis/apple-podcasts/top.js +45 -0
- package/clis/apple-podcasts/utils.js +30 -0
- package/clis/apple-podcasts/utils.test.js +57 -0
- package/clis/archive/archive.test.js +262 -0
- package/clis/archive/item.js +92 -0
- package/clis/archive/search.js +115 -0
- package/clis/archive/snapshots.js +129 -0
- package/clis/archive/wayback.js +83 -0
- package/clis/arxiv/arxiv.test.js +112 -0
- package/clis/arxiv/author.js +44 -0
- package/clis/arxiv/paper.js +22 -0
- package/clis/arxiv/recent.js +33 -0
- package/clis/arxiv/search.js +36 -0
- package/clis/arxiv/utils.js +112 -0
- package/clis/autohome/__fixtures__/catalog.html +11 -0
- package/clis/autohome/__fixtures__/koubei.json +116 -0
- package/clis/autohome/autohome.test.js +115 -0
- package/clis/autohome/brand.js +108 -0
- package/clis/autohome/score.js +103 -0
- package/clis/autohome/utils.js +157 -0
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/band/auth.js +62 -0
- package/clis/band/bands.js +73 -0
- package/clis/band/mentions.js +128 -0
- package/clis/band/post.js +176 -0
- package/clis/band/posts.js +95 -0
- package/clis/barchart/flow.js +116 -0
- package/clis/barchart/greeks.js +208 -0
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/barchart/options.js +107 -0
- package/clis/barchart/quote.js +135 -0
- package/clis/bbc/news.js +42 -0
- package/clis/bbc/topic.js +57 -0
- package/clis/bbc/utils.js +79 -0
- package/clis/bilibili/auth.js +36 -0
- package/clis/bilibili/comment.js +107 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +136 -0
- package/clis/bilibili/comments.test.js +142 -0
- package/clis/bilibili/download.js +178 -0
- package/clis/bilibili/download.test.js +173 -0
- package/clis/bilibili/dynamic.js +34 -0
- package/clis/bilibili/dynamic.test.js +68 -0
- package/clis/bilibili/favorite.js +47 -0
- package/clis/bilibili/feed.js +220 -0
- package/clis/bilibili/follow.js +140 -0
- package/clis/bilibili/follow.test.js +203 -0
- package/clis/bilibili/following.js +44 -0
- package/clis/bilibili/history.js +45 -0
- package/clis/bilibili/hot.js +40 -0
- package/clis/bilibili/hot.test.js +17 -0
- package/clis/bilibili/me.js +13 -0
- package/clis/bilibili/ranking.js +25 -0
- package/clis/bilibili/search.js +24 -0
- package/clis/bilibili/subtitle.js +141 -0
- package/clis/bilibili/subtitle.test.js +240 -0
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/unfollow.js +121 -0
- package/clis/bilibili/user-videos.js +39 -0
- package/clis/bilibili/utils.js +273 -0
- package/clis/bilibili/utils.test.js +97 -0
- package/clis/bilibili/video.js +155 -0
- package/clis/bilibili/video.test.js +308 -0
- package/clis/binance/asks.js +22 -0
- package/clis/binance/commands.test.js +70 -0
- package/clis/binance/depth.js +21 -0
- package/clis/binance/gainers.js +23 -0
- package/clis/binance/klines.js +22 -0
- package/clis/binance/losers.js +23 -0
- package/clis/binance/pairs.js +22 -0
- package/clis/binance/price.js +19 -0
- package/clis/binance/prices.js +20 -0
- package/clis/binance/ticker.js +22 -0
- package/clis/binance/top.js +22 -0
- package/clis/binance/trades.js +21 -0
- package/clis/bloomberg/businessweek.js +125 -0
- package/clis/bloomberg/businessweek.test.js +149 -0
- package/clis/bloomberg/crypto.js +18 -0
- package/clis/bloomberg/economics.js +18 -0
- package/clis/bloomberg/feeds.js +16 -0
- package/clis/bloomberg/green.js +18 -0
- package/clis/bloomberg/industries.js +18 -0
- package/clis/bloomberg/main.js +18 -0
- package/clis/bloomberg/markets.js +18 -0
- package/clis/bloomberg/news.js +106 -0
- package/clis/bloomberg/opinions.js +18 -0
- package/clis/bloomberg/politics.js +18 -0
- package/clis/bloomberg/pursuits.js +18 -0
- package/clis/bloomberg/tech.js +18 -0
- package/clis/bloomberg/utils.js +380 -0
- package/clis/bloomberg/utils.test.js +129 -0
- package/clis/bluesky/feeds.js +28 -0
- package/clis/bluesky/followers.js +28 -0
- package/clis/bluesky/following.js +28 -0
- package/clis/bluesky/profile.js +30 -0
- package/clis/bluesky/search.js +29 -0
- package/clis/bluesky/starter-packs.js +29 -0
- package/clis/bluesky/thread.js +31 -0
- package/clis/bluesky/trending.js +20 -0
- package/clis/bluesky/user.js +35 -0
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/boss/auth.js +47 -0
- package/clis/boss/batchgreet.js +61 -0
- package/clis/boss/chatlist.js +116 -0
- package/clis/boss/chatlist.test.js +211 -0
- package/clis/boss/chatmsg.js +117 -0
- package/clis/boss/chatmsg.test.js +230 -0
- package/clis/boss/detail.js +63 -0
- package/clis/boss/exchange.js +46 -0
- package/clis/boss/greet.js +49 -0
- package/clis/boss/invite.js +64 -0
- package/clis/boss/joblist.js +31 -0
- package/clis/boss/mark.js +70 -0
- package/clis/boss/recommend.js +45 -0
- package/clis/boss/resume.js +150 -0
- package/clis/boss/search.js +197 -0
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +44 -0
- package/clis/boss/stats.js +66 -0
- package/clis/boss/utils.js +468 -0
- package/clis/boss/utils.test.js +34 -0
- package/clis/brave/search.js +80 -0
- package/clis/brave/search.test.js +76 -0
- package/clis/chaoxing/assignments.js +76 -0
- package/clis/chaoxing/auth.js +54 -0
- package/clis/chaoxing/exams.js +75 -0
- package/clis/chaoxing/utils.js +223 -0
- package/clis/chaoxing/utils.test.js +45 -0
- package/clis/chatgpt/ask.js +129 -0
- package/clis/chatgpt/auth.js +52 -0
- package/clis/chatgpt/commands.test.js +204 -0
- package/clis/chatgpt/detail.js +63 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/history.js +39 -0
- package/clis/chatgpt/image.js +167 -0
- package/clis/chatgpt/image.test.js +207 -0
- package/clis/chatgpt/model.js +31 -0
- package/clis/chatgpt/model.test.js +36 -0
- package/clis/chatgpt/new.js +32 -0
- package/clis/chatgpt/project-file-add.js +74 -0
- package/clis/chatgpt/project-list.js +37 -0
- package/clis/chatgpt/read.js +44 -0
- package/clis/chatgpt/send.js +68 -0
- package/clis/chatgpt/status.js +29 -0
- package/clis/chatgpt/utils.js +2226 -0
- package/clis/chatgpt/utils.test.js +1453 -0
- package/clis/chatgpt-app/ask.js +87 -0
- package/clis/chatgpt-app/ax.js +603 -0
- package/clis/chatgpt-app/ax.test.js +95 -0
- package/clis/chatgpt-app/commands.test.js +45 -0
- package/clis/chatgpt-app/model.js +25 -0
- package/clis/chatgpt-app/new.js +61 -0
- package/clis/chatgpt-app/read.js +32 -0
- package/clis/chatgpt-app/send.js +37 -0
- package/clis/chatgpt-app/status.js +26 -0
- package/clis/chatwise/ask.js +54 -0
- package/clis/chatwise/composer.test.js +186 -0
- package/clis/chatwise/export.js +47 -0
- package/clis/chatwise/history.js +61 -0
- package/clis/chatwise/model.js +85 -0
- package/clis/chatwise/new.js +2 -0
- package/clis/chatwise/read.js +40 -0
- package/clis/chatwise/screenshot.js +2 -0
- package/clis/chatwise/send.js +28 -0
- package/clis/chatwise/status.js +2 -0
- package/clis/chatwise/utils.js +143 -0
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/claude/ask.js +144 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/auth.js +55 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +38 -0
- package/clis/claude/history.js +33 -0
- package/clis/claude/new.js +29 -0
- package/clis/claude/read.js +27 -0
- package/clis/claude/send.js +48 -0
- package/clis/claude/status.js +26 -0
- package/clis/claude/utils.js +463 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/cnki/search.js +61 -0
- package/clis/cnki/search.test.js +18 -0
- package/clis/codex/_actions.js +270 -0
- package/clis/codex/archive.js +37 -0
- package/clis/codex/ask.js +78 -0
- package/clis/codex/dump.js +2 -0
- package/clis/codex/export.js +38 -0
- package/clis/codex/extract-diff.js +45 -0
- package/clis/codex/history.js +27 -0
- package/clis/codex/model.js +264 -0
- package/clis/codex/new.js +2 -0
- package/clis/codex/pin.js +30 -0
- package/clis/codex/projects.js +28 -0
- package/clis/codex/read.js +41 -0
- package/clis/codex/rename.js +86 -0
- package/clis/codex/screenshot.js +2 -0
- package/clis/codex/send.js +51 -0
- package/clis/codex/sidebar.js +356 -0
- package/clis/codex/sidebar.test.js +388 -0
- package/clis/codex/status.js +2 -0
- package/clis/coingecko/categories.js +75 -0
- package/clis/coingecko/coin.js +107 -0
- package/clis/coingecko/coingecko.test.js +109 -0
- package/clis/coingecko/derivatives.js +84 -0
- package/clis/coingecko/exchanges.js +74 -0
- package/clis/coingecko/global.js +71 -0
- package/clis/coingecko/top.js +64 -0
- package/clis/coingecko/trending.js +55 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/coupang/add-to-cart.js +149 -0
- package/clis/coupang/auth.js +48 -0
- package/clis/coupang/coupang.test.js +159 -0
- package/clis/coupang/product.js +257 -0
- package/clis/coupang/search.js +474 -0
- package/clis/coupang/utils.js +316 -0
- package/clis/coupang/utils.test.js +62 -0
- package/clis/crates/crate.js +62 -0
- package/clis/crates/search.js +44 -0
- package/clis/crates/utils.js +72 -0
- package/clis/ctrip/auth.js +50 -0
- package/clis/ctrip/ctrip.test.js +719 -0
- package/clis/ctrip/flight.js +136 -0
- package/clis/ctrip/hotel-search.js +132 -0
- package/clis/ctrip/hotel-suggest.js +45 -0
- package/clis/ctrip/search.js +41 -0
- package/clis/ctrip/utils.js +473 -0
- package/clis/cursor/ask.js +74 -0
- package/clis/cursor/composer.js +43 -0
- package/clis/cursor/dump.js +2 -0
- package/clis/cursor/export.js +52 -0
- package/clis/cursor/extract-code.js +36 -0
- package/clis/cursor/history.js +44 -0
- package/clis/cursor/model.js +54 -0
- package/clis/cursor/new.js +2 -0
- package/clis/cursor/read.js +45 -0
- package/clis/cursor/screenshot.js +2 -0
- package/clis/cursor/send.js +41 -0
- package/clis/cursor/status.js +2 -0
- package/clis/dblp/author.js +133 -0
- package/clis/dblp/dblp.test.js +397 -0
- package/clis/dblp/paper.js +40 -0
- package/clis/dblp/search.js +45 -0
- package/clis/dblp/utils.js +290 -0
- package/clis/dblp/venue.js +64 -0
- package/clis/deepseek/ask.js +158 -0
- package/clis/deepseek/ask.test.js +399 -0
- package/clis/deepseek/auth.js +54 -0
- package/clis/deepseek/detail.js +46 -0
- package/clis/deepseek/detail.test.js +81 -0
- package/clis/deepseek/history.js +27 -0
- package/clis/deepseek/new.js +33 -0
- package/clis/deepseek/read.js +25 -0
- package/clis/deepseek/send.js +140 -0
- package/clis/deepseek/send.test.js +107 -0
- package/clis/deepseek/status.js +26 -0
- package/clis/deepseek/utils.js +487 -0
- package/clis/deepseek/utils.test.js +370 -0
- package/clis/defillama/defillama.test.js +99 -0
- package/clis/defillama/protocol.js +84 -0
- package/clis/defillama/protocols.js +55 -0
- package/clis/defillama/utils.js +99 -0
- package/clis/devto/devto.test.js +236 -0
- package/clis/devto/latest.js +74 -0
- package/clis/devto/read.js +103 -0
- package/clis/devto/tag.js +36 -0
- package/clis/devto/top.js +30 -0
- package/clis/devto/user.js +35 -0
- package/clis/dianping/__fixtures__/search.html +168 -0
- package/clis/dianping/__fixtures__/shop.html +6 -0
- package/clis/dianping/auth.js +49 -0
- package/clis/dianping/cityResolver.js +185 -0
- package/clis/dianping/dianping.test.js +578 -0
- package/clis/dianping/search.js +157 -0
- package/clis/dianping/shop.js +173 -0
- package/clis/dianping/utils.js +157 -0
- package/clis/dictionary/examples.js +28 -0
- package/clis/dictionary/search.js +30 -0
- package/clis/dictionary/synonyms.js +28 -0
- package/clis/discord-app/channels.js +26 -0
- package/clis/discord-app/commands.test.js +395 -0
- package/clis/discord-app/delete.js +115 -0
- package/clis/discord-app/goto.js +38 -0
- package/clis/discord-app/members.js +39 -0
- package/clis/discord-app/read.js +42 -0
- package/clis/discord-app/search.js +67 -0
- package/clis/discord-app/send.js +28 -0
- package/clis/discord-app/servers.js +26 -0
- package/clis/discord-app/status.js +17 -0
- package/clis/discord-app/thread-read.js +45 -0
- package/clis/discord-app/threads.js +37 -0
- package/clis/discord-app/utils.js +610 -0
- package/clis/dockerhub/image.js +52 -0
- package/clis/dockerhub/search.js +47 -0
- package/clis/dockerhub/utils.js +100 -0
- package/clis/dongchedi/__fixtures__/search.json +60 -0
- package/clis/dongchedi/__fixtures__/series-detail.json +911 -0
- package/clis/dongchedi/__fixtures__/series-score.json +1396 -0
- package/clis/dongchedi/dongchedi.test.js +191 -0
- package/clis/dongchedi/koubei.js +85 -0
- package/clis/dongchedi/models.js +91 -0
- package/clis/dongchedi/score.js +83 -0
- package/clis/dongchedi/search.js +83 -0
- package/clis/dongchedi/series.js +87 -0
- package/clis/dongchedi/specs.js +115 -0
- package/clis/dongchedi/utils.js +180 -0
- package/clis/douban/auth.js +50 -0
- package/clis/douban/book-hot.js +15 -0
- package/clis/douban/download.js +68 -0
- package/clis/douban/download.test.js +170 -0
- package/clis/douban/marks.js +116 -0
- package/clis/douban/movie-hot.js +15 -0
- package/clis/douban/movie-hot.test.js +14 -0
- package/clis/douban/photos.js +35 -0
- package/clis/douban/reviews.js +107 -0
- package/clis/douban/search.js +18 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +46 -0
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/top250.js +68 -0
- package/clis/douban/utils.js +657 -0
- package/clis/douban/utils.test.js +488 -0
- package/clis/doubao/ask.js +40 -0
- package/clis/doubao/auth.js +66 -0
- package/clis/doubao/detail.js +35 -0
- package/clis/doubao/detail.test.js +42 -0
- package/clis/doubao/history.js +30 -0
- package/clis/doubao/history.test.js +37 -0
- package/clis/doubao/meeting-summary.js +41 -0
- package/clis/doubao/meeting-transcript.js +38 -0
- package/clis/doubao/new.js +22 -0
- package/clis/doubao/read.js +21 -0
- package/clis/doubao/send.js +24 -0
- package/clis/doubao/status.js +26 -0
- package/clis/doubao/utils.js +1156 -0
- package/clis/doubao/utils.test.js +396 -0
- package/clis/doubao-app/ask.js +54 -0
- package/clis/doubao-app/dump.js +25 -0
- package/clis/doubao-app/new.js +21 -0
- package/clis/doubao-app/read.js +19 -0
- package/clis/doubao-app/screenshot.js +19 -0
- package/clis/doubao-app/send.js +28 -0
- package/clis/doubao-app/status.js +17 -0
- package/clis/doubao-app/utils.js +110 -0
- package/clis/douyin/_shared/browser-fetch.js +66 -0
- package/clis/douyin/_shared/browser-fetch.test.js +64 -0
- package/clis/douyin/_shared/creation-id.js +5 -0
- package/clis/douyin/_shared/creation-id.test.js +22 -0
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/imagex-upload.js +53 -0
- package/clis/douyin/_shared/imagex-upload.test.js +87 -0
- package/clis/douyin/_shared/public-api.js +29 -0
- package/clis/douyin/_shared/sts2.js +21 -0
- package/clis/douyin/_shared/sts2.test.js +27 -0
- package/clis/douyin/_shared/text-extra.js +15 -0
- package/clis/douyin/_shared/text-extra.test.js +37 -0
- package/clis/douyin/_shared/timing.js +22 -0
- package/clis/douyin/_shared/timing.test.js +28 -0
- package/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
- package/clis/douyin/_shared/tos-upload.js +331 -0
- package/clis/douyin/_shared/tos-upload.test.js +229 -0
- package/clis/douyin/_shared/transcode.js +45 -0
- package/clis/douyin/_shared/transcode.test.js +91 -0
- package/clis/douyin/_shared/types.js +1 -0
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/activities.js +23 -0
- package/clis/douyin/activities.test.js +55 -0
- package/clis/douyin/auth.js +39 -0
- package/clis/douyin/collections.js +23 -0
- package/clis/douyin/collections.test.js +45 -0
- package/clis/douyin/delete.js +152 -0
- package/clis/douyin/delete.test.js +100 -0
- package/clis/douyin/draft.js +355 -0
- package/clis/douyin/draft.test.js +335 -0
- package/clis/douyin/drafts.js +24 -0
- package/clis/douyin/drafts.test.js +11 -0
- package/clis/douyin/hashtag.js +114 -0
- package/clis/douyin/hashtag.test.js +171 -0
- package/clis/douyin/location.js +25 -0
- package/clis/douyin/location.test.js +23 -0
- package/clis/douyin/profile.js +29 -0
- package/clis/douyin/profile.test.js +46 -0
- package/clis/douyin/publish-upload-id.test.js +206 -0
- package/clis/douyin/publish.js +341 -0
- package/clis/douyin/publish.test.js +38 -0
- package/clis/douyin/search.js +308 -0
- package/clis/douyin/search.test.js +307 -0
- package/clis/douyin/stats.js +28 -0
- package/clis/douyin/stats.test.js +22 -0
- package/clis/douyin/update.js +32 -0
- package/clis/douyin/update.test.js +11 -0
- package/clis/douyin/user-videos.js +82 -0
- package/clis/douyin/user-videos.test.js +151 -0
- package/clis/douyin/videos.js +52 -0
- package/clis/douyin/videos.test.js +54 -0
- package/clis/duckduckgo/search.js +131 -0
- package/clis/duckduckgo/search.test.js +128 -0
- package/clis/duckduckgo/suggest.js +45 -0
- package/clis/duckduckgo/suggest.test.js +66 -0
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +53 -0
- package/clis/eastmoney/convertible.js +74 -0
- package/clis/eastmoney/etf.js +66 -0
- package/clis/eastmoney/holders.js +79 -0
- package/clis/eastmoney/hot-rank.js +51 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +97 -0
- package/clis/eastmoney/kline.js +88 -0
- package/clis/eastmoney/kuaixun.js +55 -0
- package/clis/eastmoney/longhu.js +68 -0
- package/clis/eastmoney/money-flow.js +79 -0
- package/clis/eastmoney/northbound.js +58 -0
- package/clis/eastmoney/quote.js +108 -0
- package/clis/eastmoney/rank.js +95 -0
- package/clis/eastmoney/sectors.js +77 -0
- package/clis/endoflife/endoflife.test.js +51 -0
- package/clis/endoflife/product.js +55 -0
- package/clis/endoflife/utils.js +89 -0
- package/clis/facebook/__fixtures__/notifications-page.html +13 -0
- package/clis/facebook/add-friend.js +44 -0
- package/clis/facebook/auth.js +43 -0
- package/clis/facebook/events.js +41 -0
- package/clis/facebook/feed.js +305 -0
- package/clis/facebook/feed.test.js +169 -0
- package/clis/facebook/friends.js +39 -0
- package/clis/facebook/groups.js +47 -0
- package/clis/facebook/join-group.js +45 -0
- package/clis/facebook/marketplace-inbox.js +84 -0
- package/clis/facebook/marketplace-listings.js +84 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/facebook/memories.js +36 -0
- package/clis/facebook/notifications.js +333 -0
- package/clis/facebook/notifications.test.js +458 -0
- package/clis/facebook/profile.js +38 -0
- package/clis/facebook/search.js +39 -0
- package/clis/facebook/search.test.js +55 -0
- package/clis/flathub/app.js +71 -0
- package/clis/flathub/flathub.test.js +90 -0
- package/clis/flathub/search.js +80 -0
- package/clis/flathub/utils.js +114 -0
- package/clis/flomo/auth.js +55 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/gemini/ask.js +51 -0
- package/clis/gemini/ask.test.js +100 -0
- package/clis/gemini/auth.js +48 -0
- package/clis/gemini/commands.test.js +212 -0
- package/clis/gemini/deep-research-result.js +96 -0
- package/clis/gemini/deep-research-result.test.js +155 -0
- package/clis/gemini/deep-research.js +103 -0
- package/clis/gemini/deep-research.test.js +181 -0
- package/clis/gemini/detail.js +82 -0
- package/clis/gemini/history.js +70 -0
- package/clis/gemini/image.js +111 -0
- package/clis/gemini/new.js +22 -0
- package/clis/gemini/read.js +36 -0
- package/clis/gemini/reply-state.test.js +671 -0
- package/clis/gemini/status.js +32 -0
- package/clis/gemini/utils.js +2056 -0
- package/clis/gemini/utils.test.js +392 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/gitee/auth.js +41 -0
- package/clis/gitee/index.js +3 -0
- package/clis/gitee/search.js +137 -0
- package/clis/gitee/search.test.js +65 -0
- package/clis/gitee/trending.js +568 -0
- package/clis/gitee/user.js +200 -0
- package/clis/gitee/user.test.js +63 -0
- package/clis/github/auth.js +44 -0
- package/clis/github-trending/repos.js +168 -0
- package/clis/github-trending/repos.test.js +149 -0
- package/clis/google/news.js +59 -0
- package/clis/google/search.js +138 -0
- package/clis/google/suggest.js +35 -0
- package/clis/google/trends.js +39 -0
- package/clis/google/utils.js +23 -0
- package/clis/google/utils.test.js +75 -0
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +73 -0
- package/clis/google-scholar/search.test.js +71 -0
- package/clis/goproxy/goproxy.test.js +103 -0
- package/clis/goproxy/module.js +47 -0
- package/clis/goproxy/utils.js +165 -0
- package/clis/goproxy/versions.js +59 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/__fixtures__/recent.html +16 -0
- package/clis/gov-policy/__fixtures__/search.html +41 -0
- package/clis/gov-policy/gov-policy.test.js +224 -0
- package/clis/gov-policy/recent.js +90 -0
- package/clis/gov-policy/search.js +91 -0
- package/clis/gov-policy/utils.js +54 -0
- package/clis/grok/ask.js +77 -0
- package/clis/grok/ask.test.js +29 -0
- package/clis/grok/auth.js +52 -0
- package/clis/grok/delete.js +54 -0
- package/clis/grok/detail.js +60 -0
- package/clis/grok/export-all.js +409 -0
- package/clis/grok/export-utils.js +94 -0
- package/clis/grok/export.js +189 -0
- package/clis/grok/export.test.js +210 -0
- package/clis/grok/history.js +48 -0
- package/clis/grok/image.js +343 -0
- package/clis/grok/image.test.ts +127 -0
- package/clis/grok/new.js +20 -0
- package/clis/grok/pin.js +68 -0
- package/clis/grok/read.js +39 -0
- package/clis/grok/send.js +50 -0
- package/clis/grok/status.js +41 -0
- package/clis/grok/utils.js +640 -0
- package/clis/grok/utils.test.js +141 -0
- package/clis/guazi/__fixtures__/detail.html +7 -0
- package/clis/guazi/__fixtures__/list.html +5 -0
- package/clis/guazi/browse.js +95 -0
- package/clis/guazi/car.js +110 -0
- package/clis/guazi/guazi.test.js +126 -0
- package/clis/guazi/utils.js +133 -0
- package/clis/hackernews/ask.js +31 -0
- package/clis/hackernews/best.js +31 -0
- package/clis/hackernews/hackernews.test.js +132 -0
- package/clis/hackernews/jobs.js +29 -0
- package/clis/hackernews/new.js +31 -0
- package/clis/hackernews/read.js +188 -0
- package/clis/hackernews/search.js +38 -0
- package/clis/hackernews/show.js +31 -0
- package/clis/hackernews/top.js +31 -0
- package/clis/hackernews/user.js +23 -0
- package/clis/hf/auth.js +41 -0
- package/clis/hf/datasets.js +88 -0
- package/clis/hf/hf.test.js +16 -0
- package/clis/hf/models.js +91 -0
- package/clis/hf/paper.js +79 -0
- package/clis/hf/spaces.js +101 -0
- package/clis/hf/top.js +121 -0
- package/clis/homebrew/cask.js +39 -0
- package/clis/homebrew/formula.js +41 -0
- package/clis/homebrew/popular.js +54 -0
- package/clis/homebrew/utils.js +100 -0
- package/clis/huodongxing/events.js +322 -0
- package/clis/huodongxing/events.test.js +296 -0
- package/clis/hupu/__fixtures__/hot-home.html +64 -0
- package/clis/hupu/auth.js +47 -0
- package/clis/hupu/detail.js +72 -0
- package/clis/hupu/hot.js +163 -0
- package/clis/hupu/hot.test.js +224 -0
- package/clis/hupu/like.js +76 -0
- package/clis/hupu/mentions.js +160 -0
- package/clis/hupu/reply.js +72 -0
- package/clis/hupu/search.js +60 -0
- package/clis/hupu/unlike.js +76 -0
- package/clis/hupu/utils.js +319 -0
- package/clis/imdb/person.js +204 -0
- package/clis/imdb/reviews.js +89 -0
- package/clis/imdb/search.js +162 -0
- package/clis/imdb/title.js +94 -0
- package/clis/imdb/top.js +54 -0
- package/clis/imdb/trending.js +53 -0
- package/clis/imdb/utils.js +285 -0
- package/clis/imdb/utils.test.js +88 -0
- package/clis/indeed/indeed.test.js +375 -0
- package/clis/indeed/job.js +86 -0
- package/clis/indeed/search.js +110 -0
- package/clis/indeed/utils.js +152 -0
- package/clis/instagram/_shared/private-publish.js +1030 -0
- package/clis/instagram/_shared/private-publish.test.js +705 -0
- package/clis/instagram/_shared/protocol-capture.js +282 -0
- package/clis/instagram/_shared/protocol-capture.test.js +114 -0
- package/clis/instagram/_shared/runtime-info.js +81 -0
- package/clis/instagram/auth.js +57 -0
- package/clis/instagram/collection-create.js +58 -0
- package/clis/instagram/collection-delete.js +92 -0
- package/clis/instagram/comment.js +48 -0
- package/clis/instagram/download.js +226 -0
- package/clis/instagram/download.test.js +118 -0
- package/clis/instagram/explore.js +42 -0
- package/clis/instagram/follow.js +44 -0
- package/clis/instagram/followers.js +46 -0
- package/clis/instagram/following.js +90 -0
- package/clis/instagram/instagram.test.js +381 -0
- package/clis/instagram/like.js +46 -0
- package/clis/instagram/note.js +223 -0
- package/clis/instagram/note.test.js +53 -0
- package/clis/instagram/post.js +1497 -0
- package/clis/instagram/post.test.js +1647 -0
- package/clis/instagram/profile.js +40 -0
- package/clis/instagram/reel.js +827 -0
- package/clis/instagram/reel.test.js +167 -0
- package/clis/instagram/save.js +46 -0
- package/clis/instagram/saved.js +53 -0
- package/clis/instagram/search.js +39 -0
- package/clis/instagram/story.js +116 -0
- package/clis/instagram/story.test.js +134 -0
- package/clis/instagram/unfollow.js +41 -0
- package/clis/instagram/unlike.js +46 -0
- package/clis/instagram/unsave.js +46 -0
- package/clis/instagram/user.js +49 -0
- package/clis/jd/add-cart.js +72 -0
- package/clis/jd/auth.js +46 -0
- package/clis/jd/cart.js +80 -0
- package/clis/jd/commands.test.js +41 -0
- package/clis/jd/detail.js +63 -0
- package/clis/jd/item.js +730 -0
- package/clis/jd/item.test.js +354 -0
- package/clis/jd/item.test.ts +517 -0
- package/clis/jd/reviews.js +55 -0
- package/clis/jd/search.js +66 -0
- package/clis/jianyu/auth.js +62 -0
- package/clis/jianyu/detail.js +21 -0
- package/clis/jianyu/search.js +634 -0
- package/clis/jianyu/search.test.js +153 -0
- package/clis/jianyu/shared/china-bid-search.js +165 -0
- package/clis/jianyu/shared/procurement-contract.js +324 -0
- package/clis/jianyu/shared/procurement-contract.test.js +72 -0
- package/clis/jianyu/shared/procurement-detail.js +107 -0
- package/clis/jianyu/shared/procurement-detail.test.js +84 -0
- package/clis/jike/auth.js +48 -0
- package/clis/jike/comment.js +107 -0
- package/clis/jike/create.js +106 -0
- package/clis/jike/feed.js +68 -0
- package/clis/jike/like.js +61 -0
- package/clis/jike/notifications.js +169 -0
- package/clis/jike/post.js +72 -0
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/repost.js +103 -0
- package/clis/jike/search.js +68 -0
- package/clis/jike/topic.js +65 -0
- package/clis/jike/user.js +65 -0
- package/clis/jike/utils.js +25 -0
- package/clis/jimeng/auth.js +46 -0
- package/clis/jimeng/generate.js +84 -0
- package/clis/jimeng/history.js +48 -0
- package/clis/jimeng/new.js +44 -0
- package/clis/jimeng/workspaces.js +42 -0
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/juejin/hot.js +37 -0
- package/clis/juejin/juejin.test.js +241 -0
- package/clis/juejin/recommend.js +61 -0
- package/clis/juejin/utils.js +199 -0
- package/clis/ke/auth.js +46 -0
- package/clis/ke/chengjiao.js +78 -0
- package/clis/ke/ershoufang.js +101 -0
- package/clis/ke/utils.js +104 -0
- package/clis/ke/xiaoqu.js +78 -0
- package/clis/ke/zufang.js +95 -0
- package/clis/kimi/_utils.js +110 -0
- package/clis/kimi/audit-extras.js +268 -0
- package/clis/kimi/auth.js +57 -0
- package/clis/kimi/chat.js +470 -0
- package/clis/kimi/kimi.test.js +187 -0
- package/clis/kimi/storage.js +169 -0
- package/clis/kimi/ui.js +314 -0
- package/clis/kimi/usage.js +144 -0
- package/clis/lesswrong/_helpers.js +75 -0
- package/clis/lesswrong/comments.js +65 -0
- package/clis/lesswrong/curated.js +31 -0
- package/clis/lesswrong/frontpage.js +31 -0
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +31 -0
- package/clis/lesswrong/read.js +46 -0
- package/clis/lesswrong/sequences.js +28 -0
- package/clis/lesswrong/shortform.js +31 -0
- package/clis/lesswrong/tag.js +46 -0
- package/clis/lesswrong/tags.js +28 -0
- package/clis/lesswrong/top-month.js +31 -0
- package/clis/lesswrong/top-week.js +31 -0
- package/clis/lesswrong/top-year.js +31 -0
- package/clis/lesswrong/top.js +31 -0
- package/clis/lesswrong/user-posts.js +42 -0
- package/clis/lesswrong/user.js +45 -0
- package/clis/lichess/lichess.test.js +85 -0
- package/clis/lichess/top.js +46 -0
- package/clis/lichess/user.js +91 -0
- package/clis/lichess/utils.js +97 -0
- package/clis/linkedin/auth.js +62 -0
- package/clis/linkedin/connect.js +524 -0
- package/clis/linkedin/connect.test.js +328 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/search.js +462 -0
- package/clis/linkedin/search.test.js +222 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +511 -0
- package/clis/linkedin/timeline.test.js +81 -0
- package/clis/linkedin-learning/auth.js +62 -0
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/linux-do/auth.js +55 -0
- package/clis/linux-do/categories.js +50 -0
- package/clis/linux-do/feed.js +395 -0
- package/clis/linux-do/feed.test.js +153 -0
- package/clis/linux-do/search.js +29 -0
- package/clis/linux-do/tags.js +28 -0
- package/clis/linux-do/topic-content.js +155 -0
- package/clis/linux-do/topic-content.test.js +59 -0
- package/clis/linux-do/topic.js +53 -0
- package/clis/linux-do/user-posts.js +57 -0
- package/clis/linux-do/user-topics.js +37 -0
- package/clis/lobsters/active.js +29 -0
- package/clis/lobsters/domain.js +92 -0
- package/clis/lobsters/hot.js +29 -0
- package/clis/lobsters/lobsters.test.js +169 -0
- package/clis/lobsters/newest.js +29 -0
- package/clis/lobsters/read.js +196 -0
- package/clis/lobsters/tag.js +35 -0
- package/clis/maimai/auth.js +47 -0
- package/clis/maimai/search-talents.js +192 -0
- package/clis/maimai/search-talents.test.js +29 -0
- package/clis/manus/_utils.js +133 -0
- package/clis/manus/auth.js +54 -0
- package/clis/manus/connectors.js +40 -0
- package/clis/manus/credits.js +39 -0
- package/clis/manus/list.js +66 -0
- package/clis/manus/manus.test.js +382 -0
- package/clis/manus/read.js +63 -0
- package/clis/manus/skills.js +61 -0
- package/clis/manus/status.js +43 -0
- package/clis/maven/artifact.js +49 -0
- package/clis/maven/search.js +51 -0
- package/clis/maven/utils.js +110 -0
- package/clis/mdn/search.js +97 -0
- package/clis/medium/feed.js +16 -0
- package/clis/medium/search.js +16 -0
- package/clis/medium/tag.js +135 -0
- package/clis/medium/user.js +16 -0
- package/clis/medium/utils.js +79 -0
- package/clis/mubu/doc.js +41 -0
- package/clis/mubu/docs.js +44 -0
- package/clis/mubu/notes.js +245 -0
- package/clis/mubu/recent.js +28 -0
- package/clis/mubu/search.js +63 -0
- package/clis/mubu/utils.js +304 -0
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/auth.js +55 -0
- package/clis/notebooklm/compat.test.js +16 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/current.js +28 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/get.js +37 -0
- package/clis/notebooklm/history.js +25 -0
- package/clis/notebooklm/history.test.js +58 -0
- package/clis/notebooklm/list.js +36 -0
- package/clis/notebooklm/note-list.js +28 -0
- package/clis/notebooklm/note-list.test.js +56 -0
- package/clis/notebooklm/notes-get.js +47 -0
- package/clis/notebooklm/notes-get.test.js +72 -0
- package/clis/notebooklm/open.js +42 -0
- package/clis/notebooklm/open.test.js +63 -0
- package/clis/notebooklm/rpc.js +203 -0
- package/clis/notebooklm/rpc.test.js +131 -0
- package/clis/notebooklm/shared.js +3 -0
- package/clis/notebooklm/source-fulltext.js +44 -0
- package/clis/notebooklm/source-fulltext.test.js +106 -0
- package/clis/notebooklm/source-get.js +40 -0
- package/clis/notebooklm/source-get.test.js +84 -0
- package/clis/notebooklm/source-guide.js +44 -0
- package/clis/notebooklm/source-guide.test.js +104 -0
- package/clis/notebooklm/source-list.js +30 -0
- package/clis/notebooklm/status.js +31 -0
- package/clis/notebooklm/summary.js +30 -0
- package/clis/notebooklm/summary.test.js +78 -0
- package/clis/notebooklm/utils.js +814 -0
- package/clis/notebooklm/utils.test.js +449 -0
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/nowcoder/auth.js +65 -0
- package/clis/nowcoder/companies.js +24 -0
- package/clis/nowcoder/creators.js +28 -0
- package/clis/nowcoder/detail.js +62 -0
- package/clis/nowcoder/experience.js +37 -0
- package/clis/nowcoder/hot.js +25 -0
- package/clis/nowcoder/jobs.js +22 -0
- package/clis/nowcoder/notifications.js +30 -0
- package/clis/nowcoder/papers.js +41 -0
- package/clis/nowcoder/practice.js +38 -0
- package/clis/nowcoder/recommend.js +31 -0
- package/clis/nowcoder/referral.js +40 -0
- package/clis/nowcoder/salary.js +41 -0
- package/clis/nowcoder/search.js +50 -0
- package/clis/nowcoder/suggest.js +34 -0
- package/clis/nowcoder/topics.js +28 -0
- package/clis/nowcoder/trending.js +26 -0
- package/clis/npm/downloads.js +59 -0
- package/clis/npm/package.js +70 -0
- package/clis/npm/search.js +49 -0
- package/clis/npm/utils.js +76 -0
- package/clis/nuget/nuget.test.js +111 -0
- package/clis/nuget/package.js +101 -0
- package/clis/nuget/search.js +69 -0
- package/clis/nuget/utils.js +87 -0
- package/clis/nvd/cve.js +121 -0
- package/clis/oeis/oeis.test.js +88 -0
- package/clis/oeis/search.js +63 -0
- package/clis/oeis/sequence.js +71 -0
- package/clis/oeis/utils.js +88 -0
- package/clis/ones/common.js +144 -0
- package/clis/ones/enrich-tasks.js +37 -0
- package/clis/ones/login.js +81 -0
- package/clis/ones/logout.js +18 -0
- package/clis/ones/me.js +31 -0
- package/clis/ones/my-tasks.js +121 -0
- package/clis/ones/resolve-labels.js +64 -0
- package/clis/ones/task-helpers.js +212 -0
- package/clis/ones/task-helpers.test.js +12 -0
- package/clis/ones/task.js +67 -0
- package/clis/ones/tasks.js +80 -0
- package/clis/ones/token-info.js +43 -0
- package/clis/ones/worklog.js +268 -0
- package/clis/ones/worklog.test.js +20 -0
- package/clis/openalex/search.js +69 -0
- package/clis/openalex/utils.js +160 -0
- package/clis/openalex/work.js +65 -0
- package/clis/openfda/drug-label.js +74 -0
- package/clis/openfda/food-recall.js +65 -0
- package/clis/openfda/openfda.test.js +114 -0
- package/clis/openfda/utils.js +67 -0
- package/clis/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +427 -0
- package/clis/openreview/paper.js +43 -0
- package/clis/openreview/reviews.js +131 -0
- package/clis/openreview/search.js +46 -0
- package/clis/openreview/utils.js +172 -0
- package/clis/openreview/venue.js +63 -0
- package/clis/osv/osv.test.js +97 -0
- package/clis/osv/query.js +72 -0
- package/clis/osv/utils.js +169 -0
- package/clis/osv/vulnerability.js +54 -0
- package/clis/packagist/package.js +49 -0
- package/clis/packagist/search.js +43 -0
- package/clis/packagist/utils.js +113 -0
- package/clis/paperreview/commands.test.js +243 -0
- package/clis/paperreview/feedback.js +53 -0
- package/clis/paperreview/review.js +38 -0
- package/clis/paperreview/submit.js +86 -0
- package/clis/paperreview/utils.js +197 -0
- package/clis/paperreview/utils.test.js +49 -0
- package/clis/pixiv/auth.js +64 -0
- package/clis/pixiv/detail.js +66 -0
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/download.js +82 -0
- package/clis/pixiv/download.test.js +97 -0
- package/clis/pixiv/illusts.js +66 -0
- package/clis/pixiv/illusts.test.js +99 -0
- package/clis/pixiv/ranking.js +60 -0
- package/clis/pixiv/search.js +44 -0
- package/clis/pixiv/search.test.js +83 -0
- package/clis/pixiv/user.js +59 -0
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +98 -0
- package/clis/powerchina/auth.js +52 -0
- package/clis/powerchina/search.js +251 -0
- package/clis/powerchina/search.test.js +93 -0
- package/clis/producthunt/browse.js +100 -0
- package/clis/producthunt/hot.js +111 -0
- package/clis/producthunt/posts.js +29 -0
- package/clis/producthunt/today.js +36 -0
- package/clis/producthunt/utils.js +99 -0
- package/clis/producthunt/utils.test.js +64 -0
- package/clis/pubmed/article.js +55 -0
- package/clis/pubmed/author.js +64 -0
- package/clis/pubmed/citations.js +36 -0
- package/clis/pubmed/clinical-trial.js +59 -0
- package/clis/pubmed/journal.js +59 -0
- package/clis/pubmed/mesh.js +47 -0
- package/clis/pubmed/pubmed.test.js +464 -0
- package/clis/pubmed/related.js +45 -0
- package/clis/pubmed/review.js +58 -0
- package/clis/pubmed/search.js +75 -0
- package/clis/pubmed/utils.js +325 -0
- package/clis/pypi/downloads.js +66 -0
- package/clis/pypi/package.js +79 -0
- package/clis/pypi/utils.js +55 -0
- package/clis/qoder/_utils.js +191 -0
- package/clis/qoder/composer.js +45 -0
- package/clis/qoder/history.js +49 -0
- package/clis/qoder/qoder.test.js +125 -0
- package/clis/qoder/quest.js +147 -0
- package/clis/qoder/read.js +29 -0
- package/clis/qoder/status.js +21 -0
- package/clis/qoder/ui.js +332 -0
- package/clis/quark/auth.js +47 -0
- package/clis/quark/ls.js +64 -0
- package/clis/quark/mkdir.js +37 -0
- package/clis/quark/mv.js +54 -0
- package/clis/quark/rename.js +27 -0
- package/clis/quark/rm.js +25 -0
- package/clis/quark/save.js +81 -0
- package/clis/quark/share-tree.js +46 -0
- package/clis/quark/utils.js +146 -0
- package/clis/quark/utils.test.js +58 -0
- package/clis/qwen/ask.js +104 -0
- package/clis/qwen/ask.test.js +36 -0
- package/clis/qwen/auth.js +56 -0
- package/clis/qwen/detail.js +62 -0
- package/clis/qwen/history.js +61 -0
- package/clis/qwen/image.js +179 -0
- package/clis/qwen/new.js +23 -0
- package/clis/qwen/read.js +41 -0
- package/clis/qwen/send.js +55 -0
- package/clis/qwen/status.js +37 -0
- package/clis/qwen/utils.js +461 -0
- package/clis/qwen/utils.test.js +128 -0
- package/clis/reddit/auth.js +52 -0
- package/clis/reddit/comment.js +58 -0
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +70 -0
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +146 -0
- package/clis/reddit/home.test.js +170 -0
- package/clis/reddit/hot.js +81 -0
- package/clis/reddit/hot.test.js +32 -0
- package/clis/reddit/popular.js +79 -0
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/read.js +588 -0
- package/clis/reddit/read.test.js +402 -0
- package/clis/reddit/reply.js +182 -0
- package/clis/reddit/reply.test.js +89 -0
- package/clis/reddit/save.js +52 -0
- package/clis/reddit/saved.js +50 -0
- package/clis/reddit/search.js +102 -0
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit-info.js +116 -0
- package/clis/reddit/subreddit-info.test.js +163 -0
- package/clis/reddit/subreddit.js +97 -0
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribe.js +51 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvote.js +65 -0
- package/clis/reddit/upvoted.js +50 -0
- package/clis/reddit/user-comments.js +45 -0
- package/clis/reddit/user-posts.js +43 -0
- package/clis/reddit/user.js +38 -0
- package/clis/reddit/whoami.js +83 -0
- package/clis/reddit/whoami.test.js +105 -0
- package/clis/rednote/auth.js +52 -0
- package/clis/rednote/comments.js +85 -0
- package/clis/rednote/download.js +59 -0
- package/clis/rednote/feed.js +27 -0
- package/clis/rednote/navigation.test.js +26 -0
- package/clis/rednote/note.js +68 -0
- package/clis/rednote/notifications.js +139 -0
- package/clis/rednote/rednote.test.js +240 -0
- package/clis/rednote/search.js +107 -0
- package/clis/rednote/user.js +55 -0
- package/clis/rest-countries/country.js +65 -0
- package/clis/rest-countries/region.js +64 -0
- package/clis/rest-countries/rest-countries.test.js +83 -0
- package/clis/rest-countries/utils.js +126 -0
- package/clis/reuters/article-detail.js +53 -0
- package/clis/reuters/auth.js +67 -0
- package/clis/reuters/reuters.test.js +299 -0
- package/clis/reuters/search.js +63 -0
- package/clis/reuters/utils.js +159 -0
- package/clis/rfc/rfc.js +52 -0
- package/clis/rfc/rfc.test.js +74 -0
- package/clis/rfc/utils.js +72 -0
- package/clis/rubygems/gem.js +42 -0
- package/clis/rubygems/search.js +47 -0
- package/clis/rubygems/utils.js +86 -0
- package/clis/semanticscholar/citations.js +61 -0
- package/clis/semanticscholar/paper.js +52 -0
- package/clis/semanticscholar/recommendations.js +47 -0
- package/clis/semanticscholar/search.js +48 -0
- package/clis/semanticscholar/semanticscholar.test.js +279 -0
- package/clis/semanticscholar/utils.js +196 -0
- package/clis/sinablog/article.js +15 -0
- package/clis/sinablog/hot.js +15 -0
- package/clis/sinablog/search.js +52 -0
- package/clis/sinablog/user.js +16 -0
- package/clis/sinablog/utils.js +186 -0
- package/clis/sinafinance/news.js +62 -0
- package/clis/sinafinance/rolling-news.js +41 -0
- package/clis/sinafinance/stock-rank.js +66 -0
- package/clis/sinafinance/stock.js +121 -0
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/slock/api-base-canary.test.js +62 -0
- package/clis/slock/attachment-download.js +72 -0
- package/clis/slock/attachment-download.test.js +102 -0
- package/clis/slock/attachment-upload.js +112 -0
- package/clis/slock/attachment-upload.test.js +131 -0
- package/clis/slock/attachment-url.js +53 -0
- package/clis/slock/attachment-url.test.js +41 -0
- package/clis/slock/auth-verify.js +19 -0
- package/clis/slock/bookmark-add.js +43 -0
- package/clis/slock/bookmark-add.test.js +38 -0
- package/clis/slock/bookmark-list.js +50 -0
- package/clis/slock/bookmark-list.test.js +47 -0
- package/clis/slock/bookmark-remove.js +42 -0
- package/clis/slock/bookmark-remove.test.js +24 -0
- package/clis/slock/channel-action.js +63 -0
- package/clis/slock/channel-action.test.js +65 -0
- package/clis/slock/channel-archive.js +9 -0
- package/clis/slock/channel-create.js +48 -0
- package/clis/slock/channel-create.test.js +40 -0
- package/clis/slock/channel-files.js +51 -0
- package/clis/slock/channel-files.test.js +32 -0
- package/clis/slock/channel-info.js +42 -0
- package/clis/slock/channel-info.test.js +25 -0
- package/clis/slock/channel-join.js +9 -0
- package/clis/slock/channel-leave.js +9 -0
- package/clis/slock/channel-list.js +40 -0
- package/clis/slock/channel-list.test.js +37 -0
- package/clis/slock/channel-mark.js +71 -0
- package/clis/slock/channel-mark.test.js +49 -0
- package/clis/slock/channel-members.js +64 -0
- package/clis/slock/channel-members.test.js +44 -0
- package/clis/slock/channel-unarchive.js +11 -0
- package/clis/slock/cross-command.test.js +21 -0
- package/clis/slock/dm-list.js +41 -0
- package/clis/slock/dm-list.test.js +26 -0
- package/clis/slock/error-detail-canary.test.js +144 -0
- package/clis/slock/errors.js +30 -0
- package/clis/slock/errors.test.js +29 -0
- package/clis/slock/in-page.js +213 -0
- package/clis/slock/in-page.test.js +362 -0
- package/clis/slock/inbox-done.js +40 -0
- package/clis/slock/inbox-done.test.js +21 -0
- package/clis/slock/inbox-read-all.js +32 -0
- package/clis/slock/inbox-read-all.test.js +20 -0
- package/clis/slock/inbox.js +74 -0
- package/clis/slock/inbox.test.js +46 -0
- package/clis/slock/login.js +3 -0
- package/clis/slock/message-read.js +168 -0
- package/clis/slock/message-read.test.js +83 -0
- package/clis/slock/message-search.js +72 -0
- package/clis/slock/message-search.test.js +61 -0
- package/clis/slock/message-send.js +149 -0
- package/clis/slock/message-send.test.js +127 -0
- package/clis/slock/reaction-add.js +42 -0
- package/clis/slock/reaction-add.test.js +36 -0
- package/clis/slock/reaction-remove.js +42 -0
- package/clis/slock/reaction-remove.test.js +29 -0
- package/clis/slock/resolve.js +74 -0
- package/clis/slock/resolve.test.js +102 -0
- package/clis/slock/server-list.js +38 -0
- package/clis/slock/server-list.test.js +24 -0
- package/clis/slock/server-override-canary.test.js +87 -0
- package/clis/slock/server-use.js +53 -0
- package/clis/slock/server-use.test.js +48 -0
- package/clis/slock/shared.js +14 -0
- package/clis/slock/short-id-canary.test.js +86 -0
- package/clis/slock/site-session-canary.test.js +49 -0
- package/clis/slock/task-claim.js +72 -0
- package/clis/slock/task-claim.test.js +81 -0
- package/clis/slock/task-convert.js +106 -0
- package/clis/slock/task-convert.test.js +82 -0
- package/clis/slock/task-create.js +82 -0
- package/clis/slock/task-create.test.js +65 -0
- package/clis/slock/task-delete.js +55 -0
- package/clis/slock/task-delete.test.js +52 -0
- package/clis/slock/task-get.js +59 -0
- package/clis/slock/task-get.test.js +45 -0
- package/clis/slock/task-list-server.js +63 -0
- package/clis/slock/task-list-server.test.js +48 -0
- package/clis/slock/task-list.js +70 -0
- package/clis/slock/task-list.test.js +59 -0
- package/clis/slock/task-status.js +93 -0
- package/clis/slock/task-status.test.js +76 -0
- package/clis/slock/task-unclaim.js +66 -0
- package/clis/slock/task-unclaim.test.js +49 -0
- package/clis/slock/thread-done.js +9 -0
- package/clis/slock/thread-follow.js +46 -0
- package/clis/slock/thread-follow.test.js +34 -0
- package/clis/slock/thread-list.js +44 -0
- package/clis/slock/thread-list.test.js +24 -0
- package/clis/slock/thread-state.js +45 -0
- package/clis/slock/thread-state.test.js +38 -0
- package/clis/slock/thread-undone.js +9 -0
- package/clis/slock/thread-unfollow.js +9 -0
- package/clis/slock/unread-summary.js +49 -0
- package/clis/slock/unread-summary.test.js +31 -0
- package/clis/slock/whoami.js +12 -0
- package/clis/slock/whoami.test.js +23 -0
- package/clis/smzdm/search.js +149 -0
- package/clis/smzdm/search.test.js +114 -0
- package/clis/spotify/spotify.js +327 -0
- package/clis/spotify/utils.js +66 -0
- package/clis/spotify/utils.test.js +67 -0
- package/clis/stackoverflow/bounties.js +35 -0
- package/clis/stackoverflow/hot.js +32 -0
- package/clis/stackoverflow/read.js +314 -0
- package/clis/stackoverflow/related.js +66 -0
- package/clis/stackoverflow/search.js +35 -0
- package/clis/stackoverflow/stackoverflow.test.js +404 -0
- package/clis/stackoverflow/tag.js +60 -0
- package/clis/stackoverflow/unanswered.js +33 -0
- package/clis/stackoverflow/user.js +50 -0
- package/clis/stackoverflow/utils.js +118 -0
- package/clis/steam/app.js +67 -0
- package/clis/steam/search.js +58 -0
- package/clis/steam/steam.test.js +46 -0
- package/clis/steam/top-sellers.js +26 -0
- package/clis/steam/utils.js +107 -0
- package/clis/substack/feed.js +16 -0
- package/clis/substack/publication.js +16 -0
- package/clis/substack/search.js +79 -0
- package/clis/substack/utils.js +136 -0
- package/clis/substack/utils.test.js +44 -0
- package/clis/suno/auth.js +63 -0
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -0
- package/clis/taobao/add-cart.js +150 -0
- package/clis/taobao/auth.js +58 -0
- package/clis/taobao/cart.js +96 -0
- package/clis/taobao/commands.test.js +41 -0
- package/clis/taobao/detail.js +71 -0
- package/clis/taobao/reviews.js +77 -0
- package/clis/taobao/search.js +97 -0
- package/clis/tdx/hot-rank.js +48 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/test-utils.js +61 -0
- package/clis/ths/hot-rank.js +50 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/tieba/commands.test.js +79 -0
- package/clis/tieba/hot.js +48 -0
- package/clis/tieba/posts.js +86 -0
- package/clis/tieba/read.js +141 -0
- package/clis/tieba/search.js +109 -0
- package/clis/tieba/utils.js +240 -0
- package/clis/tieba/utils.test.js +290 -0
- package/clis/tiktok/auth.js +65 -0
- package/clis/tiktok/comment.js +145 -0
- package/clis/tiktok/creator-videos.js +270 -0
- package/clis/tiktok/creator-videos.test.js +113 -0
- package/clis/tiktok/explore.js +144 -0
- package/clis/tiktok/follow.js +122 -0
- package/clis/tiktok/following.js +164 -0
- package/clis/tiktok/friends.js +146 -0
- package/clis/tiktok/like.js +34 -0
- package/clis/tiktok/live.js +144 -0
- package/clis/tiktok/notifications.js +153 -0
- package/clis/tiktok/profile.js +55 -0
- package/clis/tiktok/refactor.test.js +389 -0
- package/clis/tiktok/save.js +30 -0
- package/clis/tiktok/search.js +40 -0
- package/clis/tiktok/unfollow.js +131 -0
- package/clis/tiktok/unlike.js +34 -0
- package/clis/tiktok/unsave.js +32 -0
- package/clis/tiktok/user.js +216 -0
- package/clis/tiktok/utils.js +505 -0
- package/clis/tiktok/write-refactor.test.js +370 -0
- package/clis/toutiao/articles.js +55 -0
- package/clis/toutiao/auth.js +70 -0
- package/clis/toutiao/hot.js +63 -0
- package/clis/toutiao/toutiao.test.js +378 -0
- package/clis/toutiao/utils.js +161 -0
- package/clis/trae-cn/activity.js +22 -0
- package/clis/trae-cn/approve.js +52 -0
- package/clis/trae-cn/ask.js +99 -0
- package/clis/trae-cn/dump.js +5 -0
- package/clis/trae-cn/export.js +40 -0
- package/clis/trae-cn/model.js +22 -0
- package/clis/trae-cn/new.js +63 -0
- package/clis/trae-cn/read.js +23 -0
- package/clis/trae-cn/screenshot.js +5 -0
- package/clis/trae-cn/select-model.js +25 -0
- package/clis/trae-cn/send.js +23 -0
- package/clis/trae-cn/setup.js +75 -0
- package/clis/trae-cn/status.js +27 -0
- package/clis/trae-cn/targets.js +96 -0
- package/clis/trae-cn/trae-cn.test.js +412 -0
- package/clis/trae-cn/utils.js +750 -0
- package/clis/trae-cn/watch.js +119 -0
- package/clis/trae-solo/_actions.js +60 -0
- package/clis/trae-solo/_fs.js +118 -0
- package/clis/trae-solo/_state.js +111 -0
- package/clis/trae-solo/automation.js +97 -0
- package/clis/trae-solo/history.js +54 -0
- package/clis/trae-solo/mode.js +81 -0
- package/clis/trae-solo/model.js +134 -0
- package/clis/trae-solo/renderer-storage.js +170 -0
- package/clis/trae-solo/settings.js +54 -0
- package/clis/trae-solo/skill-fs.js +123 -0
- package/clis/trae-solo/skill.js +220 -0
- package/clis/trae-solo/state-fs.js +156 -0
- package/clis/trae-solo/status.js +2 -0
- package/clis/trae-solo/task-fs.js +181 -0
- package/clis/trae-solo/trae-solo.test.js +67 -0
- package/clis/trae-solo/user-rules.js +29 -0
- package/clis/trae-solo/workspaces-fs.js +113 -0
- package/clis/tvmaze/search.js +61 -0
- package/clis/tvmaze/show.js +60 -0
- package/clis/tvmaze/tvmaze.test.js +93 -0
- package/clis/tvmaze/utils.js +110 -0
- package/clis/twitter/accept.js +204 -0
- package/clis/twitter/article.js +170 -0
- package/clis/twitter/auth.js +41 -0
- package/clis/twitter/block.js +90 -0
- package/clis/twitter/bookmark-folder.js +190 -0
- package/clis/twitter/bookmark-folder.test.js +396 -0
- package/clis/twitter/bookmark-folders.js +114 -0
- package/clis/twitter/bookmark-folders.test.js +152 -0
- package/clis/twitter/bookmark.js +74 -0
- package/clis/twitter/bookmark.test.js +74 -0
- package/clis/twitter/bookmarks.js +181 -0
- package/clis/twitter/bookmarks.test.js +206 -0
- package/clis/twitter/delete.js +85 -0
- package/clis/twitter/delete.test.js +85 -0
- package/clis/twitter/device-follow.js +194 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +481 -0
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/follow-batch.js +162 -0
- package/clis/twitter/follow-batch.test.js +137 -0
- package/clis/twitter/follow.js +67 -0
- package/clis/twitter/followers.js +193 -0
- package/clis/twitter/followers.test.js +62 -0
- package/clis/twitter/following.js +274 -0
- package/clis/twitter/following.test.js +359 -0
- package/clis/twitter/hide-reply.js +87 -0
- package/clis/twitter/hide-reply.test.js +76 -0
- package/clis/twitter/like.js +80 -0
- package/clis/twitter/like.test.js +73 -0
- package/clis/twitter/likes.js +241 -0
- package/clis/twitter/likes.test.js +220 -0
- package/clis/twitter/list-add-batch.js +32 -0
- package/clis/twitter/list-add-core.js +245 -0
- package/clis/twitter/list-add.js +20 -0
- package/clis/twitter/list-add.test.js +133 -0
- package/clis/twitter/list-batch-utils.js +95 -0
- package/clis/twitter/list-batch.test.js +113 -0
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-delete.js +158 -0
- package/clis/twitter/list-delete.test.js +102 -0
- package/clis/twitter/list-remove-batch.js +32 -0
- package/clis/twitter/list-remove-core.js +291 -0
- package/clis/twitter/list-remove.js +20 -0
- package/clis/twitter/list-remove.test.js +147 -0
- package/clis/twitter/list-tweets.js +199 -0
- package/clis/twitter/list-tweets.test.js +198 -0
- package/clis/twitter/lists.js +178 -0
- package/clis/twitter/lists.test.js +253 -0
- package/clis/twitter/notifications.js +128 -0
- package/clis/twitter/post.js +316 -0
- package/clis/twitter/post.test.js +275 -0
- package/clis/twitter/profile.js +174 -0
- package/clis/twitter/profile.test.js +150 -0
- package/clis/twitter/quote.js +172 -0
- package/clis/twitter/quote.test.js +194 -0
- package/clis/twitter/reply-dm.js +183 -0
- package/clis/twitter/reply.js +248 -0
- package/clis/twitter/reply.test.js +237 -0
- package/clis/twitter/retweet.js +94 -0
- package/clis/twitter/retweet.test.js +73 -0
- package/clis/twitter/search.js +346 -0
- package/clis/twitter/search.test.js +400 -0
- package/clis/twitter/shared.js +564 -0
- package/clis/twitter/shared.test.js +1012 -0
- package/clis/twitter/thread.js +164 -0
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +211 -0
- package/clis/twitter/timeline.test.js +104 -0
- package/clis/twitter/trending.js +58 -0
- package/clis/twitter/trending.test.js +15 -0
- package/clis/twitter/tweets.js +314 -0
- package/clis/twitter/tweets.test.js +362 -0
- package/clis/twitter/unblock.js +73 -0
- package/clis/twitter/unbookmark.js +71 -0
- package/clis/twitter/unbookmark.test.js +73 -0
- package/clis/twitter/unfollow.js +73 -0
- package/clis/twitter/unlike.js +80 -0
- package/clis/twitter/unlike.test.js +75 -0
- package/clis/twitter/unretweet.js +94 -0
- package/clis/twitter/unretweet.test.js +73 -0
- package/clis/twitter/utils.js +323 -0
- package/clis/twitter/utils.test.js +169 -0
- package/clis/uisdc/news.js +105 -0
- package/clis/uisdc/news.test.js +66 -0
- package/clis/uiverse/_shared.js +427 -0
- package/clis/uiverse/_shared.test.js +62 -0
- package/clis/uiverse/code.js +49 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +73 -0
- package/clis/upwork/auth.js +53 -0
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/v2ex/auth.js +44 -0
- package/clis/v2ex/daily.js +99 -0
- package/clis/v2ex/hot.js +26 -0
- package/clis/v2ex/latest.js +26 -0
- package/clis/v2ex/me.js +100 -0
- package/clis/v2ex/member.js +28 -0
- package/clis/v2ex/node.js +39 -0
- package/clis/v2ex/nodes.js +26 -0
- package/clis/v2ex/notifications.js +72 -0
- package/clis/v2ex/replies.js +27 -0
- package/clis/v2ex/topic.js +31 -0
- package/clis/v2ex/user.js +34 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +491 -0
- package/clis/web/read.test.js +392 -0
- package/clis/wechat-channels/auth.js +62 -0
- package/clis/wechat-channels/publish.js +732 -0
- package/clis/wechat-channels/publish.test.js +144 -0
- package/clis/weibo/auth.js +53 -0
- package/clis/weibo/comments.js +53 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/envelope.test.js +85 -0
- package/clis/weibo/favorites.js +170 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/feed.js +69 -0
- package/clis/weibo/hot.js +39 -0
- package/clis/weibo/me.js +75 -0
- package/clis/weibo/post.js +75 -0
- package/clis/weibo/publish.js +306 -0
- package/clis/weibo/publish.test.js +192 -0
- package/clis/weibo/search.js +80 -0
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weibo/user.js +63 -0
- package/clis/weibo/utils.js +59 -0
- package/clis/weibo/utils.test.js +36 -0
- package/clis/weixin/create-draft.js +226 -0
- package/clis/weixin/download.js +371 -0
- package/clis/weixin/download.test.js +106 -0
- package/clis/weixin/drafts.js +66 -0
- package/clis/weixin/drafts.test.js +69 -0
- package/clis/weixin/search.js +157 -0
- package/clis/weixin/search.test.js +227 -0
- package/clis/weread/ai-outline.js +171 -0
- package/clis/weread/ai-outline.test.js +83 -0
- package/clis/weread/auth.js +59 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/book.js +233 -0
- package/clis/weread/commands.test.js +398 -0
- package/clis/weread/highlights.js +24 -0
- package/clis/weread/notebooks.js +22 -0
- package/clis/weread/notes.js +30 -0
- package/clis/weread/private-api-regression.test.js +369 -0
- package/clis/weread/ranking.js +29 -0
- package/clis/weread/search-regression.test.js +494 -0
- package/clis/weread/search.js +186 -0
- package/clis/weread/shelf.js +66 -0
- package/clis/weread/utils.js +321 -0
- package/clis/weread/utils.test.js +128 -0
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikidata/entity.js +60 -0
- package/clis/wikidata/search.js +50 -0
- package/clis/wikidata/utils.js +117 -0
- package/clis/wikidata/wikidata.test.js +83 -0
- package/clis/wikipedia/page.js +95 -0
- package/clis/wikipedia/random.js +20 -0
- package/clis/wikipedia/search.js +31 -0
- package/clis/wikipedia/summary.js +24 -0
- package/clis/wikipedia/trending.js +40 -0
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/wikipedia/utils.js +31 -0
- package/clis/wttr/current.js +63 -0
- package/clis/wttr/forecast.js +71 -0
- package/clis/wttr/utils.js +50 -0
- package/clis/wttr/wttr.test.js +84 -0
- package/clis/xianyu/auth.js +68 -0
- package/clis/xianyu/chat.js +74 -0
- package/clis/xianyu/chat.test.js +84 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/item.js +153 -0
- package/clis/xianyu/item.test.js +56 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/publish.js +485 -0
- package/clis/xianyu/publish.test.js +220 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xianyu/search.js +135 -0
- package/clis/xianyu/search.test.js +17 -0
- package/clis/xianyu/utils.js +8 -0
- package/clis/xiaoe/auth.js +55 -0
- package/clis/xiaoe/catalog.js +227 -0
- package/clis/xiaoe/content.js +175 -0
- package/clis/xiaoe/courses.js +127 -0
- package/clis/xiaoe/detail.js +36 -0
- package/clis/xiaoe/play-url.js +121 -0
- package/clis/xiaoe/xiaoe.test.js +486 -0
- package/clis/xiaohongshu/ask.js +426 -0
- package/clis/xiaohongshu/ask.test.js +271 -0
- package/clis/xiaohongshu/auth.js +53 -0
- package/clis/xiaohongshu/collection-helpers.js +283 -0
- package/clis/xiaohongshu/collection-helpers.test.js +125 -0
- package/clis/xiaohongshu/comments.js +228 -0
- package/clis/xiaohongshu/comments.test.js +320 -0
- package/clis/xiaohongshu/creator-note-detail.js +482 -0
- package/clis/xiaohongshu/creator-note-detail.test.js +451 -0
- package/clis/xiaohongshu/creator-notes-summary.js +98 -0
- package/clis/xiaohongshu/creator-notes-summary.test.js +94 -0
- package/clis/xiaohongshu/creator-notes.js +479 -0
- package/clis/xiaohongshu/creator-notes.test.js +281 -0
- package/clis/xiaohongshu/creator-profile.js +55 -0
- package/clis/xiaohongshu/creator-stats.js +76 -0
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/download.js +249 -0
- package/clis/xiaohongshu/download.test.js +339 -0
- package/clis/xiaohongshu/draft-clear.js +91 -0
- package/clis/xiaohongshu/draft-delete.js +103 -0
- package/clis/xiaohongshu/draft-open.js +43 -0
- package/clis/xiaohongshu/draft-utils.js +142 -0
- package/clis/xiaohongshu/drafts.js +28 -0
- package/clis/xiaohongshu/drafts.test.js +172 -0
- package/clis/xiaohongshu/feed.js +163 -0
- package/clis/xiaohongshu/feed.test.js +126 -0
- package/clis/xiaohongshu/follow.js +226 -0
- package/clis/xiaohongshu/follow.test.js +211 -0
- package/clis/xiaohongshu/liked.js +29 -0
- package/clis/xiaohongshu/liked.test.js +82 -0
- package/clis/xiaohongshu/navigation.test.js +38 -0
- package/clis/xiaohongshu/note-helpers.js +69 -0
- package/clis/xiaohongshu/note.js +104 -0
- package/clis/xiaohongshu/note.test.js +249 -0
- package/clis/xiaohongshu/notifications.js +46 -0
- package/clis/xiaohongshu/publish.js +1423 -0
- package/clis/xiaohongshu/publish.test.js +1141 -0
- package/clis/xiaohongshu/saved.js +29 -0
- package/clis/xiaohongshu/saved.test.js +143 -0
- package/clis/xiaohongshu/search.js +330 -0
- package/clis/xiaohongshu/search.test.js +325 -0
- package/clis/xiaohongshu/unfollow.js +285 -0
- package/clis/xiaohongshu/user-helpers.js +78 -0
- package/clis/xiaohongshu/user-helpers.test.js +160 -0
- package/clis/xiaohongshu/user.js +140 -0
- package/clis/xiaohongshu/user.test.js +141 -0
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +54 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +34 -0
- package/clis/xiaoyuzhou/podcast-episodes.js +41 -0
- package/clis/xiaoyuzhou/podcast-episodes.test.js +78 -0
- package/clis/xiaoyuzhou/podcast.js +33 -0
- package/clis/xiaoyuzhou/transcript.js +77 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +15 -0
- package/clis/xiaoyuzhou/utils.test.js +39 -0
- package/clis/xueqiu/auth.js +63 -0
- package/clis/xueqiu/comments.js +355 -0
- package/clis/xueqiu/comments.test.js +696 -0
- package/clis/xueqiu/danjuan-utils.js +126 -0
- package/clis/xueqiu/danjuan-utils.test.js +41 -0
- package/clis/xueqiu/earnings-date.js +45 -0
- package/clis/xueqiu/feed.js +39 -0
- package/clis/xueqiu/fund-holdings.js +29 -0
- package/clis/xueqiu/fund-snapshot.js +26 -0
- package/clis/xueqiu/groups.js +23 -0
- package/clis/xueqiu/hot-stock.js +32 -0
- package/clis/xueqiu/hot.js +37 -0
- package/clis/xueqiu/kline.js +44 -0
- package/clis/xueqiu/search.js +37 -0
- package/clis/xueqiu/stock.js +65 -0
- package/clis/xueqiu/utils.js +48 -0
- package/clis/xueqiu/utils.test.js +26 -0
- package/clis/xueqiu/watchlist.js +37 -0
- package/clis/yahoo/search.js +92 -0
- package/clis/yahoo/search.test.js +94 -0
- package/clis/yahoo-finance/quote.js +74 -0
- package/clis/yollomi/background.js +46 -0
- package/clis/yollomi/edit.js +57 -0
- package/clis/yollomi/face-swap.js +44 -0
- package/clis/yollomi/generate.js +101 -0
- package/clis/yollomi/models.js +34 -0
- package/clis/yollomi/object-remover.js +43 -0
- package/clis/yollomi/remove-bg.js +39 -0
- package/clis/yollomi/restore.js +39 -0
- package/clis/yollomi/try-on.js +47 -0
- package/clis/yollomi/upload.js +72 -0
- package/clis/yollomi/upscale.js +54 -0
- package/clis/yollomi/utils.js +180 -0
- package/clis/yollomi/video.js +57 -0
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/auth.js +54 -0
- package/clis/youtube/channel.js +227 -0
- package/clis/youtube/channel.test.js +283 -0
- package/clis/youtube/comments.js +96 -0
- package/clis/youtube/feed.js +121 -0
- package/clis/youtube/feed.test.js +131 -0
- package/clis/youtube/history.js +119 -0
- package/clis/youtube/like.js +67 -0
- package/clis/youtube/playlist.js +98 -0
- package/clis/youtube/search.js +100 -0
- package/clis/youtube/subscribe.js +76 -0
- package/clis/youtube/subscriptions.js +58 -0
- package/clis/youtube/transcript-group.js +226 -0
- package/clis/youtube/transcript-group.test.js +99 -0
- package/clis/youtube/transcript.js +656 -0
- package/clis/youtube/transcript.test.js +296 -0
- package/clis/youtube/unlike.js +67 -0
- package/clis/youtube/unsubscribe.js +76 -0
- package/clis/youtube/utils.js +220 -0
- package/clis/youtube/utils.test.js +84 -0
- package/clis/youtube/video.js +152 -0
- package/clis/youtube/video.test.js +111 -0
- package/clis/youtube/watch-later.js +77 -0
- package/clis/yuanbao/ask.js +362 -0
- package/clis/yuanbao/ask.test.js +124 -0
- package/clis/yuanbao/auth.js +59 -0
- package/clis/yuanbao/detail.js +65 -0
- package/clis/yuanbao/history.js +51 -0
- package/clis/yuanbao/new.js +72 -0
- package/clis/yuanbao/new.test.js +30 -0
- package/clis/yuanbao/read.js +38 -0
- package/clis/yuanbao/send.js +57 -0
- package/clis/yuanbao/shared.js +341 -0
- package/clis/yuanbao/shared.test.js +80 -0
- package/clis/yuanbao/status.js +44 -0
- package/clis/zhihu/answer-comments.js +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +216 -0
- package/clis/zhihu/answer-detail.test.js +338 -0
- package/clis/zhihu/answer.js +54 -0
- package/clis/zhihu/answer.test.js +54 -0
- package/clis/zhihu/auth.js +59 -0
- package/clis/zhihu/collection.js +199 -0
- package/clis/zhihu/collection.test.js +337 -0
- package/clis/zhihu/collections.js +128 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +55 -0
- package/clis/zhihu/comment.test.js +50 -0
- package/clis/zhihu/download.js +80 -0
- package/clis/zhihu/download.test.js +12 -0
- package/clis/zhihu/favorite.js +87 -0
- package/clis/zhihu/favorite.test.js +59 -0
- package/clis/zhihu/follow.js +50 -0
- package/clis/zhihu/follow.test.js +42 -0
- package/clis/zhihu/followers.js +40 -0
- package/clis/zhihu/followers.test.js +42 -0
- package/clis/zhihu/following.js +40 -0
- package/clis/zhihu/following.test.js +44 -0
- package/clis/zhihu/hot.js +44 -0
- package/clis/zhihu/like.js +47 -0
- package/clis/zhihu/like.test.js +41 -0
- package/clis/zhihu/paginate.js +128 -0
- package/clis/zhihu/pins.js +41 -0
- package/clis/zhihu/pins.test.js +44 -0
- package/clis/zhihu/question.js +113 -0
- package/clis/zhihu/question.test.js +251 -0
- package/clis/zhihu/recommend.js +103 -0
- package/clis/zhihu/recommend.test.js +143 -0
- package/clis/zhihu/search.js +206 -0
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/target.js +91 -0
- package/clis/zhihu/target.test.js +77 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/clis/zhihu/user-answers.js +41 -0
- package/clis/zhihu/user-answers.test.js +69 -0
- package/clis/zhihu/user-arg.js +34 -0
- package/clis/zhihu/user-articles.js +40 -0
- package/clis/zhihu/user-articles.test.js +44 -0
- package/clis/zhihu/user.js +59 -0
- package/clis/zhihu/user.test.js +73 -0
- package/clis/zhihu/write-shared.js +228 -0
- package/clis/zhihu/write-shared.test.js +176 -0
- package/clis/zlibrary/commands.test.js +65 -0
- package/clis/zlibrary/info.js +48 -0
- package/clis/zlibrary/search.js +47 -0
- package/clis/zlibrary/utils.js +136 -0
- package/clis/zsxq/auth.js +68 -0
- package/clis/zsxq/dynamics.js +48 -0
- package/clis/zsxq/groups.js +33 -0
- package/clis/zsxq/search.js +44 -0
- package/clis/zsxq/search.test.js +24 -0
- package/clis/zsxq/topic.js +50 -0
- package/clis/zsxq/topic.test.js +30 -0
- package/clis/zsxq/topics.js +26 -0
- package/clis/zsxq/topics.test.js +24 -0
- package/clis/zsxq/utils.js +233 -0
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/adapter-shadow.d.ts +11 -0
- package/dist/src/adapter-shadow.js +72 -0
- package/dist/src/adapter-shadow.test.d.ts +1 -0
- package/dist/src/adapter-shadow.test.js +49 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.d.ts +1 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/analyze.d.ts +118 -0
- package/dist/src/browser/analyze.js +405 -0
- package/dist/src/browser/analyze.test.d.ts +1 -0
- package/dist/src/browser/analyze.test.js +209 -0
- package/dist/src/browser/article-extract.d.ts +57 -0
- package/dist/src/browser/article-extract.e2e.test.d.ts +1 -0
- package/dist/src/browser/article-extract.e2e.test.js +105 -0
- package/dist/src/browser/article-extract.js +169 -0
- package/dist/src/browser/article-extract.test.d.ts +1 -0
- package/dist/src/browser/article-extract.test.js +94 -0
- package/dist/src/browser/ax-snapshot.d.ts +37 -0
- package/dist/src/browser/ax-snapshot.js +217 -0
- package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
- package/dist/src/browser/ax-snapshot.test.js +91 -0
- package/dist/src/browser/base-page.d.ts +151 -0
- package/dist/src/browser/base-page.js +1060 -0
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +803 -0
- package/dist/src/browser/bridge.d.ts +28 -0
- package/dist/src/browser/bridge.js +172 -0
- package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -0
- package/dist/src/browser/cdp.d.ts +46 -0
- package/dist/src/browser/cdp.js +536 -0
- package/dist/src/browser/cdp.test.d.ts +1 -0
- package/dist/src/browser/cdp.test.js +79 -0
- package/dist/src/browser/compound.d.ts +59 -0
- package/dist/src/browser/compound.js +112 -0
- package/dist/src/browser/compound.test.d.ts +1 -0
- package/dist/src/browser/compound.test.js +175 -0
- package/dist/src/browser/daemon-client.d.ts +133 -0
- package/dist/src/browser/daemon-client.js +154 -0
- package/dist/src/browser/daemon-client.test.d.ts +1 -0
- package/dist/src/browser/daemon-client.test.js +199 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
- package/dist/src/browser/daemon-lifecycle.js +67 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/dom-helpers.d.ts +41 -0
- package/dist/src/browser/dom-helpers.js +239 -0
- package/dist/src/browser/dom-helpers.test.d.ts +1 -0
- package/dist/src/browser/dom-helpers.test.js +106 -0
- package/dist/src/browser/dom-snapshot.d.ts +93 -0
- package/dist/src/browser/dom-snapshot.js +894 -0
- package/dist/src/browser/dom-snapshot.test.d.ts +11 -0
- package/dist/src/browser/dom-snapshot.test.js +313 -0
- package/dist/src/browser/errors.d.ts +38 -0
- package/dist/src/browser/errors.js +78 -0
- package/dist/src/browser/errors.test.d.ts +1 -0
- package/dist/src/browser/errors.test.js +60 -0
- package/dist/src/browser/extract.d.ts +69 -0
- package/dist/src/browser/extract.js +132 -0
- package/dist/src/browser/extract.test.d.ts +1 -0
- package/dist/src/browser/extract.test.js +129 -0
- package/dist/src/browser/find.d.ts +84 -0
- package/dist/src/browser/find.js +398 -0
- package/dist/src/browser/find.test.d.ts +1 -0
- package/dist/src/browser/find.test.js +180 -0
- package/dist/src/browser/html-tree.d.ts +75 -0
- package/dist/src/browser/html-tree.js +112 -0
- package/dist/src/browser/html-tree.test.d.ts +1 -0
- package/dist/src/browser/html-tree.test.js +181 -0
- package/dist/src/browser/index.d.ts +14 -0
- package/dist/src/browser/index.js +12 -0
- package/dist/src/browser/network-cache.d.ts +49 -0
- package/dist/src/browser/network-cache.js +78 -0
- package/dist/src/browser/network-cache.test.d.ts +1 -0
- package/dist/src/browser/network-cache.test.js +75 -0
- package/dist/src/browser/network-key.d.ts +22 -0
- package/dist/src/browser/network-key.js +66 -0
- package/dist/src/browser/network-key.test.d.ts +1 -0
- package/dist/src/browser/network-key.test.js +49 -0
- package/dist/src/browser/page.d.ts +83 -0
- package/dist/src/browser/page.js +453 -0
- package/dist/src/browser/page.test.d.ts +1 -0
- package/dist/src/browser/page.test.js +406 -0
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/browser/shape-filter.d.ts +52 -0
- package/dist/src/browser/shape-filter.js +101 -0
- package/dist/src/browser/shape-filter.test.d.ts +1 -0
- package/dist/src/browser/shape-filter.test.js +101 -0
- package/dist/src/browser/shape.d.ts +23 -0
- package/dist/src/browser/shape.js +95 -0
- package/dist/src/browser/shape.test.d.ts +1 -0
- package/dist/src/browser/shape.test.js +82 -0
- package/dist/src/browser/stealth.d.ts +11 -0
- package/dist/src/browser/stealth.js +359 -0
- package/dist/src/browser/stealth.test.d.ts +1 -0
- package/dist/src/browser/stealth.test.js +134 -0
- package/dist/src/browser/tabs.d.ts +13 -0
- package/dist/src/browser/tabs.js +70 -0
- package/dist/src/browser/target-errors.d.ts +39 -0
- package/dist/src/browser/target-errors.js +45 -0
- package/dist/src/browser/target-errors.test.d.ts +1 -0
- package/dist/src/browser/target-errors.test.js +94 -0
- package/dist/src/browser/target-resolver.d.ts +158 -0
- package/dist/src/browser/target-resolver.js +570 -0
- package/dist/src/browser/target-resolver.test.d.ts +1 -0
- package/dist/src/browser/target-resolver.test.js +118 -0
- package/dist/src/browser/utils.d.ts +20 -0
- package/dist/src/browser/utils.js +64 -0
- package/dist/src/browser/utils.test.d.ts +1 -0
- package/dist/src/browser/utils.test.js +29 -0
- package/dist/src/browser/verify-fixture.d.ts +65 -0
- package/dist/src/browser/verify-fixture.js +318 -0
- package/dist/src/browser/verify-fixture.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.test.js +219 -0
- package/dist/src/browser/visual-refs.d.ts +11 -0
- package/dist/src/browser/visual-refs.js +108 -0
- package/dist/src/browser.test.d.ts +1 -0
- package/dist/src/browser.test.js +287 -0
- package/dist/src/build-manifest.d.ts +103 -0
- package/dist/src/build-manifest.js +363 -0
- package/dist/src/build-manifest.test.d.ts +1 -0
- package/dist/src/build-manifest.test.js +341 -0
- package/dist/src/capabilityRouting.d.ts +19 -0
- package/dist/src/capabilityRouting.js +56 -0
- package/dist/src/capabilityRouting.test.d.ts +1 -0
- package/dist/src/capabilityRouting.test.js +63 -0
- package/dist/src/cli-argv-preprocess.d.ts +63 -0
- package/dist/src/cli-argv-preprocess.js +303 -0
- package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
- package/dist/src/cli-argv-preprocess.test.js +304 -0
- package/dist/src/cli.d.ts +79 -0
- package/dist/src/cli.js +3357 -0
- package/dist/src/cli.test.d.ts +1 -0
- package/dist/src/cli.test.js +3003 -0
- package/dist/src/commanderAdapter.d.ts +21 -0
- package/dist/src/commanderAdapter.js +208 -0
- package/dist/src/commanderAdapter.test.d.ts +1 -0
- package/dist/src/commanderAdapter.test.js +352 -0
- package/dist/src/commands/auth.d.ts +40 -0
- package/dist/src/commands/auth.js +434 -0
- package/dist/src/commands/auth.test.d.ts +1 -0
- package/dist/src/commands/auth.test.js +252 -0
- package/dist/src/commands/daemon.d.ts +9 -0
- package/dist/src/commands/daemon.js +98 -0
- package/dist/src/commands/daemon.test.d.ts +1 -0
- package/dist/src/commands/daemon.test.js +283 -0
- package/dist/src/completion-fast.d.ts +22 -0
- package/dist/src/completion-fast.js +88 -0
- package/dist/src/completion-shared.d.ts +13 -0
- package/dist/src/completion-shared.js +60 -0
- package/dist/src/completion.d.ts +21 -0
- package/dist/src/completion.js +68 -0
- package/dist/src/completion.test.d.ts +1 -0
- package/dist/src/completion.test.js +24 -0
- package/dist/src/constants.d.ts +15 -0
- package/dist/src/constants.js +32 -0
- package/dist/src/convention-audit.d.ts +50 -0
- package/dist/src/convention-audit.js +553 -0
- package/dist/src/convention-audit.test.d.ts +1 -0
- package/dist/src/convention-audit.test.js +247 -0
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +22 -0
- package/dist/src/daemon.js +437 -0
- package/dist/src/daemon.test.d.ts +1 -0
- package/dist/src/daemon.test.js +60 -0
- package/dist/src/discovery.d.ts +43 -0
- package/dist/src/discovery.js +266 -0
- package/dist/src/doctor.d.ts +39 -0
- package/dist/src/doctor.js +230 -0
- package/dist/src/doctor.test.d.ts +1 -0
- package/dist/src/doctor.test.js +263 -0
- package/dist/src/download/article-download.d.ts +72 -0
- package/dist/src/download/article-download.js +305 -0
- package/dist/src/download/article-download.test.d.ts +1 -0
- package/dist/src/download/article-download.test.js +235 -0
- package/dist/src/download/index.d.ts +71 -0
- package/dist/src/download/index.js +363 -0
- package/dist/src/download/index.test.d.ts +1 -0
- package/dist/src/download/index.test.js +174 -0
- package/dist/src/download/media-download.d.ts +49 -0
- package/dist/src/download/media-download.js +127 -0
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.d.ts +36 -0
- package/dist/src/download/progress.js +120 -0
- package/dist/src/download/progress.test.d.ts +1 -0
- package/dist/src/download/progress.test.js +36 -0
- package/dist/src/electron-apps.d.ts +31 -0
- package/dist/src/electron-apps.js +91 -0
- package/dist/src/electron-apps.test.d.ts +1 -0
- package/dist/src/electron-apps.test.js +76 -0
- package/dist/src/engine.test.d.ts +1 -0
- package/dist/src/engine.test.js +312 -0
- package/dist/src/errors.d.ts +114 -0
- package/dist/src/errors.js +174 -0
- package/dist/src/errors.test.d.ts +1 -0
- package/dist/src/errors.test.js +109 -0
- package/dist/src/execution.d.ts +24 -0
- package/dist/src/execution.js +509 -0
- package/dist/src/execution.test.d.ts +1 -0
- package/dist/src/execution.test.js +645 -0
- package/dist/src/extension-manifest-regression.test.d.ts +1 -0
- package/dist/src/extension-manifest-regression.test.js +12 -0
- package/dist/src/external-clis.yaml +109 -0
- package/dist/src/external.d.ts +49 -0
- package/dist/src/external.js +211 -0
- package/dist/src/external.test.d.ts +1 -0
- package/dist/src/external.test.js +110 -0
- package/dist/src/help.d.ts +88 -0
- package/dist/src/help.js +555 -0
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +58 -0
- package/dist/src/hooks.d.ts +46 -0
- package/dist/src/hooks.js +58 -0
- package/dist/src/hooks.test.d.ts +4 -0
- package/dist/src/hooks.test.js +92 -0
- package/dist/src/interceptor.d.ts +44 -0
- package/dist/src/interceptor.js +183 -0
- package/dist/src/interceptor.test.d.ts +4 -0
- package/dist/src/interceptor.test.js +81 -0
- package/dist/src/launcher.d.ts +41 -0
- package/dist/src/launcher.js +226 -0
- package/dist/src/launcher.test.d.ts +1 -0
- package/dist/src/launcher.test.js +153 -0
- package/dist/src/logger.d.ts +26 -0
- package/dist/src/logger.js +49 -0
- package/dist/src/main.d.ts +5 -0
- package/dist/src/main.js +171 -0
- package/dist/src/manifest-types.d.ts +43 -0
- package/dist/src/manifest-types.js +9 -0
- package/dist/src/node-network.d.ts +10 -0
- package/dist/src/node-network.js +174 -0
- package/dist/src/node-network.test.d.ts +1 -0
- package/dist/src/node-network.test.js +55 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/output.d.ts +14 -0
- package/dist/src/output.js +152 -0
- package/dist/src/output.test.d.ts +1 -0
- package/dist/src/output.test.js +53 -0
- package/dist/src/package-exports.test.d.ts +1 -0
- package/dist/src/package-exports.test.js +112 -0
- package/dist/src/package-paths.d.ts +8 -0
- package/dist/src/package-paths.js +41 -0
- package/dist/src/pipeline/executor.d.ts +11 -0
- package/dist/src/pipeline/executor.js +93 -0
- package/dist/src/pipeline/executor.test.d.ts +4 -0
- package/dist/src/pipeline/executor.test.js +182 -0
- package/dist/src/pipeline/index.d.ts +5 -0
- package/dist/src/pipeline/index.js +5 -0
- package/dist/src/pipeline/registry.d.ts +28 -0
- package/dist/src/pipeline/registry.js +55 -0
- package/dist/src/pipeline/steps/browser.d.ts +13 -0
- package/dist/src/pipeline/steps/browser.js +86 -0
- package/dist/src/pipeline/steps/download.d.ts +18 -0
- package/dist/src/pipeline/steps/download.js +252 -0
- package/dist/src/pipeline/steps/download.test.d.ts +1 -0
- package/dist/src/pipeline/steps/download.test.js +102 -0
- package/dist/src/pipeline/steps/fetch.d.ts +5 -0
- package/dist/src/pipeline/steps/fetch.js +122 -0
- package/dist/src/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/src/pipeline/steps/fetch.test.js +117 -0
- package/dist/src/pipeline/steps/intercept.d.ts +5 -0
- package/dist/src/pipeline/steps/intercept.js +50 -0
- package/dist/src/pipeline/steps/tap.d.ts +12 -0
- package/dist/src/pipeline/steps/tap.js +93 -0
- package/dist/src/pipeline/steps/transform.d.ts +9 -0
- package/dist/src/pipeline/steps/transform.js +70 -0
- package/dist/src/pipeline/template.d.ts +17 -0
- package/dist/src/pipeline/template.js +338 -0
- package/dist/src/pipeline/template.test.d.ts +4 -0
- package/dist/src/pipeline/template.test.js +175 -0
- package/dist/src/pipeline/transform.test.d.ts +4 -0
- package/dist/src/pipeline/transform.test.js +133 -0
- package/dist/src/plugin-manifest.d.ts +70 -0
- package/dist/src/plugin-manifest.js +160 -0
- package/dist/src/plugin-manifest.test.d.ts +4 -0
- package/dist/src/plugin-manifest.test.js +179 -0
- package/dist/src/plugin-scaffold.d.ts +28 -0
- package/dist/src/plugin-scaffold.js +143 -0
- package/dist/src/plugin-scaffold.test.d.ts +4 -0
- package/dist/src/plugin-scaffold.test.js +83 -0
- package/dist/src/plugin.d.ts +145 -0
- package/dist/src/plugin.js +1245 -0
- package/dist/src/plugin.test.d.ts +4 -0
- package/dist/src/plugin.test.js +1324 -0
- package/dist/src/registry-api.d.ts +13 -0
- package/dist/src/registry-api.js +10 -0
- package/dist/src/registry.d.ts +123 -0
- package/dist/src/registry.js +125 -0
- package/dist/src/registry.test.d.ts +4 -0
- package/dist/src/registry.test.js +209 -0
- package/dist/src/runtime-detect.d.ts +31 -0
- package/dist/src/runtime-detect.js +47 -0
- package/dist/src/runtime-detect.test.d.ts +1 -0
- package/dist/src/runtime-detect.test.js +39 -0
- package/dist/src/runtime.d.ts +47 -0
- package/dist/src/runtime.js +66 -0
- package/dist/src/scripts/framework.d.ts +7 -0
- package/dist/src/scripts/framework.js +25 -0
- package/dist/src/scripts/interact.d.ts +4 -0
- package/dist/src/scripts/interact.js +20 -0
- package/dist/src/scripts/store.d.ts +13 -0
- package/dist/src/scripts/store.js +48 -0
- package/dist/src/serialization.d.ts +42 -0
- package/dist/src/serialization.js +102 -0
- package/dist/src/serialization.test.d.ts +1 -0
- package/dist/src/serialization.test.js +78 -0
- package/dist/src/skills.d.ts +14 -0
- package/dist/src/skills.js +127 -0
- package/dist/src/skills.test.d.ts +1 -0
- package/dist/src/skills.test.js +71 -0
- package/dist/src/snapshotFormatter.d.ts +11 -0
- package/dist/src/snapshotFormatter.js +338 -0
- package/dist/src/snapshotFormatter.test.d.ts +7 -0
- package/dist/src/snapshotFormatter.test.js +521 -0
- package/dist/src/tui.d.ts +28 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/tui.test.d.ts +1 -0
- package/dist/src/tui.test.js +19 -0
- package/dist/src/types.d.ts +236 -0
- package/dist/src/types.js +7 -0
- package/dist/src/update-check.d.ts +66 -0
- package/dist/src/update-check.js +202 -0
- package/dist/src/update-check.test.d.ts +1 -0
- package/dist/src/update-check.test.js +116 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +152 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/dist/src/validate.d.ts +21 -0
- package/dist/src/validate.js +129 -0
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/dist/src/verify.d.ts +26 -0
- package/dist/src/verify.js +67 -0
- package/dist/src/version.d.ts +4 -0
- package/dist/src/version.js +22 -0
- package/dist/src/weixin-download.test.d.ts +1 -0
- package/dist/src/weixin-download.test.js +45 -0
- package/package.json +109 -0
- package/scripts/check-doc-coverage.sh +81 -0
- package/scripts/check-listing-id-pairing.mjs +193 -0
- package/scripts/check-silent-column-drop.mjs +105 -0
- package/scripts/check-typed-error-lint.mjs +118 -0
- package/scripts/clean-dist.cjs +13 -0
- package/scripts/copy-yaml.cjs +12 -0
- package/scripts/fetch-adapters.js +293 -0
- package/scripts/postinstall.js +174 -0
- package/scripts/silent-column-drop-baseline.json +910 -0
- package/scripts/typed-error-lint-baseline.json +1162 -0
- package/skills/opencli-adapter-author/SKILL.md +256 -0
- package/skills/opencli-adapter-author/references/adapter-template.md +471 -0
- package/skills/opencli-adapter-author/references/api-discovery.md +295 -0
- package/skills/opencli-adapter-author/references/coverage-matrix.md +81 -0
- package/skills/opencli-adapter-author/references/field-conventions.md +172 -0
- package/skills/opencli-adapter-author/references/field-decode-playbook.md +181 -0
- package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +196 -0
- package/skills/opencli-adapter-author/references/output-design.md +149 -0
- package/skills/opencli-adapter-author/references/site-memory/bilibili.md +70 -0
- package/skills/opencli-adapter-author/references/site-memory/eastmoney.md +70 -0
- package/skills/opencli-adapter-author/references/site-memory/tonghuashun.md +51 -0
- package/skills/opencli-adapter-author/references/site-memory/xueqiu.md +58 -0
- package/skills/opencli-adapter-author/references/site-memory.md +231 -0
- package/skills/opencli-adapter-author/references/site-recon.md +134 -0
- package/skills/opencli-adapter-author/references/strategy-selection.md +169 -0
- package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +162 -0
- package/skills/opencli-adapter-author/references/typed-errors.md +234 -0
- package/skills/opencli-autofix/SKILL.md +297 -0
- package/skills/opencli-browser/SKILL.md +444 -0
- package/skills/opencli-browser-sitemap/SKILL.md +96 -0
- package/skills/opencli-sitemap-author/SKILL.md +161 -0
- package/skills/opencli-sitemap-author/references/sitemap-schema.md +660 -0
- package/skills/opencli-usage/SKILL.md +169 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,3357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point: registers built-in commands and wires up Commander.
|
|
3
|
+
*
|
|
4
|
+
* Built-in commands are registered inline here (list, validate, explore, etc.).
|
|
5
|
+
* Dynamic adapter commands are registered via commanderAdapter.ts.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { Command, InvalidArgumentError, Option } from 'commander';
|
|
12
|
+
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
|
|
13
|
+
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
14
|
+
import { serializeCommand, formatArgSummary } from './serialization.js';
|
|
15
|
+
import { render as renderOutput } from './output.js';
|
|
16
|
+
import { PKG_VERSION } from './version.js';
|
|
17
|
+
import { printCompletionScript } from './completion.js';
|
|
18
|
+
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled, formatExternalCliLabel } from './external.js';
|
|
19
|
+
import { listOpenCliSkills, readOpenCliSkill } from './skills.js';
|
|
20
|
+
import { registerAllCommands } from './commanderAdapter.js';
|
|
21
|
+
import { classifyAdapter, formatRootAdapterHelpText, installCommanderNamespaceStructuredHelp, installStructuredHelp, leadingPositionalFromUsage, rootHelpData } from './help.js';
|
|
22
|
+
import { EXIT_CODES, getErrorMessage, BrowserConnectError, CliError } from './errors.js';
|
|
23
|
+
import { TargetError } from './browser/target-errors.js';
|
|
24
|
+
import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
|
|
25
|
+
import { buildFindJs, buildSemanticFindJs, isFindError } from './browser/find.js';
|
|
26
|
+
import { inferShape } from './browser/shape.js';
|
|
27
|
+
import { assignKeys } from './browser/network-key.js';
|
|
28
|
+
import { DEFAULT_TTL_MS, findEntry, loadNetworkCache, saveNetworkCache } from './browser/network-cache.js';
|
|
29
|
+
import { parseFilter, shapeMatchesFilter } from './browser/shape-filter.js';
|
|
30
|
+
import { buildHtmlTreeJs } from './browser/html-tree.js';
|
|
31
|
+
import { buildExtractHtmlJs, runExtractFromHtml } from './browser/extract.js';
|
|
32
|
+
import { analyzeSite } from './browser/analyze.js';
|
|
33
|
+
import { registerAuthCommands } from './commands/auth.js';
|
|
34
|
+
import { daemonRestart, daemonStatus, daemonStop } from './commands/daemon.js';
|
|
35
|
+
import { log } from './logger.js';
|
|
36
|
+
import { bindTab, BrowserCommandError, fetchDaemonStatus, sendCommand } from './browser/daemon-client.js';
|
|
37
|
+
import { aliasForContextId, loadProfileConfig, renameProfile, resolveProfileContextId, setDefaultProfile } from './browser/profile.js';
|
|
38
|
+
import { formatDaemonVersion, isDaemonStale } from './browser/daemon-version.js';
|
|
39
|
+
const CLI_FILE = fileURLToPath(import.meta.url);
|
|
40
|
+
const BROWSER_TAB_OPTION_DESCRIPTION = 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"';
|
|
41
|
+
const FOLLOW_POLL_MS = 1_000;
|
|
42
|
+
function parseDurationMs(raw, flagName) {
|
|
43
|
+
if (raw === undefined || raw === null || raw === '')
|
|
44
|
+
return null;
|
|
45
|
+
const str = String(raw).trim();
|
|
46
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(str);
|
|
47
|
+
if (!match)
|
|
48
|
+
return { error: `--${flagName} must be a duration like 500ms, 30s, 2m, got "${str}"` };
|
|
49
|
+
const value = Number.parseFloat(match[1]);
|
|
50
|
+
const unit = match[2] ?? 'ms';
|
|
51
|
+
const multiplier = unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : unit === 's' ? 1_000 : 1;
|
|
52
|
+
return Math.round(value * multiplier);
|
|
53
|
+
}
|
|
54
|
+
function timestampFromRaw(value) {
|
|
55
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : Date.now();
|
|
56
|
+
}
|
|
57
|
+
function toIsoTimestamp(timestamp) {
|
|
58
|
+
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp <= 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
return new Date(timestamp).toISOString();
|
|
61
|
+
}
|
|
62
|
+
function filterByTimeWindow(items, opts, now = Date.now()) {
|
|
63
|
+
const sinceTs = opts.sinceMs != null ? now - opts.sinceMs : undefined;
|
|
64
|
+
const untilTs = opts.untilMs != null ? now - opts.untilMs : undefined;
|
|
65
|
+
return items.filter((item) => {
|
|
66
|
+
const ts = item.timestamp ?? now;
|
|
67
|
+
if (sinceTs !== undefined && ts < sinceTs)
|
|
68
|
+
return false;
|
|
69
|
+
if (untilTs !== undefined && ts > untilTs)
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function selectFreshByTimestamp(items, lastSeenTs) {
|
|
75
|
+
const fresh = items.filter((item) => Number(item.timestamp ?? 0) > lastSeenTs);
|
|
76
|
+
const nextSeenTs = fresh.length > 0
|
|
77
|
+
? Math.max(lastSeenTs, ...fresh.map((item) => Number(item.timestamp ?? 0)).filter(Number.isFinite))
|
|
78
|
+
: lastSeenTs;
|
|
79
|
+
return { fresh, lastSeenTs: nextSeenTs };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalize raw capture entries (from daemon/CDP `readNetworkCapture` or
|
|
83
|
+
* the JS interceptor's `window.__opencli_net`) into a consistent shape.
|
|
84
|
+
* Response preview is parsed as JSON when possible, otherwise kept as string.
|
|
85
|
+
* `bodyFullSize` / `bodyTruncated` surface capture-layer truncation so the
|
|
86
|
+
* agent-facing envelope can warn when the body isn't whole.
|
|
87
|
+
*/
|
|
88
|
+
async function captureNetworkItems(page) {
|
|
89
|
+
if (page.readNetworkCapture) {
|
|
90
|
+
const raw = await page.readNetworkCapture();
|
|
91
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
92
|
+
return raw.map((e) => {
|
|
93
|
+
const preview = e.responsePreview ?? null;
|
|
94
|
+
let body = null;
|
|
95
|
+
if (preview) {
|
|
96
|
+
try {
|
|
97
|
+
body = JSON.parse(preview);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
body = preview;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const fullSize = typeof e.responseBodyFullSize === 'number'
|
|
104
|
+
? e.responseBodyFullSize
|
|
105
|
+
: (preview ? preview.length : 0);
|
|
106
|
+
const truncated = e.responseBodyTruncated === true;
|
|
107
|
+
return {
|
|
108
|
+
url: e.url || '',
|
|
109
|
+
method: e.method || 'GET',
|
|
110
|
+
status: e.responseStatus || 0,
|
|
111
|
+
size: fullSize,
|
|
112
|
+
ct: e.responseContentType || '',
|
|
113
|
+
body,
|
|
114
|
+
bodyFullSize: fullSize,
|
|
115
|
+
bodyTruncated: truncated,
|
|
116
|
+
timestamp: timestampFromRaw(e.timestamp),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const raw = await page.evaluate(`(function(){ var out = window.__opencli_net || []; window.__opencli_net = []; return JSON.stringify(out); })()`);
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
return parsed.map((item) => ({ ...item, timestamp: timestampFromRaw(item.timestamp) }));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
if (process.env.OPENCLI_VERBOSE)
|
|
128
|
+
log.warn(`[network] Failed to parse interceptor buffer: ${typeof raw === 'string' ? raw.slice(0, 200) : String(raw)}`);
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Drop static-resource / telemetry noise so agents see only API-shaped traffic. */
|
|
133
|
+
function filterNetworkItems(items) {
|
|
134
|
+
return items.filter((r) => {
|
|
135
|
+
const ct = r.ct?.toLowerCase() ?? '';
|
|
136
|
+
return ((ct.includes('json') || ct.includes('xml') || ct.includes('text/plain') || ct.includes('javascript')) &&
|
|
137
|
+
!/\.(js|css|png|jpg|gif|svg|woff|ico|map)(\?|$)/i.test(r.url) &&
|
|
138
|
+
!/analytics|tracking|telemetry|beacon|pixel|gtag|fbevents/i.test(r.url));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Exit codes by network error code — usage errors vs runtime failures. */
|
|
142
|
+
const NETWORK_ERROR_EXIT = {
|
|
143
|
+
invalid_args: EXIT_CODES.USAGE_ERROR,
|
|
144
|
+
invalid_filter: EXIT_CODES.USAGE_ERROR,
|
|
145
|
+
invalid_max_body: EXIT_CODES.USAGE_ERROR,
|
|
146
|
+
};
|
|
147
|
+
/** Emit a structured error JSON so agents can branch on `error.code` without regex. */
|
|
148
|
+
function emitNetworkError(code, message, extra = {}) {
|
|
149
|
+
console.log(JSON.stringify({ error: { code, message, ...extra } }, null, 2));
|
|
150
|
+
process.exitCode = NETWORK_ERROR_EXIT[code] ?? EXIT_CODES.GENERIC_ERROR;
|
|
151
|
+
}
|
|
152
|
+
const SITEMAP_HINT = 'Site sitemap available. For navigation context, use the opencli-browser-sitemap skill; treat browser state as truth if it disagrees.';
|
|
153
|
+
function siteNameCandidatesFromUrl(url, registry = getRegistry()) {
|
|
154
|
+
let host;
|
|
155
|
+
try {
|
|
156
|
+
host = new URL(url).hostname.toLowerCase().replace(/^www\./, '');
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const scored = new Map();
|
|
162
|
+
for (const command of registry.values()) {
|
|
163
|
+
if (!command.domain)
|
|
164
|
+
continue;
|
|
165
|
+
let domainHost = command.domain.toLowerCase().trim();
|
|
166
|
+
try {
|
|
167
|
+
domainHost = new URL(domainHost.includes('://') ? domainHost : `https://${domainHost}`).hostname.toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
domainHost = domainHost.split('/')[0] ?? domainHost;
|
|
171
|
+
}
|
|
172
|
+
domainHost = domainHost.replace(/^www\./, '');
|
|
173
|
+
if (!domainHost)
|
|
174
|
+
continue;
|
|
175
|
+
if (host === domainHost || host.endsWith(`.${domainHost}`)) {
|
|
176
|
+
scored.set(command.site, Math.max(scored.get(command.site) ?? 0, domainHost.length));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const registrySites = [...scored.entries()]
|
|
180
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
181
|
+
.map(([site]) => site);
|
|
182
|
+
const hostParts = host.split('.').filter(Boolean);
|
|
183
|
+
const fallback = hostParts.length >= 2 ? hostParts[hostParts.length - 2] : hostParts[0];
|
|
184
|
+
return [...new Set([...registrySites, ...(fallback ? [fallback] : [])])];
|
|
185
|
+
}
|
|
186
|
+
function firstExistingSitemapPath(paths, fileExists) {
|
|
187
|
+
return paths.find((candidate) => fileExists(candidate));
|
|
188
|
+
}
|
|
189
|
+
function sitemapPathsForSite(site, opts) {
|
|
190
|
+
const safeSite = site.replace(/[^a-zA-Z0-9_-]+/g, '-');
|
|
191
|
+
if (!safeSite)
|
|
192
|
+
return {};
|
|
193
|
+
const localBase = path.join(opts.homeDir, '.opencli', 'sites', safeSite);
|
|
194
|
+
return {
|
|
195
|
+
local: firstExistingSitemapPath([
|
|
196
|
+
path.join(localBase, 'sitemap'),
|
|
197
|
+
path.join(localBase, 'sitemap.md'),
|
|
198
|
+
], opts.fileExists),
|
|
199
|
+
global: firstExistingSitemapPath([
|
|
200
|
+
path.join(opts.packageRoot, 'sitemaps', safeSite),
|
|
201
|
+
path.join(opts.packageRoot, 'sitemaps', `${safeSite}.md`),
|
|
202
|
+
], opts.fileExists),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export function resolveSitemapAvailabilityForUrl(url, options = {}) {
|
|
206
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
207
|
+
const packageRoot = options.packageRoot ?? findPackageRoot(CLI_FILE);
|
|
208
|
+
const registry = options.registry ?? getRegistry();
|
|
209
|
+
const fileExists = options.fileExists ?? fs.existsSync;
|
|
210
|
+
for (const site of siteNameCandidatesFromUrl(url, registry)) {
|
|
211
|
+
const paths = sitemapPathsForSite(site, { homeDir, packageRoot, fileExists });
|
|
212
|
+
if (!paths.local && !paths.global)
|
|
213
|
+
continue;
|
|
214
|
+
const source = paths.local && paths.global ? 'local+global' : paths.local ? 'local' : 'global';
|
|
215
|
+
return {
|
|
216
|
+
site,
|
|
217
|
+
available: true,
|
|
218
|
+
source,
|
|
219
|
+
hint: SITEMAP_HINT,
|
|
220
|
+
paths,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function getBrowserSitemapHintStatePath(scope) {
|
|
226
|
+
const safeScope = scope.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
|
227
|
+
return path.join(getBrowserCacheDir(), 'browser-sitemap-hints', `${safeScope}.json`);
|
|
228
|
+
}
|
|
229
|
+
function loadBrowserSitemapHintState(scope) {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(fs.readFileSync(getBrowserSitemapHintStatePath(scope), 'utf-8'));
|
|
232
|
+
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.seenSites)) {
|
|
233
|
+
return {
|
|
234
|
+
seenSites: parsed.seenSites.filter((site) => typeof site === 'string'),
|
|
235
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date(0).toISOString(),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// First command in this browser session has no hint cache yet.
|
|
241
|
+
}
|
|
242
|
+
return { seenSites: [], updatedAt: new Date(0).toISOString() };
|
|
243
|
+
}
|
|
244
|
+
function markBrowserSitemapHintSeen(scope, site) {
|
|
245
|
+
const state = loadBrowserSitemapHintState(scope);
|
|
246
|
+
if (!state.seenSites.includes(site))
|
|
247
|
+
state.seenSites.push(site);
|
|
248
|
+
const target = getBrowserSitemapHintStatePath(scope);
|
|
249
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
250
|
+
fs.writeFileSync(target, JSON.stringify({ seenSites: state.seenSites, updatedAt: new Date().toISOString() }), 'utf-8');
|
|
251
|
+
}
|
|
252
|
+
function sitemapHintForBrowserUrl(url, scope, opts) {
|
|
253
|
+
const sitemap = resolveSitemapAvailabilityForUrl(url);
|
|
254
|
+
if (!sitemap)
|
|
255
|
+
return null;
|
|
256
|
+
if (!opts.oncePerSession)
|
|
257
|
+
return sitemap;
|
|
258
|
+
const state = loadBrowserSitemapHintState(scope);
|
|
259
|
+
if (state.seenSites.includes(sitemap.site))
|
|
260
|
+
return null;
|
|
261
|
+
markBrowserSitemapHintSeen(scope, sitemap.site);
|
|
262
|
+
return sitemap;
|
|
263
|
+
}
|
|
264
|
+
export function checkSiteMemory(site) {
|
|
265
|
+
const siteDir = path.join(os.homedir(), '.opencli', 'sites', site);
|
|
266
|
+
const endpointsPath = path.join(siteDir, 'endpoints.json');
|
|
267
|
+
const notesPath = path.join(siteDir, 'notes.md');
|
|
268
|
+
let endpointsCount = 0;
|
|
269
|
+
let endpointsPresent = fs.existsSync(endpointsPath);
|
|
270
|
+
if (endpointsPresent) {
|
|
271
|
+
try {
|
|
272
|
+
const parsed = JSON.parse(fs.readFileSync(endpointsPath, 'utf-8'));
|
|
273
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
274
|
+
endpointsCount = Object.keys(parsed).length;
|
|
275
|
+
}
|
|
276
|
+
else if (Array.isArray(parsed)) {
|
|
277
|
+
endpointsCount = parsed.length;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
endpointsPresent = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const notesPresent = fs.existsSync(notesPath);
|
|
285
|
+
return {
|
|
286
|
+
ok: endpointsPresent && endpointsCount > 0 && notesPresent,
|
|
287
|
+
siteDir,
|
|
288
|
+
endpoints: { present: endpointsPresent, count: endpointsCount, path: endpointsPath },
|
|
289
|
+
notes: { present: notesPresent, path: notesPath },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
export function printSiteMemoryReport(report, strict) {
|
|
293
|
+
if (report.ok) {
|
|
294
|
+
console.log(` ✓ Memory: endpoints.json (${report.endpoints.count}), notes.md present at ${report.siteDir}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const marker = strict ? '✗' : '⚠';
|
|
298
|
+
const missing = [];
|
|
299
|
+
if (!report.endpoints.present)
|
|
300
|
+
missing.push('endpoints.json');
|
|
301
|
+
else if (report.endpoints.count === 0)
|
|
302
|
+
missing.push('endpoints.json (empty)');
|
|
303
|
+
if (!report.notes.present)
|
|
304
|
+
missing.push('notes.md');
|
|
305
|
+
console.log(` ${marker} Memory: missing ${missing.join(', ')} under ${report.siteDir}`);
|
|
306
|
+
console.log(` Write the endpoint you just verified + a 1-line session note so the next agent starts from minute 0, not minute 95.`);
|
|
307
|
+
if (!strict) {
|
|
308
|
+
console.log(` (Re-run with --strict-memory to fail instead of warn.)`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Coerce adapter JSON output into a row array. Accepts `[{...}]`, single `{}`, or `{items:[...]}`-style envelopes. */
|
|
312
|
+
export function normalizeVerifyRows(data) {
|
|
313
|
+
if (Array.isArray(data)) {
|
|
314
|
+
return data.map((r) => (r && typeof r === 'object' ? r : { value: r }));
|
|
315
|
+
}
|
|
316
|
+
if (data && typeof data === 'object') {
|
|
317
|
+
const obj = data;
|
|
318
|
+
for (const k of ['rows', 'items', 'data', 'results']) {
|
|
319
|
+
if (Array.isArray(obj[k])) {
|
|
320
|
+
return obj[k].map((r) => (r && typeof r === 'object' ? r : { value: r }));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return [obj];
|
|
324
|
+
}
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
/** Render up to 10 rows as a compact padded table for eyeball inspection during verify. */
|
|
328
|
+
export function renderVerifyPreview(rows, opts = {}) {
|
|
329
|
+
const maxRows = opts.maxRows ?? 10;
|
|
330
|
+
const maxCols = opts.maxCols ?? 6;
|
|
331
|
+
const cellMax = opts.cellMax ?? 40;
|
|
332
|
+
if (rows.length === 0)
|
|
333
|
+
return ' (no rows)';
|
|
334
|
+
const allCols = Array.from(new Set(rows.flatMap((r) => Object.keys(r))));
|
|
335
|
+
const cols = allCols.slice(0, maxCols);
|
|
336
|
+
const shown = rows.slice(0, maxRows);
|
|
337
|
+
const cellOf = (v) => {
|
|
338
|
+
if (v === null || v === undefined)
|
|
339
|
+
return '';
|
|
340
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
341
|
+
return s.replace(/\s+/g, ' ').slice(0, cellMax);
|
|
342
|
+
};
|
|
343
|
+
const widths = cols.map((c) => Math.max(c.length, ...shown.map((r) => cellOf(r[c]).length)));
|
|
344
|
+
const fmtRow = (vals) => vals.map((v, i) => v.padEnd(widths[i])).join(' ');
|
|
345
|
+
const out = [];
|
|
346
|
+
out.push(` ${fmtRow(cols)}`);
|
|
347
|
+
out.push(` ${widths.map((w) => '-'.repeat(w)).join(' ')}`);
|
|
348
|
+
for (const r of shown)
|
|
349
|
+
out.push(` ${fmtRow(cols.map((c) => cellOf(r[c])))}`);
|
|
350
|
+
if (rows.length > maxRows)
|
|
351
|
+
out.push(` ... and ${rows.length - maxRows} more row(s)`);
|
|
352
|
+
if (allCols.length > maxCols)
|
|
353
|
+
out.push(` (${allCols.length - maxCols} more column(s) hidden)`);
|
|
354
|
+
return out.join('\n');
|
|
355
|
+
}
|
|
356
|
+
function getBrowserCacheDir() {
|
|
357
|
+
return process.env.OPENCLI_CACHE_DIR || path.join(os.homedir(), '.opencli', 'cache');
|
|
358
|
+
}
|
|
359
|
+
function getBrowserTargetStatePath(scope) {
|
|
360
|
+
const safeSession = scope.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
|
361
|
+
return path.join(getBrowserCacheDir(), 'browser-state', `${safeSession}.json`);
|
|
362
|
+
}
|
|
363
|
+
function loadBrowserTargetState(scope) {
|
|
364
|
+
try {
|
|
365
|
+
const raw = fs.readFileSync(getBrowserTargetStatePath(scope), 'utf-8');
|
|
366
|
+
const parsed = JSON.parse(raw);
|
|
367
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function saveBrowserTargetState(defaultPage, scope) {
|
|
374
|
+
const target = getBrowserTargetStatePath(scope);
|
|
375
|
+
if (!defaultPage) {
|
|
376
|
+
fs.rmSync(target, { force: true });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
380
|
+
fs.writeFileSync(target, JSON.stringify({ defaultPage, updatedAt: new Date().toISOString() }), 'utf-8');
|
|
381
|
+
}
|
|
382
|
+
function hasBrowserTabTarget(tabs, targetPage) {
|
|
383
|
+
return tabs.some((tab) => {
|
|
384
|
+
return typeof tab === 'object'
|
|
385
|
+
&& tab !== null
|
|
386
|
+
&& 'page' in tab
|
|
387
|
+
&& typeof tab.page === 'string'
|
|
388
|
+
&& tab.page === targetPage;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async function resolveBrowserTargetInSession(page, targetPage, opts) {
|
|
392
|
+
const candidate = targetPage.trim();
|
|
393
|
+
if (!candidate)
|
|
394
|
+
return undefined;
|
|
395
|
+
let tabs;
|
|
396
|
+
try {
|
|
397
|
+
tabs = await page.tabs();
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (opts.source === 'saved') {
|
|
401
|
+
saveBrowserTargetState(undefined, opts.scope);
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
throw new Error(`Target tab ${candidate} could not be validated in the current browser session. ` +
|
|
405
|
+
'The Browser Bridge session may have restarted; re-run "opencli browser tab list" and choose a current target.', { cause: err });
|
|
406
|
+
}
|
|
407
|
+
if (Array.isArray(tabs) && hasBrowserTabTarget(tabs, candidate)) {
|
|
408
|
+
return candidate;
|
|
409
|
+
}
|
|
410
|
+
if (opts.source === 'saved') {
|
|
411
|
+
saveBrowserTargetState(undefined, opts.scope);
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
throw new Error(`Target tab ${candidate} is not part of the current browser session. ` +
|
|
415
|
+
'The Browser Bridge session may have restarted; re-run "opencli browser tab list" and choose a current target.');
|
|
416
|
+
}
|
|
417
|
+
function getBrowserScope(session, contextId) {
|
|
418
|
+
return contextId ? `${contextId}:${session}` : session;
|
|
419
|
+
}
|
|
420
|
+
async function resolveStoredBrowserTarget(page, scope) {
|
|
421
|
+
const defaultPage = loadBrowserTargetState(scope)?.defaultPage?.trim();
|
|
422
|
+
if (!defaultPage)
|
|
423
|
+
return undefined;
|
|
424
|
+
return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
|
|
425
|
+
}
|
|
426
|
+
/** Create a browser page for browser commands. Uses a named browser session for continuity. */
|
|
427
|
+
async function getBrowserPage(session, targetPage, contextId, opts = {}) {
|
|
428
|
+
const { BrowserBridge } = await import('./browser/index.js');
|
|
429
|
+
const bridge = new BrowserBridge();
|
|
430
|
+
// Internal GC timeout for browser sessions. Not the per-command runtime timeout.
|
|
431
|
+
const envTimeout = process.env.OPENCLI_BROWSER_IDLE_TIMEOUT;
|
|
432
|
+
const idleTimeout = envTimeout ? parseInt(envTimeout, 10) : undefined;
|
|
433
|
+
const page = await bridge.connect({
|
|
434
|
+
timeout: 30,
|
|
435
|
+
session,
|
|
436
|
+
surface: 'browser',
|
|
437
|
+
...(contextId && { contextId }),
|
|
438
|
+
...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
|
|
439
|
+
windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
|
|
440
|
+
});
|
|
441
|
+
const targetScope = getBrowserScope(session, contextId);
|
|
442
|
+
const resolvedTargetPage = targetPage
|
|
443
|
+
? await resolveBrowserTargetInSession(page, targetPage, { scope: targetScope, source: 'explicit' })
|
|
444
|
+
: await resolveStoredBrowserTarget(page, targetScope);
|
|
445
|
+
if (resolvedTargetPage) {
|
|
446
|
+
if (!page.setActivePage) {
|
|
447
|
+
throw new Error('This browser session does not support explicit tab targeting');
|
|
448
|
+
}
|
|
449
|
+
page.setActivePage(resolvedTargetPage);
|
|
450
|
+
}
|
|
451
|
+
return page;
|
|
452
|
+
}
|
|
453
|
+
function getBrowserWindowMode(command, defaultMode) {
|
|
454
|
+
const optionRaw = getCommandOption(command, 'window');
|
|
455
|
+
if (optionRaw !== undefined && optionRaw !== '') {
|
|
456
|
+
if (optionRaw === 'foreground' || optionRaw === 'background')
|
|
457
|
+
return optionRaw;
|
|
458
|
+
throw new Error(`--window must be one of: foreground, background. Received: "${String(optionRaw)}"`);
|
|
459
|
+
}
|
|
460
|
+
const envRaw = process.env.OPENCLI_WINDOW;
|
|
461
|
+
if (envRaw !== undefined && envRaw !== '') {
|
|
462
|
+
if (envRaw === 'foreground' || envRaw === 'background')
|
|
463
|
+
return envRaw;
|
|
464
|
+
throw new Error(`OPENCLI_WINDOW must be one of: foreground, background. Received: "${envRaw}"`);
|
|
465
|
+
}
|
|
466
|
+
return defaultMode;
|
|
467
|
+
}
|
|
468
|
+
function addBrowserTabOption(command) {
|
|
469
|
+
return command.option('--tab <targetId>', BROWSER_TAB_OPTION_DESCRIPTION);
|
|
470
|
+
}
|
|
471
|
+
function getBrowserTargetId(command) {
|
|
472
|
+
if (!command)
|
|
473
|
+
return undefined;
|
|
474
|
+
const opts = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
|
|
475
|
+
return typeof opts.tab === 'string' && opts.tab.trim() ? opts.tab.trim() : undefined;
|
|
476
|
+
}
|
|
477
|
+
function getCommandOption(command, option) {
|
|
478
|
+
let current = command;
|
|
479
|
+
while (current) {
|
|
480
|
+
const opts = current.opts();
|
|
481
|
+
if (Object.prototype.hasOwnProperty.call(opts, option) && opts[option] !== undefined)
|
|
482
|
+
return opts[option];
|
|
483
|
+
current = current.parent;
|
|
484
|
+
}
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
function getBrowserSession(command) {
|
|
488
|
+
// The CLI surface is `opencli browser <session> <subcommand>`. main.ts rewrites
|
|
489
|
+
// argv to insert `--session <name>` before commander parses it; this helper
|
|
490
|
+
// reads back the rewritten flag.
|
|
491
|
+
const raw = getCommandOption(command, 'session');
|
|
492
|
+
if (typeof raw === 'string' && raw.trim())
|
|
493
|
+
return raw.trim();
|
|
494
|
+
throw new Error('<session> is a required positional argument: opencli browser <session> <command>');
|
|
495
|
+
}
|
|
496
|
+
function getBrowserContextId(command) {
|
|
497
|
+
const raw = getCommandOption(command, 'profile');
|
|
498
|
+
return resolveProfileContextId(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
|
|
499
|
+
}
|
|
500
|
+
function getPageSession(page) {
|
|
501
|
+
const session = page.session;
|
|
502
|
+
if (typeof session === 'string' && session.trim())
|
|
503
|
+
return session.trim();
|
|
504
|
+
throw new Error('Browser page is missing a session');
|
|
505
|
+
}
|
|
506
|
+
function getPageScope(page) {
|
|
507
|
+
const contextId = page.contextId;
|
|
508
|
+
return getBrowserScope(getPageSession(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
|
|
509
|
+
}
|
|
510
|
+
function snapshotMetricText(snapshot) {
|
|
511
|
+
return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
|
|
512
|
+
}
|
|
513
|
+
function snapshotMetrics(snapshot, elapsedMs) {
|
|
514
|
+
const text = snapshotMetricText(snapshot);
|
|
515
|
+
const interactiveMatch = text.match(/^interactive:\s*(\d+)\s*$/m);
|
|
516
|
+
return {
|
|
517
|
+
ok: true,
|
|
518
|
+
chars: text.length,
|
|
519
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
520
|
+
lines: text ? text.split(/\r?\n/).length : 0,
|
|
521
|
+
approx_tokens: Math.ceil(text.length / 4),
|
|
522
|
+
refs: (text.match(/(^|\n)\s*\[\d+\]/g) ?? []).length,
|
|
523
|
+
frame_sections: (text.match(/(^|\n)frame /g) ?? []).length,
|
|
524
|
+
...(interactiveMatch ? { interactive: Number(interactiveMatch[1]) } : {}),
|
|
525
|
+
elapsed_ms: elapsedMs,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
async function snapshotSourceMetrics(page, source) {
|
|
529
|
+
const started = Date.now();
|
|
530
|
+
try {
|
|
531
|
+
const snapshot = await page.snapshot({ viewportExpand: 2000, source });
|
|
532
|
+
return snapshotMetrics(snapshot, Date.now() - started);
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
elapsed_ms: Date.now() - started,
|
|
538
|
+
error: {
|
|
539
|
+
...(err instanceof Error && 'code' in err ? { code: String(err.code) } : {}),
|
|
540
|
+
message: err instanceof Error ? err.message : String(err),
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function resolveBrowserTabTarget(targetId, opts) {
|
|
546
|
+
if (typeof targetId === 'string' && targetId.trim())
|
|
547
|
+
return targetId.trim();
|
|
548
|
+
const tab = opts instanceof Command ? opts.opts().tab : opts?.tab;
|
|
549
|
+
if (typeof tab === 'string' && tab.trim())
|
|
550
|
+
return tab.trim();
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
function parsePositiveIntOption(val, label, fallback) {
|
|
554
|
+
if (val === undefined)
|
|
555
|
+
return fallback;
|
|
556
|
+
const parsed = parseInt(val, 10);
|
|
557
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
558
|
+
console.error(`[cli] Invalid ${label}="${val}", using default ${fallback}`);
|
|
559
|
+
return fallback;
|
|
560
|
+
}
|
|
561
|
+
return parsed;
|
|
562
|
+
}
|
|
563
|
+
function parseScreenshotDim(val, label) {
|
|
564
|
+
if (!/^\d+$/.test(val)) {
|
|
565
|
+
throw new InvalidArgumentError(`--${label} must be a positive integer (got "${val}")`);
|
|
566
|
+
}
|
|
567
|
+
const parsed = parseInt(val, 10);
|
|
568
|
+
if (parsed <= 0) {
|
|
569
|
+
throw new InvalidArgumentError(`--${label} must be a positive integer (got "${val}")`);
|
|
570
|
+
}
|
|
571
|
+
return parsed;
|
|
572
|
+
}
|
|
573
|
+
function applyVerbose(opts) {
|
|
574
|
+
if (opts.verbose)
|
|
575
|
+
process.env.OPENCLI_VERBOSE = '1';
|
|
576
|
+
}
|
|
577
|
+
function formatChildCommandSummary(command) {
|
|
578
|
+
return [...new Set(command.commands.map(child => child.name()))]
|
|
579
|
+
.sort((a, b) => a.localeCompare(b))
|
|
580
|
+
.join(', ');
|
|
581
|
+
}
|
|
582
|
+
function applyRootSubcommandSummaries(program) {
|
|
583
|
+
for (const command of program.commands) {
|
|
584
|
+
if (command.commands.length === 0)
|
|
585
|
+
continue;
|
|
586
|
+
const summary = formatChildCommandSummary(command);
|
|
587
|
+
if (summary)
|
|
588
|
+
command.description(summary);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
592
|
+
const program = new Command();
|
|
593
|
+
// enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
|
|
594
|
+
// prerequisite for passThroughOptions to forward --help/--version to external binaries
|
|
595
|
+
program
|
|
596
|
+
.name('opencli')
|
|
597
|
+
.description('Make any website your CLI. Zero setup. AI-powered.')
|
|
598
|
+
.version(PKG_VERSION)
|
|
599
|
+
.option('--profile <name>', 'Chrome profile/context alias for Browser Bridge commands')
|
|
600
|
+
.enablePositionalOptions();
|
|
601
|
+
// ── Built-in: list ────────────────────────────────────────────────────────
|
|
602
|
+
program
|
|
603
|
+
.command('list')
|
|
604
|
+
.description('List all available CLI commands')
|
|
605
|
+
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
606
|
+
.action((opts) => {
|
|
607
|
+
const registry = getRegistry();
|
|
608
|
+
const commands = [...new Set(registry.values())].sort((a, b) => fullName(a).localeCompare(fullName(b)));
|
|
609
|
+
const fmt = opts.format;
|
|
610
|
+
const isStructured = fmt === 'json' || fmt === 'yaml';
|
|
611
|
+
if (fmt !== 'table') {
|
|
612
|
+
const rows = isStructured
|
|
613
|
+
? commands.map(serializeCommand)
|
|
614
|
+
: commands.map(c => ({
|
|
615
|
+
command: fullName(c),
|
|
616
|
+
site: c.site,
|
|
617
|
+
name: c.name,
|
|
618
|
+
aliases: c.aliases?.join(', ') ?? '',
|
|
619
|
+
description: c.description,
|
|
620
|
+
access: c.access,
|
|
621
|
+
strategy: strategyLabel(c),
|
|
622
|
+
browser: !!c.browser,
|
|
623
|
+
args: formatArgSummary(c.args),
|
|
624
|
+
}));
|
|
625
|
+
renderOutput(rows, {
|
|
626
|
+
fmt,
|
|
627
|
+
columns: ['command', 'site', 'name', 'aliases', 'description', 'access', 'strategy', 'browser', 'args',
|
|
628
|
+
...(isStructured ? ['columns', 'domain'] : [])],
|
|
629
|
+
title: 'opencli/list',
|
|
630
|
+
source: 'opencli list',
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Table (default) — grouped by adapter kind (app vs site), then by site name.
|
|
635
|
+
// classifyAdapter() reads the `domain` field: DNS-style domains are sites;
|
|
636
|
+
// localhost/loopback endpoints and bare app names are apps.
|
|
637
|
+
const appsBySite = new Map();
|
|
638
|
+
const sitesBySite = new Map();
|
|
639
|
+
for (const cmd of commands) {
|
|
640
|
+
const target = classifyAdapter(cmd.domain) === 'app' ? appsBySite : sitesBySite;
|
|
641
|
+
const g = target.get(cmd.site) ?? [];
|
|
642
|
+
g.push(cmd);
|
|
643
|
+
target.set(cmd.site, g);
|
|
644
|
+
}
|
|
645
|
+
const renderSiteGroup = (site, cmds) => {
|
|
646
|
+
console.log(` ${site}`);
|
|
647
|
+
for (const cmd of cmds) {
|
|
648
|
+
const label = strategyLabel(cmd);
|
|
649
|
+
const tag = label === 'public'
|
|
650
|
+
? '[public]'
|
|
651
|
+
: `[${label}]`;
|
|
652
|
+
const aliases = cmd.aliases?.length ? ` (aliases: ${cmd.aliases.join(', ')})` : '';
|
|
653
|
+
console.log(` ${cmd.name} ${tag}${aliases}${cmd.description ? ` — ${cmd.description}` : ''}`);
|
|
654
|
+
}
|
|
655
|
+
console.log();
|
|
656
|
+
};
|
|
657
|
+
console.log();
|
|
658
|
+
console.log(' opencli' + ' — available commands');
|
|
659
|
+
console.log();
|
|
660
|
+
if (appsBySite.size > 0) {
|
|
661
|
+
console.log(' App adapters');
|
|
662
|
+
console.log();
|
|
663
|
+
for (const [site, cmds] of appsBySite)
|
|
664
|
+
renderSiteGroup(site, cmds);
|
|
665
|
+
}
|
|
666
|
+
if (sitesBySite.size > 0) {
|
|
667
|
+
console.log(' Site adapters');
|
|
668
|
+
console.log();
|
|
669
|
+
for (const [site, cmds] of sitesBySite)
|
|
670
|
+
renderSiteGroup(site, cmds);
|
|
671
|
+
}
|
|
672
|
+
const externalClis = loadExternalClis();
|
|
673
|
+
if (externalClis.length > 0) {
|
|
674
|
+
console.log(' external CLIs');
|
|
675
|
+
for (const ext of externalClis) {
|
|
676
|
+
const isInstalled = isBinaryInstalled(ext.binary);
|
|
677
|
+
const tag = isInstalled ? '[installed]' : '[auto-install]';
|
|
678
|
+
console.log(` ${formatExternalCliLabel(ext)} ${tag}${ext.description ? ` — ${ext.description}` : ''}`);
|
|
679
|
+
}
|
|
680
|
+
console.log();
|
|
681
|
+
}
|
|
682
|
+
console.log(` ${commands.length} built-in commands across ${appsBySite.size} apps + ${sitesBySite.size} sites, ${externalClis.length} external CLIs`);
|
|
683
|
+
console.log();
|
|
684
|
+
});
|
|
685
|
+
// ── Built-in: validate / verify ───────────────────────────────────────────
|
|
686
|
+
program
|
|
687
|
+
.command('validate')
|
|
688
|
+
.description('Validate CLI definitions')
|
|
689
|
+
.argument('[target]', 'site or site/name')
|
|
690
|
+
.action(async (target) => {
|
|
691
|
+
const { validateClisWithTarget, renderValidationReport } = await import('./validate.js');
|
|
692
|
+
console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target)));
|
|
693
|
+
});
|
|
694
|
+
program
|
|
695
|
+
.command('verify')
|
|
696
|
+
.description('Validate + smoke test')
|
|
697
|
+
.argument('[target]')
|
|
698
|
+
.option('--smoke', 'Run smoke tests', false)
|
|
699
|
+
.action(async (target, opts) => {
|
|
700
|
+
const { verifyClis, renderVerifyReport } = await import('./verify.js');
|
|
701
|
+
const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
|
|
702
|
+
console.log(renderVerifyReport(r));
|
|
703
|
+
process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
|
|
704
|
+
});
|
|
705
|
+
const skillsCmd = program
|
|
706
|
+
.command('skills')
|
|
707
|
+
.description('Read bundled OpenCLI skills');
|
|
708
|
+
skillsCmd
|
|
709
|
+
.command('list')
|
|
710
|
+
.description('List bundled opencli-* skills')
|
|
711
|
+
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
712
|
+
.action((opts) => {
|
|
713
|
+
const rows = listOpenCliSkills();
|
|
714
|
+
renderOutput(rows, {
|
|
715
|
+
fmt: opts.format,
|
|
716
|
+
fmtExplicit: !!opts.format,
|
|
717
|
+
columns: ['name', 'description', 'version', 'path'],
|
|
718
|
+
title: 'opencli/skills/list',
|
|
719
|
+
source: 'opencli skills list',
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
skillsCmd
|
|
723
|
+
.command('read')
|
|
724
|
+
.description("Print an opencli-* skill's SKILL.md or reference file")
|
|
725
|
+
.argument('<skill>', 'Skill name, or skill/path like opencli-browser/references/foo.md')
|
|
726
|
+
.argument('[path]', 'Path under the skill directory')
|
|
727
|
+
.option('--json', 'Output a JSON envelope instead of raw markdown', false)
|
|
728
|
+
.action((skill, skillPath, opts) => {
|
|
729
|
+
let result;
|
|
730
|
+
try {
|
|
731
|
+
result = readOpenCliSkill(skill, skillPath ?? '');
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
735
|
+
if (err instanceof CliError && err.hint)
|
|
736
|
+
console.error(`Hint: ${err.hint}`);
|
|
737
|
+
process.exitCode = err instanceof CliError ? err.exitCode : EXIT_CODES.GENERIC_ERROR;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (opts.json) {
|
|
741
|
+
console.log(JSON.stringify(result, null, 2));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
process.stdout.write(result.content);
|
|
745
|
+
if (!result.content.endsWith('\n'))
|
|
746
|
+
process.stdout.write('\n');
|
|
747
|
+
});
|
|
748
|
+
const authCmd = registerAuthCommands(program);
|
|
749
|
+
program
|
|
750
|
+
.command('convention-audit')
|
|
751
|
+
.description('Scan adapters for agent-native convention violations')
|
|
752
|
+
.argument('[target]', 'site or site/name')
|
|
753
|
+
.option('--site <site>', 'Limit audit to one site')
|
|
754
|
+
.option('-f, --format <fmt>', 'Output format: table, json, yaml', 'table')
|
|
755
|
+
.option('--strict', 'Exit non-zero when violations are found', false)
|
|
756
|
+
.action(async (target, opts) => {
|
|
757
|
+
const { runConventionAudit, renderConventionAuditText } = await import('./convention-audit.js');
|
|
758
|
+
const report = runConventionAudit({
|
|
759
|
+
projectRoot: findPackageRoot(CLI_FILE),
|
|
760
|
+
target,
|
|
761
|
+
site: opts.site,
|
|
762
|
+
});
|
|
763
|
+
const fmt = String(opts.format ?? 'table').toLowerCase();
|
|
764
|
+
if (fmt === 'json' || fmt === 'yaml' || fmt === 'yml') {
|
|
765
|
+
renderOutput(report, { fmt });
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
console.log(renderConventionAuditText(report));
|
|
769
|
+
}
|
|
770
|
+
if (opts.strict && !report.ok)
|
|
771
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
772
|
+
});
|
|
773
|
+
// ── Built-in: browser (browser control for Claude Code skill) ───────────────
|
|
774
|
+
//
|
|
775
|
+
// Make websites accessible for AI agents.
|
|
776
|
+
// All commands wrapped in browserAction() for consistent error handling.
|
|
777
|
+
const browser = program
|
|
778
|
+
.command('browser')
|
|
779
|
+
// --session is an internal hidden option used by the daemon protocol and direct
|
|
780
|
+
// program.parseAsync callers (tests). User-facing surface is the <session>
|
|
781
|
+
// positional; main.ts argv preprocessor rewrites positional -> --session.
|
|
782
|
+
.addOption(new Option('--session <name>', 'Internal — set automatically from the <session> positional').hideHelp())
|
|
783
|
+
.option('--window <mode>', 'Browser window mode: foreground or background')
|
|
784
|
+
.description('Browser control — navigate, click, type, extract, wait (no LLM needed)')
|
|
785
|
+
.usage('<session> <command> [options]')
|
|
786
|
+
.addHelpText('after', `
|
|
787
|
+
<session> is a required positional: pass the name of the browser session every subcommand should operate on. Reuse the same name across calls to keep the tab/state alive; pick a different name to isolate parallel browser work.
|
|
788
|
+
|
|
789
|
+
Examples:
|
|
790
|
+
$ opencli browser work open https://x.com
|
|
791
|
+
$ opencli browser work open https://x.com --window background
|
|
792
|
+
$ opencli browser work click 12
|
|
793
|
+
$ opencli browser work state
|
|
794
|
+
$ opencli browser work bind
|
|
795
|
+
$ opencli browser work unbind
|
|
796
|
+
`);
|
|
797
|
+
const originalBrowserDescription = browser.description();
|
|
798
|
+
/**
|
|
799
|
+
* Resolve a `<target>` (numeric ref or CSS selector) via the unified resolver.
|
|
800
|
+
* Returns the CSS match count so callers can propagate `matches_n` into the
|
|
801
|
+
* JSON envelope printed back to the agent.
|
|
802
|
+
*/
|
|
803
|
+
async function resolveRef(page, ref, opts = {}) {
|
|
804
|
+
const resolution = await page.evaluate(resolveTargetJs(ref, opts));
|
|
805
|
+
if (!resolution.ok) {
|
|
806
|
+
throw new TargetError({
|
|
807
|
+
code: resolution.code,
|
|
808
|
+
message: resolution.message,
|
|
809
|
+
hint: resolution.hint,
|
|
810
|
+
candidates: resolution.candidates,
|
|
811
|
+
matches_n: resolution.matches_n,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
return { matches_n: resolution.matches_n, match_level: resolution.match_level };
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Parse `--nth <n>` flag, returning the parsed 0-based index or a usage error.
|
|
818
|
+
* The surface mirrors `--depth` etc. in `browser get html --as json`: the flag
|
|
819
|
+
* is optional, must be a non-negative integer when present, and on failure we
|
|
820
|
+
* emit the structured error envelope rather than throwing past the command.
|
|
821
|
+
*/
|
|
822
|
+
function parseNthFlag(raw) {
|
|
823
|
+
if (raw === undefined || raw === null || raw === '')
|
|
824
|
+
return null;
|
|
825
|
+
const str = String(raw);
|
|
826
|
+
if (!/^\d+$/.test(str)) {
|
|
827
|
+
return { error: `--nth must be a non-negative integer, got "${str}"` };
|
|
828
|
+
}
|
|
829
|
+
return Number.parseInt(str, 10);
|
|
830
|
+
}
|
|
831
|
+
/** Emit the `{ error: { code, message, hint?, candidates?, matches_n? } }` envelope used by the selector-first commands. */
|
|
832
|
+
function emitTargetError(err) {
|
|
833
|
+
console.log(JSON.stringify({
|
|
834
|
+
error: {
|
|
835
|
+
code: err.code,
|
|
836
|
+
message: err.message,
|
|
837
|
+
hint: err.hint,
|
|
838
|
+
...(err.candidates && { candidates: err.candidates }),
|
|
839
|
+
...(err.matches_n !== undefined && { matches_n: err.matches_n }),
|
|
840
|
+
},
|
|
841
|
+
}, null, 2));
|
|
842
|
+
}
|
|
843
|
+
function isJavaScriptDialogMessage(message) {
|
|
844
|
+
const normalized = message.toLowerCase();
|
|
845
|
+
return normalized.includes('javascript dialog');
|
|
846
|
+
}
|
|
847
|
+
function emitJavaScriptDialogError(message) {
|
|
848
|
+
console.log(JSON.stringify({
|
|
849
|
+
error: {
|
|
850
|
+
code: 'javascript_dialog_open',
|
|
851
|
+
message,
|
|
852
|
+
hint: 'Handle the modal first: opencli browser dialog accept (or dismiss). Use --text for prompt dialogs.',
|
|
853
|
+
},
|
|
854
|
+
}, null, 2));
|
|
855
|
+
}
|
|
856
|
+
/** Wrap browser actions with error handling and optional --json output */
|
|
857
|
+
function browserAction(fn) {
|
|
858
|
+
return async (...args) => {
|
|
859
|
+
let page = null;
|
|
860
|
+
try {
|
|
861
|
+
const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
|
|
862
|
+
const targetPage = getBrowserTargetId(command);
|
|
863
|
+
const session = getBrowserSession(command);
|
|
864
|
+
const contextId = getBrowserContextId(command);
|
|
865
|
+
const windowMode = getBrowserWindowMode(command, 'foreground');
|
|
866
|
+
page = await getBrowserPage(session, targetPage, contextId, { windowMode });
|
|
867
|
+
await fn(page, ...args);
|
|
868
|
+
}
|
|
869
|
+
catch (err) {
|
|
870
|
+
if (err instanceof BrowserConnectError) {
|
|
871
|
+
log.error(err.message);
|
|
872
|
+
if (err.hint)
|
|
873
|
+
log.error(`Hint: ${err.hint}`);
|
|
874
|
+
}
|
|
875
|
+
else if (err instanceof BrowserCommandError) {
|
|
876
|
+
if (isJavaScriptDialogMessage(err.message)) {
|
|
877
|
+
emitJavaScriptDialogError(err.message);
|
|
878
|
+
}
|
|
879
|
+
else if (err.code) {
|
|
880
|
+
console.log(JSON.stringify({
|
|
881
|
+
error: {
|
|
882
|
+
code: err.code,
|
|
883
|
+
message: err.message,
|
|
884
|
+
...(err.hint ? { hint: err.hint } : {}),
|
|
885
|
+
},
|
|
886
|
+
}, null, 2));
|
|
887
|
+
}
|
|
888
|
+
log.error(err.message);
|
|
889
|
+
if (err.hint)
|
|
890
|
+
log.error(`Hint: ${err.hint}`);
|
|
891
|
+
}
|
|
892
|
+
else if (err instanceof TargetError) {
|
|
893
|
+
// Agent-facing structured envelope on stdout + short human line on stderr.
|
|
894
|
+
emitTargetError(err);
|
|
895
|
+
log.error(`[${err.code}] ${err.message}`);
|
|
896
|
+
if (err.hint)
|
|
897
|
+
log.error(`Hint: ${err.hint}`);
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
const msg = getErrorMessage(err);
|
|
901
|
+
if (isJavaScriptDialogMessage(msg)) {
|
|
902
|
+
emitJavaScriptDialogError(msg);
|
|
903
|
+
log.error(msg);
|
|
904
|
+
}
|
|
905
|
+
else if (msg.includes('attach failed') || msg.includes('chrome-extension://')) {
|
|
906
|
+
log.error(`Browser attach failed — another extension may be interfering. Try disabling 1Password.`);
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
log.error(msg);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
browser.command('bind')
|
|
917
|
+
.description('Bind the current Chrome tab/window to the browser session named by <session>')
|
|
918
|
+
.action(async (optsOrCommand, maybeCommand) => {
|
|
919
|
+
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
920
|
+
const session = getBrowserSession(command);
|
|
921
|
+
try {
|
|
922
|
+
const { BrowserBridge } = await import('./browser/index.js');
|
|
923
|
+
const bridge = new BrowserBridge();
|
|
924
|
+
const contextId = getBrowserContextId(command);
|
|
925
|
+
await bridge.connect({ timeout: 30, session, surface: 'browser', ...(contextId && { contextId }) });
|
|
926
|
+
const data = await bindTab(session, { ...(contextId && { contextId }) });
|
|
927
|
+
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
928
|
+
console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
if (err instanceof BrowserCommandError && err.code) {
|
|
932
|
+
console.log(JSON.stringify({
|
|
933
|
+
error: {
|
|
934
|
+
code: err.code,
|
|
935
|
+
message: err.message,
|
|
936
|
+
...(err.hint ? { hint: err.hint } : {}),
|
|
937
|
+
},
|
|
938
|
+
}, null, 2));
|
|
939
|
+
}
|
|
940
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
941
|
+
if (err instanceof BrowserCommandError && err.hint)
|
|
942
|
+
log.error(`Hint: ${err.hint}`);
|
|
943
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
browser.command('unbind')
|
|
947
|
+
.description('Detach the bound browser session named by <session> without closing the user tab/window')
|
|
948
|
+
.action(async (optsOrCommand, maybeCommand) => {
|
|
949
|
+
const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
|
|
950
|
+
const session = getBrowserSession(command);
|
|
951
|
+
try {
|
|
952
|
+
const { BrowserBridge } = await import('./browser/index.js');
|
|
953
|
+
const bridge = new BrowserBridge();
|
|
954
|
+
const contextId = getBrowserContextId(command);
|
|
955
|
+
await bridge.connect({ timeout: 30, session, surface: 'browser', ...(contextId && { contextId }) });
|
|
956
|
+
await sendCommand('close-window', { session, surface: 'browser', ...(contextId && { contextId }) });
|
|
957
|
+
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
958
|
+
console.log(JSON.stringify({ unbound: true, session }, null, 2));
|
|
959
|
+
}
|
|
960
|
+
catch (err) {
|
|
961
|
+
if (err instanceof BrowserCommandError && err.code) {
|
|
962
|
+
console.log(JSON.stringify({
|
|
963
|
+
error: {
|
|
964
|
+
code: err.code,
|
|
965
|
+
message: err.message,
|
|
966
|
+
...(err.hint ? { hint: err.hint } : {}),
|
|
967
|
+
},
|
|
968
|
+
}, null, 2));
|
|
969
|
+
}
|
|
970
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
971
|
+
if (err instanceof BrowserCommandError && err.hint)
|
|
972
|
+
log.error(`Hint: ${err.hint}`);
|
|
973
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
const browserTab = browser
|
|
977
|
+
.command('tab')
|
|
978
|
+
.description('Tab management — list, create, and close tabs in the browser session');
|
|
979
|
+
browserTab.command('list')
|
|
980
|
+
.description('List tabs in the browser session with target IDs')
|
|
981
|
+
.action(browserAction(async (page) => {
|
|
982
|
+
const tabs = await page.tabs();
|
|
983
|
+
console.log(JSON.stringify(tabs, null, 2));
|
|
984
|
+
}));
|
|
985
|
+
browserTab.command('new')
|
|
986
|
+
.argument('[url]', 'Optional URL to open in the new tab')
|
|
987
|
+
.description('Create a new tab and print its target ID')
|
|
988
|
+
.action(browserAction(async (page, url) => {
|
|
989
|
+
if (!page.newTab) {
|
|
990
|
+
throw new Error('This browser session does not support creating tabs');
|
|
991
|
+
}
|
|
992
|
+
const createdPage = await page.newTab(url);
|
|
993
|
+
console.log(JSON.stringify({
|
|
994
|
+
page: createdPage,
|
|
995
|
+
url: url ?? null,
|
|
996
|
+
}, null, 2));
|
|
997
|
+
}));
|
|
998
|
+
addBrowserTabOption(browserTab.command('select')
|
|
999
|
+
.argument('[targetId]', 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"')
|
|
1000
|
+
.description('Select a tab by target ID and make it the default browser tab'))
|
|
1001
|
+
.action(browserAction(async (page, targetId, opts) => {
|
|
1002
|
+
const resolvedTarget = resolveBrowserTabTarget(targetId, opts);
|
|
1003
|
+
if (!resolvedTarget) {
|
|
1004
|
+
throw new Error('Target tab required. Pass it as an argument or --tab <targetId>.');
|
|
1005
|
+
}
|
|
1006
|
+
await page.selectTab(resolvedTarget);
|
|
1007
|
+
saveBrowserTargetState(resolvedTarget, getPageScope(page));
|
|
1008
|
+
console.log(JSON.stringify({ selected: resolvedTarget }, null, 2));
|
|
1009
|
+
}));
|
|
1010
|
+
addBrowserTabOption(browserTab.command('close')
|
|
1011
|
+
.argument('[targetId]', 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"')
|
|
1012
|
+
.description('Close a tab by target ID'))
|
|
1013
|
+
.action(browserAction(async (page, targetId, opts) => {
|
|
1014
|
+
const resolvedTarget = resolveBrowserTabTarget(targetId, opts);
|
|
1015
|
+
if (!page.closeTab) {
|
|
1016
|
+
throw new Error('This browser session does not support closing tabs');
|
|
1017
|
+
}
|
|
1018
|
+
if (!resolvedTarget) {
|
|
1019
|
+
throw new Error('Target tab required. Pass it as an argument or --tab <targetId>.');
|
|
1020
|
+
}
|
|
1021
|
+
const validatedTarget = await resolveBrowserTargetInSession(page, resolvedTarget, {
|
|
1022
|
+
scope: getPageScope(page),
|
|
1023
|
+
source: 'explicit',
|
|
1024
|
+
});
|
|
1025
|
+
if (!validatedTarget) {
|
|
1026
|
+
throw new Error(`Target tab ${resolvedTarget} is not part of the current browser session.`);
|
|
1027
|
+
}
|
|
1028
|
+
await page.closeTab(validatedTarget);
|
|
1029
|
+
const scope = getPageScope(page);
|
|
1030
|
+
if (loadBrowserTargetState(scope)?.defaultPage === validatedTarget) {
|
|
1031
|
+
saveBrowserTargetState(undefined, scope);
|
|
1032
|
+
}
|
|
1033
|
+
console.log(JSON.stringify({ closed: validatedTarget }, null, 2));
|
|
1034
|
+
}));
|
|
1035
|
+
// ── Navigation ──
|
|
1036
|
+
/**
|
|
1037
|
+
* Network interceptor JS — injected on every open/navigate to capture
|
|
1038
|
+
* fetch/XHR bodies when the session-level capture channel (CDP/extension)
|
|
1039
|
+
* isn't available. Keeps parity with the CDP path's truncation contract:
|
|
1040
|
+
* when a body exceeds the per-entry cap, we keep a string prefix and set
|
|
1041
|
+
* `bodyTruncated: true` + `bodyFullSize: <original length>` so `browser
|
|
1042
|
+
* network` can propagate a visible signal to the agent instead of
|
|
1043
|
+
* silently dropping the body. Per-entry cap is 1 MiB and the ring is
|
|
1044
|
+
* capped at 200 entries, bounding worst-case in-page memory.
|
|
1045
|
+
*/
|
|
1046
|
+
const NETWORK_INTERCEPTOR_JS = `(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=1048576,F=window.fetch;function capture(url,method,status,text,ct){if(window.__opencli_net.length>=M)return;var full=text?text.length:0,trunc=full>B,stored=trunc?text.slice(0,B):text,body=null;if(stored){if(trunc){body=stored}else{try{body=JSON.parse(stored)}catch(e){body=stored}}}var e={url:url,method:method||'GET',status:status,size:full,ct:ct,body:body,timestamp:Date.now()};if(trunc){e.bodyTruncated=true;e.bodyFullSize=full}window.__opencli_net.push(e)}window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();capture(r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),(arguments[1]&&arguments[1].method)||'GET',r.status,t,ct)}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if(ct.includes('json')||ct.includes('text')){capture(x._ou,x._om||'GET',x.status,x.responseText||'',ct)}}catch(e){}});return S.apply(this,arguments)}})()`;
|
|
1047
|
+
addBrowserTabOption(browser.command('open').argument('<url>').description('Open URL in the browser session'))
|
|
1048
|
+
.action(browserAction(async (page, url, opts) => {
|
|
1049
|
+
// Start session-level capture before navigation (catches initial requests)
|
|
1050
|
+
const hasSessionCapture = await page.startNetworkCapture?.() ?? false;
|
|
1051
|
+
await page.goto(url);
|
|
1052
|
+
await page.wait(2);
|
|
1053
|
+
// Fallback: inject JS interceptor when session capture is unavailable
|
|
1054
|
+
if (!hasSessionCapture) {
|
|
1055
|
+
try {
|
|
1056
|
+
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
1057
|
+
}
|
|
1058
|
+
catch { /* non-fatal */ }
|
|
1059
|
+
}
|
|
1060
|
+
const currentUrl = await page.getCurrentUrl?.() ?? url;
|
|
1061
|
+
const sitemap = sitemapHintForBrowserUrl(currentUrl, getPageScope(page), { oncePerSession: true });
|
|
1062
|
+
console.log(JSON.stringify({
|
|
1063
|
+
url: currentUrl,
|
|
1064
|
+
...(page.getActivePage?.() ? { page: page.getActivePage?.() } : {}),
|
|
1065
|
+
...(sitemap ? { sitemap } : {}),
|
|
1066
|
+
}, null, 2));
|
|
1067
|
+
}));
|
|
1068
|
+
addBrowserTabOption(browser.command('back').description('Go back in browser history'))
|
|
1069
|
+
.action(browserAction(async (page, opts) => {
|
|
1070
|
+
await page.evaluate('history.back()');
|
|
1071
|
+
await page.wait(2);
|
|
1072
|
+
console.log('Navigated back');
|
|
1073
|
+
}));
|
|
1074
|
+
addBrowserTabOption(browser.command('scroll').argument('<direction>', 'up or down').option('--amount <pixels>', 'Pixels to scroll', '500'))
|
|
1075
|
+
.description('Scroll page')
|
|
1076
|
+
.action(browserAction(async (page, direction, opts) => {
|
|
1077
|
+
if (direction !== 'up' && direction !== 'down') {
|
|
1078
|
+
console.error(`Invalid direction "${direction}". Use "up" or "down".`);
|
|
1079
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
await page.scroll(direction, parseInt(opts.amount, 10));
|
|
1083
|
+
console.log(`Scrolled ${direction}`);
|
|
1084
|
+
}));
|
|
1085
|
+
// ── Inspect ──
|
|
1086
|
+
addBrowserTabOption(browser.command('state').description('Page state: URL, title, interactive elements with [N] indices')
|
|
1087
|
+
.option('--source <source>', 'Snapshot backend: dom (default) or ax prototype', 'dom')
|
|
1088
|
+
.option('--compare-sources', 'Print DOM vs AX snapshot metrics for observation promotion decisions', false))
|
|
1089
|
+
.action(browserAction(async (page, opts) => {
|
|
1090
|
+
if (opts.compareSources === true) {
|
|
1091
|
+
const [dom, ax] = await Promise.all([
|
|
1092
|
+
snapshotSourceMetrics(page, 'dom'),
|
|
1093
|
+
snapshotSourceMetrics(page, 'ax'),
|
|
1094
|
+
]);
|
|
1095
|
+
console.log(JSON.stringify({
|
|
1096
|
+
url: await page.getCurrentUrl?.() ?? '',
|
|
1097
|
+
sources: { dom, ax },
|
|
1098
|
+
}, null, 2));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const source = String(opts.source ?? 'dom').toLowerCase();
|
|
1102
|
+
if (source !== 'dom' && source !== 'ax') {
|
|
1103
|
+
console.log(JSON.stringify({
|
|
1104
|
+
error: {
|
|
1105
|
+
code: 'invalid_source',
|
|
1106
|
+
message: `--source must be "dom" or "ax", got "${opts.source}"`,
|
|
1107
|
+
},
|
|
1108
|
+
}, null, 2));
|
|
1109
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const snapshot = await page.snapshot({ viewportExpand: 2000, source: source });
|
|
1113
|
+
const url = await page.getCurrentUrl?.() ?? '';
|
|
1114
|
+
console.log(`URL: ${url}\n`);
|
|
1115
|
+
console.log(typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2));
|
|
1116
|
+
}));
|
|
1117
|
+
addBrowserTabOption(browser.command('frames').description('List cross-origin iframe targets in snapshot order'))
|
|
1118
|
+
.action(browserAction(async (page) => {
|
|
1119
|
+
const frames = await page.frames?.() ?? [];
|
|
1120
|
+
console.log(JSON.stringify(frames, null, 2));
|
|
1121
|
+
}));
|
|
1122
|
+
addBrowserTabOption(browser.command('screenshot').argument('[path]', 'Save to file (base64 if omitted)'))
|
|
1123
|
+
.option('--full-page', 'Capture the full scrollable page, not just the viewport', false)
|
|
1124
|
+
.option('--annotate', 'Overlay visible browser state ref labels on the screenshot', false)
|
|
1125
|
+
.option('--width <n>', 'Override viewport width in CSS pixels for this screenshot only', (v) => parseScreenshotDim(v, 'width'))
|
|
1126
|
+
.option('--height <n>', 'Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)', (v) => parseScreenshotDim(v, 'height'))
|
|
1127
|
+
.description('Take screenshot')
|
|
1128
|
+
.action(browserAction(async (page, path, opts) => {
|
|
1129
|
+
const shotOpts = {
|
|
1130
|
+
fullPage: opts.fullPage === true,
|
|
1131
|
+
annotate: opts.annotate === true,
|
|
1132
|
+
width: opts.width,
|
|
1133
|
+
height: opts.height,
|
|
1134
|
+
};
|
|
1135
|
+
const capture = opts.annotate === true
|
|
1136
|
+
? (page.annotatedScreenshot ?? page.screenshot).bind(page)
|
|
1137
|
+
: page.screenshot.bind(page);
|
|
1138
|
+
if (path) {
|
|
1139
|
+
await capture({ ...shotOpts, path });
|
|
1140
|
+
console.log(`Screenshot saved to: ${path}`);
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
console.log(await capture({ ...shotOpts, format: 'png' }));
|
|
1144
|
+
}
|
|
1145
|
+
}));
|
|
1146
|
+
addBrowserTabOption(browser.command('console'))
|
|
1147
|
+
.option('--level <level>', 'Console level: all, error, warning, log, info, debug', 'all')
|
|
1148
|
+
.option('--since <duration>', 'Only include messages from the last duration (for example: 30s, 2m)')
|
|
1149
|
+
.option('--until <duration>', 'Only include messages older than the duration from now')
|
|
1150
|
+
.option('--follow', 'Continuously print new console messages as JSON lines', false)
|
|
1151
|
+
.description('Read recent browser console messages')
|
|
1152
|
+
.action(browserAction(async (page, opts) => {
|
|
1153
|
+
const sinceMs = parseDurationMs(opts.since, 'since');
|
|
1154
|
+
const untilMs = parseDurationMs(opts.until, 'until');
|
|
1155
|
+
if (sinceMs && typeof sinceMs === 'object') {
|
|
1156
|
+
console.log(JSON.stringify({ error: { code: 'invalid_since', message: sinceMs.error } }, null, 2));
|
|
1157
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (untilMs && typeof untilMs === 'object') {
|
|
1161
|
+
console.log(JSON.stringify({ error: { code: 'invalid_until', message: untilMs.error } }, null, 2));
|
|
1162
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const normalize = (messages) => messages.map((message) => {
|
|
1166
|
+
if (message && typeof message === 'object') {
|
|
1167
|
+
const record = message;
|
|
1168
|
+
return {
|
|
1169
|
+
...record,
|
|
1170
|
+
timestamp: timestampFromRaw(record.timestamp),
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
return { type: 'log', text: String(message), timestamp: Date.now() };
|
|
1174
|
+
});
|
|
1175
|
+
const filter = (messages) => filterByTimeWindow(messages, { sinceMs, untilMs }).filter((message) => {
|
|
1176
|
+
if (opts.level === 'all')
|
|
1177
|
+
return true;
|
|
1178
|
+
const type = String(message.type ?? message.level ?? '').toLowerCase();
|
|
1179
|
+
return opts.level === 'error'
|
|
1180
|
+
? type === 'error' || type === 'warning'
|
|
1181
|
+
: type === String(opts.level).toLowerCase();
|
|
1182
|
+
});
|
|
1183
|
+
if (opts.follow) {
|
|
1184
|
+
let lastSeenTs = 0;
|
|
1185
|
+
while (true) {
|
|
1186
|
+
const messages = filter(normalize(await page.consoleMessages('all')));
|
|
1187
|
+
const next = selectFreshByTimestamp(messages, lastSeenTs);
|
|
1188
|
+
for (const message of next.fresh) {
|
|
1189
|
+
console.log(JSON.stringify({
|
|
1190
|
+
...message,
|
|
1191
|
+
timestamp: toIsoTimestamp(message.timestamp),
|
|
1192
|
+
}));
|
|
1193
|
+
}
|
|
1194
|
+
lastSeenTs = next.lastSeenTs;
|
|
1195
|
+
await new Promise((resolve) => setTimeout(resolve, FOLLOW_POLL_MS));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const messages = filter(normalize(await page.consoleMessages(opts.level)));
|
|
1199
|
+
console.log(JSON.stringify({
|
|
1200
|
+
session: getPageSession(page),
|
|
1201
|
+
captured_at: new Date().toISOString(),
|
|
1202
|
+
count: messages.length,
|
|
1203
|
+
messages: messages.map((message) => ({
|
|
1204
|
+
...message,
|
|
1205
|
+
timestamp: toIsoTimestamp(message.timestamp),
|
|
1206
|
+
})),
|
|
1207
|
+
}, null, 2));
|
|
1208
|
+
}));
|
|
1209
|
+
// ── Analyze (site recon, agent-native) ──
|
|
1210
|
+
//
|
|
1211
|
+
// Mechanizes the `site-recon.md` decision tree into one CLI call. The agent
|
|
1212
|
+
// calls `browser analyze <url>` and gets back:
|
|
1213
|
+
//
|
|
1214
|
+
// - pattern: A/B/C/D (mapped from network + SSR-globals signals)
|
|
1215
|
+
// - anti_bot: vendor + evidence + the one-liner for "what to do next"
|
|
1216
|
+
// - api_candidates: captured endpoints scored as real data vs telemetry
|
|
1217
|
+
// - initial_state: which window globals are populated
|
|
1218
|
+
// - nearest_adapter: existing commands for the same site, if any
|
|
1219
|
+
// - recommended_next_step: a single imperative sentence
|
|
1220
|
+
//
|
|
1221
|
+
// Intent: replace the "open → eyeball network → curl → WAF → try again"
|
|
1222
|
+
// feedback loop with a single deterministic verdict. Without this, agents
|
|
1223
|
+
// burn ~20min per WAF-protected site re-discovering anti-bot posture.
|
|
1224
|
+
addBrowserTabOption(browser.command('analyze').argument('<url>'))
|
|
1225
|
+
.description('Classify site: anti-bot vendor, real-data API candidates, pattern (A/B/C/D), nearest adapter, next step')
|
|
1226
|
+
.action(browserAction(async (page, url) => {
|
|
1227
|
+
const hasSessionCapture = await page.startNetworkCapture?.() ?? false;
|
|
1228
|
+
await page.goto(url);
|
|
1229
|
+
await page.wait(2);
|
|
1230
|
+
if (!hasSessionCapture) {
|
|
1231
|
+
try {
|
|
1232
|
+
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
1233
|
+
}
|
|
1234
|
+
catch { /* non-fatal */ }
|
|
1235
|
+
}
|
|
1236
|
+
await captureNetworkItems(page);
|
|
1237
|
+
// Best-effort: give the page another beat so XHR after DOMContentLoaded lands.
|
|
1238
|
+
await page.wait(1);
|
|
1239
|
+
const rawItems = await captureNetworkItems(page);
|
|
1240
|
+
const networkEntries = rawItems.map((e) => ({
|
|
1241
|
+
url: e.url,
|
|
1242
|
+
status: e.status,
|
|
1243
|
+
contentType: e.ct,
|
|
1244
|
+
bodyPreview: typeof e.body === 'string'
|
|
1245
|
+
? e.body.slice(0, 2000)
|
|
1246
|
+
: (e.body ? JSON.stringify(e.body).slice(0, 2000) : null),
|
|
1247
|
+
}));
|
|
1248
|
+
const probeJs = `(function(){
|
|
1249
|
+
return {
|
|
1250
|
+
cookieNames: (document.cookie || '').split(';').map(function(c){ return c.trim().split('=')[0]; }).filter(Boolean),
|
|
1251
|
+
initialState: {
|
|
1252
|
+
__INITIAL_STATE__: typeof window.__INITIAL_STATE__ !== 'undefined',
|
|
1253
|
+
__NUXT__: typeof window.__NUXT__ !== 'undefined',
|
|
1254
|
+
__NEXT_DATA__: typeof window.__NEXT_DATA__ !== 'undefined',
|
|
1255
|
+
__APOLLO_STATE__: typeof window.__APOLLO_STATE__ !== 'undefined',
|
|
1256
|
+
},
|
|
1257
|
+
title: document.title || '',
|
|
1258
|
+
finalUrl: location.href,
|
|
1259
|
+
};
|
|
1260
|
+
})()`;
|
|
1261
|
+
const probe = await page.evaluate(probeJs);
|
|
1262
|
+
const browserCookieNames = (await page.getCookies({ url: probe.finalUrl || url }).catch(() => []))
|
|
1263
|
+
.map((c) => c.name)
|
|
1264
|
+
.filter(Boolean);
|
|
1265
|
+
const cookieNames = [...new Set([...probe.cookieNames, ...browserCookieNames])];
|
|
1266
|
+
const signals = {
|
|
1267
|
+
requestedUrl: url,
|
|
1268
|
+
finalUrl: probe.finalUrl,
|
|
1269
|
+
cookieNames,
|
|
1270
|
+
networkEntries,
|
|
1271
|
+
initialState: probe.initialState,
|
|
1272
|
+
title: probe.title,
|
|
1273
|
+
};
|
|
1274
|
+
const report = analyzeSite(signals, getRegistry());
|
|
1275
|
+
const sitemap = resolveSitemapAvailabilityForUrl(probe.finalUrl || url);
|
|
1276
|
+
console.log(JSON.stringify({
|
|
1277
|
+
...report,
|
|
1278
|
+
...(sitemap ? { sitemap } : {}),
|
|
1279
|
+
}, null, 2));
|
|
1280
|
+
}));
|
|
1281
|
+
// ── Find (structured CSS query, agent-native) ──
|
|
1282
|
+
//
|
|
1283
|
+
// `browser find --css <sel>` lets agents jump straight from a semantic
|
|
1284
|
+
// selector to a JSON list of matching elements, without having to parse
|
|
1285
|
+
// the free-text state snapshot to recover indices.
|
|
1286
|
+
const addSemanticLocatorOptions = (cmd) => cmd
|
|
1287
|
+
.option('--role <role>', 'Semantic role (button, link, textbox, option, etc.)')
|
|
1288
|
+
.option('--name <text>', 'Accessible name contains text (aria-label, label, title, placeholder, or visible text)')
|
|
1289
|
+
.option('--label <text>', 'Associated label contains text')
|
|
1290
|
+
.option('--text <text>', 'Visible text contains text')
|
|
1291
|
+
.option('--testid <id>', 'data-testid / data-test / test-id contains id');
|
|
1292
|
+
const addPrefixedSemanticLocatorOptions = (cmd, prefix) => cmd
|
|
1293
|
+
.option(`--${prefix}-role <role>`, `${prefix} semantic role`)
|
|
1294
|
+
.option(`--${prefix}-name <text>`, `${prefix} accessible name contains text`)
|
|
1295
|
+
.option(`--${prefix}-label <text>`, `${prefix} associated label contains text`)
|
|
1296
|
+
.option(`--${prefix}-text <text>`, `${prefix} visible text contains text`)
|
|
1297
|
+
.option(`--${prefix}-testid <id>`, `${prefix} data-testid / data-test / test-id contains id`);
|
|
1298
|
+
const semanticLocatorFromOptions = (opts) => {
|
|
1299
|
+
const locator = {};
|
|
1300
|
+
for (const key of ['role', 'name', 'label', 'text', 'testid']) {
|
|
1301
|
+
const value = opts[key];
|
|
1302
|
+
if (typeof value === 'string' && value.trim())
|
|
1303
|
+
locator[key] = value.trim();
|
|
1304
|
+
}
|
|
1305
|
+
return Object.keys(locator).length > 0 ? locator : null;
|
|
1306
|
+
};
|
|
1307
|
+
const prefixedSemanticLocatorFromOptions = (opts, prefix) => {
|
|
1308
|
+
const locator = {};
|
|
1309
|
+
const map = {
|
|
1310
|
+
role: `${prefix}Role`,
|
|
1311
|
+
name: `${prefix}Name`,
|
|
1312
|
+
label: `${prefix}Label`,
|
|
1313
|
+
text: `${prefix}Text`,
|
|
1314
|
+
testid: `${prefix}Testid`,
|
|
1315
|
+
};
|
|
1316
|
+
for (const key of ['role', 'name', 'label', 'text', 'testid']) {
|
|
1317
|
+
const value = opts[map[key]];
|
|
1318
|
+
if (typeof value === 'string' && value.trim())
|
|
1319
|
+
locator[key] = value.trim();
|
|
1320
|
+
}
|
|
1321
|
+
return Object.keys(locator).length > 0 ? locator : null;
|
|
1322
|
+
};
|
|
1323
|
+
const semanticTargetFromLocator = async (page, locator, mode) => {
|
|
1324
|
+
const result = await page.evaluate(buildSemanticFindJs({ ...locator, limit: 6 }));
|
|
1325
|
+
if (isFindError(result))
|
|
1326
|
+
return result;
|
|
1327
|
+
if (mode === 'write' && result.matches_n !== 1) {
|
|
1328
|
+
return {
|
|
1329
|
+
error: {
|
|
1330
|
+
code: 'semantic_ambiguous',
|
|
1331
|
+
message: `Semantic locator matched ${result.matches_n} elements; write actions require a unique target.`,
|
|
1332
|
+
hint: 'Add --name/--label/--text/--testid or use browser find with a narrower locator.',
|
|
1333
|
+
matches_n: result.matches_n,
|
|
1334
|
+
entries: result.entries,
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
const first = result.entries[0];
|
|
1339
|
+
if (!first) {
|
|
1340
|
+
return {
|
|
1341
|
+
error: {
|
|
1342
|
+
code: 'semantic_not_found',
|
|
1343
|
+
message: 'Semantic locator matched 0 elements',
|
|
1344
|
+
hint: 'Try browser state, --source ax, or relax the semantic locator.',
|
|
1345
|
+
},
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
const target = String(first.ref);
|
|
1349
|
+
if (mode === 'read') {
|
|
1350
|
+
return {
|
|
1351
|
+
target,
|
|
1352
|
+
...(result.matches_n > 1 ? { total_matches: result.matches_n } : {}),
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return target;
|
|
1356
|
+
};
|
|
1357
|
+
const semanticTargetFromOptions = async (page, opts, mode) => {
|
|
1358
|
+
const locator = semanticLocatorFromOptions(opts);
|
|
1359
|
+
if (!locator)
|
|
1360
|
+
return null;
|
|
1361
|
+
return semanticTargetFromLocator(page, locator, mode);
|
|
1362
|
+
};
|
|
1363
|
+
const resolveExplicitOrSemanticTarget = async (page, target, opts, mode) => {
|
|
1364
|
+
const explicit = typeof target === 'string' && target.trim() ? target.trim() : '';
|
|
1365
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts);
|
|
1366
|
+
if (explicit && hasSemantic) {
|
|
1367
|
+
return {
|
|
1368
|
+
error: {
|
|
1369
|
+
code: 'usage_error',
|
|
1370
|
+
message: 'Pass either <target> or semantic locator flags, not both.',
|
|
1371
|
+
},
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
if (explicit)
|
|
1375
|
+
return explicit;
|
|
1376
|
+
const semantic = await semanticTargetFromOptions(page, opts, mode);
|
|
1377
|
+
if (semantic)
|
|
1378
|
+
return semantic;
|
|
1379
|
+
return {
|
|
1380
|
+
error: {
|
|
1381
|
+
code: 'usage_error',
|
|
1382
|
+
message: 'Missing target. Pass a numeric ref/CSS selector, or semantic flags like --role button --name Submit.',
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
};
|
|
1386
|
+
const printTargetResolutionError = (resolved) => {
|
|
1387
|
+
console.log(JSON.stringify(resolved, null, 2));
|
|
1388
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1389
|
+
};
|
|
1390
|
+
const resolveWriteTargetOrPrint = async (page, target, opts) => {
|
|
1391
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts, 'write');
|
|
1392
|
+
if (typeof resolvedTarget === 'string')
|
|
1393
|
+
return resolvedTarget;
|
|
1394
|
+
if ('error' in resolvedTarget)
|
|
1395
|
+
printTargetResolutionError(resolvedTarget);
|
|
1396
|
+
return null;
|
|
1397
|
+
};
|
|
1398
|
+
const resolveWriteTargetAndValueOrPrint = async (page, targetOrValue, value, opts, valueLabel) => {
|
|
1399
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts);
|
|
1400
|
+
if (hasSemantic && value !== undefined) {
|
|
1401
|
+
printTargetResolutionError({
|
|
1402
|
+
error: {
|
|
1403
|
+
code: 'usage_error',
|
|
1404
|
+
message: `When using semantic locator flags, pass only <${valueLabel}> as the positional argument.`,
|
|
1405
|
+
},
|
|
1406
|
+
});
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
const resolvedValue = hasSemantic ? targetOrValue : value;
|
|
1410
|
+
if (resolvedValue === undefined) {
|
|
1411
|
+
printTargetResolutionError({
|
|
1412
|
+
error: {
|
|
1413
|
+
code: 'usage_error',
|
|
1414
|
+
message: `Missing ${valueLabel}.`,
|
|
1415
|
+
hint: hasSemantic
|
|
1416
|
+
? `With semantic locator flags, pass the ${valueLabel} as the only positional argument.`
|
|
1417
|
+
: `Pass both a target and ${valueLabel}.`,
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, hasSemantic ? undefined : targetOrValue, opts);
|
|
1423
|
+
if (!resolvedTarget)
|
|
1424
|
+
return null;
|
|
1425
|
+
return { target: resolvedTarget, value: String(resolvedValue) };
|
|
1426
|
+
};
|
|
1427
|
+
const resolvePrefixedWriteTargetOrPrint = async (page, target, opts, prefix, label) => {
|
|
1428
|
+
const explicit = typeof target === 'string' && target.trim() ? target.trim() : '';
|
|
1429
|
+
const locator = prefixedSemanticLocatorFromOptions(opts, prefix);
|
|
1430
|
+
if (explicit && locator) {
|
|
1431
|
+
printTargetResolutionError({
|
|
1432
|
+
error: {
|
|
1433
|
+
code: 'usage_error',
|
|
1434
|
+
message: `Pass either <${label}> or --${prefix}-* semantic locator flags, not both.`,
|
|
1435
|
+
},
|
|
1436
|
+
});
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
if (explicit)
|
|
1440
|
+
return explicit;
|
|
1441
|
+
if (locator) {
|
|
1442
|
+
const resolved = await semanticTargetFromLocator(page, locator, 'write');
|
|
1443
|
+
if (typeof resolved === 'string')
|
|
1444
|
+
return resolved;
|
|
1445
|
+
if ('error' in resolved)
|
|
1446
|
+
printTargetResolutionError(resolved);
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
printTargetResolutionError({
|
|
1450
|
+
error: {
|
|
1451
|
+
code: 'usage_error',
|
|
1452
|
+
message: `Missing ${label}. Pass a numeric ref/CSS selector, or --${prefix}-role/--${prefix}-name semantic flags.`,
|
|
1453
|
+
},
|
|
1454
|
+
});
|
|
1455
|
+
return null;
|
|
1456
|
+
};
|
|
1457
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('find'))
|
|
1458
|
+
.option('--css <selector>', 'CSS selector (required)')
|
|
1459
|
+
.option('--limit <n>', 'Max entries returned', '50')
|
|
1460
|
+
.option('--text-max <n>', 'Max chars of trimmed text per entry', '120')
|
|
1461
|
+
.description('Find DOM elements by CSS or semantic locator — returns JSON {matches_n, entries[]}'))
|
|
1462
|
+
.action(browserAction(async (page, opts) => {
|
|
1463
|
+
const locator = semanticLocatorFromOptions(opts);
|
|
1464
|
+
if ((!opts.css || typeof opts.css !== 'string') && !locator) {
|
|
1465
|
+
console.log(JSON.stringify({
|
|
1466
|
+
error: {
|
|
1467
|
+
code: 'usage_error',
|
|
1468
|
+
message: '--css <selector> or a semantic locator flag is required',
|
|
1469
|
+
hint: 'Examples: opencli browser find --css ".btn.primary"; opencli browser find --role button --name Save',
|
|
1470
|
+
},
|
|
1471
|
+
}, null, 2));
|
|
1472
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (opts.css && locator) {
|
|
1476
|
+
console.log(JSON.stringify({
|
|
1477
|
+
error: {
|
|
1478
|
+
code: 'usage_error',
|
|
1479
|
+
message: 'Pass either --css or semantic locator flags, not both.',
|
|
1480
|
+
},
|
|
1481
|
+
}, null, 2));
|
|
1482
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const limit = parseNthFlag(opts.limit);
|
|
1486
|
+
if (limit && typeof limit === 'object' && 'error' in limit) {
|
|
1487
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: limit.error.replace('--nth', '--limit') } }, null, 2));
|
|
1488
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const textMax = parseNthFlag(opts.textMax);
|
|
1492
|
+
if (textMax && typeof textMax === 'object' && 'error' in textMax) {
|
|
1493
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: textMax.error.replace('--nth', '--text-max') } }, null, 2));
|
|
1494
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const result = await page.evaluate(locator
|
|
1498
|
+
? buildSemanticFindJs({
|
|
1499
|
+
...locator,
|
|
1500
|
+
limit: limit ?? undefined,
|
|
1501
|
+
textMax: textMax ?? undefined,
|
|
1502
|
+
})
|
|
1503
|
+
: buildFindJs(opts.css, {
|
|
1504
|
+
limit: limit ?? undefined,
|
|
1505
|
+
textMax: textMax ?? undefined,
|
|
1506
|
+
}));
|
|
1507
|
+
if (isFindError(result)) {
|
|
1508
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1509
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1513
|
+
}));
|
|
1514
|
+
// ── Get commands (structured data extraction) ──
|
|
1515
|
+
const get = browser.command('get').description('Get page properties');
|
|
1516
|
+
addBrowserTabOption(get.command('title').description('Page title'))
|
|
1517
|
+
.action(browserAction(async (page) => {
|
|
1518
|
+
console.log(await page.evaluate('document.title'));
|
|
1519
|
+
}));
|
|
1520
|
+
addBrowserTabOption(get.command('url').description('Current page URL'))
|
|
1521
|
+
.action(browserAction(async (page) => {
|
|
1522
|
+
console.log(await page.getCurrentUrl?.() ?? await page.evaluate('location.href'));
|
|
1523
|
+
}));
|
|
1524
|
+
// Read commands (`get text/value/attributes`) always emit a JSON envelope:
|
|
1525
|
+
//
|
|
1526
|
+
// { value, matches_n } — success
|
|
1527
|
+
// { error: { code, message, hint, matches_n? } } — structured failure
|
|
1528
|
+
//
|
|
1529
|
+
// `<target>` accepts either a numeric ref (from `browser state`/`browser find`)
|
|
1530
|
+
// or a CSS selector. On multi-match CSS, the first element wins and the real
|
|
1531
|
+
// match count is exposed via `matches_n`; `--nth <n>` picks a specific one.
|
|
1532
|
+
const runGetCommand = async (page, target, opts, evalJs, field) => {
|
|
1533
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts, 'read');
|
|
1534
|
+
if (typeof resolvedTarget !== 'string' && 'error' in resolvedTarget) {
|
|
1535
|
+
console.log(JSON.stringify(resolvedTarget, null, 2));
|
|
1536
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const targetRef = typeof resolvedTarget === 'string' ? resolvedTarget : resolvedTarget.target;
|
|
1540
|
+
const totalMatches = typeof resolvedTarget === 'string' ? undefined : resolvedTarget.total_matches;
|
|
1541
|
+
const nth = parseNthFlag(opts.nth);
|
|
1542
|
+
if (nth && typeof nth === 'object' && 'error' in nth) {
|
|
1543
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: nth.error } }, null, 2));
|
|
1544
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const { matches_n, match_level } = await resolveRef(page, targetRef, {
|
|
1548
|
+
firstOnMulti: nth === null,
|
|
1549
|
+
...(typeof nth === 'number' ? { nth } : {}),
|
|
1550
|
+
});
|
|
1551
|
+
const raw = await page.evaluate(evalJs);
|
|
1552
|
+
let value;
|
|
1553
|
+
if (field === 'attributes') {
|
|
1554
|
+
// getAttributesResolvedJs stringifies the attribute record — parse it back so
|
|
1555
|
+
// the JSON envelope contains a real object rather than a nested JSON string.
|
|
1556
|
+
try {
|
|
1557
|
+
value = raw == null ? {} : JSON.parse(String(raw));
|
|
1558
|
+
}
|
|
1559
|
+
catch {
|
|
1560
|
+
value = raw;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
1564
|
+
value = raw ?? null;
|
|
1565
|
+
}
|
|
1566
|
+
console.log(JSON.stringify({
|
|
1567
|
+
value,
|
|
1568
|
+
matches_n,
|
|
1569
|
+
match_level,
|
|
1570
|
+
...(totalMatches && totalMatches > 1 ? { total_matches: totalMatches } : {}),
|
|
1571
|
+
}, null, 2));
|
|
1572
|
+
};
|
|
1573
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('text'))
|
|
1574
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1575
|
+
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1576
|
+
.description('Element text content — JSON envelope {value, matches_n}'))
|
|
1577
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getTextResolvedJs(), 'text')));
|
|
1578
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('value'))
|
|
1579
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1580
|
+
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1581
|
+
.description('Input/textarea value — JSON envelope {value, matches_n}'))
|
|
1582
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getValueResolvedJs(), 'value')));
|
|
1583
|
+
addBrowserTabOption(get.command('html')
|
|
1584
|
+
.option('--selector <css>', 'CSS selector scope (first match)')
|
|
1585
|
+
.option('--as <format>', 'Output format: "html" (default) or "json" for structured tree', 'html')
|
|
1586
|
+
.option('--max <n>', 'Max characters of raw HTML to return (0 = unlimited)', '0')
|
|
1587
|
+
.option('--depth <n>', '(--as json) Max tree depth below root (0 = root only, 0 disables = unlimited via empty)', '')
|
|
1588
|
+
.option('--children-max <n>', '(--as json) Max element children kept per node (empty = unlimited)', '')
|
|
1589
|
+
.option('--text-max <n>', '(--as json) Max chars of direct text kept per node (empty = unlimited)', '')
|
|
1590
|
+
.description('Page HTML (or scoped); use --as json for a {tag, attrs, text, children} tree'))
|
|
1591
|
+
.action(browserAction(async (page, opts) => {
|
|
1592
|
+
const format = String(opts.as || 'html').toLowerCase();
|
|
1593
|
+
if (format !== 'html' && format !== 'json') {
|
|
1594
|
+
console.log(JSON.stringify({ error: { code: 'invalid_format', message: `--as must be "html" or "json", got "${opts.as}"` } }, null, 2));
|
|
1595
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
// `--max` is validated up-front (before touching the page) so a bad value
|
|
1599
|
+
// gets the same structured error regardless of selector/format path.
|
|
1600
|
+
const rawMax = String(opts.max ?? '0');
|
|
1601
|
+
if (!/^\d+$/.test(rawMax)) {
|
|
1602
|
+
console.log(JSON.stringify({ error: { code: 'invalid_max', message: `--max must be a non-negative integer, got "${opts.max}"` } }, null, 2));
|
|
1603
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
const max = Number.parseInt(rawMax, 10);
|
|
1607
|
+
if (format === 'json') {
|
|
1608
|
+
const parseBudget = (flag, value) => {
|
|
1609
|
+
const raw = value === undefined || value === null ? '' : String(value);
|
|
1610
|
+
if (raw === '')
|
|
1611
|
+
return null;
|
|
1612
|
+
if (!/^\d+$/.test(raw))
|
|
1613
|
+
return { error: `${flag} must be a non-negative integer, got "${raw}"` };
|
|
1614
|
+
return Number.parseInt(raw, 10);
|
|
1615
|
+
};
|
|
1616
|
+
const depth = parseBudget('--depth', opts.depth);
|
|
1617
|
+
const childrenMax = parseBudget('--children-max', opts.childrenMax);
|
|
1618
|
+
const textMax = parseBudget('--text-max', opts.textMax);
|
|
1619
|
+
for (const budget of [depth, childrenMax, textMax]) {
|
|
1620
|
+
if (budget && typeof budget === 'object' && 'error' in budget) {
|
|
1621
|
+
console.log(JSON.stringify({ error: { code: 'invalid_budget', message: budget.error } }, null, 2));
|
|
1622
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const js = buildHtmlTreeJs({
|
|
1627
|
+
selector: opts.selector ?? null,
|
|
1628
|
+
depth: depth,
|
|
1629
|
+
childrenMax: childrenMax,
|
|
1630
|
+
textMax: textMax,
|
|
1631
|
+
});
|
|
1632
|
+
const result = await page.evaluate(js);
|
|
1633
|
+
if (result && typeof result === 'object' && 'invalidSelector' in result && result.invalidSelector) {
|
|
1634
|
+
console.log(JSON.stringify({
|
|
1635
|
+
error: { code: 'invalid_selector', message: `Selector "${opts.selector}" is not a valid CSS selector: ${result.reason}` },
|
|
1636
|
+
}, null, 2));
|
|
1637
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const ok = result;
|
|
1641
|
+
if (!ok || ok.matched === 0) {
|
|
1642
|
+
console.log(JSON.stringify({
|
|
1643
|
+
error: {
|
|
1644
|
+
code: 'selector_not_found',
|
|
1645
|
+
message: opts.selector
|
|
1646
|
+
? `Selector "${opts.selector}" matched 0 elements.`
|
|
1647
|
+
: 'Page has no documentElement.',
|
|
1648
|
+
},
|
|
1649
|
+
}, null, 2));
|
|
1650
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
console.log(JSON.stringify(ok, null, 2));
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
// Raw HTML path — unbounded by default; --max optionally caps with a visible marker.
|
|
1657
|
+
// Selector lookup is wrapped in try/catch inside page context so an invalid
|
|
1658
|
+
// selector returns a structured signal instead of throwing through page.evaluate.
|
|
1659
|
+
const sel = opts.selector ? JSON.stringify(opts.selector) : 'null';
|
|
1660
|
+
const rawResult = await page.evaluate(`(() => {
|
|
1661
|
+
const s = ${sel};
|
|
1662
|
+
if (s) {
|
|
1663
|
+
try {
|
|
1664
|
+
const el = document.querySelector(s);
|
|
1665
|
+
return { kind: 'ok', html: el ? el.outerHTML : null };
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
return { kind: 'invalid_selector', reason: (e && e.message) || String(e) };
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return { kind: 'ok', html: document.documentElement ? document.documentElement.outerHTML : null };
|
|
1671
|
+
})()`);
|
|
1672
|
+
if (rawResult.kind === 'invalid_selector') {
|
|
1673
|
+
console.log(JSON.stringify({
|
|
1674
|
+
error: { code: 'invalid_selector', message: `Selector "${opts.selector}" is not a valid CSS selector: ${rawResult.reason}` },
|
|
1675
|
+
}, null, 2));
|
|
1676
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const html = rawResult.html;
|
|
1680
|
+
if (html === null) {
|
|
1681
|
+
if (opts.selector) {
|
|
1682
|
+
console.log(JSON.stringify({
|
|
1683
|
+
error: { code: 'selector_not_found', message: `Selector "${opts.selector}" matched 0 elements.` },
|
|
1684
|
+
}, null, 2));
|
|
1685
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
console.log('(empty)');
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (max > 0 && html.length > max) {
|
|
1692
|
+
console.log(`<!-- opencli: truncated ${max} of ${html.length} chars; re-run without --max (or --max 0) for full -->\n${html.slice(0, max)}`);
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
console.log(html);
|
|
1696
|
+
}));
|
|
1697
|
+
addBrowserTabOption(addSemanticLocatorOptions(get.command('attributes'))
|
|
1698
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1699
|
+
.option('--nth <n>', 'Pick the nth match (0-based) when <target> is a multi-match CSS selector')
|
|
1700
|
+
.description('Element attributes — JSON envelope {value, matches_n}'))
|
|
1701
|
+
.action(browserAction(async (page, target, opts) => runGetCommand(page, target, opts ?? {}, getAttributesResolvedJs(), 'attributes')));
|
|
1702
|
+
// ── Interact ──
|
|
1703
|
+
//
|
|
1704
|
+
// Write commands (`click/type/select`) share the same `<target>` contract
|
|
1705
|
+
// as the read commands but *reject* multi-match CSS as `selector_ambiguous`
|
|
1706
|
+
// unless the caller passes `--nth <n>`. That asymmetry is intentional:
|
|
1707
|
+
// clicking "one of three buttons" at random is almost never what the agent
|
|
1708
|
+
// meant. Every branch emits a JSON envelope on stdout; error envelopes go
|
|
1709
|
+
// through the unified TargetError handler in browserAction.
|
|
1710
|
+
/**
|
|
1711
|
+
* Parse the `--nth` flag and convert it to `ResolveOptions`.
|
|
1712
|
+
* Returns `{ error }` when the flag was malformed (so the command can
|
|
1713
|
+
* print the structured usage error and exit) or `{ opts }` to feed
|
|
1714
|
+
* into resolveRef / page.click / page.typeText.
|
|
1715
|
+
*/
|
|
1716
|
+
function nthToResolveOpts(raw) {
|
|
1717
|
+
const parsed = parseNthFlag(raw);
|
|
1718
|
+
if (parsed && typeof parsed === 'object' && 'error' in parsed)
|
|
1719
|
+
return parsed;
|
|
1720
|
+
if (typeof parsed === 'number')
|
|
1721
|
+
return { opts: { nth: parsed } };
|
|
1722
|
+
return { opts: {} };
|
|
1723
|
+
}
|
|
1724
|
+
function resolveUploadFilePaths(rawFiles) {
|
|
1725
|
+
const inputs = Array.isArray(rawFiles) ? rawFiles : [];
|
|
1726
|
+
if (inputs.length === 0) {
|
|
1727
|
+
return {
|
|
1728
|
+
error: {
|
|
1729
|
+
code: 'usage_error',
|
|
1730
|
+
message: 'At least one file path is required.',
|
|
1731
|
+
hint: 'Example: opencli browser upload "input[type=file]" ./receipt.pdf',
|
|
1732
|
+
},
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
const files = [];
|
|
1736
|
+
for (const input of inputs) {
|
|
1737
|
+
const raw = String(input);
|
|
1738
|
+
const expanded = raw === '~' || raw.startsWith(`~${path.sep}`)
|
|
1739
|
+
? path.join(os.homedir(), raw.slice(2))
|
|
1740
|
+
: raw;
|
|
1741
|
+
const resolved = path.resolve(expanded);
|
|
1742
|
+
if (!fs.existsSync(resolved)) {
|
|
1743
|
+
return { error: { code: 'file_not_found', message: `File not found: ${resolved}` } };
|
|
1744
|
+
}
|
|
1745
|
+
const stat = fs.statSync(resolved);
|
|
1746
|
+
if (!stat.isFile()) {
|
|
1747
|
+
return { error: { code: 'not_a_file', message: `Not a regular file: ${resolved}` } };
|
|
1748
|
+
}
|
|
1749
|
+
files.push(resolved);
|
|
1750
|
+
}
|
|
1751
|
+
return { files };
|
|
1752
|
+
}
|
|
1753
|
+
function parseResolveFlag(raw, flag) {
|
|
1754
|
+
const parsed = parseNthFlag(raw);
|
|
1755
|
+
if (parsed && typeof parsed === 'object' && 'error' in parsed) {
|
|
1756
|
+
return { error: parsed.error.replace('--nth', flag) };
|
|
1757
|
+
}
|
|
1758
|
+
if (typeof parsed === 'number')
|
|
1759
|
+
return { opts: { nth: parsed } };
|
|
1760
|
+
return { opts: {} };
|
|
1761
|
+
}
|
|
1762
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('click'))
|
|
1763
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1764
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1765
|
+
.description('Click element — JSON envelope {clicked, target, matches_n}'))
|
|
1766
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1767
|
+
const resolvedTarget = await resolveExplicitOrSemanticTarget(page, target, opts ?? {}, 'write');
|
|
1768
|
+
if (typeof resolvedTarget !== 'string') {
|
|
1769
|
+
console.log(JSON.stringify(resolvedTarget, null, 2));
|
|
1770
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1774
|
+
if ('error' in parsed) {
|
|
1775
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1776
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const { matches_n, match_level } = await page.click(resolvedTarget, parsed.opts);
|
|
1780
|
+
console.log(JSON.stringify({ clicked: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1781
|
+
}));
|
|
1782
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('type'))
|
|
1783
|
+
.argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
|
|
1784
|
+
.argument('[text]', 'Text to type')
|
|
1785
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1786
|
+
.description('Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}'))
|
|
1787
|
+
.action(browserAction(async (page, targetOrText, text, opts) => {
|
|
1788
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrText, text, opts ?? {}, 'text');
|
|
1789
|
+
if (!resolved)
|
|
1790
|
+
return;
|
|
1791
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1792
|
+
if ('error' in parsed) {
|
|
1793
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1794
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
// Click first (focuses the field), wait briefly, then type.
|
|
1798
|
+
await page.click(resolved.target, parsed.opts);
|
|
1799
|
+
await page.wait(0.3);
|
|
1800
|
+
const { matches_n, match_level } = await page.typeText(resolved.target, resolved.value, parsed.opts);
|
|
1801
|
+
// __resolved is already set by the resolver call inside page.typeText
|
|
1802
|
+
const isAutocomplete = await page.evaluate(isAutocompleteResolvedJs());
|
|
1803
|
+
if (isAutocomplete)
|
|
1804
|
+
await page.wait(0.4);
|
|
1805
|
+
console.log(JSON.stringify({
|
|
1806
|
+
typed: true,
|
|
1807
|
+
text: resolved.value,
|
|
1808
|
+
target: resolved.target,
|
|
1809
|
+
matches_n,
|
|
1810
|
+
match_level,
|
|
1811
|
+
autocomplete: !!isAutocomplete,
|
|
1812
|
+
}, null, 2));
|
|
1813
|
+
}));
|
|
1814
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('hover'))
|
|
1815
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1816
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1817
|
+
.description('Move the mouse over an element — JSON envelope {hovered, target, matches_n}'))
|
|
1818
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1819
|
+
if (typeof page.hover !== 'function')
|
|
1820
|
+
throw new Error('browser hover is not supported by this browser backend');
|
|
1821
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1822
|
+
if (!resolvedTarget)
|
|
1823
|
+
return;
|
|
1824
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1825
|
+
if ('error' in parsed) {
|
|
1826
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1827
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
const { matches_n, match_level } = await page.hover(resolvedTarget, parsed.opts);
|
|
1831
|
+
console.log(JSON.stringify({ hovered: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1832
|
+
}));
|
|
1833
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('focus'))
|
|
1834
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1835
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1836
|
+
.description('Focus an element — JSON envelope {focused, target, matches_n}'))
|
|
1837
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1838
|
+
if (typeof page.focus !== 'function')
|
|
1839
|
+
throw new Error('browser focus is not supported by this browser backend');
|
|
1840
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1841
|
+
if (!resolvedTarget)
|
|
1842
|
+
return;
|
|
1843
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1844
|
+
if ('error' in parsed) {
|
|
1845
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1846
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const { focused, matches_n, match_level } = await page.focus(resolvedTarget, parsed.opts);
|
|
1850
|
+
console.log(JSON.stringify({ focused, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1851
|
+
}));
|
|
1852
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('dblclick'))
|
|
1853
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1854
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1855
|
+
.description('Double-click element — JSON envelope {dblclicked, target, matches_n}'))
|
|
1856
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1857
|
+
if (typeof page.dblClick !== 'function')
|
|
1858
|
+
throw new Error('browser dblclick is not supported by this browser backend');
|
|
1859
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1860
|
+
if (!resolvedTarget)
|
|
1861
|
+
return;
|
|
1862
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1863
|
+
if ('error' in parsed) {
|
|
1864
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1865
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const { matches_n, match_level } = await page.dblClick(resolvedTarget, parsed.opts);
|
|
1869
|
+
console.log(JSON.stringify({ dblclicked: true, target: resolvedTarget, matches_n, match_level }, null, 2));
|
|
1870
|
+
}));
|
|
1871
|
+
const runCheckCommand = async (page, target, opts, checked) => {
|
|
1872
|
+
if (typeof page.setChecked !== 'function')
|
|
1873
|
+
throw new Error(`browser ${checked ? 'check' : 'uncheck'} is not supported by this browser backend`);
|
|
1874
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts);
|
|
1875
|
+
if (!resolvedTarget)
|
|
1876
|
+
return;
|
|
1877
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1878
|
+
if ('error' in parsed) {
|
|
1879
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1880
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
const result = await page.setChecked(resolvedTarget, checked, parsed.opts);
|
|
1884
|
+
console.log(JSON.stringify({
|
|
1885
|
+
checked: result.checked,
|
|
1886
|
+
changed: result.changed,
|
|
1887
|
+
target: resolvedTarget,
|
|
1888
|
+
matches_n: result.matches_n,
|
|
1889
|
+
match_level: result.match_level,
|
|
1890
|
+
...(result.kind ? { kind: result.kind } : {}),
|
|
1891
|
+
}, null, 2));
|
|
1892
|
+
};
|
|
1893
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('check'))
|
|
1894
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1895
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1896
|
+
.description('Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}'))
|
|
1897
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1898
|
+
await runCheckCommand(page, target, opts ?? {}, true);
|
|
1899
|
+
}));
|
|
1900
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('uncheck'))
|
|
1901
|
+
.argument('[target]', 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.')
|
|
1902
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1903
|
+
.description('Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}'))
|
|
1904
|
+
.action(browserAction(async (page, target, opts) => {
|
|
1905
|
+
await runCheckCommand(page, target, opts ?? {}, false);
|
|
1906
|
+
}));
|
|
1907
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('upload'))
|
|
1908
|
+
.argument('[targetOrFile]', 'Numeric ref/CSS target, or first file when using --role/--name/etc.')
|
|
1909
|
+
.argument('[files...]', 'Local file path(s) to attach')
|
|
1910
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1911
|
+
.description('Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}'))
|
|
1912
|
+
.action(browserAction(async (page, targetOrFile, files, opts) => {
|
|
1913
|
+
if (typeof page.uploadFiles !== 'function')
|
|
1914
|
+
throw new Error('browser upload is not supported by this browser backend');
|
|
1915
|
+
const hasSemantic = !!semanticLocatorFromOptions(opts ?? {});
|
|
1916
|
+
const target = hasSemantic ? undefined : targetOrFile;
|
|
1917
|
+
const resolvedTarget = await resolveWriteTargetOrPrint(page, target, opts ?? {});
|
|
1918
|
+
if (!resolvedTarget)
|
|
1919
|
+
return;
|
|
1920
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1921
|
+
if ('error' in parsed) {
|
|
1922
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1923
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const rawFiles = hasSemantic
|
|
1927
|
+
? [targetOrFile, ...(Array.isArray(files) ? files : [])].filter((value) => value !== undefined)
|
|
1928
|
+
: files;
|
|
1929
|
+
const resolvedFiles = resolveUploadFilePaths(rawFiles);
|
|
1930
|
+
if ('error' in resolvedFiles) {
|
|
1931
|
+
console.log(JSON.stringify({ error: resolvedFiles.error }, null, 2));
|
|
1932
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const result = await page.uploadFiles(resolvedTarget, resolvedFiles.files, parsed.opts);
|
|
1936
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1937
|
+
}));
|
|
1938
|
+
addBrowserTabOption(addPrefixedSemanticLocatorOptions(addPrefixedSemanticLocatorOptions(browser.command('drag'), 'from'), 'to')
|
|
1939
|
+
.argument('[source]', 'Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.')
|
|
1940
|
+
.argument('[target]', 'Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.')
|
|
1941
|
+
.option('--from-nth <n>', 'When <source> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1942
|
+
.option('--to-nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1943
|
+
.description('Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}'))
|
|
1944
|
+
.action(browserAction(async (page, source, target, opts) => {
|
|
1945
|
+
if (typeof page.drag !== 'function')
|
|
1946
|
+
throw new Error('browser drag is not supported by this browser backend');
|
|
1947
|
+
const resolvedSource = await resolvePrefixedWriteTargetOrPrint(page, source, opts ?? {}, 'from', 'source');
|
|
1948
|
+
if (!resolvedSource)
|
|
1949
|
+
return;
|
|
1950
|
+
const resolvedTarget = await resolvePrefixedWriteTargetOrPrint(page, target, opts ?? {}, 'to', 'target');
|
|
1951
|
+
if (!resolvedTarget)
|
|
1952
|
+
return;
|
|
1953
|
+
const from = parseResolveFlag(opts?.fromNth, '--from-nth');
|
|
1954
|
+
if ('error' in from) {
|
|
1955
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: from.error } }, null, 2));
|
|
1956
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const to = parseResolveFlag(opts?.toNth, '--to-nth');
|
|
1960
|
+
if ('error' in to) {
|
|
1961
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: to.error } }, null, 2));
|
|
1962
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const result = await page.drag(resolvedSource, resolvedTarget, { from: from.opts, to: to.opts });
|
|
1966
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1967
|
+
}));
|
|
1968
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('fill'))
|
|
1969
|
+
.argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
|
|
1970
|
+
.argument('[text]', 'Text to set exactly')
|
|
1971
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
1972
|
+
.description('Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}'))
|
|
1973
|
+
.action(browserAction(async (page, targetOrText, text, opts) => {
|
|
1974
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrText, text, opts ?? {}, 'text');
|
|
1975
|
+
if (!resolved)
|
|
1976
|
+
return;
|
|
1977
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
1978
|
+
if ('error' in parsed) {
|
|
1979
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
1980
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
const result = await page.fillText(resolved.target, resolved.value, parsed.opts);
|
|
1984
|
+
if (!result.verified)
|
|
1985
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
1986
|
+
console.log(JSON.stringify({
|
|
1987
|
+
filled: result.filled,
|
|
1988
|
+
verified: result.verified,
|
|
1989
|
+
target: resolved.target,
|
|
1990
|
+
text: resolved.value,
|
|
1991
|
+
actual: result.actual,
|
|
1992
|
+
length: result.length,
|
|
1993
|
+
matches_n: result.matches_n,
|
|
1994
|
+
match_level: result.match_level,
|
|
1995
|
+
...(result.mode ? { mode: result.mode } : {}),
|
|
1996
|
+
}, null, 2));
|
|
1997
|
+
}));
|
|
1998
|
+
addBrowserTabOption(addSemanticLocatorOptions(browser.command('select'))
|
|
1999
|
+
.argument('[targetOrOption]', 'Numeric ref/CSS target, or option text when using --role/--name/etc.')
|
|
2000
|
+
.argument('[option]', 'Option text (or value) to select')
|
|
2001
|
+
.option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
|
|
2002
|
+
.description('Select dropdown option — JSON envelope {selected, target, matches_n}'))
|
|
2003
|
+
.action(browserAction(async (page, targetOrOption, option, opts) => {
|
|
2004
|
+
const resolved = await resolveWriteTargetAndValueOrPrint(page, targetOrOption, option, opts ?? {}, 'option');
|
|
2005
|
+
if (!resolved)
|
|
2006
|
+
return;
|
|
2007
|
+
const parsed = nthToResolveOpts(opts?.nth);
|
|
2008
|
+
if ('error' in parsed) {
|
|
2009
|
+
console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
|
|
2010
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const { matches_n, match_level } = await resolveRef(page, resolved.target, parsed.opts);
|
|
2014
|
+
const result = await page.evaluate(selectResolvedJs(resolved.value));
|
|
2015
|
+
if (result?.error) {
|
|
2016
|
+
// The select-specific "Not a <select>" / "Option not found" errors
|
|
2017
|
+
// are domain-level failures — emit a structured envelope so agents
|
|
2018
|
+
// can branch on code rather than scrape a log line.
|
|
2019
|
+
console.log(JSON.stringify({
|
|
2020
|
+
error: {
|
|
2021
|
+
code: result.error === 'Not a <select>' ? 'not_a_select' : 'option_not_found',
|
|
2022
|
+
message: result.error,
|
|
2023
|
+
...(result.available && { available: result.available }),
|
|
2024
|
+
matches_n,
|
|
2025
|
+
},
|
|
2026
|
+
}, null, 2));
|
|
2027
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
console.log(JSON.stringify({
|
|
2031
|
+
selected: result?.selected ?? resolved.value,
|
|
2032
|
+
target: resolved.target,
|
|
2033
|
+
matches_n,
|
|
2034
|
+
match_level,
|
|
2035
|
+
}, null, 2));
|
|
2036
|
+
}));
|
|
2037
|
+
addBrowserTabOption(browser.command('keys').argument('<key>', 'Key to press (Enter, Escape, Tab, Control+a)'))
|
|
2038
|
+
.description('Press keyboard key')
|
|
2039
|
+
.action(browserAction(async (page, key) => {
|
|
2040
|
+
await page.pressKey(key);
|
|
2041
|
+
console.log(`Pressed: ${key}`);
|
|
2042
|
+
}));
|
|
2043
|
+
const browserDialog = browser
|
|
2044
|
+
.command('dialog')
|
|
2045
|
+
.description('Handle a blocking JavaScript alert/confirm/prompt dialog');
|
|
2046
|
+
addBrowserTabOption(browserDialog.command('accept')
|
|
2047
|
+
.option('--text <text>', 'Prompt text to submit for prompt() dialogs')
|
|
2048
|
+
.description('Accept the currently open JavaScript dialog'))
|
|
2049
|
+
.action(browserAction(async (page, opts) => {
|
|
2050
|
+
if (!page.handleJavaScriptDialog) {
|
|
2051
|
+
throw new Error('This browser session does not support JavaScript dialog handling');
|
|
2052
|
+
}
|
|
2053
|
+
try {
|
|
2054
|
+
await page.handleJavaScriptDialog(true, opts?.text);
|
|
2055
|
+
}
|
|
2056
|
+
catch (err) {
|
|
2057
|
+
const message = getErrorMessage(err);
|
|
2058
|
+
if (message.toLowerCase().includes('no dialog')) {
|
|
2059
|
+
console.log(JSON.stringify({
|
|
2060
|
+
error: {
|
|
2061
|
+
code: 'no_javascript_dialog',
|
|
2062
|
+
message: 'No JavaScript dialog is currently open.',
|
|
2063
|
+
},
|
|
2064
|
+
}, null, 2));
|
|
2065
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
throw err;
|
|
2069
|
+
}
|
|
2070
|
+
console.log(JSON.stringify({ handled: true, action: 'accept', ...(opts?.text !== undefined && { text: opts.text }) }, null, 2));
|
|
2071
|
+
}));
|
|
2072
|
+
addBrowserTabOption(browserDialog.command('dismiss')
|
|
2073
|
+
.description('Dismiss the currently open JavaScript dialog'))
|
|
2074
|
+
.action(browserAction(async (page) => {
|
|
2075
|
+
if (!page.handleJavaScriptDialog) {
|
|
2076
|
+
throw new Error('This browser session does not support JavaScript dialog handling');
|
|
2077
|
+
}
|
|
2078
|
+
try {
|
|
2079
|
+
await page.handleJavaScriptDialog(false);
|
|
2080
|
+
}
|
|
2081
|
+
catch (err) {
|
|
2082
|
+
const message = getErrorMessage(err);
|
|
2083
|
+
if (message.toLowerCase().includes('no dialog')) {
|
|
2084
|
+
console.log(JSON.stringify({
|
|
2085
|
+
error: {
|
|
2086
|
+
code: 'no_javascript_dialog',
|
|
2087
|
+
message: 'No JavaScript dialog is currently open.',
|
|
2088
|
+
},
|
|
2089
|
+
}, null, 2));
|
|
2090
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
throw err;
|
|
2094
|
+
}
|
|
2095
|
+
console.log(JSON.stringify({ handled: true, action: 'dismiss' }, null, 2));
|
|
2096
|
+
}));
|
|
2097
|
+
// ── Wait commands ──
|
|
2098
|
+
addBrowserTabOption(browser.command('wait'))
|
|
2099
|
+
.argument('<type>', 'selector, text, time, xhr, or download')
|
|
2100
|
+
.argument('[value]', 'CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern')
|
|
2101
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', '10000')
|
|
2102
|
+
.description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)')
|
|
2103
|
+
.action(browserAction(async (page, type, value, opts) => {
|
|
2104
|
+
const timeout = parseInt(opts.timeout, 10);
|
|
2105
|
+
if (type === 'time') {
|
|
2106
|
+
const seconds = parseFloat(value ?? '2');
|
|
2107
|
+
await page.wait(seconds);
|
|
2108
|
+
console.log(`Waited ${seconds}s`);
|
|
2109
|
+
}
|
|
2110
|
+
else if (type === 'selector') {
|
|
2111
|
+
if (!value) {
|
|
2112
|
+
console.error('Missing CSS selector');
|
|
2113
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
await page.wait({ selector: value, timeout: timeout / 1000 });
|
|
2117
|
+
console.log(`Element "${value}" appeared`);
|
|
2118
|
+
}
|
|
2119
|
+
else if (type === 'text') {
|
|
2120
|
+
if (!value) {
|
|
2121
|
+
console.error('Missing text');
|
|
2122
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
await page.wait({ text: value, timeout: timeout / 1000 });
|
|
2126
|
+
console.log(`Text "${value}" appeared`);
|
|
2127
|
+
}
|
|
2128
|
+
else if (type === 'xhr') {
|
|
2129
|
+
// Poll the capture ring until an entry matches the URL regex — turns
|
|
2130
|
+
// the common "open page, wait N seconds, hope the data landed" idiom
|
|
2131
|
+
// into a deterministic barrier keyed on the API the agent actually
|
|
2132
|
+
// cares about. Prevents silent "empty DOM" failures on slow SPAs.
|
|
2133
|
+
if (!value) {
|
|
2134
|
+
console.error('Missing XHR URL regex');
|
|
2135
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
let re;
|
|
2139
|
+
try {
|
|
2140
|
+
re = new RegExp(value);
|
|
2141
|
+
}
|
|
2142
|
+
catch (err) {
|
|
2143
|
+
console.error(`Invalid regex "${value}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2144
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
const hasSessionCapture = await page.startNetworkCapture?.() ?? false;
|
|
2148
|
+
if (!hasSessionCapture) {
|
|
2149
|
+
try {
|
|
2150
|
+
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
2151
|
+
}
|
|
2152
|
+
catch { /* non-fatal */ }
|
|
2153
|
+
}
|
|
2154
|
+
await captureNetworkItems(page);
|
|
2155
|
+
const deadline = Date.now() + timeout;
|
|
2156
|
+
const pollMs = 400;
|
|
2157
|
+
let matched = null;
|
|
2158
|
+
while (Date.now() < deadline && !matched) {
|
|
2159
|
+
const items = await captureNetworkItems(page);
|
|
2160
|
+
matched = items.find((e) => re.test(e.url)) ?? null;
|
|
2161
|
+
if (!matched)
|
|
2162
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
2163
|
+
}
|
|
2164
|
+
if (!matched) {
|
|
2165
|
+
console.log(JSON.stringify({
|
|
2166
|
+
error: {
|
|
2167
|
+
code: 'xhr_not_seen',
|
|
2168
|
+
message: `No captured XHR matched /${value}/ within ${timeout}ms`,
|
|
2169
|
+
hint: 'Check the pattern against `browser network` output; the endpoint may not have fired yet, or capture is disabled.',
|
|
2170
|
+
},
|
|
2171
|
+
}, null, 2));
|
|
2172
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
console.log(JSON.stringify({
|
|
2176
|
+
matched: { url: matched.url, status: matched.status, contentType: matched.ct },
|
|
2177
|
+
}, null, 2));
|
|
2178
|
+
}
|
|
2179
|
+
else if (type === 'download') {
|
|
2180
|
+
if (typeof page.waitForDownload !== 'function') {
|
|
2181
|
+
console.log(JSON.stringify({
|
|
2182
|
+
error: {
|
|
2183
|
+
code: 'download_wait_unavailable',
|
|
2184
|
+
message: 'The active browser backend does not support download lifecycle waits.',
|
|
2185
|
+
hint: 'Use the Browser Bridge extension version 1.0.8 or newer, then retry the command.',
|
|
2186
|
+
},
|
|
2187
|
+
}, null, 2));
|
|
2188
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const result = await page.waitForDownload(String(value ?? ''), timeout);
|
|
2192
|
+
if (!result.downloaded) {
|
|
2193
|
+
const code = result.state === 'interrupted' && result.id !== undefined ? 'download_failed' : 'download_not_seen';
|
|
2194
|
+
console.log(JSON.stringify({
|
|
2195
|
+
error: {
|
|
2196
|
+
code,
|
|
2197
|
+
message: result.error ?? `No download matched "${value ?? '*'}" within ${timeout}ms`,
|
|
2198
|
+
hint: 'Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly.',
|
|
2199
|
+
},
|
|
2200
|
+
download: result,
|
|
2201
|
+
}, null, 2));
|
|
2202
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2206
|
+
}
|
|
2207
|
+
else {
|
|
2208
|
+
console.error(`Unknown wait type "${type}". Use: selector, text, time, xhr, or download`);
|
|
2209
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2210
|
+
}
|
|
2211
|
+
}));
|
|
2212
|
+
// ── Extract ──
|
|
2213
|
+
addBrowserTabOption(browser.command('eval')
|
|
2214
|
+
.argument('<js>', 'JavaScript code')
|
|
2215
|
+
.option('--frame <index>', 'Cross-origin iframe index from "browser frames"')
|
|
2216
|
+
.description('Execute JS in page context, return result'))
|
|
2217
|
+
.action(browserAction(async (page, js, opts) => {
|
|
2218
|
+
let result;
|
|
2219
|
+
if (opts.frame !== undefined) {
|
|
2220
|
+
const frameIndex = Number.parseInt(opts.frame, 10);
|
|
2221
|
+
if (!Number.isInteger(frameIndex) || frameIndex < 0) {
|
|
2222
|
+
console.error(`Invalid frame index "${opts.frame}". Use a 0-based index from "browser frames".`);
|
|
2223
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (!page.evaluateInFrame) {
|
|
2227
|
+
throw new Error('This browser session does not support frame-targeted evaluation');
|
|
2228
|
+
}
|
|
2229
|
+
result = await page.evaluateInFrame(js, frameIndex);
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
result = await page.evaluate(js);
|
|
2233
|
+
}
|
|
2234
|
+
if (typeof result === 'string')
|
|
2235
|
+
console.log(result);
|
|
2236
|
+
else
|
|
2237
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2238
|
+
}));
|
|
2239
|
+
// ── Extract (content reading) ──
|
|
2240
|
+
//
|
|
2241
|
+
// `extract` answers the "read this page" question that `get html` / `get text`
|
|
2242
|
+
// can't: denoise → markdown → paragraph-aware chunking. Agents walk long pages
|
|
2243
|
+
// by passing back the `next_start_char` cursor instead of juggling selectors.
|
|
2244
|
+
addBrowserTabOption(browser.command('extract')
|
|
2245
|
+
.option('--selector <css>', 'CSS selector scope; defaults to <main>/<article>/<body>')
|
|
2246
|
+
.option('--chunk-size <chars>', 'Target chunk size in chars', '20000')
|
|
2247
|
+
.option('--start <char>', 'Start offset (use next_start_char from a previous extract)', '0')
|
|
2248
|
+
.description('Extract page content as markdown, paragraph-aware chunks for long pages'))
|
|
2249
|
+
.action(browserAction(async (page, opts) => {
|
|
2250
|
+
const rawChunk = String(opts.chunkSize ?? '20000');
|
|
2251
|
+
if (!/^\d+$/.test(rawChunk) || Number.parseInt(rawChunk, 10) <= 0) {
|
|
2252
|
+
console.log(JSON.stringify({ error: { code: 'invalid_chunk_size', message: `--chunk-size must be a positive integer, got "${opts.chunkSize}"` } }, null, 2));
|
|
2253
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const rawStart = String(opts.start ?? '0');
|
|
2257
|
+
if (!/^\d+$/.test(rawStart)) {
|
|
2258
|
+
console.log(JSON.stringify({ error: { code: 'invalid_start', message: `--start must be a non-negative integer, got "${opts.start}"` } }, null, 2));
|
|
2259
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
const chunkSize = Number.parseInt(rawChunk, 10);
|
|
2263
|
+
const start = Number.parseInt(rawStart, 10);
|
|
2264
|
+
const selector = typeof opts.selector === 'string' && opts.selector.length > 0 ? opts.selector : null;
|
|
2265
|
+
const js = buildExtractHtmlJs(selector);
|
|
2266
|
+
const res = await page.evaluate(js);
|
|
2267
|
+
if (!res) {
|
|
2268
|
+
console.log(JSON.stringify({ error: { code: 'extract_failed', message: 'Page returned no root element.' } }, null, 2));
|
|
2269
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
if ('invalidSelector' in res) {
|
|
2273
|
+
console.log(JSON.stringify({ error: { code: 'invalid_selector', message: `Selector "${selector}" is not a valid CSS selector: ${res.reason}` } }, null, 2));
|
|
2274
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
if ('notFound' in res) {
|
|
2278
|
+
console.log(JSON.stringify({ error: { code: 'selector_not_found', message: selector ? `Selector "${selector}" matched 0 elements.` : 'Page has no body/main/article element.' } }, null, 2));
|
|
2279
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const envelope = runExtractFromHtml({
|
|
2283
|
+
html: res.html,
|
|
2284
|
+
url: res.url,
|
|
2285
|
+
title: res.title,
|
|
2286
|
+
selector,
|
|
2287
|
+
start,
|
|
2288
|
+
chunkSize,
|
|
2289
|
+
});
|
|
2290
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
2291
|
+
}));
|
|
2292
|
+
// ── Network (API discovery) ──
|
|
2293
|
+
//
|
|
2294
|
+
// Default output is JSON (agent-native). Each entry carries a stable `key`
|
|
2295
|
+
// (GraphQL operationName or `METHOD host+pathname`) so agents can fetch
|
|
2296
|
+
// full bodies with `--detail <key>` even after subsequent commands.
|
|
2297
|
+
// Captures are persisted per browser session under ~/.opencli/cache/browser-network/.
|
|
2298
|
+
addBrowserTabOption(browser.command('network'))
|
|
2299
|
+
.option('--detail <key>', 'Emit full body for the entry with this key')
|
|
2300
|
+
.option('--all', 'Include static resources (js/css/images/telemetry)')
|
|
2301
|
+
.option('--raw', 'Emit full bodies for every entry (skip shape preview)')
|
|
2302
|
+
.option('--filter <fields>', 'Comma-separated field names; keep only entries whose body shape has ALL names as path segments')
|
|
2303
|
+
.option('--since <duration>', 'Only include entries from the last duration (for example: 30s, 2m)')
|
|
2304
|
+
.option('--until <duration>', 'Only include entries older than the duration from now')
|
|
2305
|
+
.option('--follow', 'Continuously print new matching entries as JSON lines', false)
|
|
2306
|
+
.option('--failed', 'Only include failed HTTP requests (status 0 or >= 400)', false)
|
|
2307
|
+
.option('--max-body <chars>', 'With --detail: cap the emitted body at N chars (0 = unlimited, default)', '0')
|
|
2308
|
+
.option('--ttl <ms>', 'Cache TTL in ms for --detail lookups', String(DEFAULT_TTL_MS))
|
|
2309
|
+
.description('Capture network requests as shape previews; retrieve full bodies by key')
|
|
2310
|
+
.action(browserAction(async (page, opts) => {
|
|
2311
|
+
const ttlMs = parsePositiveIntOption(opts.ttl, 'ttl', DEFAULT_TTL_MS);
|
|
2312
|
+
const session = getPageSession(page);
|
|
2313
|
+
const hasDetail = typeof opts.detail === 'string' && opts.detail.length > 0;
|
|
2314
|
+
const hasFilter = typeof opts.filter === 'string';
|
|
2315
|
+
const sinceMs = parseDurationMs(opts.since, 'since');
|
|
2316
|
+
const untilMs = parseDurationMs(opts.until, 'until');
|
|
2317
|
+
if (sinceMs && typeof sinceMs === 'object') {
|
|
2318
|
+
emitNetworkError('invalid_since', sinceMs.error);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (untilMs && typeof untilMs === 'object') {
|
|
2322
|
+
emitNetworkError('invalid_until', untilMs.error);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
// --detail and --filter do different things (one request by key vs. narrow
|
|
2326
|
+
// the list by shape), don't compose, and combining them has no sensible
|
|
2327
|
+
// semantic. Reject up front with a structured error instead of silently
|
|
2328
|
+
// dropping one.
|
|
2329
|
+
if (hasDetail && hasFilter) {
|
|
2330
|
+
emitNetworkError('invalid_args', '--filter and --detail cannot be used together (one narrows a list, the other fetches a specific entry).');
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
let filterFields = null;
|
|
2334
|
+
if (hasFilter) {
|
|
2335
|
+
const parsed = parseFilter(opts.filter);
|
|
2336
|
+
if ('reason' in parsed) {
|
|
2337
|
+
emitNetworkError('invalid_filter', parsed.reason);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
filterFields = parsed.fields;
|
|
2341
|
+
}
|
|
2342
|
+
if (hasDetail && opts.follow) {
|
|
2343
|
+
emitNetworkError('invalid_args', '--follow cannot be used with --detail.');
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
// --detail short-circuits: read from cache only, no live capture needed.
|
|
2347
|
+
if (hasDetail) {
|
|
2348
|
+
const res = loadNetworkCache(session, { ttlMs });
|
|
2349
|
+
if (res.status === 'missing') {
|
|
2350
|
+
emitNetworkError('cache_missing', `No cached capture. Run "browser network" first (in session "${session}").`);
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
if (res.status === 'expired') {
|
|
2354
|
+
emitNetworkError('cache_expired', `Cache is stale (age ${res.ageMs}ms > ttl ${ttlMs}ms). Re-run "browser network" to refresh.`);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
if (res.status === 'corrupt' || !res.file) {
|
|
2358
|
+
emitNetworkError('cache_corrupt', 'Cache file is malformed; re-run "browser network" to regenerate.');
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
const entry = findEntry(res.file, opts.detail);
|
|
2362
|
+
if (!entry) {
|
|
2363
|
+
emitNetworkError('key_not_found', `Key "${opts.detail}" not in cache.`, {
|
|
2364
|
+
available_keys: res.file.entries.map((e) => e.key),
|
|
2365
|
+
});
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
const rawMaxBody = String(opts.maxBody ?? '0');
|
|
2369
|
+
if (!/^\d+$/.test(rawMaxBody)) {
|
|
2370
|
+
emitNetworkError('invalid_max_body', `--max-body must be a non-negative integer, got "${opts.maxBody}"`);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
const maxBody = Number.parseInt(rawMaxBody, 10);
|
|
2374
|
+
// Body shape/source:
|
|
2375
|
+
// - If capture already truncated it (entry.body_truncated), the body is a string.
|
|
2376
|
+
// - If the adapter stored a JSON value, it parsed cleanly at capture time; leave it.
|
|
2377
|
+
// - --max-body applies a transport-level cap when the caller wants to keep output small.
|
|
2378
|
+
let outputBody = entry.body;
|
|
2379
|
+
let transportTruncated = false;
|
|
2380
|
+
if (maxBody > 0 && typeof entry.body === 'string' && entry.body.length > maxBody) {
|
|
2381
|
+
outputBody = entry.body.slice(0, maxBody);
|
|
2382
|
+
transportTruncated = true;
|
|
2383
|
+
}
|
|
2384
|
+
const captureTruncated = entry.body_truncated === true;
|
|
2385
|
+
const detailEnvelope = {
|
|
2386
|
+
key: entry.key,
|
|
2387
|
+
url: entry.url,
|
|
2388
|
+
method: entry.method,
|
|
2389
|
+
status: entry.status,
|
|
2390
|
+
ct: entry.ct,
|
|
2391
|
+
size: entry.size,
|
|
2392
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: toIsoTimestamp(entry.timestamp) } : {}),
|
|
2393
|
+
shape: inferShape(entry.body),
|
|
2394
|
+
body: outputBody,
|
|
2395
|
+
};
|
|
2396
|
+
if (captureTruncated || transportTruncated) {
|
|
2397
|
+
detailEnvelope.body_truncated = true;
|
|
2398
|
+
detailEnvelope.body_full_size = entry.body_full_size ?? entry.size;
|
|
2399
|
+
detailEnvelope.body_truncation_reason = captureTruncated
|
|
2400
|
+
? 'capture-limit'
|
|
2401
|
+
: 'max-body';
|
|
2402
|
+
}
|
|
2403
|
+
console.log(JSON.stringify(detailEnvelope, null, 2));
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
if (opts.follow) {
|
|
2407
|
+
if (!await page.startNetworkCapture?.()) {
|
|
2408
|
+
try {
|
|
2409
|
+
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
2410
|
+
}
|
|
2411
|
+
catch { /* non-fatal */ }
|
|
2412
|
+
}
|
|
2413
|
+
while (true) {
|
|
2414
|
+
const rawItems = await captureNetworkItems(page).catch((err) => {
|
|
2415
|
+
emitNetworkError('capture_failed', `Could not read network capture: ${err.message}`);
|
|
2416
|
+
return [];
|
|
2417
|
+
});
|
|
2418
|
+
let items = opts.all ? rawItems : filterNetworkItems(rawItems);
|
|
2419
|
+
items = filterByTimeWindow(items, { sinceMs, untilMs });
|
|
2420
|
+
if (opts.failed)
|
|
2421
|
+
items = items.filter((item) => item.status === 0 || item.status >= 400);
|
|
2422
|
+
const keyed = assignKeys(items);
|
|
2423
|
+
for (const item of keyed) {
|
|
2424
|
+
console.log(JSON.stringify({
|
|
2425
|
+
key: item.key,
|
|
2426
|
+
timestamp: toIsoTimestamp(item.timestamp),
|
|
2427
|
+
method: item.method,
|
|
2428
|
+
status: item.status,
|
|
2429
|
+
url: item.url,
|
|
2430
|
+
ct: item.ct,
|
|
2431
|
+
size: item.size,
|
|
2432
|
+
...(item.bodyTruncated ? { body_truncated: true } : {}),
|
|
2433
|
+
}));
|
|
2434
|
+
}
|
|
2435
|
+
await new Promise((resolve) => setTimeout(resolve, FOLLOW_POLL_MS));
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
// Fresh capture path.
|
|
2439
|
+
let rawItems;
|
|
2440
|
+
try {
|
|
2441
|
+
rawItems = await captureNetworkItems(page);
|
|
2442
|
+
}
|
|
2443
|
+
catch (err) {
|
|
2444
|
+
emitNetworkError('capture_failed', `Could not read network capture: ${err.message}`);
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
let items = opts.all ? rawItems : filterNetworkItems(rawItems);
|
|
2448
|
+
items = filterByTimeWindow(items, { sinceMs, untilMs });
|
|
2449
|
+
if (opts.failed)
|
|
2450
|
+
items = items.filter((item) => item.status === 0 || item.status >= 400);
|
|
2451
|
+
const filteredOut = rawItems.length - items.length;
|
|
2452
|
+
const keyed = assignKeys(items);
|
|
2453
|
+
const cacheEntries = keyed.map((it) => ({
|
|
2454
|
+
key: it.key,
|
|
2455
|
+
url: it.url,
|
|
2456
|
+
method: it.method,
|
|
2457
|
+
status: it.status,
|
|
2458
|
+
size: it.size,
|
|
2459
|
+
ct: it.ct,
|
|
2460
|
+
body: it.body,
|
|
2461
|
+
...(typeof it.timestamp === 'number' ? { timestamp: it.timestamp } : {}),
|
|
2462
|
+
...(it.bodyTruncated ? { body_truncated: true } : {}),
|
|
2463
|
+
...(it.bodyTruncated && typeof it.bodyFullSize === 'number'
|
|
2464
|
+
? { body_full_size: it.bodyFullSize }
|
|
2465
|
+
: {}),
|
|
2466
|
+
}));
|
|
2467
|
+
// Soft failure: the caller already has the data, so surface a warning
|
|
2468
|
+
// via the output envelope rather than erroring out the whole command.
|
|
2469
|
+
let cacheWarning = null;
|
|
2470
|
+
try {
|
|
2471
|
+
saveNetworkCache(session, cacheEntries);
|
|
2472
|
+
}
|
|
2473
|
+
catch (err) {
|
|
2474
|
+
cacheWarning = `Could not persist capture cache: ${err.message}. --detail lookups may miss this capture.`;
|
|
2475
|
+
}
|
|
2476
|
+
// Pair each cache entry with its shape up front so --filter can read
|
|
2477
|
+
// segments without recomputing, and the --raw view can keep the full
|
|
2478
|
+
// body. Cache persistence above stored the unfiltered set on purpose:
|
|
2479
|
+
// later `--detail <key>` lookups must still see requests that the
|
|
2480
|
+
// current --filter narrowed out.
|
|
2481
|
+
const shaped = cacheEntries.map((e) => ({ entry: e, shape: inferShape(e.body) }));
|
|
2482
|
+
const visible = filterFields
|
|
2483
|
+
? shaped.filter((s) => shapeMatchesFilter(s.shape, filterFields))
|
|
2484
|
+
: shaped;
|
|
2485
|
+
const filterDropped = filterFields ? shaped.length - visible.length : 0;
|
|
2486
|
+
const envelope = {
|
|
2487
|
+
session,
|
|
2488
|
+
captured_at: new Date().toISOString(),
|
|
2489
|
+
count: visible.length,
|
|
2490
|
+
filtered_out: filteredOut,
|
|
2491
|
+
};
|
|
2492
|
+
if (filterFields) {
|
|
2493
|
+
envelope.filter = filterFields;
|
|
2494
|
+
envelope.filter_dropped = filterDropped;
|
|
2495
|
+
}
|
|
2496
|
+
if (cacheWarning)
|
|
2497
|
+
envelope.cache_warning = cacheWarning;
|
|
2498
|
+
const truncatedCount = visible.filter((s) => s.entry.body_truncated).length;
|
|
2499
|
+
if (truncatedCount > 0) {
|
|
2500
|
+
envelope.body_truncated_count = truncatedCount;
|
|
2501
|
+
envelope.body_truncated_hint = 'Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix.';
|
|
2502
|
+
}
|
|
2503
|
+
if (opts.raw) {
|
|
2504
|
+
envelope.entries = visible.map((s) => ({
|
|
2505
|
+
...s.entry,
|
|
2506
|
+
...(typeof s.entry.timestamp === 'number' ? { timestamp: toIsoTimestamp(s.entry.timestamp) } : {}),
|
|
2507
|
+
}));
|
|
2508
|
+
}
|
|
2509
|
+
else {
|
|
2510
|
+
envelope.entries = visible.map((s) => ({
|
|
2511
|
+
key: s.entry.key,
|
|
2512
|
+
method: s.entry.method,
|
|
2513
|
+
...(typeof s.entry.timestamp === 'number' ? { timestamp: toIsoTimestamp(s.entry.timestamp) } : {}),
|
|
2514
|
+
status: s.entry.status,
|
|
2515
|
+
url: s.entry.url,
|
|
2516
|
+
ct: s.entry.ct,
|
|
2517
|
+
size: s.entry.size,
|
|
2518
|
+
shape: s.shape,
|
|
2519
|
+
...(s.entry.body_truncated ? { body_truncated: true } : {}),
|
|
2520
|
+
}));
|
|
2521
|
+
envelope.detail_hint = 'Run "browser network --detail <key>" for full body.';
|
|
2522
|
+
}
|
|
2523
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
2524
|
+
}));
|
|
2525
|
+
// ── Init (adapter scaffolding) ──
|
|
2526
|
+
browser.command('init')
|
|
2527
|
+
.argument('<name>', 'Adapter name in site/command format (e.g. hn/top)')
|
|
2528
|
+
.description('Generate adapter scaffold in ~/.opencli/clis/')
|
|
2529
|
+
.action(async (name) => {
|
|
2530
|
+
try {
|
|
2531
|
+
const parts = name.split('/');
|
|
2532
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
2533
|
+
console.error('Name must be site/command format (e.g. hn/top)');
|
|
2534
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
const [site, command] = parts;
|
|
2538
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(site) || !/^[a-zA-Z0-9_-]+$/.test(command)) {
|
|
2539
|
+
console.error('Name parts must be alphanumeric/dash/underscore only');
|
|
2540
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const os = await import('node:os');
|
|
2544
|
+
const fs = await import('node:fs');
|
|
2545
|
+
const path = await import('node:path');
|
|
2546
|
+
const dir = path.join(os.homedir(), '.opencli', 'clis', site);
|
|
2547
|
+
const filePath = path.join(dir, `${command}.js`);
|
|
2548
|
+
if (fs.existsSync(filePath)) {
|
|
2549
|
+
console.log(`Adapter already exists: ${filePath}`);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
let domain = site;
|
|
2553
|
+
const template = `import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2554
|
+
|
|
2555
|
+
cli({
|
|
2556
|
+
site: '${site}',
|
|
2557
|
+
name: '${command}',
|
|
2558
|
+
description: '', // TODO: describe what this command does
|
|
2559
|
+
access: 'read', // TODO: 'read' for queries, 'write' for remote/account state changes
|
|
2560
|
+
example: 'opencli ${site} ${command} -f yaml',
|
|
2561
|
+
domain: '${domain}',
|
|
2562
|
+
strategy: Strategy.PUBLIC, // TODO: PUBLIC (no auth), COOKIE (needs login), UI (DOM interaction)
|
|
2563
|
+
browser: false, // TODO: set true if needs browser
|
|
2564
|
+
args: [
|
|
2565
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of items' },
|
|
2566
|
+
],
|
|
2567
|
+
columns: [], // TODO: field names for table output (e.g. ['title', 'score', 'url'])
|
|
2568
|
+
func: async (kwargs) => {
|
|
2569
|
+
// TODO: implement data fetching
|
|
2570
|
+
// Prefer API calls (fetch) over browser automation
|
|
2571
|
+
// If you set browser: true, change this to: async (page, kwargs) => { ... }
|
|
2572
|
+
return [];
|
|
2573
|
+
},
|
|
2574
|
+
});
|
|
2575
|
+
`;
|
|
2576
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2577
|
+
fs.writeFileSync(filePath, template, 'utf-8');
|
|
2578
|
+
console.log(`Created: ${filePath}`);
|
|
2579
|
+
console.log('First time on this site? Run: opencli browser analyze <url>');
|
|
2580
|
+
console.log(`Edit the file to implement your adapter, then run: opencli browser verify ${name}`);
|
|
2581
|
+
}
|
|
2582
|
+
catch (err) {
|
|
2583
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2584
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
// ── Verify (test adapter) ──
|
|
2588
|
+
browser.command('verify')
|
|
2589
|
+
.argument('<name>', 'Adapter name in site/command format (e.g. hn/top)')
|
|
2590
|
+
.option('--write-fixture', 'Write a starter fixture to ~/.opencli/sites/<site>/verify/<command>.json if none exists')
|
|
2591
|
+
.option('--update-fixture', 'Overwrite an existing fixture with one derived from current output')
|
|
2592
|
+
.option('--no-fixture', 'Ignore any fixture file for this run (no value-level validation)')
|
|
2593
|
+
.option('--strict-memory', 'Fail (not just warn) when ~/.opencli/sites/<site>/endpoints.json or notes.md is missing')
|
|
2594
|
+
.option('--seed-args <value>', 'Seed args when no fixture exists; use JSON array/object for multiple args or flags')
|
|
2595
|
+
.option('--trace <mode>', 'Trace capture for the adapter subprocess: off, on, retain-on-failure', 'off')
|
|
2596
|
+
.description('Execute an adapter and validate output; uses fixture at ~/.opencli/sites/<site>/verify/<cmd>.json when present')
|
|
2597
|
+
.action(async (name, opts = {}) => {
|
|
2598
|
+
try {
|
|
2599
|
+
const parts = name.split('/');
|
|
2600
|
+
if (parts.length !== 2) {
|
|
2601
|
+
console.error('Name must be site/command format');
|
|
2602
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
const [site, command] = parts;
|
|
2606
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(site) || !/^[a-zA-Z0-9_-]+$/.test(command)) {
|
|
2607
|
+
console.error('Name parts must be alphanumeric/dash/underscore only');
|
|
2608
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
const { execFileSync } = await import('node:child_process');
|
|
2612
|
+
const { loadFixture, writeFixture, deriveFixture, validateRows, validateRowShape, fixturePath, expandFixtureArgs, parseSeedArgs } = await import('./browser/verify-fixture.js');
|
|
2613
|
+
const filePath = path.join(os.homedir(), '.opencli', 'clis', site, `${command}.js`);
|
|
2614
|
+
if (!fs.existsSync(filePath)) {
|
|
2615
|
+
console.error(`Adapter not found: ${filePath}`);
|
|
2616
|
+
console.error(`Run "opencli browser init ${name}" to create it.`);
|
|
2617
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
console.log(`🔍 Verifying ${name}...\n`);
|
|
2621
|
+
console.log(` Loading: ${filePath}`);
|
|
2622
|
+
const useFixture = opts.fixture !== false;
|
|
2623
|
+
let fixture = useFixture ? loadFixture(site, command) : null;
|
|
2624
|
+
// Build adapter args: fixture.args override the legacy --limit 3 heuristic.
|
|
2625
|
+
// - object form { "limit": 3 } → `--limit 3`
|
|
2626
|
+
// - array form ["123", "--limit", "3"] → verbatim (for positional subjects)
|
|
2627
|
+
const adapterSrc = fs.readFileSync(filePath, 'utf-8');
|
|
2628
|
+
const hasLimitArg = /['"]limit['"]/.test(adapterSrc);
|
|
2629
|
+
const seedArgs = parseSeedArgs(opts.seedArgs);
|
|
2630
|
+
const explicitArgs = fixture?.args ?? seedArgs;
|
|
2631
|
+
const cliArgs = expandFixtureArgs(explicitArgs);
|
|
2632
|
+
if (explicitArgs === undefined && cliArgs.length === 0 && hasLimitArg)
|
|
2633
|
+
cliArgs.push('--limit', '3');
|
|
2634
|
+
const traceArgs = opts.trace && opts.trace !== 'off' ? ['--trace', opts.trace] : [];
|
|
2635
|
+
const argDisplay = [...cliArgs, ...traceArgs].join(' ');
|
|
2636
|
+
const invocation = resolveBrowserVerifyInvocation();
|
|
2637
|
+
// Always request JSON so we can validate structurally.
|
|
2638
|
+
const execArgs = [...invocation.args, site, command, ...cliArgs, ...traceArgs, '--format', 'json'];
|
|
2639
|
+
let rawJson;
|
|
2640
|
+
try {
|
|
2641
|
+
rawJson = execFileSync(invocation.binary, execArgs, {
|
|
2642
|
+
cwd: invocation.cwd,
|
|
2643
|
+
timeout: 30000,
|
|
2644
|
+
encoding: 'utf-8',
|
|
2645
|
+
env: process.env,
|
|
2646
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2647
|
+
...(invocation.shell ? { shell: true } : {}),
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
catch (err) {
|
|
2651
|
+
console.log(` Executing: opencli ${site} ${command} ${argDisplay}\n`);
|
|
2652
|
+
const execErr = err;
|
|
2653
|
+
if (execErr.stdout)
|
|
2654
|
+
console.log(String(execErr.stdout));
|
|
2655
|
+
if (execErr.stderr)
|
|
2656
|
+
console.error(String(execErr.stderr).slice(0, 500));
|
|
2657
|
+
console.log(`\n ✗ Adapter failed. Fix the code and try again.`);
|
|
2658
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
console.log(` Executing: opencli ${site} ${command} ${argDisplay}\n`);
|
|
2662
|
+
let rows;
|
|
2663
|
+
try {
|
|
2664
|
+
rows = normalizeVerifyRows(JSON.parse(rawJson));
|
|
2665
|
+
}
|
|
2666
|
+
catch {
|
|
2667
|
+
console.log(rawJson);
|
|
2668
|
+
console.log('\n ✗ Could not parse adapter output as JSON. Is `--format json` broken?');
|
|
2669
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
console.log(renderVerifyPreview(rows));
|
|
2673
|
+
console.log(`\n → ${rows.length} row${rows.length === 1 ? '' : 's'}`);
|
|
2674
|
+
const shapeFailures = validateRowShape(rows);
|
|
2675
|
+
if (shapeFailures.length > 0) {
|
|
2676
|
+
console.log(`\n ✗ Adapter output violates row shape conventions:`);
|
|
2677
|
+
for (const f of shapeFailures.slice(0, 20)) {
|
|
2678
|
+
const where = f.rowIndex !== undefined ? `row[${f.rowIndex}] ` : '';
|
|
2679
|
+
console.log(` - [${f.rule}] ${where}${f.detail}`);
|
|
2680
|
+
}
|
|
2681
|
+
if (shapeFailures.length > 20) {
|
|
2682
|
+
console.log(` ... and ${shapeFailures.length - 20} more failure(s)`);
|
|
2683
|
+
}
|
|
2684
|
+
console.log(`\n Keep rows agent-native: <=12 top-level keys, nesting depth <=1, and id-shaped fields at top level.`);
|
|
2685
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
// ── Fixture handling ───────────────────────────────────────────
|
|
2689
|
+
if (opts.writeFixture || opts.updateFixture) {
|
|
2690
|
+
if (fixture && !opts.updateFixture) {
|
|
2691
|
+
console.log(`\n Fixture already exists at ${fixturePath(site, command)}.`);
|
|
2692
|
+
console.log(` Use --update-fixture to overwrite.`);
|
|
2693
|
+
}
|
|
2694
|
+
else {
|
|
2695
|
+
const fixtureArgs = explicitArgs !== undefined
|
|
2696
|
+
? explicitArgs
|
|
2697
|
+
: (hasLimitArg ? { limit: 3 } : undefined);
|
|
2698
|
+
const derived = deriveFixture(rows, fixtureArgs);
|
|
2699
|
+
const p = writeFixture(site, command, derived);
|
|
2700
|
+
console.log(`\n ${fixture ? '↻ Updated' : '✎ Wrote'} fixture: ${p}`);
|
|
2701
|
+
console.log(` Review and hand-tune the derived expectations (add patterns / notEmpty, tighten rowCount).`);
|
|
2702
|
+
fixture = derived;
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
if (!fixture) {
|
|
2706
|
+
console.log(`\n ✓ Adapter runs. (No fixture at ${fixturePath(site, command)} — consider --write-fixture to seed one.)`);
|
|
2707
|
+
const memoryReport = checkSiteMemory(site);
|
|
2708
|
+
printSiteMemoryReport(memoryReport, opts.strictMemory);
|
|
2709
|
+
if (!memoryReport.ok && opts.strictMemory) {
|
|
2710
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2711
|
+
}
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
const failures = validateRows(rows, fixture);
|
|
2715
|
+
if (failures.length === 0) {
|
|
2716
|
+
console.log(`\n ✓ Adapter matches fixture (${fixturePath(site, command)}).`);
|
|
2717
|
+
const memoryReport = checkSiteMemory(site);
|
|
2718
|
+
printSiteMemoryReport(memoryReport, opts.strictMemory);
|
|
2719
|
+
if (!memoryReport.ok && opts.strictMemory) {
|
|
2720
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2721
|
+
}
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
console.log(`\n ✗ Adapter output does not match fixture:`);
|
|
2725
|
+
for (const f of failures.slice(0, 20)) {
|
|
2726
|
+
const where = f.rowIndex !== undefined ? `row[${f.rowIndex}] ` : '';
|
|
2727
|
+
console.log(` - [${f.rule}] ${where}${f.detail}`);
|
|
2728
|
+
}
|
|
2729
|
+
if (failures.length > 20) {
|
|
2730
|
+
console.log(` ... and ${failures.length - 20} more failure(s)`);
|
|
2731
|
+
}
|
|
2732
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2733
|
+
}
|
|
2734
|
+
catch (err) {
|
|
2735
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2736
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
// ── Session ──
|
|
2740
|
+
browser.command('close').description('Release the current browser session tab lease')
|
|
2741
|
+
.action(browserAction(async (page) => {
|
|
2742
|
+
await page.closeWindow?.();
|
|
2743
|
+
console.log('Browser session tab lease released');
|
|
2744
|
+
}));
|
|
2745
|
+
// ── Built-in: doctor / completion ──────────────────────────────────────────
|
|
2746
|
+
program
|
|
2747
|
+
.command('doctor')
|
|
2748
|
+
.description('Diagnose opencli browser bridge connectivity')
|
|
2749
|
+
.option('-v, --verbose', 'Debug output')
|
|
2750
|
+
.action(async (opts) => {
|
|
2751
|
+
applyVerbose(opts);
|
|
2752
|
+
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
|
|
2753
|
+
const report = await runBrowserDoctor({ cliVersion: PKG_VERSION });
|
|
2754
|
+
console.log(renderBrowserDoctorReport(report));
|
|
2755
|
+
});
|
|
2756
|
+
program
|
|
2757
|
+
.command('completion')
|
|
2758
|
+
.description('Output shell completion script')
|
|
2759
|
+
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
2760
|
+
.action((shell) => {
|
|
2761
|
+
printCompletionScript(shell);
|
|
2762
|
+
});
|
|
2763
|
+
// ── Plugin management ──────────────────────────────────────────────────────
|
|
2764
|
+
const pluginCmd = program.command('plugin').description('Manage opencli plugins');
|
|
2765
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
2766
|
+
const originalPluginDescription = pluginCmd.description();
|
|
2767
|
+
pluginCmd
|
|
2768
|
+
.command('install')
|
|
2769
|
+
.description('Install a plugin from a git repository')
|
|
2770
|
+
.argument('<source>', 'Plugin source (e.g. github:user/repo)')
|
|
2771
|
+
.action(async (source) => {
|
|
2772
|
+
const { installPlugin } = await import('./plugin.js');
|
|
2773
|
+
const { discoverPlugins } = await import('./discovery.js');
|
|
2774
|
+
try {
|
|
2775
|
+
const result = installPlugin(source);
|
|
2776
|
+
await discoverPlugins();
|
|
2777
|
+
if (Array.isArray(result)) {
|
|
2778
|
+
if (result.length === 0) {
|
|
2779
|
+
console.log('No plugins were installed (all skipped or incompatible).');
|
|
2780
|
+
}
|
|
2781
|
+
else {
|
|
2782
|
+
console.log(`\u2705 Installed ${result.length} plugin(s) from monorepo: ${result.join(', ')}`);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
else {
|
|
2786
|
+
console.log(`\u2705 Plugin "${result}" installed successfully. Commands are ready to use.`);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
catch (err) {
|
|
2790
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2791
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
pluginCmd
|
|
2795
|
+
.command('uninstall')
|
|
2796
|
+
.description('Uninstall a plugin')
|
|
2797
|
+
.argument('<name>', 'Plugin name')
|
|
2798
|
+
.action(async (name) => {
|
|
2799
|
+
const { uninstallPlugin } = await import('./plugin.js');
|
|
2800
|
+
try {
|
|
2801
|
+
uninstallPlugin(name);
|
|
2802
|
+
console.log(`✅ Plugin "${name}" uninstalled.`);
|
|
2803
|
+
}
|
|
2804
|
+
catch (err) {
|
|
2805
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2806
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
pluginCmd
|
|
2810
|
+
.command('update')
|
|
2811
|
+
.description('Update a plugin (or all plugins) to the latest version')
|
|
2812
|
+
.argument('[name]', 'Plugin name (required unless --all is passed)')
|
|
2813
|
+
.option('--all', 'Update all installed plugins')
|
|
2814
|
+
.action(async (name, opts) => {
|
|
2815
|
+
if (!name && !opts.all) {
|
|
2816
|
+
console.error('Error: Please specify a plugin name or use the --all flag.');
|
|
2817
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
if (name && opts.all) {
|
|
2821
|
+
console.error('Error: Cannot specify both a plugin name and --all.');
|
|
2822
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
const { updatePlugin, updateAllPlugins } = await import('./plugin.js');
|
|
2826
|
+
const { discoverPlugins } = await import('./discovery.js');
|
|
2827
|
+
if (opts.all) {
|
|
2828
|
+
const results = updateAllPlugins();
|
|
2829
|
+
if (results.length > 0) {
|
|
2830
|
+
await discoverPlugins();
|
|
2831
|
+
}
|
|
2832
|
+
let hasErrors = false;
|
|
2833
|
+
console.log(' Update Results:');
|
|
2834
|
+
for (const result of results) {
|
|
2835
|
+
if (result.success) {
|
|
2836
|
+
console.log(` ✓ ${result.name}`);
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
hasErrors = true;
|
|
2840
|
+
console.log(` ✗ ${result.name} — ${String(result.error)}`);
|
|
2841
|
+
}
|
|
2842
|
+
if (results.length === 0) {
|
|
2843
|
+
console.log(' No plugins installed.');
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
console.log();
|
|
2847
|
+
if (hasErrors) {
|
|
2848
|
+
console.error('Completed with some errors.');
|
|
2849
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2850
|
+
}
|
|
2851
|
+
else {
|
|
2852
|
+
console.log('✅ All plugins updated successfully.');
|
|
2853
|
+
}
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
try {
|
|
2857
|
+
updatePlugin(name);
|
|
2858
|
+
await discoverPlugins();
|
|
2859
|
+
console.log(`✅ Plugin "${name}" updated successfully.`);
|
|
2860
|
+
}
|
|
2861
|
+
catch (err) {
|
|
2862
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2863
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
pluginCmd
|
|
2867
|
+
.command('list')
|
|
2868
|
+
.description('List installed plugins')
|
|
2869
|
+
.option('-f, --format <fmt>', 'Output format: table, json', 'table')
|
|
2870
|
+
.action(async (opts) => {
|
|
2871
|
+
const { listPlugins } = await import('./plugin.js');
|
|
2872
|
+
const plugins = listPlugins();
|
|
2873
|
+
if (plugins.length === 0) {
|
|
2874
|
+
console.log(' No plugins installed.');
|
|
2875
|
+
console.log(' Install one with: opencli plugin install github:user/repo');
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
if (opts.format === 'json') {
|
|
2879
|
+
renderOutput(plugins, {
|
|
2880
|
+
fmt: 'json',
|
|
2881
|
+
columns: ['name', 'commands', 'source'],
|
|
2882
|
+
title: 'opencli/plugins',
|
|
2883
|
+
source: 'opencli plugin list',
|
|
2884
|
+
});
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
console.log();
|
|
2888
|
+
console.log(' Installed plugins');
|
|
2889
|
+
console.log();
|
|
2890
|
+
// Group by monorepo
|
|
2891
|
+
const standalone = plugins.filter((p) => !p.monorepoName);
|
|
2892
|
+
const monoGroups = new Map();
|
|
2893
|
+
for (const p of plugins) {
|
|
2894
|
+
if (!p.monorepoName)
|
|
2895
|
+
continue;
|
|
2896
|
+
const g = monoGroups.get(p.monorepoName) ?? [];
|
|
2897
|
+
g.push(p);
|
|
2898
|
+
monoGroups.set(p.monorepoName, g);
|
|
2899
|
+
}
|
|
2900
|
+
for (const p of standalone) {
|
|
2901
|
+
const version = p.version ? ` @${p.version}` : '';
|
|
2902
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
2903
|
+
const cmds = p.commands.length > 0 ? ` (${p.commands.join(', ')})` : '';
|
|
2904
|
+
const src = p.source ? ` ← ${p.source}` : '';
|
|
2905
|
+
console.log(` ${p.name}${version}${desc}${cmds}${src}`);
|
|
2906
|
+
}
|
|
2907
|
+
for (const [mono, group] of monoGroups) {
|
|
2908
|
+
console.log();
|
|
2909
|
+
console.log(` 📦 ${mono}` + ' (monorepo)');
|
|
2910
|
+
for (const p of group) {
|
|
2911
|
+
const version = p.version ? ` @${p.version}` : '';
|
|
2912
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
2913
|
+
const cmds = p.commands.length > 0 ? ` (${p.commands.join(', ')})` : '';
|
|
2914
|
+
console.log(` ${p.name}${version}${desc}${cmds}`);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
console.log();
|
|
2918
|
+
console.log(` ${plugins.length} plugin(s) installed`);
|
|
2919
|
+
console.log();
|
|
2920
|
+
});
|
|
2921
|
+
pluginCmd
|
|
2922
|
+
.command('create')
|
|
2923
|
+
.description('Create a new plugin scaffold')
|
|
2924
|
+
.argument('<name>', 'Plugin name (lowercase, hyphens allowed)')
|
|
2925
|
+
.option('-d, --dir <path>', 'Output directory (default: ./<name>)')
|
|
2926
|
+
.option('--description <text>', 'Plugin description')
|
|
2927
|
+
.action(async (name, opts) => {
|
|
2928
|
+
const { createPluginScaffold } = await import('./plugin-scaffold.js');
|
|
2929
|
+
try {
|
|
2930
|
+
const result = createPluginScaffold(name, {
|
|
2931
|
+
dir: opts.dir,
|
|
2932
|
+
description: opts.description,
|
|
2933
|
+
});
|
|
2934
|
+
console.log(`✅ Plugin scaffold created at ${result.dir}`);
|
|
2935
|
+
console.log();
|
|
2936
|
+
console.log(' Files created:');
|
|
2937
|
+
for (const f of result.files) {
|
|
2938
|
+
console.log(` ${f}`);
|
|
2939
|
+
}
|
|
2940
|
+
console.log();
|
|
2941
|
+
console.log(' Next steps:');
|
|
2942
|
+
console.log(` cd ${result.dir}`);
|
|
2943
|
+
console.log(` opencli plugin install file://${result.dir}`);
|
|
2944
|
+
console.log(` opencli ${name} hello`);
|
|
2945
|
+
}
|
|
2946
|
+
catch (err) {
|
|
2947
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
2948
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
// ── Built-in: adapter management ─────────────────────────────────────────
|
|
2952
|
+
const adapterCmd = program.command('adapter').description('Manage CLI adapters');
|
|
2953
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
2954
|
+
const originalAdapterDescription = adapterCmd.description();
|
|
2955
|
+
adapterCmd
|
|
2956
|
+
.command('status')
|
|
2957
|
+
.description('Show which sites have local overrides vs using official baseline')
|
|
2958
|
+
.action(async () => {
|
|
2959
|
+
const os = await import('node:os');
|
|
2960
|
+
const userClisDir = path.join(os.homedir(), '.opencli', 'clis');
|
|
2961
|
+
const builtinClisDir = BUILTIN_CLIS;
|
|
2962
|
+
try {
|
|
2963
|
+
const userEntries = await fs.promises.readdir(userClisDir, { withFileTypes: true });
|
|
2964
|
+
const userSites = userEntries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
2965
|
+
let builtinSites = [];
|
|
2966
|
+
try {
|
|
2967
|
+
const builtinEntries = await fs.promises.readdir(builtinClisDir, { withFileTypes: true });
|
|
2968
|
+
builtinSites = builtinEntries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
2969
|
+
}
|
|
2970
|
+
catch { /* no builtin dir */ }
|
|
2971
|
+
if (userSites.length === 0) {
|
|
2972
|
+
console.log('No local adapter overrides. All sites use the official baseline.');
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
console.log(`Local overrides in ~/.opencli/clis/ (${userSites.length} sites):\n`);
|
|
2976
|
+
for (const site of userSites) {
|
|
2977
|
+
const isOfficial = builtinSites.includes(site);
|
|
2978
|
+
const label = isOfficial ? 'override' : 'custom';
|
|
2979
|
+
console.log(` ${site} [${label}]`);
|
|
2980
|
+
}
|
|
2981
|
+
console.log(`\nOfficial baseline: ${builtinSites.length} sites in package`);
|
|
2982
|
+
}
|
|
2983
|
+
catch {
|
|
2984
|
+
console.log('No local adapter overrides. All sites use the official baseline.');
|
|
2985
|
+
}
|
|
2986
|
+
});
|
|
2987
|
+
adapterCmd
|
|
2988
|
+
.command('eject')
|
|
2989
|
+
.description('Copy an official adapter to ~/.opencli/clis/ for local editing')
|
|
2990
|
+
.argument('<site>', 'Site name (e.g. twitter, bilibili)')
|
|
2991
|
+
.action(async (site) => {
|
|
2992
|
+
const os = await import('node:os');
|
|
2993
|
+
const userClisDir = path.join(os.homedir(), '.opencli', 'clis');
|
|
2994
|
+
const builtinSiteDir = path.join(BUILTIN_CLIS, site);
|
|
2995
|
+
const userSiteDir = path.join(userClisDir, site);
|
|
2996
|
+
try {
|
|
2997
|
+
await fs.promises.access(builtinSiteDir);
|
|
2998
|
+
}
|
|
2999
|
+
catch {
|
|
3000
|
+
console.error(`Error: Site "${site}" not found in official adapters.`);
|
|
3001
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
try {
|
|
3005
|
+
await fs.promises.access(userSiteDir);
|
|
3006
|
+
console.error(`Site "${site}" already exists in ~/.opencli/clis/. Use "opencli adapter reset ${site}" first to restore official version.`);
|
|
3007
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
catch { /* good, doesn't exist yet */ }
|
|
3011
|
+
fs.cpSync(builtinSiteDir, userSiteDir, { recursive: true });
|
|
3012
|
+
console.log(`✅ Ejected "${site}" to ~/.opencli/clis/${site}/`);
|
|
3013
|
+
console.log('You can now edit the adapter files. Changes take effect immediately.');
|
|
3014
|
+
console.log('Note: Official updates to this adapter will overwrite your changes.');
|
|
3015
|
+
});
|
|
3016
|
+
adapterCmd
|
|
3017
|
+
.command('reset')
|
|
3018
|
+
.description('Remove local override and restore official adapter version')
|
|
3019
|
+
.argument('[site]', 'Site name (e.g. twitter, bilibili)')
|
|
3020
|
+
.option('--all', 'Reset all local overrides')
|
|
3021
|
+
.action(async (site, opts) => {
|
|
3022
|
+
const os = await import('node:os');
|
|
3023
|
+
const userClisDir = path.join(os.homedir(), '.opencli', 'clis');
|
|
3024
|
+
if (opts.all) {
|
|
3025
|
+
try {
|
|
3026
|
+
const userEntries = await fs.promises.readdir(userClisDir, { withFileTypes: true });
|
|
3027
|
+
const dirs = userEntries.filter(e => e.isDirectory());
|
|
3028
|
+
if (dirs.length === 0) {
|
|
3029
|
+
console.log('No local sites to reset.');
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
for (const dir of dirs) {
|
|
3033
|
+
fs.rmSync(path.join(userClisDir, dir.name), { recursive: true, force: true });
|
|
3034
|
+
}
|
|
3035
|
+
console.log(`✅ Reset ${dirs.length} site(s). All adapters now use official baseline.`);
|
|
3036
|
+
}
|
|
3037
|
+
catch {
|
|
3038
|
+
console.log('No local sites to reset.');
|
|
3039
|
+
}
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (!site) {
|
|
3043
|
+
console.error('Error: Please specify a site name or use --all.');
|
|
3044
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
const userSiteDir = path.join(userClisDir, site);
|
|
3048
|
+
try {
|
|
3049
|
+
await fs.promises.access(userSiteDir);
|
|
3050
|
+
}
|
|
3051
|
+
catch {
|
|
3052
|
+
console.error(`Site "${site}" has no local override.`);
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
const isOfficial = fs.existsSync(path.join(BUILTIN_CLIS, site));
|
|
3056
|
+
fs.rmSync(userSiteDir, { recursive: true, force: true });
|
|
3057
|
+
console.log(isOfficial
|
|
3058
|
+
? `✅ Reset "${site}". Now using official baseline.`
|
|
3059
|
+
: `✅ Removed custom site "${site}".`);
|
|
3060
|
+
});
|
|
3061
|
+
// ── Built-in: browser profile selection ──────────────────────────────────
|
|
3062
|
+
const profileCmd = program.command('profile').description('Manage Browser Bridge Chrome profiles');
|
|
3063
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
3064
|
+
const originalProfileDescription = profileCmd.description();
|
|
3065
|
+
profileCmd
|
|
3066
|
+
.command('list')
|
|
3067
|
+
.description('List Chrome profiles connected through the Browser Bridge extension')
|
|
3068
|
+
.action(async () => {
|
|
3069
|
+
const status = await fetchDaemonStatus();
|
|
3070
|
+
const config = loadProfileConfig();
|
|
3071
|
+
const profiles = status?.profiles ?? [];
|
|
3072
|
+
if (!status) {
|
|
3073
|
+
console.log('Daemon is not running. Run opencli doctor after opening Chrome.');
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
if (isDaemonStale(status, PKG_VERSION) || !Array.isArray(status.profiles)) {
|
|
3077
|
+
console.log(`Daemon ${formatDaemonVersion(status)} is stale for CLI v${PKG_VERSION}.`);
|
|
3078
|
+
console.log('Run: opencli daemon restart');
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
if (profiles.length === 0) {
|
|
3082
|
+
console.log('No Browser Bridge profiles connected.');
|
|
3083
|
+
console.log('Open a Chrome profile with the OpenCLI extension installed, then run opencli profile list again.');
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
const knownContextIds = new Set(profiles.map((profile) => profile.contextId));
|
|
3087
|
+
console.log('Connected Browser Bridge profiles');
|
|
3088
|
+
console.log();
|
|
3089
|
+
for (const profile of profiles) {
|
|
3090
|
+
const alias = aliasForContextId(config, profile.contextId);
|
|
3091
|
+
const defaultMark = config.defaultContextId === profile.contextId ? ' default' : '';
|
|
3092
|
+
const aliasText = alias ? ` ${alias}` : '';
|
|
3093
|
+
const version = profile.extensionVersion ? ` v${profile.extensionVersion}` : ' version unknown';
|
|
3094
|
+
console.log(` ${profile.contextId}${aliasText}${defaultMark} — connected${version}`);
|
|
3095
|
+
}
|
|
3096
|
+
const disconnectedAliases = Object.entries(config.aliases)
|
|
3097
|
+
.filter(([, contextId]) => !knownContextIds.has(contextId));
|
|
3098
|
+
if (disconnectedAliases.length > 0 || (config.defaultContextId && !knownContextIds.has(config.defaultContextId))) {
|
|
3099
|
+
console.log();
|
|
3100
|
+
console.log('Disconnected saved profiles:');
|
|
3101
|
+
const shown = new Set();
|
|
3102
|
+
for (const [alias, contextId] of disconnectedAliases) {
|
|
3103
|
+
shown.add(contextId);
|
|
3104
|
+
console.log(` ${contextId} ${alias} — not connected`);
|
|
3105
|
+
}
|
|
3106
|
+
if (config.defaultContextId && !shown.has(config.defaultContextId) && !knownContextIds.has(config.defaultContextId)) {
|
|
3107
|
+
console.log(` ${config.defaultContextId} — default, not connected`);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
});
|
|
3111
|
+
profileCmd
|
|
3112
|
+
.command('rename')
|
|
3113
|
+
.description('Assign a local alias to a connected Browser Bridge profile')
|
|
3114
|
+
.argument('<contextId>', 'Profile contextId from opencli profile list')
|
|
3115
|
+
.argument('<alias>', 'Local alias, e.g. work or personal')
|
|
3116
|
+
.action((contextId, alias) => {
|
|
3117
|
+
try {
|
|
3118
|
+
renameProfile(contextId, alias);
|
|
3119
|
+
console.log(`Profile ${contextId} is now aliased as ${alias}.`);
|
|
3120
|
+
}
|
|
3121
|
+
catch (err) {
|
|
3122
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
3123
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
3124
|
+
}
|
|
3125
|
+
});
|
|
3126
|
+
profileCmd
|
|
3127
|
+
.command('use')
|
|
3128
|
+
.description('Set the default Browser Bridge profile for future commands')
|
|
3129
|
+
.argument('<profile>', 'Profile alias or contextId')
|
|
3130
|
+
.action((profile) => {
|
|
3131
|
+
try {
|
|
3132
|
+
const config = setDefaultProfile(profile);
|
|
3133
|
+
console.log(`Default Browser Bridge profile: ${config.defaultContextId ?? profile}`);
|
|
3134
|
+
}
|
|
3135
|
+
catch (err) {
|
|
3136
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
3137
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
// ── Built-in: daemon ──────────────────────────────────────────────────────
|
|
3141
|
+
const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
|
|
3142
|
+
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
|
|
3143
|
+
const originalDaemonDescription = daemonCmd.description();
|
|
3144
|
+
daemonCmd
|
|
3145
|
+
.command('status')
|
|
3146
|
+
.description('Show daemon status')
|
|
3147
|
+
.action(async () => { await daemonStatus(); });
|
|
3148
|
+
daemonCmd
|
|
3149
|
+
.command('stop')
|
|
3150
|
+
.description('Stop the daemon')
|
|
3151
|
+
.action(async () => { await daemonStop(); });
|
|
3152
|
+
daemonCmd
|
|
3153
|
+
.command('restart')
|
|
3154
|
+
.description('Restart the daemon')
|
|
3155
|
+
.action(async () => { await daemonRestart(); });
|
|
3156
|
+
// ── External CLIs ─────────────────────────────────────────────────────────
|
|
3157
|
+
const externalClis = loadExternalClis();
|
|
3158
|
+
const externalCmd = program
|
|
3159
|
+
.command('external')
|
|
3160
|
+
.description('Manage external CLI passthrough commands');
|
|
3161
|
+
externalCmd
|
|
3162
|
+
.command('install')
|
|
3163
|
+
.description('Install an external CLI')
|
|
3164
|
+
.argument('<name>', 'Name of the external CLI')
|
|
3165
|
+
.action((name) => {
|
|
3166
|
+
const ext = externalClis.find(e => e.name === name);
|
|
3167
|
+
if (!ext) {
|
|
3168
|
+
console.error(`External CLI '${name}' not found in registry.`);
|
|
3169
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
installExternalCli(ext);
|
|
3173
|
+
});
|
|
3174
|
+
externalCmd
|
|
3175
|
+
.command('register')
|
|
3176
|
+
.description('Register an external CLI')
|
|
3177
|
+
.argument('<name>', 'Name of the CLI')
|
|
3178
|
+
.option('--binary <bin>', 'Binary name if different from name')
|
|
3179
|
+
.option('--install <cmd>', 'Auto-install command')
|
|
3180
|
+
.option('--desc <text>', 'Description')
|
|
3181
|
+
.action((name, opts) => {
|
|
3182
|
+
registerExternalCli(name, { binary: opts.binary, install: opts.install, description: opts.desc });
|
|
3183
|
+
});
|
|
3184
|
+
externalCmd
|
|
3185
|
+
.command('list')
|
|
3186
|
+
.description('List registered external CLIs')
|
|
3187
|
+
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
3188
|
+
.action((opts) => {
|
|
3189
|
+
const rows = loadExternalClis().map((ext) => ({
|
|
3190
|
+
name: ext.name,
|
|
3191
|
+
package: ext.package ?? '',
|
|
3192
|
+
binary: ext.binary,
|
|
3193
|
+
installed: isBinaryInstalled(ext.binary),
|
|
3194
|
+
description: ext.description ?? '',
|
|
3195
|
+
homepage: ext.homepage ?? '',
|
|
3196
|
+
tags: ext.tags?.join(', ') ?? '',
|
|
3197
|
+
}));
|
|
3198
|
+
renderOutput(rows, {
|
|
3199
|
+
fmt: opts.format,
|
|
3200
|
+
columns: ['name', 'package', 'binary', 'installed', 'description', 'homepage', 'tags'],
|
|
3201
|
+
title: 'opencli/external/list',
|
|
3202
|
+
source: 'opencli external list',
|
|
3203
|
+
});
|
|
3204
|
+
});
|
|
3205
|
+
function passthroughExternal(name, parsedArgs) {
|
|
3206
|
+
const args = parsedArgs ?? (() => {
|
|
3207
|
+
const idx = process.argv.indexOf(name);
|
|
3208
|
+
return process.argv.slice(idx + 1);
|
|
3209
|
+
})();
|
|
3210
|
+
try {
|
|
3211
|
+
executeExternalCli(name, args, externalClis);
|
|
3212
|
+
}
|
|
3213
|
+
catch (err) {
|
|
3214
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
3215
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
for (const ext of externalClis) {
|
|
3219
|
+
if (program.commands.some(c => c.name() === ext.name))
|
|
3220
|
+
continue;
|
|
3221
|
+
program
|
|
3222
|
+
.command(ext.name)
|
|
3223
|
+
.description(`(External) ${ext.description || ext.name}`)
|
|
3224
|
+
.argument('[args...]')
|
|
3225
|
+
.allowUnknownOption()
|
|
3226
|
+
.passThroughOptions()
|
|
3227
|
+
.helpOption(false)
|
|
3228
|
+
.action((args) => passthroughExternal(ext.name, args));
|
|
3229
|
+
}
|
|
3230
|
+
// ── Antigravity serve (long-running, special case) ────────────────────────
|
|
3231
|
+
const antigravityCmd = program.command('antigravity').description('antigravity commands');
|
|
3232
|
+
antigravityCmd
|
|
3233
|
+
.command('serve')
|
|
3234
|
+
.description('Start Anthropic-compatible API proxy for Antigravity')
|
|
3235
|
+
.option('--port <port>', 'Server port (default: 8082)', '8082')
|
|
3236
|
+
.option('--timeout <seconds>', 'Maximum time to wait for a reply (default: 120s)')
|
|
3237
|
+
.action(async (opts) => {
|
|
3238
|
+
// @ts-expect-error JS adapter — no type declarations
|
|
3239
|
+
const { startServe } = await import('../../clis/antigravity/serve.js');
|
|
3240
|
+
await startServe({
|
|
3241
|
+
port: parseInt(opts.port, 10),
|
|
3242
|
+
timeout: opts.timeout ? parsePositiveIntOption(opts.timeout, '--timeout', 120) : undefined,
|
|
3243
|
+
});
|
|
3244
|
+
});
|
|
3245
|
+
// ── Dynamic adapter commands ──────────────────────────────────────────────
|
|
3246
|
+
const siteGroups = new Map();
|
|
3247
|
+
siteGroups.set('antigravity', antigravityCmd);
|
|
3248
|
+
const siteNames = registerAllCommands(program, siteGroups);
|
|
3249
|
+
applyRootSubcommandSummaries(program);
|
|
3250
|
+
// ── Help-text grouping: External CLIs / App adapters / Site adapters ──
|
|
3251
|
+
// Classification derives from each adapter's `domain` field — see classifyAdapter.
|
|
3252
|
+
// External CLIs are taken from the externalClis registry (passthrough binaries).
|
|
3253
|
+
const externalNames = externalClis.map(ext => ext.name);
|
|
3254
|
+
const externalHelpEntries = externalClis.map(ext => ({
|
|
3255
|
+
name: ext.name,
|
|
3256
|
+
label: formatExternalCliLabel(ext),
|
|
3257
|
+
}));
|
|
3258
|
+
const siteDomains = new Map();
|
|
3259
|
+
for (const [, cmd] of getRegistry()) {
|
|
3260
|
+
if (!siteDomains.has(cmd.site))
|
|
3261
|
+
siteDomains.set(cmd.site, cmd.domain);
|
|
3262
|
+
}
|
|
3263
|
+
const apps = [];
|
|
3264
|
+
const sites = [];
|
|
3265
|
+
for (const site of siteNames) {
|
|
3266
|
+
if (classifyAdapter(siteDomains.get(site)) === 'app')
|
|
3267
|
+
apps.push(site);
|
|
3268
|
+
else
|
|
3269
|
+
sites.push(site);
|
|
3270
|
+
}
|
|
3271
|
+
const adapterGroups = { external: externalHelpEntries, apps, sites };
|
|
3272
|
+
const adapterNameSet = new Set([...externalNames, ...siteNames]);
|
|
3273
|
+
installCommanderNamespaceStructuredHelp(browser, { globalCommand: program, description: originalBrowserDescription });
|
|
3274
|
+
installCommanderNamespaceStructuredHelp(authCmd, { globalCommand: program, description: 'Inspect website login status' });
|
|
3275
|
+
installCommanderNamespaceStructuredHelp(daemonCmd, { globalCommand: program, description: originalDaemonDescription });
|
|
3276
|
+
installCommanderNamespaceStructuredHelp(pluginCmd, { globalCommand: program, description: originalPluginDescription });
|
|
3277
|
+
installCommanderNamespaceStructuredHelp(adapterCmd, { globalCommand: program, description: originalAdapterDescription });
|
|
3278
|
+
installCommanderNamespaceStructuredHelp(profileCmd, { globalCommand: program, description: originalProfileDescription });
|
|
3279
|
+
program.configureHelp({
|
|
3280
|
+
visibleCommands: (command) => command.commands.filter(child => command !== program || !adapterNameSet.has(child.name())),
|
|
3281
|
+
});
|
|
3282
|
+
// When an ancestor command declares a leading positional via `.usage(...)`
|
|
3283
|
+
// (e.g. `browser` -> `<session> <command> [options]`), inject the positional
|
|
3284
|
+
// between that ancestor's name and the next path segment so the help Usage
|
|
3285
|
+
// line is accurate: `Usage: opencli browser <session> click [target] [options]`
|
|
3286
|
+
// instead of `opencli browser click [target] [options]`. Commander does NOT
|
|
3287
|
+
// inherit configureHelp into subcommands, so we walk the descendant tree and
|
|
3288
|
+
// apply the override on each.
|
|
3289
|
+
const ancestorAwareCommandUsage = (cmd) => {
|
|
3290
|
+
const ancestors = [];
|
|
3291
|
+
let ancestor = cmd.parent;
|
|
3292
|
+
while (ancestor) {
|
|
3293
|
+
const positional = leadingPositionalFromUsage(ancestor);
|
|
3294
|
+
ancestors.unshift(positional ? `${ancestor.name()} ${positional}` : ancestor.name());
|
|
3295
|
+
ancestor = ancestor.parent;
|
|
3296
|
+
}
|
|
3297
|
+
return [...ancestors, cmd.name(), cmd.usage()].filter(Boolean).join(' ').trim();
|
|
3298
|
+
};
|
|
3299
|
+
function applyAncestorAwareUsage(cmd) {
|
|
3300
|
+
cmd.configureHelp({ commandUsage: ancestorAwareCommandUsage });
|
|
3301
|
+
for (const sub of cmd.commands)
|
|
3302
|
+
applyAncestorAwareUsage(sub);
|
|
3303
|
+
}
|
|
3304
|
+
applyAncestorAwareUsage(browser);
|
|
3305
|
+
installStructuredHelp(program, () => rootHelpData(program, adapterGroups), () => formatRootAdapterHelpText(adapterGroups));
|
|
3306
|
+
// ── Unknown command fallback ──────────────────────────────────────────────
|
|
3307
|
+
// Security: do NOT auto-discover and register arbitrary system binaries.
|
|
3308
|
+
// Only explicitly registered external CLIs are allowed.
|
|
3309
|
+
program.on('command:*', (operands) => {
|
|
3310
|
+
const binary = operands[0];
|
|
3311
|
+
console.error(`error: unknown command '${binary}'`);
|
|
3312
|
+
if (isBinaryInstalled(binary)) {
|
|
3313
|
+
console.error(` Tip: '${binary}' exists on your PATH. Use 'opencli external register ${binary}' to add it as an external CLI.`);
|
|
3314
|
+
}
|
|
3315
|
+
program.outputHelp();
|
|
3316
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
3317
|
+
});
|
|
3318
|
+
return program;
|
|
3319
|
+
}
|
|
3320
|
+
export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
3321
|
+
createProgram(BUILTIN_CLIS, USER_CLIS).parse();
|
|
3322
|
+
}
|
|
3323
|
+
export { findPackageRoot };
|
|
3324
|
+
export function resolveBrowserVerifyInvocation(opts = {}) {
|
|
3325
|
+
const platform = opts.platform ?? process.platform;
|
|
3326
|
+
const fileExists = opts.fileExists ?? fs.existsSync;
|
|
3327
|
+
const readFile = opts.readFile ?? ((filePath) => fs.readFileSync(filePath, 'utf-8'));
|
|
3328
|
+
const projectRoot = opts.projectRoot ?? findPackageRoot(CLI_FILE, fileExists);
|
|
3329
|
+
for (const builtEntry of getBuiltEntryCandidates(projectRoot, readFile)) {
|
|
3330
|
+
if (fileExists(builtEntry)) {
|
|
3331
|
+
return {
|
|
3332
|
+
binary: process.execPath,
|
|
3333
|
+
args: [builtEntry],
|
|
3334
|
+
cwd: projectRoot,
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
const sourceEntry = path.join(projectRoot, 'src', 'main.ts');
|
|
3339
|
+
if (!fileExists(sourceEntry)) {
|
|
3340
|
+
throw new Error(`Could not find opencli entrypoint under ${projectRoot}. Expected built entry from package.json or src/main.ts.`);
|
|
3341
|
+
}
|
|
3342
|
+
const localTsxBin = path.join(projectRoot, 'node_modules', '.bin', platform === 'win32' ? 'tsx.cmd' : 'tsx');
|
|
3343
|
+
if (fileExists(localTsxBin)) {
|
|
3344
|
+
return {
|
|
3345
|
+
binary: localTsxBin,
|
|
3346
|
+
args: [sourceEntry],
|
|
3347
|
+
cwd: projectRoot,
|
|
3348
|
+
...(platform === 'win32' ? { shell: true } : {}),
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
return {
|
|
3352
|
+
binary: platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
3353
|
+
args: ['tsx', sourceEntry],
|
|
3354
|
+
cwd: projectRoot,
|
|
3355
|
+
...(platform === 'win32' ? { shell: true } : {}),
|
|
3356
|
+
};
|
|
3357
|
+
}
|