@jackwener/opencli 1.0.6 → 1.1.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/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
- package/.github/pull_request_template.md +7 -0
- package/.github/workflows/doc-check.yml +36 -0
- package/.github/workflows/docs.yml +7 -42
- package/CHANGELOG.md +23 -0
- package/CLI-EXPLORER.md +9 -8
- package/README.md +51 -10
- package/README.zh-CN.md +29 -11
- package/SKILL.md +102 -33
- package/dist/browser/cdp.js +6 -1
- package/dist/browser/page.d.ts +4 -1
- package/dist/browser/page.js +7 -1
- package/dist/build-manifest.js +23 -16
- package/dist/cli-manifest.json +951 -296
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +225 -148
- package/dist/clis/antigravity/serve.js +296 -47
- package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
- package/dist/clis/apple-podcasts/commands.test.js +76 -0
- package/dist/clis/apple-podcasts/search.js +2 -2
- package/dist/clis/apple-podcasts/top.js +9 -2
- package/dist/clis/arxiv/paper.js +21 -0
- package/dist/clis/arxiv/search.js +24 -0
- package/dist/clis/arxiv/utils.d.ts +18 -0
- package/dist/clis/arxiv/utils.js +49 -0
- package/dist/clis/bilibili/dynamic.js +1 -1
- package/dist/clis/bilibili/favorite.js +1 -1
- package/dist/clis/bilibili/feed.js +1 -1
- package/dist/clis/bilibili/following.js +1 -1
- package/dist/clis/bilibili/history.js +1 -1
- package/dist/clis/bilibili/me.js +1 -1
- package/dist/clis/bilibili/ranking.js +1 -1
- package/dist/clis/bilibili/search.js +3 -3
- package/dist/clis/bilibili/subtitle.js +1 -1
- package/dist/clis/bilibili/user-videos.js +1 -1
- package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
- package/dist/clis/bloomberg/businessweek.d.ts +1 -0
- package/dist/clis/bloomberg/businessweek.js +17 -0
- package/dist/clis/bloomberg/economics.d.ts +1 -0
- package/dist/clis/bloomberg/economics.js +17 -0
- package/dist/clis/bloomberg/feeds.d.ts +1 -0
- package/dist/clis/bloomberg/feeds.js +15 -0
- package/dist/clis/bloomberg/industries.d.ts +1 -0
- package/dist/clis/bloomberg/industries.js +17 -0
- package/dist/clis/bloomberg/main.d.ts +1 -0
- package/dist/clis/bloomberg/main.js +17 -0
- package/dist/clis/bloomberg/markets.d.ts +1 -0
- package/dist/clis/bloomberg/markets.js +17 -0
- package/dist/clis/bloomberg/news.d.ts +1 -0
- package/dist/clis/bloomberg/news.js +105 -0
- package/dist/clis/bloomberg/opinions.d.ts +1 -0
- package/dist/clis/bloomberg/opinions.js +17 -0
- package/dist/clis/bloomberg/politics.d.ts +1 -0
- package/dist/clis/bloomberg/politics.js +17 -0
- package/dist/clis/bloomberg/tech.d.ts +1 -0
- package/dist/clis/bloomberg/tech.js +17 -0
- package/dist/clis/bloomberg/utils.d.ts +34 -0
- package/dist/clis/bloomberg/utils.js +364 -0
- package/dist/clis/bloomberg/utils.test.d.ts +1 -0
- package/dist/clis/bloomberg/utils.test.js +129 -0
- package/dist/clis/boss/batchgreet.d.ts +1 -0
- package/dist/clis/boss/batchgreet.js +147 -0
- package/dist/clis/boss/chatlist.js +2 -2
- package/dist/clis/boss/detail.js +2 -2
- package/dist/clis/boss/exchange.d.ts +1 -0
- package/dist/clis/boss/exchange.js +111 -0
- package/dist/clis/boss/greet.d.ts +1 -0
- package/dist/clis/boss/greet.js +175 -0
- package/dist/clis/boss/invite.d.ts +1 -0
- package/dist/clis/boss/invite.js +158 -0
- package/dist/clis/boss/joblist.d.ts +1 -0
- package/dist/clis/boss/joblist.js +55 -0
- package/dist/clis/boss/mark.d.ts +1 -0
- package/dist/clis/boss/mark.js +141 -0
- package/dist/clis/boss/recommend.d.ts +1 -0
- package/dist/clis/boss/recommend.js +83 -0
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +1 -1
- package/dist/clis/boss/stats.d.ts +1 -0
- package/dist/clis/boss/stats.js +116 -0
- package/dist/clis/chaoxing/assignments.js +1 -1
- package/dist/clis/chaoxing/exams.js +1 -1
- package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
- package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
- package/dist/clis/chaoxing/utils.test.d.ts +1 -0
- package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
- package/dist/clis/chatgpt/read.js +1 -1
- package/dist/clis/chatwise/export.js +1 -1
- package/dist/clis/chatwise/model.js +2 -2
- package/dist/clis/chatwise/screenshot.js +1 -1
- package/dist/clis/codex/export.js +1 -1
- package/dist/clis/codex/model.js +2 -2
- package/dist/clis/codex/screenshot.js +1 -1
- package/dist/clis/coupang/add-to-cart.js +3 -4
- package/dist/clis/coupang/search.js +2 -4
- package/dist/clis/coupang/utils.test.d.ts +1 -0
- package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
- package/dist/clis/ctrip/search.js +1 -1
- package/dist/clis/cursor/export.js +1 -1
- package/dist/clis/cursor/model.js +2 -2
- package/dist/clis/cursor/screenshot.js +1 -1
- package/dist/clis/jike/comment.js +2 -3
- package/dist/clis/jike/create.js +1 -2
- package/dist/clis/jike/feed.js +0 -1
- package/dist/clis/jike/like.js +1 -2
- package/dist/clis/jike/notifications.js +0 -1
- package/dist/clis/jike/post.yaml +1 -0
- package/dist/clis/jike/repost.js +1 -2
- package/dist/clis/jike/search.js +2 -3
- package/dist/clis/jike/topic.yaml +1 -0
- package/dist/clis/jike/user.yaml +1 -0
- package/dist/clis/jimeng/history.yaml +0 -1
- package/dist/clis/linkedin/search.js +7 -7
- package/dist/clis/linux-do/category.yaml +1 -0
- package/dist/clis/linux-do/search.yaml +4 -3
- package/dist/clis/linux-do/topic.yaml +1 -0
- package/dist/clis/notion/export.js +1 -1
- package/dist/clis/reddit/comment.js +3 -4
- package/dist/clis/reddit/read.js +4 -5
- package/dist/clis/reddit/save.js +2 -3
- package/dist/clis/reddit/saved.js +0 -1
- package/dist/clis/reddit/search.yaml +1 -0
- package/dist/clis/reddit/subscribe.js +0 -1
- package/dist/clis/reddit/upvote.js +2 -3
- package/dist/clis/reddit/upvoted.js +0 -1
- package/dist/clis/reddit/user-comments.yaml +1 -0
- package/dist/clis/reddit/user-posts.yaml +1 -0
- package/dist/clis/reddit/user.yaml +1 -0
- package/dist/clis/reuters/search.js +1 -1
- package/dist/clis/sinafinance/news.d.ts +7 -0
- package/dist/clis/sinafinance/news.js +61 -0
- package/dist/clis/smzdm/search.js +2 -3
- package/dist/clis/stackoverflow/search.yaml +1 -0
- package/dist/clis/steam/top-sellers.yaml +29 -0
- package/dist/clis/twitter/accept.js +2 -2
- package/dist/clis/twitter/article.js +2 -2
- package/dist/clis/twitter/block.d.ts +1 -0
- package/dist/clis/twitter/block.js +88 -0
- package/dist/clis/twitter/delete.js +1 -1
- package/dist/clis/twitter/hide-reply.d.ts +1 -0
- package/dist/clis/twitter/hide-reply.js +66 -0
- package/dist/clis/twitter/like.js +1 -1
- package/dist/clis/twitter/post.js +1 -1
- package/dist/clis/twitter/reply-dm.js +1 -1
- package/dist/clis/twitter/reply.js +2 -2
- package/dist/clis/twitter/search.js +1 -1
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/trending.d.ts +1 -0
- package/dist/clis/twitter/trending.js +91 -0
- package/dist/clis/twitter/unblock.d.ts +1 -0
- package/dist/clis/twitter/unblock.js +71 -0
- package/dist/clis/v2ex/topic.yaml +1 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weread/book.js +1 -1
- package/dist/clis/weread/highlights.js +1 -1
- package/dist/clis/weread/notes.js +1 -1
- package/dist/clis/weread/search.js +1 -1
- package/dist/clis/wikipedia/search.d.ts +1 -0
- package/dist/clis/wikipedia/search.js +30 -0
- package/dist/clis/wikipedia/summary.d.ts +1 -0
- package/dist/clis/wikipedia/summary.js +28 -0
- package/dist/clis/wikipedia/utils.d.ts +8 -0
- package/dist/clis/wikipedia/utils.js +18 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +79 -5
- package/dist/clis/xiaohongshu/creator-note-detail.js +323 -70
- package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +258 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
- package/dist/clis/xiaohongshu/creator-notes.js +189 -71
- package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes.test.js +191 -0
- package/dist/clis/xiaohongshu/creator-profile.js +0 -1
- package/dist/clis/xiaohongshu/creator-stats.js +0 -1
- package/dist/clis/xiaohongshu/download.js +2 -3
- package/dist/clis/xiaohongshu/feed.yaml +0 -1
- package/dist/clis/xiaohongshu/notifications.yaml +0 -1
- package/dist/clis/xiaohongshu/search.js +2 -2
- package/dist/clis/xiaohongshu/user.js +1 -2
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/search.js +1 -1
- package/dist/clis/youtube/transcript.js +1 -1
- package/dist/clis/youtube/video.js +1 -1
- package/dist/clis/zhihu/download.js +1 -2
- package/dist/clis/zhihu/question.js +1 -1
- package/dist/clis/zhihu/search.yaml +4 -3
- package/dist/commanderAdapter.d.ts +21 -0
- package/dist/commanderAdapter.js +111 -0
- package/dist/{engine.d.ts → discovery.d.ts} +0 -6
- package/dist/{engine.js → discovery.js} +1 -98
- package/dist/download/index.d.ts +2 -6
- package/dist/download/index.js +19 -46
- package/dist/engine.test.d.ts +1 -1
- package/dist/engine.test.js +8 -7
- package/dist/execution.d.ts +22 -0
- package/dist/execution.js +129 -0
- package/dist/explore.js +121 -107
- package/dist/external-clis.yaml +48 -0
- package/dist/external.d.ts +25 -0
- package/dist/external.js +156 -0
- package/dist/main.js +1 -1
- package/dist/pipeline/steps/browser.js +8 -2
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +2 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +8 -0
- package/dist/serialization.d.ts +34 -0
- package/dist/serialization.js +63 -0
- package/dist/types.d.ts +4 -1
- package/docs/.vitepress/config.mts +14 -3
- package/docs/adapters/browser/arxiv.md +27 -0
- package/docs/adapters/browser/barchart.md +32 -0
- package/docs/adapters/browser/bloomberg.md +70 -0
- package/docs/adapters/browser/chaoxing.md +39 -0
- package/docs/adapters/browser/grok.md +35 -0
- package/docs/adapters/browser/hf.md +42 -0
- package/docs/adapters/browser/jike.md +45 -0
- package/docs/adapters/browser/jimeng.md +39 -0
- package/docs/adapters/browser/linux-do.md +45 -0
- package/docs/adapters/browser/sinafinance.md +35 -0
- package/docs/adapters/browser/stackoverflow.md +35 -0
- package/docs/adapters/browser/steam.md +26 -0
- package/docs/adapters/browser/twitter.md +3 -0
- package/docs/adapters/browser/weread.md +48 -0
- package/docs/adapters/browser/wikipedia.md +30 -0
- package/docs/adapters/browser/xiaohongshu.md +5 -1
- package/docs/adapters/desktop/chatgpt.md +3 -3
- package/docs/adapters/index.md +13 -0
- package/docs/advanced/download.md +4 -4
- package/docs/developer/architecture.md +17 -4
- package/package.json +1 -1
- package/scripts/check-doc-coverage.sh +69 -0
- package/scripts/copy-yaml.cjs +7 -0
- package/src/browser/cdp.ts +9 -4
- package/src/browser/page.ts +7 -1
- package/src/build-manifest.ts +25 -19
- package/src/cli.ts +253 -119
- package/src/clis/antigravity/serve.ts +323 -50
- package/src/clis/apple-podcasts/commands.test.ts +95 -0
- package/src/clis/apple-podcasts/search.ts +2 -2
- package/src/clis/apple-podcasts/top.ts +12 -2
- package/src/clis/arxiv/paper.ts +21 -0
- package/src/clis/arxiv/search.ts +24 -0
- package/src/clis/arxiv/utils.ts +63 -0
- package/src/clis/bilibili/dynamic.ts +1 -1
- package/src/clis/bilibili/favorite.ts +1 -1
- package/src/clis/bilibili/feed.ts +1 -1
- package/src/clis/bilibili/following.ts +1 -1
- package/src/clis/bilibili/history.ts +1 -1
- package/src/clis/bilibili/me.ts +1 -1
- package/src/clis/bilibili/ranking.ts +1 -1
- package/src/clis/bilibili/search.ts +3 -3
- package/src/clis/bilibili/subtitle.ts +1 -1
- package/src/clis/bilibili/user-videos.ts +1 -1
- package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
- package/src/clis/bloomberg/businessweek.ts +18 -0
- package/src/clis/bloomberg/economics.ts +18 -0
- package/src/clis/bloomberg/feeds.ts +16 -0
- package/src/clis/bloomberg/industries.ts +18 -0
- package/src/clis/bloomberg/main.ts +18 -0
- package/src/clis/bloomberg/markets.ts +18 -0
- package/src/clis/bloomberg/news.ts +136 -0
- package/src/clis/bloomberg/opinions.ts +18 -0
- package/src/clis/bloomberg/politics.ts +18 -0
- package/src/clis/bloomberg/tech.ts +18 -0
- package/src/clis/bloomberg/utils.test.ts +135 -0
- package/src/clis/bloomberg/utils.ts +429 -0
- package/src/clis/boss/batchgreet.ts +167 -0
- package/src/clis/boss/chatlist.ts +2 -2
- package/src/clis/boss/detail.ts +2 -2
- package/src/clis/boss/exchange.ts +126 -0
- package/src/clis/boss/greet.ts +198 -0
- package/src/clis/boss/invite.ts +177 -0
- package/src/clis/boss/joblist.ts +63 -0
- package/src/clis/boss/mark.ts +155 -0
- package/src/clis/boss/recommend.ts +94 -0
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +1 -1
- package/src/clis/boss/stats.ts +130 -0
- package/src/clis/chaoxing/assignments.ts +1 -1
- package/src/clis/chaoxing/exams.ts +1 -1
- package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
- package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
- package/src/clis/chatgpt/README.zh-CN.md +3 -3
- package/src/clis/chatgpt/read.ts +1 -1
- package/src/clis/chatwise/export.ts +1 -1
- package/src/clis/chatwise/model.ts +2 -2
- package/src/clis/chatwise/screenshot.ts +1 -1
- package/src/clis/codex/export.ts +1 -1
- package/src/clis/codex/model.ts +2 -2
- package/src/clis/codex/screenshot.ts +1 -1
- package/src/clis/coupang/add-to-cart.ts +3 -4
- package/src/clis/coupang/search.ts +2 -4
- package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
- package/src/clis/ctrip/search.ts +1 -1
- package/src/clis/cursor/export.ts +1 -1
- package/src/clis/cursor/model.ts +2 -2
- package/src/clis/cursor/screenshot.ts +1 -1
- package/src/clis/jike/comment.ts +2 -3
- package/src/clis/jike/create.ts +1 -2
- package/src/clis/jike/feed.ts +0 -1
- package/src/clis/jike/like.ts +1 -2
- package/src/clis/jike/notifications.ts +0 -1
- package/src/clis/jike/post.yaml +1 -0
- package/src/clis/jike/repost.ts +1 -2
- package/src/clis/jike/search.ts +2 -3
- package/src/clis/jike/topic.yaml +1 -0
- package/src/clis/jike/user.yaml +1 -0
- package/src/clis/jimeng/history.yaml +0 -1
- package/src/clis/linkedin/search.ts +7 -7
- package/src/clis/linux-do/category.yaml +1 -0
- package/src/clis/linux-do/search.yaml +4 -3
- package/src/clis/linux-do/topic.yaml +1 -0
- package/src/clis/notion/export.ts +1 -1
- package/src/clis/reddit/comment.ts +3 -4
- package/src/clis/reddit/read.ts +4 -5
- package/src/clis/reddit/save.ts +2 -3
- package/src/clis/reddit/saved.ts +0 -1
- package/src/clis/reddit/search.yaml +1 -0
- package/src/clis/reddit/subscribe.ts +0 -1
- package/src/clis/reddit/upvote.ts +2 -3
- package/src/clis/reddit/upvoted.ts +0 -1
- package/src/clis/reddit/user-comments.yaml +1 -0
- package/src/clis/reddit/user-posts.yaml +1 -0
- package/src/clis/reddit/user.yaml +1 -0
- package/src/clis/reuters/search.ts +1 -1
- package/src/clis/sinafinance/news.ts +76 -0
- package/src/clis/smzdm/search.ts +2 -3
- package/src/clis/stackoverflow/search.yaml +1 -0
- package/src/clis/steam/top-sellers.yaml +29 -0
- package/src/clis/twitter/accept.ts +2 -2
- package/src/clis/twitter/article.ts +2 -2
- package/src/clis/twitter/block.ts +92 -0
- package/src/clis/twitter/delete.ts +1 -1
- package/src/clis/twitter/hide-reply.ts +70 -0
- package/src/clis/twitter/like.ts +1 -1
- package/src/clis/twitter/post.ts +1 -1
- package/src/clis/twitter/reply-dm.ts +1 -1
- package/src/clis/twitter/reply.ts +2 -2
- package/src/clis/twitter/search.ts +1 -1
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/trending.ts +113 -0
- package/src/clis/twitter/unblock.ts +75 -0
- package/src/clis/v2ex/topic.yaml +1 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weread/book.ts +1 -1
- package/src/clis/weread/highlights.ts +1 -1
- package/src/clis/weread/notes.ts +1 -1
- package/src/clis/weread/search.ts +1 -1
- package/src/clis/wikipedia/search.ts +32 -0
- package/src/clis/wikipedia/summary.ts +28 -0
- package/src/clis/wikipedia/utils.ts +20 -0
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +272 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +425 -73
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
- package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
- package/src/clis/xiaohongshu/creator-notes.test.ts +211 -0
- package/src/clis/xiaohongshu/creator-notes.ts +254 -75
- package/src/clis/xiaohongshu/creator-profile.ts +0 -1
- package/src/clis/xiaohongshu/creator-stats.ts +0 -1
- package/src/clis/xiaohongshu/download.ts +2 -3
- package/src/clis/xiaohongshu/feed.yaml +0 -1
- package/src/clis/xiaohongshu/notifications.yaml +0 -1
- package/src/clis/xiaohongshu/search.ts +2 -2
- package/src/clis/xiaohongshu/user.ts +1 -2
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/search.ts +1 -1
- package/src/clis/youtube/transcript.ts +1 -1
- package/src/clis/youtube/video.ts +1 -1
- package/src/clis/zhihu/download.ts +1 -2
- package/src/clis/zhihu/question.ts +1 -1
- package/src/clis/zhihu/search.yaml +4 -3
- package/src/commanderAdapter.ts +113 -0
- package/src/daemon.ts +3 -3
- package/src/{engine.ts → discovery.ts} +1 -108
- package/src/download/index.ts +21 -54
- package/src/engine.test.ts +8 -7
- package/src/execution.ts +138 -0
- package/src/explore.ts +135 -109
- package/src/external-clis.yaml +48 -0
- package/src/external.ts +185 -0
- package/src/main.ts +1 -1
- package/src/pipeline/steps/browser.ts +7 -2
- package/src/registry.ts +5 -0
- package/src/runtime.ts +9 -0
- package/src/serialization.ts +79 -0
- package/src/types.ts +1 -1
- package/tests/e2e/browser-public.test.ts +25 -0
- package/tests/e2e/public-commands.test.ts +55 -1
- package/dist/clis/twitter/trending.yaml +0 -46
- package/src/clis/twitter/trending.yaml +0 -46
- /package/dist/{chaoxing.test.d.ts → clis/arxiv/paper.d.ts} +0 -0
- /package/dist/{coupang.test.d.ts → clis/arxiv/search.d.ts} +0 -0
- /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
- /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
- /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
- /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
|
@@ -81,76 +81,274 @@ function sleep(ms: number): Promise<void> {
|
|
|
81
81
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// ─── DOM helpers ─────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Click the 'New Conversation' button to reset context.
|
|
88
|
+
*/
|
|
89
|
+
async function startNewConversation(page: IPage): Promise<void> {
|
|
90
|
+
await page.evaluate(`
|
|
91
|
+
(() => {
|
|
92
|
+
const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
|
|
93
|
+
if (btn) btn.click();
|
|
94
|
+
})()
|
|
95
|
+
`);
|
|
96
|
+
await sleep(1000); // Give UI time to clear
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Switch the active model in Antigravity UI.
|
|
101
|
+
*/
|
|
102
|
+
async function switchModel(page: IPage, anthropicModelId: string): Promise<void> {
|
|
103
|
+
// Map standard model IDs to Antigravity UI names based on actual UI
|
|
104
|
+
let targetName = 'claude sonnet 4.6'; // Default fallback
|
|
105
|
+
const id = anthropicModelId.toLowerCase();
|
|
106
|
+
|
|
107
|
+
if (id.includes('sonnet')) {
|
|
108
|
+
targetName = 'claude sonnet 4.6';
|
|
109
|
+
} else if (id.includes('opus')) {
|
|
110
|
+
targetName = 'claude opus 4.6';
|
|
111
|
+
} else if (id.includes('gemini') && id.includes('pro')) {
|
|
112
|
+
targetName = 'gemini 3.1 pro (high)';
|
|
113
|
+
} else if (id.includes('gemini') && id.includes('flash')) {
|
|
114
|
+
targetName = 'gemini 3 flash';
|
|
115
|
+
} else if (id.includes('gpt')) {
|
|
116
|
+
targetName = 'gpt-oss 120b';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await page.evaluate(`
|
|
121
|
+
async () => {
|
|
122
|
+
const targetModelName = ${JSON.stringify(targetName)};
|
|
123
|
+
const trigger = document.querySelector('div[aria-haspopup="dialog"] > div[tabindex="0"]');
|
|
124
|
+
if (!trigger) return; // Silent fail if UI changed
|
|
125
|
+
|
|
126
|
+
// Open dropdown only if not already selected
|
|
127
|
+
if (trigger.innerText.toLowerCase().includes(targetModelName)) return;
|
|
128
|
+
|
|
129
|
+
trigger.click();
|
|
130
|
+
await new Promise(r => setTimeout(r, 200));
|
|
131
|
+
|
|
132
|
+
const spans = Array.from(document.querySelectorAll('[role="dialog"] span'));
|
|
133
|
+
const target = spans.find(s => s.innerText.toLowerCase().includes(targetModelName));
|
|
134
|
+
if (target) {
|
|
135
|
+
const optionNode = target.closest('.cursor-pointer') || target;
|
|
136
|
+
optionNode.click();
|
|
137
|
+
} else {
|
|
138
|
+
// Close if not found
|
|
139
|
+
trigger.click();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`);
|
|
143
|
+
await sleep(500); // Wait for switch
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`[serve] Warning: Could not switch to model ${targetName}:`, err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if the Antigravity UI is currently generating a response
|
|
151
|
+
* by looking for Stop/Cancel buttons or loading indicators.
|
|
152
|
+
*/
|
|
153
|
+
async function isGenerating(page: IPage): Promise<boolean> {
|
|
154
|
+
const result = await page.evaluate(`
|
|
155
|
+
(() => {
|
|
156
|
+
// Look for a cancel/stop button in the UI
|
|
157
|
+
const cancelBtn = document.querySelector('button[aria-label*="cancel" i], button[aria-label*="stop" i], button[title*="cancel" i], button[title*="stop" i]');
|
|
158
|
+
return !!cancelBtn;
|
|
159
|
+
})()
|
|
160
|
+
`);
|
|
161
|
+
return Boolean(result);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Walk from the scroll container and find the deepest element that
|
|
166
|
+
* has multiple non-empty children (our message container).
|
|
167
|
+
*/
|
|
168
|
+
function findMessageContainer(root: Element | null, depth = 0): Element | null {
|
|
169
|
+
if (!root || depth > 12) return null;
|
|
170
|
+
const nonEmpty = Array.from(root.children).filter(
|
|
171
|
+
c => (c as HTMLElement).innerText?.trim().length > 5
|
|
172
|
+
);
|
|
173
|
+
if (nonEmpty.length >= 2) return root;
|
|
174
|
+
if (nonEmpty.length === 1) return findMessageContainer(nonEmpty[0], depth + 1);
|
|
175
|
+
return root;
|
|
176
|
+
}
|
|
177
|
+
|
|
84
178
|
// ─── Antigravity CDP Operations ──────────────────────────────────────
|
|
85
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Get the full chat text for change-detection polling.
|
|
182
|
+
*/
|
|
86
183
|
async function getConversationText(page: IPage): Promise<string> {
|
|
87
184
|
const text = await page.evaluate(`
|
|
88
185
|
(() => {
|
|
89
186
|
const container = document.getElementById('conversation');
|
|
90
|
-
|
|
187
|
+
if (!container) return '';
|
|
188
|
+
// Read only the first child div (actual chat content),
|
|
189
|
+
// skipping UI chrome like file change panels, model selectors, etc.
|
|
190
|
+
const chatContent = container.children[0];
|
|
191
|
+
return chatContent ? chatContent.innerText : container.innerText;
|
|
91
192
|
})()
|
|
92
193
|
`);
|
|
93
194
|
return String(text ?? '');
|
|
94
195
|
}
|
|
95
196
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Get the text of the last assistant reply by navigating to the message container
|
|
199
|
+
* and extracting the last non-empty message block.
|
|
200
|
+
*/
|
|
201
|
+
async function getLastAssistantReply(page: IPage, userText?: string): Promise<string> {
|
|
202
|
+
const text = await page.evaluate(`
|
|
203
|
+
(() => {
|
|
204
|
+
const conv = document.getElementById('conversation')?.children[0];
|
|
205
|
+
const scroll = conv?.querySelector('.overflow-y-auto');
|
|
206
|
+
|
|
207
|
+
// Walk down until we find a container with multiple message siblings
|
|
208
|
+
function findMsgContainer(el, depth) {
|
|
209
|
+
if (!el || depth > 12) return null;
|
|
210
|
+
const nonEmpty = Array.from(el.children).filter(c => c.innerText && c.innerText.trim().length > 5);
|
|
211
|
+
if (nonEmpty.length >= 2) return el;
|
|
212
|
+
if (nonEmpty.length === 1) return findMsgContainer(nonEmpty[0], depth + 1);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const container = findMsgContainer(scroll || conv, 0);
|
|
217
|
+
if (!container) return '';
|
|
218
|
+
|
|
219
|
+
// Get all non-empty children (skip trailing empty UI divs)
|
|
220
|
+
const msgs = Array.from(container.children).filter(
|
|
221
|
+
c => c.innerText && c.innerText.trim().length > 5
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (msgs.length === 0) return '';
|
|
225
|
+
|
|
226
|
+
// The last element is the last assistant reply
|
|
227
|
+
const last = msgs[msgs.length - 1];
|
|
228
|
+
return last.innerText || '';
|
|
229
|
+
})()
|
|
230
|
+
`);
|
|
231
|
+
let reply = String(text ?? '').trim();
|
|
232
|
+
|
|
233
|
+
// Strip echoed user message from the top (Antigravity sometimes includes it)
|
|
234
|
+
if (userText && reply.startsWith(userText)) {
|
|
235
|
+
reply = reply.slice(userText.length).trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Strip thinking block: "Thought for Xs\n..." at the start
|
|
239
|
+
reply = reply.replace(/^Thought for[^\n]*\n+/i, '').trim();
|
|
240
|
+
|
|
241
|
+
// Strip "Copy" button text at the end
|
|
242
|
+
reply = reply.replace(/\s*\bCopy\b\s*$/m, '').trim();
|
|
243
|
+
|
|
244
|
+
// De-duplicate trailing repeated content (e.g., "OK\n\nOK" → "OK")
|
|
245
|
+
const half = Math.floor(reply.length / 2);
|
|
246
|
+
const firstHalf = reply.slice(0, half).trim();
|
|
247
|
+
const secondHalf = reply.slice(half).trim();
|
|
248
|
+
if (firstHalf && firstHalf === secondHalf) {
|
|
249
|
+
reply = firstHalf;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return reply;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function sendMessage(page: IPage, message: string, bridge?: CDPBridge): Promise<void> {
|
|
256
|
+
if (!bridge) {
|
|
257
|
+
// Fallback: use JS-based approach
|
|
258
|
+
await page.evaluate(`
|
|
259
|
+
(() => {
|
|
260
|
+
const container = document.getElementById('antigravity.agentSidePanelInputBox');
|
|
261
|
+
const editor = container?.querySelector('[data-lexical-editor="true"]');
|
|
262
|
+
if (!editor) throw new Error('Could not find input box');
|
|
263
|
+
editor.focus();
|
|
264
|
+
document.execCommand('insertText', false, ${JSON.stringify(message)});
|
|
265
|
+
})()
|
|
266
|
+
`);
|
|
267
|
+
await sleep(500);
|
|
268
|
+
await page.pressKey('Enter');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Get the bounding box of the Lexical editor for a physical mouse click
|
|
273
|
+
const rect = await page.evaluate(`
|
|
274
|
+
(() => {
|
|
99
275
|
const container = document.getElementById('antigravity.agentSidePanelInputBox');
|
|
100
276
|
if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
|
|
101
277
|
const editor = container.querySelector('[data-lexical-editor="true"]');
|
|
102
278
|
if (!editor) throw new Error('Could not find Antigravity input box');
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
document.execCommand('insertText', false, ${JSON.stringify(message)});
|
|
279
|
+
const r = editor.getBoundingClientRect();
|
|
280
|
+
return JSON.stringify({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
|
|
106
281
|
})()
|
|
107
282
|
`);
|
|
108
|
-
|
|
109
|
-
|
|
283
|
+
const { x, y } = JSON.parse(String(rect));
|
|
284
|
+
|
|
285
|
+
// Physical mouse click to give the element real browser focus
|
|
286
|
+
await bridge.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
287
|
+
await sleep(50);
|
|
288
|
+
await bridge.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
289
|
+
await sleep(200);
|
|
290
|
+
|
|
291
|
+
// Inject text at the CDP level (no deprecated execCommand)
|
|
292
|
+
await bridge.send('Input.insertText', { text: message });
|
|
293
|
+
await sleep(300);
|
|
294
|
+
|
|
295
|
+
// Send Enter via native CDP key event
|
|
296
|
+
await bridge.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
|
|
297
|
+
await sleep(50);
|
|
298
|
+
await bridge.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
|
|
110
299
|
}
|
|
111
300
|
|
|
112
301
|
async function waitForReply(
|
|
113
302
|
page: IPage,
|
|
114
303
|
beforeText: string,
|
|
115
|
-
opts: { timeout?: number; pollInterval?: number
|
|
116
|
-
): Promise<
|
|
304
|
+
opts: { timeout?: number; pollInterval?: number } = {},
|
|
305
|
+
): Promise<void> {
|
|
117
306
|
const timeout = opts.timeout ?? 120_000; // 2 minutes max
|
|
118
307
|
const pollInterval = opts.pollInterval ?? 500; // 500ms polling
|
|
119
|
-
const stableThreshold = opts.stableThreshold ?? 6; // 6 × 500ms = 3s stable
|
|
120
308
|
|
|
121
309
|
const deadline = Date.now() + timeout;
|
|
122
|
-
let lastText = beforeText;
|
|
123
|
-
let stableCount = 0;
|
|
124
310
|
|
|
125
|
-
// Wait a bit
|
|
311
|
+
// Wait a bit to ensure the UI transitions to "generating" state after we hit Enter
|
|
126
312
|
await sleep(1000);
|
|
127
313
|
|
|
314
|
+
let hasStartedGenerating = false;
|
|
315
|
+
let lastText = beforeText;
|
|
316
|
+
let stableCount = 0;
|
|
317
|
+
const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback
|
|
318
|
+
|
|
128
319
|
while (Date.now() < deadline) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
320
|
+
const generating = await isGenerating(page);
|
|
321
|
+
const currentText = await getConversationText(page);
|
|
322
|
+
const textChanged = currentText !== beforeText && currentText.length > 0;
|
|
323
|
+
|
|
324
|
+
if (generating) {
|
|
325
|
+
hasStartedGenerating = true;
|
|
326
|
+
stableCount = 0; // Reset stability while generating
|
|
327
|
+
} else {
|
|
328
|
+
if (hasStartedGenerating) {
|
|
329
|
+
// It actively generated and now it stopped -> DONE
|
|
330
|
+
// Provide a small buffer to let React render the final message fully
|
|
331
|
+
await sleep(500);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Fallback: If it never showed "Generating/Cancel", but text changed and is stable
|
|
336
|
+
if (textChanged) {
|
|
337
|
+
if (currentText === lastText) {
|
|
338
|
+
stableCount++;
|
|
339
|
+
if (stableCount >= stableThreshold) {
|
|
340
|
+
return; // Text has been stable for 2 seconds -> DONE
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
stableCount = 0;
|
|
344
|
+
lastText = currentText;
|
|
138
345
|
}
|
|
139
|
-
} else {
|
|
140
|
-
// Still generating
|
|
141
|
-
stableCount = 0;
|
|
142
|
-
lastText = current;
|
|
143
346
|
}
|
|
144
347
|
}
|
|
145
348
|
|
|
146
349
|
await sleep(pollInterval);
|
|
147
350
|
}
|
|
148
351
|
|
|
149
|
-
// Timeout — return whatever we have
|
|
150
|
-
const finalText = await getConversationText(page);
|
|
151
|
-
if (finalText.length > beforeText.length) {
|
|
152
|
-
return finalText.slice(beforeText.length).trim();
|
|
153
|
-
}
|
|
154
352
|
throw new Error('Timeout waiting for Antigravity reply');
|
|
155
353
|
}
|
|
156
354
|
|
|
@@ -159,6 +357,7 @@ async function waitForReply(
|
|
|
159
357
|
async function handleMessages(
|
|
160
358
|
body: AnthropicRequest,
|
|
161
359
|
page: IPage,
|
|
360
|
+
bridge?: CDPBridge,
|
|
162
361
|
): Promise<AnthropicResponse> {
|
|
163
362
|
// Extract the last user message
|
|
164
363
|
const userMessages = body.messages.filter(m => m.role === 'user');
|
|
@@ -172,16 +371,30 @@ async function handleMessages(
|
|
|
172
371
|
throw new Error('Empty user message');
|
|
173
372
|
}
|
|
174
373
|
|
|
374
|
+
// Optimization 1: New conversation if this is the first message in the session
|
|
375
|
+
if (body.messages.length === 1) {
|
|
376
|
+
console.error(`[serve] New session detected (1 message). Starting new conversation in UI.`);
|
|
377
|
+
await startNewConversation(page);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Optimization 3: Switch model if requested
|
|
381
|
+
if (body.model) {
|
|
382
|
+
await switchModel(page, body.model);
|
|
383
|
+
}
|
|
384
|
+
|
|
175
385
|
// Get conversation state before sending
|
|
176
386
|
const beforeText = await getConversationText(page);
|
|
177
387
|
|
|
178
388
|
// Send the message
|
|
179
389
|
console.error(`[serve] Sending: "${userText.slice(0, 80)}${userText.length > 80 ? '...' : ''}"`);
|
|
180
|
-
await sendMessage(page, userText);
|
|
390
|
+
await sendMessage(page, userText, bridge);
|
|
181
391
|
|
|
182
|
-
// Poll for reply
|
|
392
|
+
// Poll for reply (change detection)
|
|
183
393
|
console.error('[serve] Waiting for reply...');
|
|
184
|
-
|
|
394
|
+
await waitForReply(page, beforeText);
|
|
395
|
+
|
|
396
|
+
// Extract the actual reply text precisely from the DOM
|
|
397
|
+
const replyText = await getLastAssistantReply(page, userText);
|
|
185
398
|
console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
|
|
186
399
|
|
|
187
400
|
return {
|
|
@@ -204,17 +417,74 @@ async function handleMessages(
|
|
|
204
417
|
export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
205
418
|
const port = opts.port ?? 8082;
|
|
206
419
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.error('[serve] CDP connected successfully.');
|
|
420
|
+
// Lazy CDP connection — connect when first request comes in
|
|
421
|
+
let cdp: CDPBridge | null = null;
|
|
422
|
+
let page: IPage | null = null;
|
|
423
|
+
let requestInFlight = false;
|
|
212
424
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
425
|
+
async function ensureConnected(): Promise<IPage> {
|
|
426
|
+
if (page) {
|
|
427
|
+
try {
|
|
428
|
+
await page.evaluate('1+1');
|
|
429
|
+
return page;
|
|
430
|
+
} catch {
|
|
431
|
+
console.error('[serve] CDP connection lost, reconnecting...');
|
|
432
|
+
cdp?.close().catch(() => {});
|
|
433
|
+
cdp = null;
|
|
434
|
+
page = null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
216
437
|
|
|
217
|
-
|
|
438
|
+
const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
439
|
+
if (!endpoint) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
'OPENCLI_CDP_ENDPOINT is not set.\n' +
|
|
442
|
+
'Usage: OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve'
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Note: Antigravity chat panel lives inside editor windows, not in Launchpad.
|
|
447
|
+
// If multiple editor windows are open, set OPENCLI_CDP_TARGET to the window title.
|
|
448
|
+
if (process.env.OPENCLI_CDP_TARGET) {
|
|
449
|
+
console.error(`[serve] Using OPENCLI_CDP_TARGET=${process.env.OPENCLI_CDP_TARGET}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// List available targets for debugging
|
|
453
|
+
try {
|
|
454
|
+
const res = await fetch(`${endpoint.replace(/\/$/, '')}/json`);
|
|
455
|
+
const targets = await res.json() as Array<{ title?: string; type?: string }>;
|
|
456
|
+
const pages = targets.filter(t => t.type === 'page');
|
|
457
|
+
console.error(`[serve] Available targets: ${pages.map(t => `"${t.title}"`).join(', ')}`);
|
|
458
|
+
} catch { /* ignore */ }
|
|
459
|
+
|
|
460
|
+
console.error(`[serve] Connecting via CDP (target pattern: "${process.env.OPENCLI_CDP_TARGET}")...`);
|
|
461
|
+
cdp = new CDPBridge();
|
|
462
|
+
try {
|
|
463
|
+
page = await cdp.connect({ timeout: 15_000 });
|
|
464
|
+
} catch (err: any) {
|
|
465
|
+
cdp = null;
|
|
466
|
+
const isRefused = err?.cause?.code === 'ECONNREFUSED' || err?.message?.includes('ECONNREFUSED');
|
|
467
|
+
throw new Error(
|
|
468
|
+
isRefused
|
|
469
|
+
? `Cannot connect to Antigravity at ${endpoint}.\n` +
|
|
470
|
+
' 1. Make sure Antigravity is running\n' +
|
|
471
|
+
' 2. Launch with: --remote-debugging-port=9224'
|
|
472
|
+
: `CDP connection failed: ${err.message}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.error('[serve] ✅ CDP connected.');
|
|
477
|
+
|
|
478
|
+
// Quick verification
|
|
479
|
+
const hasUI = await page.evaluate(`
|
|
480
|
+
(() => !!document.getElementById('conversation') || !!document.getElementById('antigravity.agentSidePanelInputBox'))()
|
|
481
|
+
`);
|
|
482
|
+
if (!hasUI) {
|
|
483
|
+
console.error('[serve] ⚠️ Warning: chat UI elements not found in this target. Try setting OPENCLI_CDP_TARGET to the correct window title.');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return page;
|
|
487
|
+
}
|
|
218
488
|
|
|
219
489
|
const server = createServer(async (req, res) => {
|
|
220
490
|
// CORS preflight
|
|
@@ -266,7 +536,6 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
266
536
|
const body = JSON.parse(rawBody) as AnthropicRequest;
|
|
267
537
|
|
|
268
538
|
if (body.stream) {
|
|
269
|
-
// We don't support streaming — return error
|
|
270
539
|
jsonResponse(res, 400, {
|
|
271
540
|
type: 'error',
|
|
272
541
|
error: {
|
|
@@ -277,7 +546,9 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
277
546
|
return;
|
|
278
547
|
}
|
|
279
548
|
|
|
280
|
-
|
|
549
|
+
// Lazy connect on first request
|
|
550
|
+
const activePage = await ensureConnected();
|
|
551
|
+
const response = await handleMessages(body, activePage, cdp ?? undefined);
|
|
281
552
|
jsonResponse(res, 200, response);
|
|
282
553
|
} finally {
|
|
283
554
|
requestInFlight = false;
|
|
@@ -287,7 +558,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
287
558
|
|
|
288
559
|
// Health check
|
|
289
560
|
if (req.method === 'GET' && (pathname === '/' || pathname === '/health')) {
|
|
290
|
-
jsonResponse(res, 200, { ok: true,
|
|
561
|
+
jsonResponse(res, 200, { ok: true, cdpConnected: page !== null });
|
|
291
562
|
return;
|
|
292
563
|
}
|
|
293
564
|
|
|
@@ -296,7 +567,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
296
567
|
error: { type: 'not_found_error', message: `Not found: ${pathname}` },
|
|
297
568
|
});
|
|
298
569
|
} catch (err) {
|
|
299
|
-
console.error('[serve] Error:', err);
|
|
570
|
+
console.error('[serve] Error:', err instanceof Error ? err.message : err);
|
|
300
571
|
jsonResponse(res, 500, {
|
|
301
572
|
type: 'error',
|
|
302
573
|
error: {
|
|
@@ -310,6 +581,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
310
581
|
server.listen(port, '127.0.0.1', () => {
|
|
311
582
|
console.error(`\n[serve] ✅ Antigravity API proxy running at http://127.0.0.1:${port}`);
|
|
312
583
|
console.error(`[serve] Compatible with Anthropic /v1/messages API`);
|
|
584
|
+
console.error(`[serve] CDP connection will be established on first request.`);
|
|
313
585
|
console.error(`\n[serve] Usage with Claude Code:`);
|
|
314
586
|
console.error(` ANTHROPIC_BASE_URL=http://localhost:${port} claude\n`);
|
|
315
587
|
});
|
|
@@ -317,7 +589,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
317
589
|
// Graceful shutdown
|
|
318
590
|
const shutdown = () => {
|
|
319
591
|
console.error('\n[serve] Shutting down...');
|
|
320
|
-
cdp
|
|
592
|
+
cdp?.close().catch(() => {});
|
|
321
593
|
server.close();
|
|
322
594
|
process.exit(0);
|
|
323
595
|
};
|
|
@@ -327,3 +599,4 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
327
599
|
// Keep alive
|
|
328
600
|
await new Promise(() => {});
|
|
329
601
|
}
|
|
602
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './search.js';
|
|
4
|
+
import './top.js';
|
|
5
|
+
|
|
6
|
+
describe('apple-podcasts search command', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('uses the positional query argument for the iTunes search request', async () => {
|
|
12
|
+
const cmd = getRegistry().get('apple-podcasts/search');
|
|
13
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
14
|
+
|
|
15
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
16
|
+
ok: true,
|
|
17
|
+
json: () => Promise.resolve({
|
|
18
|
+
results: [
|
|
19
|
+
{
|
|
20
|
+
collectionId: 42,
|
|
21
|
+
collectionName: 'Machine Learning Guide',
|
|
22
|
+
artistName: 'OpenCLI',
|
|
23
|
+
trackCount: 12,
|
|
24
|
+
primaryGenreName: 'Technology',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
30
|
+
|
|
31
|
+
const result = await cmd!.func!(null as any, {
|
|
32
|
+
query: 'machine learning',
|
|
33
|
+
keyword: 'sports',
|
|
34
|
+
limit: 5,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
38
|
+
'https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5',
|
|
39
|
+
);
|
|
40
|
+
expect(result).toEqual([
|
|
41
|
+
{
|
|
42
|
+
id: 42,
|
|
43
|
+
title: 'Machine Learning Guide',
|
|
44
|
+
author: 'OpenCLI',
|
|
45
|
+
episodes: 12,
|
|
46
|
+
genre: 'Technology',
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('apple-podcasts top command', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.restoreAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('uses the canonical Apple charts host and maps ranked results', async () => {
|
|
58
|
+
const cmd = getRegistry().get('apple-podcasts/top');
|
|
59
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
60
|
+
|
|
61
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
62
|
+
ok: true,
|
|
63
|
+
json: () => Promise.resolve({
|
|
64
|
+
feed: {
|
|
65
|
+
results: [
|
|
66
|
+
{ id: '100', name: 'Top Show', artistName: 'Host A' },
|
|
67
|
+
{ id: '101', name: 'Second Show', artistName: 'Host B' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
73
|
+
|
|
74
|
+
const result = await cmd!.func!(null as any, { country: 'US', limit: 2 });
|
|
75
|
+
|
|
76
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
77
|
+
'https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json',
|
|
78
|
+
);
|
|
79
|
+
expect(result).toEqual([
|
|
80
|
+
{ rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
|
|
81
|
+
{ rank: 2, title: 'Second Show', author: 'Host B', id: '101' },
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('normalizes network failures into CliError output', async () => {
|
|
86
|
+
const cmd = getRegistry().get('apple-podcasts/top');
|
|
87
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
88
|
+
|
|
89
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
|
|
90
|
+
|
|
91
|
+
await expect(cmd!.func!(null as any, { country: 'us', limit: 3 })).rejects.toThrow(
|
|
92
|
+
'Unable to reach Apple Podcasts charts for US',
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -9,12 +9,12 @@ cli({
|
|
|
9
9
|
strategy: Strategy.PUBLIC,
|
|
10
10
|
browser: false,
|
|
11
11
|
args: [
|
|
12
|
-
{ name: '
|
|
12
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword' },
|
|
13
13
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results' },
|
|
14
14
|
],
|
|
15
15
|
columns: ['id', 'title', 'author', 'episodes', 'genre'],
|
|
16
16
|
func: async (_page, args) => {
|
|
17
|
-
const term = encodeURIComponent(args.
|
|
17
|
+
const term = encodeURIComponent(args.query);
|
|
18
18
|
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
|
19
19
|
const data = await itunesFetch(`/search?term=${term}&media=podcast&limit=${limit}`);
|
|
20
20
|
if (!data.results?.length) throw new CliError('NOT_FOUND', 'No podcasts found', `Try a different keyword`);
|
|
@@ -2,7 +2,7 @@ import { cli, Strategy } from '../../registry.js';
|
|
|
2
2
|
import { CliError } from '../../errors.js';
|
|
3
3
|
|
|
4
4
|
// Apple Marketing Tools RSS API — public, no key required
|
|
5
|
-
const CHARTS_URL = 'https://rss.
|
|
5
|
+
const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
|
|
6
6
|
|
|
7
7
|
cli({
|
|
8
8
|
site: 'apple-podcasts',
|
|
@@ -19,7 +19,17 @@ cli({
|
|
|
19
19
|
const limit = Math.max(1, Math.min(Number(args.limit), 100));
|
|
20
20
|
const country = String(args.country || 'us').trim().toLowerCase();
|
|
21
21
|
const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
|
|
22
|
-
|
|
22
|
+
let resp: Response;
|
|
23
|
+
try {
|
|
24
|
+
resp = await fetch(url);
|
|
25
|
+
} catch (error: any) {
|
|
26
|
+
const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
|
|
27
|
+
throw new CliError(
|
|
28
|
+
'FETCH_ERROR',
|
|
29
|
+
`Unable to reach Apple Podcasts charts for ${country.toUpperCase()}`,
|
|
30
|
+
`Apple charts may be temporarily unavailable (${reason}). Try again later.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
23
33
|
if (!resp.ok) throw new CliError('FETCH_ERROR', `Charts API HTTP ${resp.status}`, `Check country code: ${country}`);
|
|
24
34
|
const data = await resp.json();
|
|
25
35
|
const results = data?.feed?.results;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CliError } from '../../errors.js';
|
|
3
|
+
import { arxivFetch, parseEntries } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'arxiv',
|
|
7
|
+
name: 'paper',
|
|
8
|
+
description: 'Get arXiv paper details by ID',
|
|
9
|
+
strategy: Strategy.PUBLIC,
|
|
10
|
+
browser: false,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'id', positional: true, required: true, help: 'arXiv paper ID (e.g. 1706.03762)' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['id', 'title', 'authors', 'published', 'abstract', 'url'],
|
|
15
|
+
func: async (_page, args) => {
|
|
16
|
+
const xml = await arxivFetch(`id_list=${encodeURIComponent(args.id)}`);
|
|
17
|
+
const entries = parseEntries(xml);
|
|
18
|
+
if (!entries.length) throw new CliError('NOT_FOUND', `Paper ${args.id} not found`, 'Check the arXiv ID format, e.g. 1706.03762');
|
|
19
|
+
return entries;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CliError } from '../../errors.js';
|
|
3
|
+
import { arxivFetch, parseEntries } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'arxiv',
|
|
7
|
+
name: 'search',
|
|
8
|
+
description: 'Search arXiv papers',
|
|
9
|
+
strategy: Strategy.PUBLIC,
|
|
10
|
+
browser: false,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
|
|
13
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['id', 'title', 'authors', 'published'],
|
|
16
|
+
func: async (_page, args) => {
|
|
17
|
+
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
|
18
|
+
const query = encodeURIComponent(`all:${args.keyword}`);
|
|
19
|
+
const xml = await arxivFetch(`search_query=${query}&max_results=${limit}&sortBy=relevance`);
|
|
20
|
+
const entries = parseEntries(xml);
|
|
21
|
+
if (!entries.length) throw new CliError('NOT_FOUND', 'No papers found', 'Try a different keyword');
|
|
22
|
+
return entries.map(e => ({ id: e.id, title: e.title, authors: e.authors, published: e.published }));
|
|
23
|
+
},
|
|
24
|
+
});
|