@jackwener/opencli 1.3.3 → 1.4.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/.github/actions/setup-chrome/action.yml +5 -4
- package/.github/pull_request_template.md +3 -1
- package/.github/workflows/build-extension.yml +7 -1
- package/.github/workflows/ci.yml +46 -6
- package/.github/workflows/docs.yml +1 -1
- package/.github/workflows/e2e-headed.yml +36 -3
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/security.yml +0 -3
- package/CHANGELOG.md +78 -0
- package/CONTRIBUTING.md +6 -3
- package/PRIVACY.md +57 -0
- package/README.md +31 -4
- package/README.zh-CN.md +31 -4
- package/SKILL.md +107 -2
- package/TESTING.md +1 -0
- package/chatwise-opencli.ps1 +82 -0
- package/dist/analysis.d.ts +38 -0
- package/dist/analysis.js +166 -0
- package/dist/browser/cdp.d.ts +0 -4
- package/dist/browser/cdp.js +53 -41
- package/dist/browser/cdp.test.d.ts +1 -0
- package/dist/browser/cdp.test.js +52 -0
- package/dist/browser/dom-snapshot.d.ts +2 -2
- package/dist/browser/dom-snapshot.js +54 -1
- package/dist/browser/dom-snapshot.test.js +36 -0
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/mcp.d.ts +0 -2
- package/dist/browser/mcp.js +2 -3
- package/dist/browser/page.d.ts +4 -3
- package/dist/browser/page.js +34 -37
- package/dist/browser/stealth.d.ts +0 -2
- package/dist/browser/stealth.js +24 -9
- package/dist/browser.test.js +2 -2
- package/dist/build-manifest.js +15 -9
- package/dist/build-manifest.test.js +12 -0
- package/dist/cascade.js +4 -2
- package/dist/cli-manifest.json +1325 -256
- package/dist/cli.js +57 -29
- package/dist/clis/_shared/desktop-commands.d.ts +22 -0
- package/dist/clis/_shared/desktop-commands.js +108 -0
- package/dist/clis/antigravity/serve.js +5 -2
- package/dist/clis/apple-podcasts/search.js +2 -1
- package/dist/clis/arxiv/search.js +3 -3
- package/dist/clis/bbc/news.js +0 -1
- package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
- package/dist/clis/bilibili/dynamic.test.js +68 -0
- package/dist/clis/bilibili/favorite.js +4 -2
- package/dist/clis/bilibili/following.js +3 -2
- package/dist/clis/bilibili/subtitle.js +8 -7
- package/dist/clis/bilibili/utils.js +2 -2
- package/dist/clis/boss/batchgreet.js +1 -1
- package/dist/clis/boss/chatlist.js +1 -1
- package/dist/clis/boss/chatmsg.js +1 -1
- package/dist/clis/boss/detail.js +1 -1
- package/dist/clis/boss/exchange.js +1 -1
- package/dist/clis/boss/greet.js +1 -1
- package/dist/clis/boss/invite.js +1 -1
- package/dist/clis/boss/joblist.js +1 -1
- package/dist/clis/boss/mark.js +4 -3
- package/dist/clis/boss/recommend.js +1 -1
- package/dist/clis/boss/resume.js +1 -1
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +5 -4
- package/dist/clis/boss/stats.js +1 -1
- package/dist/clis/chatgpt/ask.js +4 -0
- package/dist/clis/chatgpt/new.js +5 -1
- package/dist/clis/chatgpt/read.js +5 -1
- package/dist/clis/chatgpt/send.js +2 -1
- package/dist/clis/chatgpt/status.js +5 -1
- package/dist/clis/chatwise/ask.js +8 -2
- package/dist/clis/chatwise/export.js +2 -0
- package/dist/clis/chatwise/history.js +2 -0
- package/dist/clis/chatwise/model.js +8 -3
- package/dist/clis/chatwise/new.js +3 -18
- package/dist/clis/chatwise/read.js +2 -0
- package/dist/clis/chatwise/screenshot.js +3 -27
- package/dist/clis/chatwise/send.js +8 -2
- package/dist/clis/chatwise/shared.d.ts +2 -0
- package/dist/clis/chatwise/shared.js +6 -0
- package/dist/clis/chatwise/status.js +3 -22
- package/dist/clis/codex/ask.js +6 -2
- package/dist/clis/codex/dump.js +2 -25
- package/dist/clis/codex/new.js +2 -25
- package/dist/clis/codex/screenshot.js +2 -27
- package/dist/clis/codex/send.js +6 -4
- package/dist/clis/codex/status.js +2 -22
- package/dist/clis/ctrip/search.js +0 -1
- package/dist/clis/cursor/ask.js +2 -1
- package/dist/clis/cursor/composer.js +2 -1
- package/dist/clis/cursor/dump.js +2 -25
- package/dist/clis/cursor/new.js +2 -18
- package/dist/clis/cursor/read.js +2 -1
- package/dist/clis/cursor/screenshot.js +1 -30
- package/dist/clis/cursor/send.js +2 -1
- package/dist/clis/cursor/status.js +2 -21
- package/dist/clis/dictionary/examples.yaml +25 -0
- package/dist/clis/dictionary/search.yaml +27 -0
- package/dist/clis/dictionary/synonyms.yaml +25 -0
- package/dist/clis/douban/book-hot.js +1 -1
- package/dist/clis/douban/movie-hot.js +1 -1
- package/dist/clis/douban/search.js +1 -1
- package/dist/clis/douban/utils.d.ts +4 -1
- package/dist/clis/douban/utils.js +156 -1
- package/dist/clis/doubao/ask.js +1 -1
- package/dist/clis/doubao/new.js +1 -1
- package/dist/clis/doubao/read.js +1 -1
- package/dist/clis/doubao/send.js +1 -1
- package/dist/clis/doubao/status.js +1 -1
- package/dist/clis/doubao-app/ask.js +1 -1
- package/dist/clis/doubao-app/new.js +1 -1
- package/dist/clis/doubao-app/read.js +1 -1
- package/dist/clis/doubao-app/send.js +1 -1
- package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
- package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
- package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.js +5 -0
- package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
- package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
- package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
- package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
- package/dist/clis/douyin/_shared/sts2.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
- package/dist/clis/douyin/_shared/text-extra.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
- package/dist/clis/douyin/_shared/timing.d.ts +2 -0
- package/dist/clis/douyin/_shared/timing.js +22 -0
- package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/timing.test.js +28 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
- package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
- package/dist/clis/douyin/_shared/tos-upload.js +295 -0
- package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
- package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
- package/dist/clis/douyin/_shared/transcode.js +45 -0
- package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/transcode.test.js +93 -0
- package/dist/clis/douyin/_shared/types.d.ts +26 -0
- package/dist/clis/douyin/_shared/types.js +1 -0
- package/dist/clis/douyin/activities.d.ts +1 -0
- package/dist/clis/douyin/activities.js +20 -0
- package/dist/clis/douyin/activities.test.d.ts +1 -0
- package/dist/clis/douyin/activities.test.js +22 -0
- package/dist/clis/douyin/collections.d.ts +1 -0
- package/dist/clis/douyin/collections.js +22 -0
- package/dist/clis/douyin/collections.test.d.ts +1 -0
- package/dist/clis/douyin/collections.test.js +23 -0
- package/dist/clis/douyin/delete.d.ts +1 -0
- package/dist/clis/douyin/delete.js +18 -0
- package/dist/clis/douyin/delete.test.d.ts +1 -0
- package/dist/clis/douyin/delete.test.js +11 -0
- package/dist/clis/douyin/draft.d.ts +14 -0
- package/dist/clis/douyin/draft.js +237 -0
- package/dist/clis/douyin/draft.test.d.ts +1 -0
- package/dist/clis/douyin/draft.test.js +11 -0
- package/dist/clis/douyin/drafts.d.ts +1 -0
- package/dist/clis/douyin/drafts.js +23 -0
- package/dist/clis/douyin/drafts.test.d.ts +1 -0
- package/dist/clis/douyin/drafts.test.js +11 -0
- package/dist/clis/douyin/hashtag.d.ts +1 -0
- package/dist/clis/douyin/hashtag.js +45 -0
- package/dist/clis/douyin/hashtag.test.d.ts +1 -0
- package/dist/clis/douyin/hashtag.test.js +25 -0
- package/dist/clis/douyin/location.d.ts +1 -0
- package/dist/clis/douyin/location.js +24 -0
- package/dist/clis/douyin/location.test.d.ts +1 -0
- package/dist/clis/douyin/location.test.js +23 -0
- package/dist/clis/douyin/profile.d.ts +1 -0
- package/dist/clis/douyin/profile.js +28 -0
- package/dist/clis/douyin/profile.test.d.ts +1 -0
- package/dist/clis/douyin/profile.test.js +11 -0
- package/dist/clis/douyin/publish.d.ts +14 -0
- package/dist/clis/douyin/publish.js +288 -0
- package/dist/clis/douyin/publish.test.d.ts +1 -0
- package/dist/clis/douyin/publish.test.js +38 -0
- package/dist/clis/douyin/stats.d.ts +1 -0
- package/dist/clis/douyin/stats.js +27 -0
- package/dist/clis/douyin/stats.test.d.ts +1 -0
- package/dist/clis/douyin/stats.test.js +22 -0
- package/dist/clis/douyin/update.d.ts +1 -0
- package/dist/clis/douyin/update.js +31 -0
- package/dist/clis/douyin/update.test.d.ts +1 -0
- package/dist/clis/douyin/update.test.js +11 -0
- package/dist/clis/douyin/videos.d.ts +1 -0
- package/dist/clis/douyin/videos.js +34 -0
- package/dist/clis/douyin/videos.test.d.ts +1 -0
- package/dist/clis/douyin/videos.test.js +11 -0
- package/dist/clis/grok/ask.d.ts +4 -0
- package/dist/clis/grok/ask.js +28 -10
- package/dist/clis/grok/ask.test.js +18 -0
- package/dist/clis/hackernews/search.yaml +1 -1
- package/dist/clis/instagram/search.yaml +2 -1
- package/dist/clis/jd/item.d.ts +1 -0
- package/dist/clis/jd/item.js +96 -0
- package/dist/clis/jd/item.test.d.ts +1 -0
- package/dist/clis/jd/item.test.js +28 -0
- package/dist/clis/jike/feed.js +1 -1
- package/dist/clis/jike/search.js +1 -1
- package/dist/clis/linkedin/search.js +5 -4
- package/dist/clis/linkedin/timeline.d.ts +21 -0
- package/dist/clis/linkedin/timeline.js +503 -0
- package/dist/clis/linkedin/timeline.test.d.ts +1 -0
- package/dist/clis/linkedin/timeline.test.js +81 -0
- package/dist/clis/linux-do/search.yaml +3 -1
- package/dist/clis/medium/feed.js +1 -1
- package/dist/clis/medium/search.js +2 -2
- package/dist/clis/medium/user.js +1 -1
- package/dist/clis/medium/{shared.js → utils.js} +2 -1
- package/dist/clis/pixiv/detail.yaml +49 -0
- package/dist/clis/pixiv/download.d.ts +7 -0
- package/dist/clis/pixiv/download.js +78 -0
- package/dist/clis/pixiv/download.test.d.ts +1 -0
- package/dist/clis/pixiv/download.test.js +87 -0
- package/dist/clis/pixiv/illusts.d.ts +8 -0
- package/dist/clis/pixiv/illusts.js +65 -0
- package/dist/clis/pixiv/illusts.test.d.ts +1 -0
- package/dist/clis/pixiv/illusts.test.js +99 -0
- package/dist/clis/pixiv/ranking.yaml +53 -0
- package/dist/clis/pixiv/search.d.ts +6 -0
- package/dist/clis/pixiv/search.js +43 -0
- package/dist/clis/pixiv/search.test.d.ts +1 -0
- package/dist/clis/pixiv/search.test.js +83 -0
- package/dist/clis/pixiv/test-utils.d.ts +12 -0
- package/dist/clis/pixiv/test-utils.js +23 -0
- package/dist/clis/pixiv/user.yaml +46 -0
- package/dist/clis/pixiv/utils.d.ts +27 -0
- package/dist/clis/pixiv/utils.js +49 -0
- package/dist/clis/reddit/comment.js +2 -1
- package/dist/clis/reddit/read.js +4 -3
- package/dist/clis/reddit/read.test.d.ts +1 -0
- package/dist/clis/reddit/read.test.js +28 -0
- package/dist/clis/reddit/save.js +2 -1
- package/dist/clis/reddit/saved.js +7 -3
- package/dist/clis/reddit/subscribe.js +2 -1
- package/dist/clis/reddit/upvote.js +2 -1
- package/dist/clis/reddit/upvoted.js +7 -3
- package/dist/clis/reuters/search.js +0 -1
- package/dist/clis/sinablog/article.js +1 -1
- package/dist/clis/sinablog/hot.js +1 -1
- package/dist/clis/sinablog/user.js +1 -1
- package/dist/clis/substack/feed.js +1 -1
- package/dist/clis/substack/publication.js +1 -1
- package/dist/clis/substack/search.js +3 -2
- package/dist/clis/substack/{shared.js → utils.js} +3 -2
- package/dist/clis/tiktok/search.yaml +2 -1
- package/dist/clis/twitter/accept.js +2 -1
- package/dist/clis/twitter/article.js +4 -1
- package/dist/clis/twitter/block.js +2 -1
- package/dist/clis/twitter/bookmark.js +2 -1
- package/dist/clis/twitter/bookmarks.js +3 -2
- package/dist/clis/twitter/delete.js +2 -1
- package/dist/clis/twitter/follow.js +2 -1
- package/dist/clis/twitter/followers.js +3 -2
- package/dist/clis/twitter/following.js +3 -2
- package/dist/clis/twitter/hide-reply.js +2 -1
- package/dist/clis/twitter/like.js +2 -1
- package/dist/clis/twitter/notifications.js +2 -1
- package/dist/clis/twitter/post.js +2 -1
- package/dist/clis/twitter/profile.js +5 -2
- package/dist/clis/twitter/reply-dm.js +2 -1
- package/dist/clis/twitter/reply.js +2 -1
- package/dist/clis/twitter/search.js +32 -13
- package/dist/clis/twitter/search.test.d.ts +1 -0
- package/dist/clis/twitter/search.test.js +156 -0
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/timeline.js +3 -2
- package/dist/clis/twitter/trending.js +3 -2
- package/dist/clis/twitter/unblock.js +2 -1
- package/dist/clis/twitter/unbookmark.js +2 -1
- package/dist/clis/twitter/unfollow.js +2 -1
- package/dist/clis/v2ex/daily.js +3 -2
- package/dist/clis/v2ex/me.js +3 -2
- package/dist/clis/v2ex/notifications.js +4 -4
- package/dist/clis/web/read.d.ts +16 -0
- package/dist/clis/web/read.js +202 -0
- package/dist/clis/weibo/comments.d.ts +1 -0
- package/dist/clis/weibo/comments.js +53 -0
- package/dist/clis/weibo/feed.d.ts +1 -0
- package/dist/clis/weibo/feed.js +56 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weibo/me.d.ts +1 -0
- package/dist/clis/weibo/me.js +76 -0
- package/dist/clis/weibo/post.d.ts +1 -0
- package/dist/clis/weibo/post.js +75 -0
- package/dist/clis/weibo/user.d.ts +1 -0
- package/dist/clis/weibo/user.js +63 -0
- package/dist/clis/weibo/utils.d.ts +6 -0
- package/dist/clis/weibo/utils.js +30 -0
- package/dist/clis/weread/search.js +3 -2
- package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
- package/dist/clis/xueqiu/danjuan-utils.js +126 -0
- package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
- package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
- package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
- package/dist/clis/xueqiu/fund-holdings.js +28 -0
- package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
- package/dist/clis/xueqiu/fund-snapshot.js +25 -0
- package/dist/clis/xueqiu/search.yaml +2 -1
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/channel.d.ts +1 -0
- package/dist/clis/youtube/channel.js +150 -0
- package/dist/clis/youtube/comments.d.ts +1 -0
- package/dist/clis/youtube/comments.js +95 -0
- package/dist/clis/youtube/search.js +0 -1
- package/dist/clis/youtube/transcript.js +5 -4
- package/dist/clis/youtube/video.js +3 -2
- package/dist/clis/zhihu/search.yaml +2 -1
- package/dist/daemon.js +7 -3
- package/dist/discovery.js +11 -10
- package/dist/doctor.js +2 -1
- package/dist/download/index.d.ts +4 -12
- package/dist/download/index.js +33 -12
- package/dist/download/index.test.js +79 -2
- package/dist/download/media-download.js +4 -2
- package/dist/engine.test.js +76 -4
- package/dist/execution.d.ts +1 -9
- package/dist/execution.js +56 -46
- package/dist/explore.js +12 -111
- package/dist/external-clis.yaml +0 -25
- package/dist/external.js +7 -5
- package/dist/external.test.js +4 -0
- package/dist/generate.d.ts +0 -9
- package/dist/generate.js +4 -20
- package/dist/hooks.d.ts +46 -0
- package/dist/hooks.js +56 -0
- package/dist/hooks.test.d.ts +4 -0
- package/dist/hooks.test.js +92 -0
- package/dist/interceptor.js +70 -23
- package/dist/main.js +2 -0
- package/dist/output.js +12 -6
- package/dist/pipeline/executor.js +1 -1
- package/dist/pipeline/steps/browser.js +1 -3
- package/dist/pipeline/steps/download.js +42 -26
- package/dist/pipeline/steps/download.test.d.ts +1 -0
- package/dist/pipeline/steps/download.test.js +101 -0
- package/dist/pipeline/steps/fetch.js +40 -22
- package/dist/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/pipeline/steps/fetch.test.js +123 -0
- package/dist/pipeline/steps/transform.js +2 -6
- package/dist/pipeline/template.js +66 -52
- package/dist/pipeline/template.test.js +28 -0
- package/dist/pipeline/transform.test.js +18 -0
- package/dist/plugin.d.ts +40 -1
- package/dist/plugin.js +214 -17
- package/dist/plugin.test.d.ts +1 -1
- package/dist/plugin.test.js +219 -3
- package/dist/record.js +6 -98
- package/dist/registry-api.d.ts +2 -0
- package/dist/registry-api.js +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +1 -2
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +14 -4
- package/dist/snapshotFormatter.d.ts +7 -14
- package/dist/snapshotFormatter.js +38 -78
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +29 -0
- package/dist/validate.js +3 -5
- package/dist/weread-search-regression.test.d.ts +1 -0
- package/dist/weread-search-regression.test.js +39 -0
- package/dist/yaml-schema.d.ts +26 -0
- package/dist/yaml-schema.js +5 -0
- package/docs/.vitepress/config.mts +16 -0
- package/docs/adapters/browser/dictionary.md +27 -0
- package/docs/adapters/browser/douyin.md +75 -0
- package/docs/adapters/browser/jd.md +27 -0
- package/docs/adapters/browser/linkedin.md +6 -0
- package/docs/adapters/browser/pixiv.md +92 -0
- package/docs/adapters/browser/twitter.md +6 -0
- package/docs/adapters/browser/web.md +30 -0
- package/docs/adapters/browser/xueqiu.md +27 -9
- package/docs/adapters/index.md +9 -2
- package/docs/comparison.md +125 -0
- package/docs/developer/contributing.md +21 -2
- package/docs/developer/testing.md +14 -8
- package/docs/developer/ts-adapter.md +18 -0
- package/docs/developer/yaml-adapter.md +16 -0
- package/docs/guide/plugins.md +10 -0
- package/docs/zh/guide/plugins.md +10 -0
- package/extension/dist/background.js +100 -35
- package/extension/manifest.json +6 -2
- package/extension/package.json +1 -1
- package/extension/popup.html +84 -0
- package/extension/popup.js +25 -0
- package/extension/src/background.test.ts +46 -1
- package/extension/src/background.ts +128 -34
- package/extension/src/cdp.ts +9 -9
- package/package.json +3 -2
- package/scripts/check-doc-coverage.sh +2 -0
- package/src/analysis.ts +170 -0
- package/src/browser/cdp.test.ts +66 -0
- package/src/browser/cdp.ts +59 -44
- package/src/browser/dom-snapshot.test.ts +42 -0
- package/src/browser/dom-snapshot.ts +56 -3
- package/src/browser/index.ts +2 -2
- package/src/browser/mcp.ts +2 -4
- package/src/browser/page.ts +34 -37
- package/src/browser/stealth.ts +24 -10
- package/src/browser.test.ts +2 -2
- package/src/build-manifest.test.ts +14 -0
- package/src/build-manifest.ts +13 -31
- package/src/cascade.ts +5 -3
- package/src/cli.ts +66 -34
- package/src/clis/_shared/desktop-commands.ts +121 -0
- package/src/clis/antigravity/serve.ts +6 -3
- package/src/clis/apple-podcasts/search.ts +2 -1
- package/src/clis/arxiv/search.ts +3 -3
- package/src/clis/bbc/news.ts +0 -1
- package/src/clis/bilibili/dynamic.test.ts +79 -0
- package/src/clis/bilibili/favorite.ts +5 -2
- package/src/clis/bilibili/following.ts +3 -2
- package/src/clis/bilibili/subtitle.ts +8 -7
- package/src/clis/bilibili/utils.ts +2 -2
- package/src/clis/boss/batchgreet.ts +1 -1
- package/src/clis/boss/chatlist.ts +1 -1
- package/src/clis/boss/chatmsg.ts +1 -1
- package/src/clis/boss/detail.ts +1 -1
- package/src/clis/boss/exchange.ts +1 -1
- package/src/clis/boss/greet.ts +1 -1
- package/src/clis/boss/invite.ts +1 -1
- package/src/clis/boss/joblist.ts +1 -1
- package/src/clis/boss/mark.ts +4 -3
- package/src/clis/boss/recommend.ts +1 -1
- package/src/clis/boss/resume.ts +1 -1
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +5 -4
- package/src/clis/boss/stats.ts +1 -1
- package/src/clis/chatgpt/ask.ts +5 -0
- package/src/clis/chatgpt/new.ts +7 -2
- package/src/clis/chatgpt/read.ts +7 -2
- package/src/clis/chatgpt/send.ts +3 -2
- package/src/clis/chatgpt/status.ts +6 -1
- package/src/clis/chatwise/ask.ts +7 -2
- package/src/clis/chatwise/export.ts +2 -0
- package/src/clis/chatwise/history.ts +2 -0
- package/src/clis/chatwise/model.ts +7 -3
- package/src/clis/chatwise/new.ts +3 -20
- package/src/clis/chatwise/read.ts +2 -0
- package/src/clis/chatwise/screenshot.ts +3 -32
- package/src/clis/chatwise/send.ts +7 -2
- package/src/clis/chatwise/shared.ts +8 -0
- package/src/clis/chatwise/status.ts +3 -24
- package/src/clis/codex/ask.ts +5 -2
- package/src/clis/codex/dump.ts +2 -27
- package/src/clis/codex/new.ts +2 -28
- package/src/clis/codex/screenshot.ts +2 -32
- package/src/clis/codex/send.ts +5 -4
- package/src/clis/codex/status.ts +2 -24
- package/src/clis/ctrip/search.ts +0 -1
- package/src/clis/cursor/ask.ts +2 -1
- package/src/clis/cursor/composer.ts +2 -1
- package/src/clis/cursor/dump.ts +2 -27
- package/src/clis/cursor/new.ts +2 -20
- package/src/clis/cursor/read.ts +2 -1
- package/src/clis/cursor/screenshot.ts +1 -36
- package/src/clis/cursor/send.ts +2 -1
- package/src/clis/cursor/status.ts +2 -22
- package/src/clis/dictionary/examples.yaml +25 -0
- package/src/clis/dictionary/search.yaml +27 -0
- package/src/clis/dictionary/synonyms.yaml +25 -0
- package/src/clis/douban/book-hot.ts +1 -1
- package/src/clis/douban/movie-hot.ts +1 -1
- package/src/clis/douban/search.ts +1 -1
- package/src/clis/douban/utils.ts +165 -1
- package/src/clis/doubao/ask.ts +1 -1
- package/src/clis/doubao/new.ts +1 -1
- package/src/clis/doubao/read.ts +1 -1
- package/src/clis/doubao/send.ts +1 -1
- package/src/clis/doubao/status.ts +1 -1
- package/src/clis/doubao-app/ask.ts +1 -1
- package/src/clis/doubao-app/new.ts +1 -1
- package/src/clis/doubao-app/read.ts +1 -1
- package/src/clis/doubao-app/send.ts +1 -1
- package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
- package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
- package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
- package/src/clis/douyin/_shared/creation-id.ts +8 -0
- package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
- package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
- package/src/clis/douyin/_shared/sts2.ts +20 -0
- package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
- package/src/clis/douyin/_shared/text-extra.ts +33 -0
- package/src/clis/douyin/_shared/timing.test.ts +38 -0
- package/src/clis/douyin/_shared/timing.ts +22 -0
- package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
- package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
- package/src/clis/douyin/_shared/tos-upload.ts +444 -0
- package/src/clis/douyin/_shared/transcode.test.ts +117 -0
- package/src/clis/douyin/_shared/transcode.ts +78 -0
- package/src/clis/douyin/_shared/types.ts +29 -0
- package/src/clis/douyin/activities.test.ts +25 -0
- package/src/clis/douyin/activities.ts +23 -0
- package/src/clis/douyin/collections.test.ts +26 -0
- package/src/clis/douyin/collections.ts +25 -0
- package/src/clis/douyin/delete.test.ts +12 -0
- package/src/clis/douyin/delete.ts +20 -0
- package/src/clis/douyin/draft.test.ts +12 -0
- package/src/clis/douyin/draft.ts +282 -0
- package/src/clis/douyin/drafts.test.ts +12 -0
- package/src/clis/douyin/drafts.ts +27 -0
- package/src/clis/douyin/hashtag.test.ts +28 -0
- package/src/clis/douyin/hashtag.ts +56 -0
- package/src/clis/douyin/location.test.ts +26 -0
- package/src/clis/douyin/location.ts +27 -0
- package/src/clis/douyin/profile.test.ts +12 -0
- package/src/clis/douyin/profile.ts +37 -0
- package/src/clis/douyin/publish.test.ts +45 -0
- package/src/clis/douyin/publish.ts +340 -0
- package/src/clis/douyin/stats.test.ts +25 -0
- package/src/clis/douyin/stats.ts +30 -0
- package/src/clis/douyin/update.test.ts +12 -0
- package/src/clis/douyin/update.ts +43 -0
- package/src/clis/douyin/videos.test.ts +12 -0
- package/src/clis/douyin/videos.ts +49 -0
- package/src/clis/grok/ask.test.ts +25 -0
- package/src/clis/grok/ask.ts +25 -12
- package/src/clis/hackernews/search.yaml +1 -1
- package/src/clis/instagram/search.yaml +2 -1
- package/src/clis/jd/item.test.ts +35 -0
- package/src/clis/jd/item.ts +101 -0
- package/src/clis/jike/feed.ts +1 -1
- package/src/clis/jike/search.ts +1 -1
- package/src/clis/linkedin/search.ts +5 -4
- package/src/clis/linkedin/timeline.test.ts +99 -0
- package/src/clis/linkedin/timeline.ts +532 -0
- package/src/clis/linux-do/search.yaml +3 -1
- package/src/clis/medium/feed.ts +1 -1
- package/src/clis/medium/search.ts +2 -2
- package/src/clis/medium/user.ts +1 -1
- package/src/clis/medium/{shared.ts → utils.ts} +2 -1
- package/src/clis/pixiv/detail.yaml +49 -0
- package/src/clis/pixiv/download.test.ts +114 -0
- package/src/clis/pixiv/download.ts +91 -0
- package/src/clis/pixiv/illusts.test.ts +115 -0
- package/src/clis/pixiv/illusts.ts +78 -0
- package/src/clis/pixiv/ranking.yaml +53 -0
- package/src/clis/pixiv/search.test.ts +97 -0
- package/src/clis/pixiv/search.ts +53 -0
- package/src/clis/pixiv/test-utils.ts +29 -0
- package/src/clis/pixiv/user.yaml +46 -0
- package/src/clis/pixiv/utils.ts +62 -0
- package/src/clis/reddit/comment.ts +2 -1
- package/src/clis/reddit/read.test.ts +34 -0
- package/src/clis/reddit/read.ts +4 -3
- package/src/clis/reddit/save.ts +2 -1
- package/src/clis/reddit/saved.ts +6 -2
- package/src/clis/reddit/subscribe.ts +2 -1
- package/src/clis/reddit/upvote.ts +2 -1
- package/src/clis/reddit/upvoted.ts +6 -2
- package/src/clis/reuters/search.ts +0 -1
- package/src/clis/sinablog/article.ts +1 -1
- package/src/clis/sinablog/hot.ts +1 -1
- package/src/clis/sinablog/user.ts +1 -1
- package/src/clis/substack/feed.ts +1 -1
- package/src/clis/substack/publication.ts +1 -1
- package/src/clis/substack/search.ts +3 -2
- package/src/clis/substack/{shared.ts → utils.ts} +3 -2
- package/src/clis/tiktok/search.yaml +2 -1
- package/src/clis/twitter/accept.ts +2 -1
- package/src/clis/twitter/article.ts +3 -1
- package/src/clis/twitter/block.ts +2 -1
- package/src/clis/twitter/bookmark.ts +2 -1
- package/src/clis/twitter/bookmarks.ts +3 -2
- package/src/clis/twitter/delete.ts +2 -1
- package/src/clis/twitter/follow.ts +2 -1
- package/src/clis/twitter/followers.ts +3 -2
- package/src/clis/twitter/following.ts +3 -2
- package/src/clis/twitter/hide-reply.ts +2 -1
- package/src/clis/twitter/like.ts +2 -1
- package/src/clis/twitter/notifications.ts +2 -1
- package/src/clis/twitter/post.ts +2 -1
- package/src/clis/twitter/profile.ts +4 -2
- package/src/clis/twitter/reply-dm.ts +2 -1
- package/src/clis/twitter/reply.ts +2 -1
- package/src/clis/twitter/search.test.ts +180 -0
- package/src/clis/twitter/search.ts +40 -14
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/timeline.ts +3 -2
- package/src/clis/twitter/trending.ts +3 -2
- package/src/clis/twitter/unblock.ts +2 -1
- package/src/clis/twitter/unbookmark.ts +2 -1
- package/src/clis/twitter/unfollow.ts +2 -1
- package/src/clis/v2ex/daily.ts +3 -2
- package/src/clis/v2ex/me.ts +3 -2
- package/src/clis/v2ex/notifications.ts +3 -4
- package/src/clis/web/read.ts +210 -0
- package/src/clis/weibo/comments.ts +54 -0
- package/src/clis/weibo/feed.ts +57 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weibo/me.ts +77 -0
- package/src/clis/weibo/post.ts +77 -0
- package/src/clis/weibo/user.ts +64 -0
- package/src/clis/weibo/utils.ts +32 -0
- package/src/clis/weread/search.ts +3 -2
- package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
- package/src/clis/xueqiu/danjuan-utils.ts +176 -0
- package/src/clis/xueqiu/fund-holdings.ts +32 -0
- package/src/clis/xueqiu/fund-snapshot.ts +27 -0
- package/src/clis/xueqiu/search.yaml +2 -1
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/channel.ts +155 -0
- package/src/clis/youtube/comments.ts +97 -0
- package/src/clis/youtube/search.ts +0 -1
- package/src/clis/youtube/transcript.ts +5 -4
- package/src/clis/youtube/video.ts +3 -2
- package/src/clis/zhihu/search.yaml +2 -1
- package/src/daemon.ts +5 -4
- package/src/discovery.ts +12 -34
- package/src/doctor.ts +3 -2
- package/src/download/index.test.ts +93 -2
- package/src/download/index.ts +44 -23
- package/src/download/media-download.ts +5 -3
- package/src/engine.test.ts +84 -3
- package/src/execution.ts +62 -46
- package/src/explore.ts +21 -90
- package/src/external-clis.yaml +0 -25
- package/src/external.test.ts +9 -0
- package/src/external.ts +12 -10
- package/src/generate.ts +4 -41
- package/src/hooks.test.ts +126 -0
- package/src/hooks.ts +90 -0
- package/src/interceptor.ts +73 -23
- package/src/main.ts +2 -0
- package/src/output.ts +14 -6
- package/src/pipeline/executor.ts +1 -1
- package/src/pipeline/steps/browser.ts +1 -3
- package/src/pipeline/steps/download.test.ts +136 -0
- package/src/pipeline/steps/download.ts +47 -34
- package/src/pipeline/steps/fetch.test.ts +179 -0
- package/src/pipeline/steps/fetch.ts +39 -23
- package/src/pipeline/steps/transform.ts +2 -6
- package/src/pipeline/template.test.ts +28 -0
- package/src/pipeline/template.ts +67 -79
- package/src/pipeline/transform.test.ts +20 -0
- package/src/plugin.test.ts +251 -3
- package/src/plugin.ts +265 -21
- package/src/record.ts +12 -84
- package/src/registry-api.ts +2 -0
- package/src/registry.ts +7 -4
- package/src/runtime.ts +14 -4
- package/src/snapshotFormatter.ts +43 -121
- package/src/utils.ts +39 -0
- package/src/validate.ts +3 -5
- package/src/weread-search-regression.test.ts +44 -0
- package/src/yaml-schema.ts +28 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
- package/tests/e2e/browser-public-extended.test.ts +162 -0
- package/tests/e2e/browser-public.test.ts +7 -146
- package/tests/e2e/plugin-management.test.ts +137 -0
- package/tests/e2e/public-commands.test.ts +34 -1
- package/vitest.config.ts +33 -8
- package/.github/workflows/pkg-pr-new.yml +0 -30
- package/dist/clis/douban/shared.d.ts +0 -4
- package/dist/clis/douban/shared.js +0 -155
- package/src/clis/douban/shared.ts +0 -165
- /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/boss/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
- /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/jike/{shared.js → utils.js} +0 -0
- /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
- /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
- /package/src/clis/boss/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
- /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
- /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
|
@@ -6,6 +6,7 @@ Use TypeScript adapters when you need browser-side logic, multi-step flows, DOM
|
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { CommandExecutionError, EmptyResultError } from '../../errors.js';
|
|
9
10
|
|
|
10
11
|
cli({
|
|
11
12
|
site: 'mysite',
|
|
@@ -34,6 +35,9 @@ cli({
|
|
|
34
35
|
})()
|
|
35
36
|
`);
|
|
36
37
|
|
|
38
|
+
if (!Array.isArray(data)) throw new CommandExecutionError('MySite returned an unexpected response');
|
|
39
|
+
if (!data.length) throw new EmptyResultError('mysite search', 'Try a different keyword');
|
|
40
|
+
|
|
37
41
|
return data.slice(0, Number(limit)).map((item: any) => ({
|
|
38
42
|
title: item.title,
|
|
39
43
|
url: item.url,
|
|
@@ -69,6 +73,20 @@ Contains parsed CLI arguments as key-value pairs. Always destructure with defaul
|
|
|
69
73
|
const { query, limit = 10, format = 'json' } = kwargs;
|
|
70
74
|
```
|
|
71
75
|
|
|
76
|
+
For most search/read/detail commands, the main subject should be positional (`opencli mysite search "rust"`, `opencli mysite article 123`) instead of a named flag such as `--query` or `--id`. Keep named flags for optional modifiers.
|
|
77
|
+
|
|
78
|
+
## Error Handling
|
|
79
|
+
|
|
80
|
+
Prefer throwing `CliError` subclasses from `src/errors.ts` for expected adapter failures:
|
|
81
|
+
|
|
82
|
+
- `AuthRequiredError` for missing login / cookies
|
|
83
|
+
- `EmptyResultError` for empty but valid responses
|
|
84
|
+
- `CommandExecutionError` for unexpected API or browser failures
|
|
85
|
+
- `TimeoutError` for site timeouts
|
|
86
|
+
- `ArgumentError` for invalid user input
|
|
87
|
+
|
|
88
|
+
Avoid raw `Error` for normal adapter control flow. This keeps top-level CLI output consistent and preserves hints for users.
|
|
89
|
+
|
|
72
90
|
## AI-Assisted Development
|
|
73
91
|
|
|
74
92
|
Use the AI workflow tools to accelerate adapter creation:
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
YAML adapters are the recommended way to add new commands when the site offers a straightforward API. They use a declarative pipeline approach — no TypeScript required.
|
|
4
4
|
|
|
5
|
+
Use YAML only when the command stays mostly declarative. If you find yourself embedding long JavaScript expressions, many fallbacks, or multi-step browser logic, move the command to a TypeScript adapter instead of growing an opaque template blob.
|
|
6
|
+
|
|
5
7
|
## Basic Structure
|
|
6
8
|
|
|
7
9
|
::: v-pre
|
|
@@ -33,6 +35,14 @@ columns: [rank, title, score, url]
|
|
|
33
35
|
```
|
|
34
36
|
:::
|
|
35
37
|
|
|
38
|
+
For most commands, keep the primary subject positional. Good examples:
|
|
39
|
+
|
|
40
|
+
- `opencli mysite search "rust"`
|
|
41
|
+
- `opencli mysite topic 123`
|
|
42
|
+
- `opencli mysite download "https://example.com/post/1"`
|
|
43
|
+
|
|
44
|
+
Prefer named flags only for optional modifiers such as `--limit`, `--sort`, `--lang`, or `--output`.
|
|
45
|
+
|
|
36
46
|
## Pipeline Steps
|
|
37
47
|
|
|
38
48
|
### `fetch`
|
|
@@ -106,3 +116,9 @@ Use `${{ ... }}` for dynamic values:
|
|
|
106
116
|
## Real Example
|
|
107
117
|
|
|
108
118
|
See [`src/clis/hackernews/top.yaml`](https://github.com/jackwener/opencli/blob/main/src/clis/hackernews/top.yaml).
|
|
119
|
+
|
|
120
|
+
## Guardrails
|
|
121
|
+
|
|
122
|
+
- Add fallbacks for optional fields in `map` expressions when upstream payloads may be sparse.
|
|
123
|
+
- Keep template expressions short and readable. If the expression starts looking like a mini program, switch to TypeScript.
|
|
124
|
+
- If you add a new adapter, also add the matching doc page plus index/sidebar entries so `doc-coverage` stays green.
|
package/docs/guide/plugins.md
CHANGED
|
@@ -11,6 +11,12 @@ opencli plugin install github:ByteYue/opencli-plugin-github-trending
|
|
|
11
11
|
# List installed plugins
|
|
12
12
|
opencli plugin list
|
|
13
13
|
|
|
14
|
+
# Update one plugin
|
|
15
|
+
opencli plugin update github-trending
|
|
16
|
+
|
|
17
|
+
# Update all installed plugins
|
|
18
|
+
opencli plugin update --all
|
|
19
|
+
|
|
14
20
|
# Use the plugin (it's just a regular command)
|
|
15
21
|
opencli github-trending repos --limit 10
|
|
16
22
|
|
|
@@ -31,6 +37,10 @@ opencli plugin install https://github.com/user/repo
|
|
|
31
37
|
|
|
32
38
|
The repo name prefix `opencli-plugin-` is automatically stripped for the local directory name. For example, `opencli-plugin-hot-digest` becomes `hot-digest`.
|
|
33
39
|
|
|
40
|
+
## Version Tracking
|
|
41
|
+
|
|
42
|
+
OpenCLI records installed plugin versions in `~/.opencli/plugins.lock.json`. Each entry stores the plugin source, current git commit hash, install time, and last update time. `opencli plugin list` shows the short commit hash when version metadata is available.
|
|
43
|
+
|
|
34
44
|
## Creating a Plugin
|
|
35
45
|
|
|
36
46
|
### Option 1: YAML Plugin (Simplest)
|
package/docs/zh/guide/plugins.md
CHANGED
|
@@ -11,6 +11,12 @@ opencli plugin install github:ByteYue/opencli-plugin-github-trending
|
|
|
11
11
|
# 列出已安装插件
|
|
12
12
|
opencli plugin list
|
|
13
13
|
|
|
14
|
+
# 更新单个插件
|
|
15
|
+
opencli plugin update github-trending
|
|
16
|
+
|
|
17
|
+
# 更新全部已安装插件
|
|
18
|
+
opencli plugin update --all
|
|
19
|
+
|
|
14
20
|
# 使用插件(本质上就是普通 command)
|
|
15
21
|
opencli github-trending today
|
|
16
22
|
|
|
@@ -31,6 +37,10 @@ opencli plugin install https://github.com/user/repo
|
|
|
31
37
|
|
|
32
38
|
如果仓库名带 `opencli-plugin-` 前缀,本地目录会自动去掉这个前缀。例如 `opencli-plugin-hot-digest` 会变成 `hot-digest`。
|
|
33
39
|
|
|
40
|
+
## 版本追踪
|
|
41
|
+
|
|
42
|
+
OpenCLI 会把已安装 plugin 的版本记录到 `~/.opencli/plugins.lock.json`。每条记录会保存 plugin source、当前 git commit hash、安装时间,以及最近一次更新时间。只要有这份元数据,`opencli plugin list` 就会显示对应的短 commit hash。
|
|
43
|
+
|
|
34
44
|
## YAML plugin 示例
|
|
35
45
|
|
|
36
46
|
```text
|
|
@@ -5,9 +5,10 @@ const WS_RECONNECT_BASE_DELAY = 2e3;
|
|
|
5
5
|
const WS_RECONNECT_MAX_DELAY = 6e4;
|
|
6
6
|
|
|
7
7
|
const attached = /* @__PURE__ */ new Set();
|
|
8
|
+
const BLANK_PAGE$1 = "data:text/html,<html></html>";
|
|
8
9
|
function isDebuggableUrl$1(url) {
|
|
9
10
|
if (!url) return true;
|
|
10
|
-
return
|
|
11
|
+
return url.startsWith("http://") || url.startsWith("https://") || url === BLANK_PAGE$1;
|
|
11
12
|
}
|
|
12
13
|
async function ensureAttached(tabId) {
|
|
13
14
|
try {
|
|
@@ -100,11 +101,11 @@ async function screenshot(tabId, options = {}) {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
|
-
function detach(tabId) {
|
|
104
|
+
async function detach(tabId) {
|
|
104
105
|
if (!attached.has(tabId)) return;
|
|
105
106
|
attached.delete(tabId);
|
|
106
107
|
try {
|
|
107
|
-
chrome.debugger.detach({ tabId });
|
|
108
|
+
await chrome.debugger.detach({ tabId });
|
|
108
109
|
} catch {
|
|
109
110
|
}
|
|
110
111
|
}
|
|
@@ -115,15 +116,9 @@ function registerListeners() {
|
|
|
115
116
|
chrome.debugger.onDetach.addListener((source) => {
|
|
116
117
|
if (source.tabId) attached.delete(source.tabId);
|
|
117
118
|
});
|
|
118
|
-
chrome.tabs.onUpdated.addListener((tabId, info) => {
|
|
119
|
+
chrome.tabs.onUpdated.addListener(async (tabId, info) => {
|
|
119
120
|
if (info.url && !isDebuggableUrl$1(info.url)) {
|
|
120
|
-
|
|
121
|
-
attached.delete(tabId);
|
|
122
|
-
try {
|
|
123
|
-
chrome.debugger.detach({ tabId });
|
|
124
|
-
} catch {
|
|
125
|
-
}
|
|
126
|
-
}
|
|
121
|
+
await detach(tabId);
|
|
127
122
|
}
|
|
128
123
|
});
|
|
129
124
|
}
|
|
@@ -188,9 +183,11 @@ function connect() {
|
|
|
188
183
|
ws?.close();
|
|
189
184
|
};
|
|
190
185
|
}
|
|
186
|
+
const MAX_EAGER_ATTEMPTS = 6;
|
|
191
187
|
function scheduleReconnect() {
|
|
192
188
|
if (reconnectTimer) return;
|
|
193
189
|
reconnectAttempts++;
|
|
190
|
+
if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return;
|
|
194
191
|
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
|
|
195
192
|
reconnectTimer = setTimeout(() => {
|
|
196
193
|
reconnectTimer = null;
|
|
@@ -229,7 +226,7 @@ async function getAutomationWindow(workspace) {
|
|
|
229
226
|
}
|
|
230
227
|
}
|
|
231
228
|
const win = await chrome.windows.create({
|
|
232
|
-
url:
|
|
229
|
+
url: BLANK_PAGE,
|
|
233
230
|
focused: false,
|
|
234
231
|
width: 1280,
|
|
235
232
|
height: 900,
|
|
@@ -273,6 +270,15 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
273
270
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
274
271
|
if (alarm.name === "keepalive") connect();
|
|
275
272
|
});
|
|
273
|
+
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
274
|
+
if (msg?.type === "getStatus") {
|
|
275
|
+
sendResponse({
|
|
276
|
+
connected: ws?.readyState === WebSocket.OPEN,
|
|
277
|
+
reconnecting: reconnectTimer !== null
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
});
|
|
276
282
|
async function handleCommand(cmd) {
|
|
277
283
|
const workspace = getWorkspaceKey(cmd.workspace);
|
|
278
284
|
resetWindowIdleTimer(workspace);
|
|
@@ -303,16 +309,41 @@ async function handleCommand(cmd) {
|
|
|
303
309
|
};
|
|
304
310
|
}
|
|
305
311
|
}
|
|
312
|
+
const BLANK_PAGE = "data:text/html,<html></html>";
|
|
306
313
|
function isDebuggableUrl(url) {
|
|
307
314
|
if (!url) return true;
|
|
308
|
-
return
|
|
315
|
+
return url.startsWith("http://") || url.startsWith("https://") || url === BLANK_PAGE;
|
|
316
|
+
}
|
|
317
|
+
function isSafeNavigationUrl(url) {
|
|
318
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
319
|
+
}
|
|
320
|
+
function normalizeUrlForComparison(url) {
|
|
321
|
+
if (!url) return "";
|
|
322
|
+
try {
|
|
323
|
+
const parsed = new URL(url);
|
|
324
|
+
if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") {
|
|
325
|
+
parsed.port = "";
|
|
326
|
+
}
|
|
327
|
+
const pathname = parsed.pathname === "/" ? "" : parsed.pathname;
|
|
328
|
+
return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`;
|
|
329
|
+
} catch {
|
|
330
|
+
return url;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function isTargetUrl(currentUrl, targetUrl) {
|
|
334
|
+
return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
|
|
309
335
|
}
|
|
310
336
|
async function resolveTabId(tabId, workspace) {
|
|
311
337
|
if (tabId !== void 0) {
|
|
312
338
|
try {
|
|
313
339
|
const tab = await chrome.tabs.get(tabId);
|
|
314
|
-
|
|
315
|
-
|
|
340
|
+
const session = automationSessions.get(workspace);
|
|
341
|
+
if (isDebuggableUrl(tab.url) && session && tab.windowId === session.windowId) return tabId;
|
|
342
|
+
if (session && tab.windowId !== session.windowId) {
|
|
343
|
+
console.warn(`[opencli] Tab ${tabId} belongs to window ${tab.windowId}, not automation window ${session.windowId}, re-resolving`);
|
|
344
|
+
} else if (!isDebuggableUrl(tab.url)) {
|
|
345
|
+
console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
|
|
346
|
+
}
|
|
316
347
|
} catch {
|
|
317
348
|
console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
|
|
318
349
|
}
|
|
@@ -323,7 +354,7 @@ async function resolveTabId(tabId, workspace) {
|
|
|
323
354
|
if (debuggableTab?.id) return debuggableTab.id;
|
|
324
355
|
const reuseTab = tabs.find((t) => t.id);
|
|
325
356
|
if (reuseTab?.id) {
|
|
326
|
-
await chrome.tabs.update(reuseTab.id, { url:
|
|
357
|
+
await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
|
|
327
358
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
328
359
|
try {
|
|
329
360
|
const updated = await chrome.tabs.get(reuseTab.id);
|
|
@@ -332,7 +363,7 @@ async function resolveTabId(tabId, workspace) {
|
|
|
332
363
|
} catch {
|
|
333
364
|
}
|
|
334
365
|
}
|
|
335
|
-
const newTab = await chrome.tabs.create({ windowId, url:
|
|
366
|
+
const newTab = await chrome.tabs.create({ windowId, url: BLANK_PAGE, active: true });
|
|
336
367
|
if (!newTab.id) throw new Error("Failed to create tab in automation window");
|
|
337
368
|
return newTab.id;
|
|
338
369
|
}
|
|
@@ -362,40 +393,58 @@ async function handleExec(cmd, workspace) {
|
|
|
362
393
|
}
|
|
363
394
|
async function handleNavigate(cmd, workspace) {
|
|
364
395
|
if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" };
|
|
396
|
+
if (!isSafeNavigationUrl(cmd.url)) {
|
|
397
|
+
return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
|
|
398
|
+
}
|
|
365
399
|
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
366
400
|
const beforeTab = await chrome.tabs.get(tabId);
|
|
367
|
-
const
|
|
401
|
+
const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
|
|
368
402
|
const targetUrl = cmd.url;
|
|
403
|
+
if (beforeTab.status === "complete" && isTargetUrl(beforeTab.url, targetUrl)) {
|
|
404
|
+
return {
|
|
405
|
+
id: cmd.id,
|
|
406
|
+
ok: true,
|
|
407
|
+
data: { title: beforeTab.title, url: beforeTab.url, tabId, timedOut: false }
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
await detach(tabId);
|
|
369
411
|
await chrome.tabs.update(tabId, { url: targetUrl });
|
|
370
412
|
let timedOut = false;
|
|
371
413
|
await new Promise((resolve) => {
|
|
372
|
-
let
|
|
414
|
+
let settled = false;
|
|
415
|
+
let checkTimer = null;
|
|
416
|
+
let timeoutTimer = null;
|
|
417
|
+
const finish = () => {
|
|
418
|
+
if (settled) return;
|
|
419
|
+
settled = true;
|
|
420
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
421
|
+
if (checkTimer) clearTimeout(checkTimer);
|
|
422
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
423
|
+
resolve();
|
|
424
|
+
};
|
|
425
|
+
const isNavigationDone = (url) => {
|
|
426
|
+
return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized;
|
|
427
|
+
};
|
|
373
428
|
const listener = (id, info, tab2) => {
|
|
374
429
|
if (id !== tabId) return;
|
|
375
|
-
if (info.
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
if (urlChanged && info.status === "complete") {
|
|
379
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
380
|
-
resolve();
|
|
430
|
+
if (info.status === "complete" && isNavigationDone(tab2.url ?? info.url)) {
|
|
431
|
+
finish();
|
|
381
432
|
}
|
|
382
433
|
};
|
|
383
434
|
chrome.tabs.onUpdated.addListener(listener);
|
|
384
|
-
setTimeout(async () => {
|
|
435
|
+
checkTimer = setTimeout(async () => {
|
|
385
436
|
try {
|
|
386
437
|
const currentTab = await chrome.tabs.get(tabId);
|
|
387
|
-
if (currentTab.
|
|
388
|
-
|
|
389
|
-
resolve();
|
|
438
|
+
if (currentTab.status === "complete" && isNavigationDone(currentTab.url)) {
|
|
439
|
+
finish();
|
|
390
440
|
}
|
|
391
441
|
} catch {
|
|
392
442
|
}
|
|
393
443
|
}, 100);
|
|
394
|
-
setTimeout(() => {
|
|
395
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
444
|
+
timeoutTimer = setTimeout(() => {
|
|
396
445
|
timedOut = true;
|
|
397
446
|
console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`);
|
|
398
|
-
|
|
447
|
+
finish();
|
|
399
448
|
}, 15e3);
|
|
400
449
|
});
|
|
401
450
|
const tab = await chrome.tabs.get(tabId);
|
|
@@ -419,8 +468,11 @@ async function handleTabs(cmd, workspace) {
|
|
|
419
468
|
return { id: cmd.id, ok: true, data };
|
|
420
469
|
}
|
|
421
470
|
case "new": {
|
|
471
|
+
if (cmd.url && !isSafeNavigationUrl(cmd.url)) {
|
|
472
|
+
return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
|
|
473
|
+
}
|
|
422
474
|
const windowId = await getAutomationWindow(workspace);
|
|
423
|
-
const tab = await chrome.tabs.create({ windowId, url: cmd.url ??
|
|
475
|
+
const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? BLANK_PAGE, active: true });
|
|
424
476
|
return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
|
|
425
477
|
}
|
|
426
478
|
case "close": {
|
|
@@ -429,18 +481,28 @@ async function handleTabs(cmd, workspace) {
|
|
|
429
481
|
const target = tabs[cmd.index];
|
|
430
482
|
if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
|
|
431
483
|
await chrome.tabs.remove(target.id);
|
|
432
|
-
detach(target.id);
|
|
484
|
+
await detach(target.id);
|
|
433
485
|
return { id: cmd.id, ok: true, data: { closed: target.id } };
|
|
434
486
|
}
|
|
435
487
|
const tabId = await resolveTabId(cmd.tabId, workspace);
|
|
436
488
|
await chrome.tabs.remove(tabId);
|
|
437
|
-
detach(tabId);
|
|
489
|
+
await detach(tabId);
|
|
438
490
|
return { id: cmd.id, ok: true, data: { closed: tabId } };
|
|
439
491
|
}
|
|
440
492
|
case "select": {
|
|
441
493
|
if (cmd.index === void 0 && cmd.tabId === void 0)
|
|
442
494
|
return { id: cmd.id, ok: false, error: "Missing index or tabId" };
|
|
443
495
|
if (cmd.tabId !== void 0) {
|
|
496
|
+
const session = automationSessions.get(workspace);
|
|
497
|
+
let tab;
|
|
498
|
+
try {
|
|
499
|
+
tab = await chrome.tabs.get(cmd.tabId);
|
|
500
|
+
} catch {
|
|
501
|
+
return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} no longer exists` };
|
|
502
|
+
}
|
|
503
|
+
if (!session || tab.windowId !== session.windowId) {
|
|
504
|
+
return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} is not in the automation window` };
|
|
505
|
+
}
|
|
444
506
|
await chrome.tabs.update(cmd.tabId, { active: true });
|
|
445
507
|
return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
|
|
446
508
|
}
|
|
@@ -455,6 +517,9 @@ async function handleTabs(cmd, workspace) {
|
|
|
455
517
|
}
|
|
456
518
|
}
|
|
457
519
|
async function handleCookies(cmd) {
|
|
520
|
+
if (!cmd.domain && !cmd.url) {
|
|
521
|
+
return { id: cmd.id, ok: false, error: "Cookie scope required: provide domain or url to avoid dumping all cookies" };
|
|
522
|
+
}
|
|
458
523
|
const details = {};
|
|
459
524
|
if (cmd.domain) details.domain = cmd.domain;
|
|
460
525
|
if (cmd.url) details.url = cmd.url;
|
package/extension/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCLI",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "1.4.1",
|
|
5
|
+
"description": "Browser automation bridge for the OpenCLI CLI tool. Executes commands in isolated Chrome windows via a local daemon.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"debugger",
|
|
8
8
|
"tabs",
|
|
@@ -22,10 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"action": {
|
|
24
24
|
"default_title": "OpenCLI",
|
|
25
|
+
"default_popup": "popup.html",
|
|
25
26
|
"default_icon": {
|
|
26
27
|
"16": "icons/icon-16.png",
|
|
27
28
|
"32": "icons/icon-32.png"
|
|
28
29
|
}
|
|
29
30
|
},
|
|
31
|
+
"content_security_policy": {
|
|
32
|
+
"extension_pages": "script-src 'self'; object-src 'self'"
|
|
33
|
+
},
|
|
30
34
|
"homepage_url": "https://github.com/jackwener/opencli"
|
|
31
35
|
}
|
package/extension/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<style>
|
|
6
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7
|
+
body {
|
|
8
|
+
width: 280px;
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
font-size: 13px;
|
|
11
|
+
color: #333;
|
|
12
|
+
background: #fff;
|
|
13
|
+
padding: 16px;
|
|
14
|
+
}
|
|
15
|
+
.header {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 8px;
|
|
19
|
+
margin-bottom: 14px;
|
|
20
|
+
}
|
|
21
|
+
.header img { width: 24px; height: 24px; }
|
|
22
|
+
.header h1 { font-size: 15px; font-weight: 600; }
|
|
23
|
+
.status-row {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: 8px;
|
|
27
|
+
padding: 10px 12px;
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
background: #f5f5f5;
|
|
30
|
+
}
|
|
31
|
+
.dot {
|
|
32
|
+
width: 8px; height: 8px;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
}
|
|
36
|
+
.dot.connected { background: #34c759; }
|
|
37
|
+
.dot.disconnected { background: #ff3b30; }
|
|
38
|
+
.dot.connecting { background: #ff9500; }
|
|
39
|
+
.status-text { font-size: 13px; color: #555; }
|
|
40
|
+
.status-text strong { color: #333; }
|
|
41
|
+
.hint {
|
|
42
|
+
margin-top: 10px;
|
|
43
|
+
padding: 8px 10px;
|
|
44
|
+
border-radius: 6px;
|
|
45
|
+
background: #f0f4ff;
|
|
46
|
+
font-size: 11px;
|
|
47
|
+
color: #666;
|
|
48
|
+
line-height: 1.5;
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
.hint code {
|
|
52
|
+
background: #e8ecf1;
|
|
53
|
+
padding: 1px 4px;
|
|
54
|
+
border-radius: 3px;
|
|
55
|
+
font-size: 11px;
|
|
56
|
+
}
|
|
57
|
+
.footer {
|
|
58
|
+
margin-top: 14px;
|
|
59
|
+
text-align: center;
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
color: #999;
|
|
62
|
+
}
|
|
63
|
+
.footer a { color: #007aff; text-decoration: none; }
|
|
64
|
+
.footer a:hover { text-decoration: underline; }
|
|
65
|
+
</style>
|
|
66
|
+
</head>
|
|
67
|
+
<body>
|
|
68
|
+
<div class="header">
|
|
69
|
+
<img src="icons/icon-48.png" alt="OpenCLI">
|
|
70
|
+
<h1>OpenCLI</h1>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="status-row">
|
|
73
|
+
<span class="dot disconnected" id="dot"></span>
|
|
74
|
+
<span class="status-text" id="status">Checking...</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="hint" id="hint">
|
|
77
|
+
This is normal. The extension connects automatically when you run any <code>opencli</code> command.
|
|
78
|
+
</div>
|
|
79
|
+
<div class="footer">
|
|
80
|
+
<a href="https://github.com/jackwener/opencli" target="_blank">Documentation</a>
|
|
81
|
+
</div>
|
|
82
|
+
<script src="popup.js"></script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Query connection status from background service worker
|
|
2
|
+
chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
|
|
3
|
+
const dot = document.getElementById('dot');
|
|
4
|
+
const status = document.getElementById('status');
|
|
5
|
+
const hint = document.getElementById('hint');
|
|
6
|
+
if (chrome.runtime.lastError || !resp) {
|
|
7
|
+
dot.className = 'dot disconnected';
|
|
8
|
+
status.innerHTML = '<strong>No daemon connected</strong>';
|
|
9
|
+
hint.style.display = 'block';
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (resp.connected) {
|
|
13
|
+
dot.className = 'dot connected';
|
|
14
|
+
status.innerHTML = '<strong>Connected to daemon</strong>';
|
|
15
|
+
hint.style.display = 'none';
|
|
16
|
+
} else if (resp.reconnecting) {
|
|
17
|
+
dot.className = 'dot connecting';
|
|
18
|
+
status.innerHTML = '<strong>Reconnecting...</strong>';
|
|
19
|
+
hint.style.display = 'none';
|
|
20
|
+
} else {
|
|
21
|
+
dot.className = 'dot disconnected';
|
|
22
|
+
status.innerHTML = '<strong>No daemon connected</strong>';
|
|
23
|
+
hint.style.display = 'block';
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
type Listener<T extends (...args: any[]) => void> = { addListener: (fn: T) => void };
|
|
4
4
|
|
|
@@ -96,9 +96,15 @@ function createChromeMock() {
|
|
|
96
96
|
describe('background tab isolation', () => {
|
|
97
97
|
beforeEach(() => {
|
|
98
98
|
vi.resetModules();
|
|
99
|
+
vi.useRealTimers();
|
|
99
100
|
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
100
101
|
});
|
|
101
102
|
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
vi.useRealTimers();
|
|
105
|
+
vi.unstubAllGlobals();
|
|
106
|
+
});
|
|
107
|
+
|
|
102
108
|
it('lists only automation-window web tabs', async () => {
|
|
103
109
|
const { chrome } = createChromeMock();
|
|
104
110
|
vi.stubGlobal('chrome', chrome);
|
|
@@ -133,6 +139,45 @@ describe('background tab isolation', () => {
|
|
|
133
139
|
expect(create).toHaveBeenCalledWith({ windowId: 1, url: 'https://new.example', active: true });
|
|
134
140
|
});
|
|
135
141
|
|
|
142
|
+
it('treats normalized same-url navigate as already complete', async () => {
|
|
143
|
+
const { chrome, tabs, update } = createChromeMock();
|
|
144
|
+
tabs[0].url = 'https://www.bilibili.com/';
|
|
145
|
+
tabs[0].title = 'bilibili';
|
|
146
|
+
tabs[0].status = 'complete';
|
|
147
|
+
vi.stubGlobal('chrome', chrome);
|
|
148
|
+
|
|
149
|
+
const mod = await import('./background');
|
|
150
|
+
mod.__test__.setAutomationWindowId('site:bilibili', 1);
|
|
151
|
+
|
|
152
|
+
const result = await mod.__test__.handleNavigate(
|
|
153
|
+
{ id: 'same-url', action: 'navigate', url: 'https://www.bilibili.com', workspace: 'site:bilibili' },
|
|
154
|
+
'site:bilibili',
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(result).toEqual({
|
|
158
|
+
id: 'same-url',
|
|
159
|
+
ok: true,
|
|
160
|
+
data: {
|
|
161
|
+
title: 'bilibili',
|
|
162
|
+
url: 'https://www.bilibili.com/',
|
|
163
|
+
tabId: 1,
|
|
164
|
+
timedOut: false,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
expect(update).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('keeps hash routes distinct when comparing target URLs', async () => {
|
|
171
|
+
const { chrome } = createChromeMock();
|
|
172
|
+
vi.stubGlobal('chrome', chrome);
|
|
173
|
+
|
|
174
|
+
const mod = await import('./background');
|
|
175
|
+
|
|
176
|
+
expect(mod.__test__.isTargetUrl('https://example.com/', 'https://example.com')).toBe(true);
|
|
177
|
+
expect(mod.__test__.isTargetUrl('https://example.com/#feed', 'https://example.com/#settings')).toBe(false);
|
|
178
|
+
expect(mod.__test__.isTargetUrl('https://example.com/app/', 'https://example.com/app')).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
136
181
|
it('reports sessions per workspace', async () => {
|
|
137
182
|
const { chrome } = createChromeMock();
|
|
138
183
|
vi.stubGlobal('chrome', chrome);
|