@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
|
@@ -48,7 +48,8 @@ pipeline:
|
|
|
48
48
|
exchange: ${{ item.exchange }}
|
|
49
49
|
price: ${{ item.price }}
|
|
50
50
|
changePercent: ${{ item.changePercent }}
|
|
51
|
+
url: ${{ item.url }}
|
|
51
52
|
|
|
52
53
|
- limit: ${{ args.limit }}
|
|
53
54
|
|
|
54
|
-
columns: [symbol, name, exchange, price, changePercent]
|
|
55
|
+
columns: [symbol, name, exchange, price, changePercent, url]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube channel — get channel info and recent videos via InnerTube API.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
6
|
+
|
|
7
|
+
cli({
|
|
8
|
+
site: 'youtube',
|
|
9
|
+
name: 'channel',
|
|
10
|
+
description: 'Get YouTube channel info and recent videos',
|
|
11
|
+
domain: 'www.youtube.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'id', required: true, positional: true, help: 'Channel ID (UCxxxx) or handle (@name)' },
|
|
15
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max recent videos (max 30)' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['field', 'value'],
|
|
18
|
+
func: async (page, kwargs) => {
|
|
19
|
+
const channelId = String(kwargs.id);
|
|
20
|
+
const limit = Math.min(kwargs.limit || 10, 30);
|
|
21
|
+
await page.goto('https://www.youtube.com');
|
|
22
|
+
await page.wait(2);
|
|
23
|
+
|
|
24
|
+
const data = await page.evaluate(`
|
|
25
|
+
(async () => {
|
|
26
|
+
const channelId = ${JSON.stringify(channelId)};
|
|
27
|
+
const limit = ${limit};
|
|
28
|
+
const cfg = window.ytcfg?.data_ || {};
|
|
29
|
+
const apiKey = cfg.INNERTUBE_API_KEY;
|
|
30
|
+
const context = cfg.INNERTUBE_CONTEXT;
|
|
31
|
+
if (!apiKey || !context) return {error: 'YouTube config not found'};
|
|
32
|
+
|
|
33
|
+
// Resolve handle to browseId if needed
|
|
34
|
+
let browseId = channelId;
|
|
35
|
+
if (channelId.startsWith('@')) {
|
|
36
|
+
const resolveResp = await fetch('/youtubei/v1/navigation/resolve_url?key=' + apiKey + '&prettyPrint=false', {
|
|
37
|
+
method: 'POST', credentials: 'include',
|
|
38
|
+
headers: {'Content-Type': 'application/json'},
|
|
39
|
+
body: JSON.stringify({context, url: 'https://www.youtube.com/' + channelId})
|
|
40
|
+
});
|
|
41
|
+
if (resolveResp.ok) {
|
|
42
|
+
const resolveData = await resolveResp.json();
|
|
43
|
+
browseId = resolveData.endpoint?.browseEndpoint?.browseId || channelId;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fetch channel data
|
|
48
|
+
const resp = await fetch('/youtubei/v1/browse?key=' + apiKey + '&prettyPrint=false', {
|
|
49
|
+
method: 'POST', credentials: 'include',
|
|
50
|
+
headers: {'Content-Type': 'application/json'},
|
|
51
|
+
body: JSON.stringify({context, browseId})
|
|
52
|
+
});
|
|
53
|
+
if (!resp.ok) return {error: 'Channel API returned HTTP ' + resp.status};
|
|
54
|
+
const data = await resp.json();
|
|
55
|
+
|
|
56
|
+
// Channel metadata
|
|
57
|
+
const metadata = data.metadata?.channelMetadataRenderer || {};
|
|
58
|
+
const header = data.header?.pageHeaderRenderer || data.header?.c4TabbedHeaderRenderer || {};
|
|
59
|
+
|
|
60
|
+
// Subscriber count from header
|
|
61
|
+
let subscriberCount = '';
|
|
62
|
+
try {
|
|
63
|
+
const rows = header.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
for (const part of (row.metadataParts || [])) {
|
|
66
|
+
const text = part.text?.content || '';
|
|
67
|
+
if (text.includes('subscriber')) subscriberCount = text;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
// Fallback for old c4TabbedHeaderRenderer format
|
|
72
|
+
if (!subscriberCount && header.subscriberCountText?.simpleText) {
|
|
73
|
+
subscriberCount = header.subscriberCountText.simpleText;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract recent videos from Home tab
|
|
77
|
+
const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
|
|
78
|
+
const homeTab = tabs.find(t => t.tabRenderer?.selected);
|
|
79
|
+
const recentVideos = [];
|
|
80
|
+
|
|
81
|
+
if (homeTab) {
|
|
82
|
+
const sections = homeTab.tabRenderer?.content?.sectionListRenderer?.contents || [];
|
|
83
|
+
for (const section of sections) {
|
|
84
|
+
for (const shelf of (section.itemSectionRenderer?.contents || [])) {
|
|
85
|
+
for (const item of (shelf.shelfRenderer?.content?.horizontalListRenderer?.items || [])) {
|
|
86
|
+
// New lockupViewModel format
|
|
87
|
+
const lvm = item.lockupViewModel;
|
|
88
|
+
if (lvm && lvm.contentType === 'LOCKUP_CONTENT_TYPE_VIDEO' && recentVideos.length < limit) {
|
|
89
|
+
const meta = lvm.metadata?.lockupMetadataViewModel;
|
|
90
|
+
const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
|
|
91
|
+
const viewsAndTime = (rows[0]?.metadataParts || []).map(p => p.text?.content).filter(Boolean).join(' | ');
|
|
92
|
+
let duration = '';
|
|
93
|
+
for (const ov of (lvm.contentImage?.thumbnailViewModel?.overlays || [])) {
|
|
94
|
+
for (const b of (ov.thumbnailBottomOverlayViewModel?.badges || [])) {
|
|
95
|
+
if (b.thumbnailBadgeViewModel?.text) duration = b.thumbnailBadgeViewModel.text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
recentVideos.push({
|
|
99
|
+
title: meta?.title?.content || '',
|
|
100
|
+
duration,
|
|
101
|
+
views: viewsAndTime,
|
|
102
|
+
url: 'https://www.youtube.com/watch?v=' + lvm.contentId,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Legacy gridVideoRenderer format
|
|
106
|
+
if (item.gridVideoRenderer && recentVideos.length < limit) {
|
|
107
|
+
const v = item.gridVideoRenderer;
|
|
108
|
+
recentVideos.push({
|
|
109
|
+
title: v.title?.runs?.[0]?.text || v.title?.simpleText || '',
|
|
110
|
+
duration: v.thumbnailOverlays?.[0]?.thumbnailOverlayTimeStatusRenderer?.text?.simpleText || '',
|
|
111
|
+
views: (v.shortViewCountText?.simpleText || '') + (v.publishedTimeText?.simpleText ? ' | ' + v.publishedTimeText.simpleText : ''),
|
|
112
|
+
url: 'https://www.youtube.com/watch?v=' + v.videoId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: metadata.title || '',
|
|
122
|
+
channelId: metadata.externalId || browseId,
|
|
123
|
+
handle: metadata.vanityChannelUrl?.split('/').pop() || '',
|
|
124
|
+
description: (metadata.description || '').substring(0, 500),
|
|
125
|
+
subscribers: subscriberCount,
|
|
126
|
+
url: metadata.channelUrl || 'https://www.youtube.com/channel/' + browseId,
|
|
127
|
+
keywords: metadata.keywords || '',
|
|
128
|
+
recentVideos,
|
|
129
|
+
};
|
|
130
|
+
})()
|
|
131
|
+
`);
|
|
132
|
+
|
|
133
|
+
if (!data || typeof data !== 'object') throw new CommandExecutionError('Failed to fetch channel data');
|
|
134
|
+
if ((data as Record<string, unknown>).error) throw new CommandExecutionError(String((data as Record<string, unknown>).error));
|
|
135
|
+
|
|
136
|
+
const result = data as Record<string, unknown>;
|
|
137
|
+
const videos = result.recentVideos as Array<Record<string, string>> | undefined;
|
|
138
|
+
delete result.recentVideos;
|
|
139
|
+
|
|
140
|
+
// Channel info as field/value pairs + recent videos as table
|
|
141
|
+
const rows = Object.entries(result).map(([field, value]) => ({
|
|
142
|
+
field,
|
|
143
|
+
value: String(value),
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
if (videos && videos.length > 0) {
|
|
147
|
+
rows.push({ field: '---', value: '--- Recent Videos ---' });
|
|
148
|
+
for (const v of videos) {
|
|
149
|
+
rows.push({ field: v.title, value: `${v.duration} | ${v.views} | ${v.url}` });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return rows;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube comments — get video comments via InnerTube API.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
6
|
+
import { parseVideoId } from './utils.js';
|
|
7
|
+
|
|
8
|
+
cli({
|
|
9
|
+
site: 'youtube',
|
|
10
|
+
name: 'comments',
|
|
11
|
+
description: 'Get YouTube video comments',
|
|
12
|
+
domain: 'www.youtube.com',
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max comments (max 100)' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['rank', 'author', 'text', 'likes', 'replies', 'time'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
const videoId = parseVideoId(kwargs.url);
|
|
21
|
+
const limit = Math.min(kwargs.limit || 20, 100);
|
|
22
|
+
await page.goto(`https://www.youtube.com/watch?v=${videoId}`);
|
|
23
|
+
await page.wait(3);
|
|
24
|
+
|
|
25
|
+
const data = await page.evaluate(`
|
|
26
|
+
(async () => {
|
|
27
|
+
const videoId = ${JSON.stringify(videoId)};
|
|
28
|
+
const limit = ${limit};
|
|
29
|
+
const cfg = window.ytcfg?.data_ || {};
|
|
30
|
+
const apiKey = cfg.INNERTUBE_API_KEY;
|
|
31
|
+
const context = cfg.INNERTUBE_CONTEXT;
|
|
32
|
+
if (!apiKey || !context) return {error: 'YouTube config not found'};
|
|
33
|
+
|
|
34
|
+
// Step 1: Get comment continuation token
|
|
35
|
+
let continuationToken = null;
|
|
36
|
+
|
|
37
|
+
// Try from current page ytInitialData
|
|
38
|
+
if (window.ytInitialData) {
|
|
39
|
+
const results = window.ytInitialData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
|
|
40
|
+
const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
|
|
41
|
+
continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fallback: fetch via next API
|
|
45
|
+
if (!continuationToken) {
|
|
46
|
+
const nextResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
|
|
47
|
+
method: 'POST', credentials: 'include',
|
|
48
|
+
headers: {'Content-Type': 'application/json'},
|
|
49
|
+
body: JSON.stringify({context, videoId})
|
|
50
|
+
});
|
|
51
|
+
if (!nextResp.ok) return {error: 'Failed to get video data: HTTP ' + nextResp.status};
|
|
52
|
+
const nextData = await nextResp.json();
|
|
53
|
+
const results = nextData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
|
|
54
|
+
const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
|
|
55
|
+
continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!continuationToken) return {error: 'No comment section found — comments may be disabled'};
|
|
59
|
+
|
|
60
|
+
// Step 2: Fetch comments
|
|
61
|
+
const commentResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
|
|
62
|
+
method: 'POST', credentials: 'include',
|
|
63
|
+
headers: {'Content-Type': 'application/json'},
|
|
64
|
+
body: JSON.stringify({context, continuation: continuationToken})
|
|
65
|
+
});
|
|
66
|
+
if (!commentResp.ok) return {error: 'Failed to fetch comments: HTTP ' + commentResp.status};
|
|
67
|
+
const commentData = await commentResp.json();
|
|
68
|
+
|
|
69
|
+
// Parse from frameworkUpdates (new ViewModel format)
|
|
70
|
+
const mutations = commentData.frameworkUpdates?.entityBatchUpdate?.mutations || [];
|
|
71
|
+
const commentEntities = mutations.filter(m => m.payload?.commentEntityPayload);
|
|
72
|
+
|
|
73
|
+
return commentEntities.slice(0, limit).map((m, i) => {
|
|
74
|
+
const p = m.payload.commentEntityPayload;
|
|
75
|
+
const props = p.properties || {};
|
|
76
|
+
const author = p.author || {};
|
|
77
|
+
const toolbar = p.toolbar || {};
|
|
78
|
+
return {
|
|
79
|
+
rank: i + 1,
|
|
80
|
+
author: author.displayName || '',
|
|
81
|
+
text: (props.content?.content || '').substring(0, 300),
|
|
82
|
+
likes: toolbar.likeCountNotliked || '0',
|
|
83
|
+
replies: toolbar.replyCount || '0',
|
|
84
|
+
time: props.publishedTime || '',
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
})()
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
if (!Array.isArray(data)) {
|
|
91
|
+
const errMsg = data && typeof data === 'object' ? String((data as Record<string, unknown>).error || '') : '';
|
|
92
|
+
if (errMsg) throw new CommandExecutionError(errMsg);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
return data;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type RawSegment,
|
|
18
18
|
type Chapter,
|
|
19
19
|
} from './transcript-group.js';
|
|
20
|
+
import { CommandExecutionError, EmptyResultError } from '../../errors.js';
|
|
20
21
|
|
|
21
22
|
cli({
|
|
22
23
|
site: 'youtube',
|
|
@@ -91,10 +92,10 @@ cli({
|
|
|
91
92
|
`);
|
|
92
93
|
|
|
93
94
|
if (!captionData || typeof captionData === 'string') {
|
|
94
|
-
throw new
|
|
95
|
+
throw new CommandExecutionError(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
|
|
95
96
|
}
|
|
96
97
|
if (captionData.error) {
|
|
97
|
-
throw new
|
|
98
|
+
throw new CommandExecutionError(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
// Warn if --lang was specified but not matched
|
|
@@ -176,10 +177,10 @@ cli({
|
|
|
176
177
|
`);
|
|
177
178
|
|
|
178
179
|
if (!Array.isArray(segments)) {
|
|
179
|
-
throw new
|
|
180
|
+
throw new CommandExecutionError((segments as any)?.error || 'Failed to parse caption segments');
|
|
180
181
|
}
|
|
181
182
|
if (segments.length === 0) {
|
|
182
|
-
throw new
|
|
183
|
+
throw new EmptyResultError('youtube transcript');
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
// Step 3: Fetch chapters (for grouped mode)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '../../registry.js';
|
|
5
5
|
import { parseVideoId } from './utils.js';
|
|
6
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
6
7
|
|
|
7
8
|
cli({
|
|
8
9
|
site: 'youtube',
|
|
@@ -104,8 +105,8 @@ cli({
|
|
|
104
105
|
})()
|
|
105
106
|
`);
|
|
106
107
|
|
|
107
|
-
if (!data || typeof data !== 'object') throw new
|
|
108
|
-
if (data.error) throw new
|
|
108
|
+
if (!data || typeof data !== 'object') throw new CommandExecutionError('Failed to extract video metadata from page');
|
|
109
|
+
if (data.error) throw new CommandExecutionError(data.error);
|
|
109
110
|
|
|
110
111
|
// Return as field/value pairs for table display
|
|
111
112
|
return Object.entries(data).map(([field, value]) => ({
|
package/src/daemon.ts
CHANGED
|
@@ -64,13 +64,14 @@ function readBody(req: IncomingMessage): Promise<string> {
|
|
|
64
64
|
return new Promise((resolve, reject) => {
|
|
65
65
|
const chunks: Buffer[] = [];
|
|
66
66
|
let size = 0;
|
|
67
|
+
let aborted = false;
|
|
67
68
|
req.on('data', (c: Buffer) => {
|
|
68
69
|
size += c.length;
|
|
69
|
-
if (size > MAX_BODY) { req.destroy(); reject(new Error('Body too large')); return; }
|
|
70
|
+
if (size > MAX_BODY) { aborted = true; req.destroy(); reject(new Error('Body too large')); return; }
|
|
70
71
|
chunks.push(c);
|
|
71
72
|
});
|
|
72
|
-
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
73
|
-
req.on('error', reject);
|
|
73
|
+
req.on('end', () => { if (!aborted) resolve(Buffer.concat(chunks).toString('utf-8')); });
|
|
74
|
+
req.on('error', (err) => { if (!aborted) reject(err); });
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -271,7 +272,7 @@ httpServer.listen(PORT, '127.0.0.1', () => {
|
|
|
271
272
|
httpServer.on('error', (err: NodeJS.ErrnoException) => {
|
|
272
273
|
if (err.code === 'EADDRINUSE') {
|
|
273
274
|
console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
|
|
274
|
-
process.exit(
|
|
275
|
+
process.exit(1);
|
|
275
276
|
}
|
|
276
277
|
console.error('[daemon] Server error:', err.message);
|
|
277
278
|
process.exit(1);
|
package/src/discovery.ts
CHANGED
|
@@ -20,31 +20,10 @@ import type { ManifestEntry } from './build-manifest.js';
|
|
|
20
20
|
|
|
21
21
|
/** Plugins directory: ~/.opencli/plugins/ */
|
|
22
22
|
export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
|
|
23
|
-
|
|
23
|
+
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
24
|
+
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
type?: string;
|
|
27
|
-
default?: unknown;
|
|
28
|
-
required?: boolean;
|
|
29
|
-
positional?: boolean;
|
|
30
|
-
description?: string;
|
|
31
|
-
help?: string;
|
|
32
|
-
choices?: string[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface YamlCliDefinition {
|
|
36
|
-
site?: string;
|
|
37
|
-
name?: string;
|
|
38
|
-
description?: string;
|
|
39
|
-
domain?: string;
|
|
40
|
-
strategy?: string;
|
|
41
|
-
browser?: boolean;
|
|
42
|
-
args?: Record<string, YamlArgDefinition>;
|
|
43
|
-
columns?: string[];
|
|
44
|
-
pipeline?: Record<string, unknown>[];
|
|
45
|
-
timeout?: number;
|
|
46
|
-
navigateBefore?: boolean | string;
|
|
47
|
-
}
|
|
26
|
+
import type { YamlCliDefinition } from './yaml-schema.js';
|
|
48
27
|
|
|
49
28
|
function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
|
|
50
29
|
if (!rawStrategy) return fallback;
|
|
@@ -52,9 +31,7 @@ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Str
|
|
|
52
31
|
return Strategy[key] ?? fallback;
|
|
53
32
|
}
|
|
54
33
|
|
|
55
|
-
|
|
56
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
|
-
}
|
|
34
|
+
import { isRecord } from './utils.js';
|
|
58
35
|
|
|
59
36
|
/**
|
|
60
37
|
* Discover and register CLI commands.
|
|
@@ -66,12 +43,12 @@ export async function discoverClis(...dirs: string[]): Promise<void> {
|
|
|
66
43
|
const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
|
|
67
44
|
try {
|
|
68
45
|
await fs.promises.access(manifestPath);
|
|
69
|
-
await loadFromManifest(manifestPath, dir);
|
|
70
|
-
continue; // Skip filesystem scan
|
|
46
|
+
const loaded = await loadFromManifest(manifestPath, dir);
|
|
47
|
+
if (loaded) continue; // Skip filesystem scan only when manifest is usable
|
|
71
48
|
} catch {
|
|
72
|
-
//
|
|
73
|
-
await discoverClisFromFs(dir);
|
|
49
|
+
// Fall through to filesystem scan
|
|
74
50
|
}
|
|
51
|
+
await discoverClisFromFs(dir);
|
|
75
52
|
}
|
|
76
53
|
}
|
|
77
54
|
|
|
@@ -80,7 +57,7 @@ export async function discoverClis(...dirs: string[]): Promise<void> {
|
|
|
80
57
|
* YAML pipelines are inlined — zero YAML parsing at runtime.
|
|
81
58
|
* TS modules are deferred — loaded lazily on first execution.
|
|
82
59
|
*/
|
|
83
|
-
async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
60
|
+
async function loadFromManifest(manifestPath: string, clisDir: string): Promise<boolean> {
|
|
84
61
|
try {
|
|
85
62
|
const raw = await fs.promises.readFile(manifestPath, 'utf-8');
|
|
86
63
|
const manifest = JSON.parse(raw) as ManifestEntry[];
|
|
@@ -126,8 +103,10 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
126
103
|
registerCommand(cmd);
|
|
127
104
|
}
|
|
128
105
|
}
|
|
106
|
+
return true;
|
|
129
107
|
} catch (err) {
|
|
130
108
|
log.warn(`Failed to load manifest ${manifestPath}: ${getErrorMessage(err)}`);
|
|
109
|
+
return false;
|
|
131
110
|
}
|
|
132
111
|
}
|
|
133
112
|
|
|
@@ -136,7 +115,6 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
136
115
|
*/
|
|
137
116
|
async function discoverClisFromFs(dir: string): Promise<void> {
|
|
138
117
|
try { await fs.promises.access(dir); } catch { return; }
|
|
139
|
-
const promises: Promise<unknown>[] = [];
|
|
140
118
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
141
119
|
|
|
142
120
|
const sitePromises = entries
|
|
@@ -269,7 +247,7 @@ async function discoverPluginDir(dir: string, site: string): Promise<void> {
|
|
|
269
247
|
async function isCliModule(filePath: string): Promise<boolean> {
|
|
270
248
|
try {
|
|
271
249
|
const source = await fs.promises.readFile(filePath, 'utf-8');
|
|
272
|
-
return
|
|
250
|
+
return PLUGIN_MODULE_PATTERN.test(source);
|
|
273
251
|
} catch (err) {
|
|
274
252
|
log.warn(`Failed to inspect module ${filePath}: ${getErrorMessage(err)}`);
|
|
275
253
|
return false;
|
package/src/doctor.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
|
10
10
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
11
11
|
import { BrowserBridge } from './browser/index.js';
|
|
12
12
|
import { listSessions } from './browser/daemon-client.js';
|
|
13
|
+
import { getErrorMessage } from './errors.js';
|
|
13
14
|
|
|
14
15
|
export type DoctorOptions = {
|
|
15
16
|
fix?: boolean;
|
|
@@ -46,8 +47,8 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise<Co
|
|
|
46
47
|
await page.evaluate('1 + 1');
|
|
47
48
|
await mcp.close();
|
|
48
49
|
return { ok: true, durationMs: Date.now() - start };
|
|
49
|
-
} catch (err
|
|
50
|
-
return { ok: false, error:
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { ok: false, error: getErrorMessage(err), durationMs: Date.now() - start };
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as http from 'node:http';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
|
|
7
|
+
|
|
8
|
+
const servers: http.Server[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
|
|
12
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
13
|
+
})));
|
|
14
|
+
servers.length = 0;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> {
|
|
18
|
+
const server = http.createServer(handler);
|
|
19
|
+
servers.push(server);
|
|
20
|
+
await new Promise<void>((resolve) => server.listen(0, hostname, resolve));
|
|
21
|
+
const address = server.address();
|
|
22
|
+
if (!address || typeof address === 'string') {
|
|
23
|
+
throw new Error('Failed to start test server');
|
|
24
|
+
}
|
|
25
|
+
return `http://${hostname}:${address.port}`;
|
|
26
|
+
}
|
|
3
27
|
|
|
4
28
|
describe('download helpers', () => {
|
|
5
29
|
it('resolves relative redirects against the original URL', () => {
|
|
@@ -13,4 +37,71 @@ describe('download helpers', () => {
|
|
|
13
37
|
{ name: 'ct0', value: 'def', domain: 'example.com' },
|
|
14
38
|
])).toBe('sid=abc; ct0=def');
|
|
15
39
|
});
|
|
40
|
+
|
|
41
|
+
it('fails after exceeding the redirect limit', async () => {
|
|
42
|
+
const baseUrl = await startServer((_req, res) => {
|
|
43
|
+
res.statusCode = 302;
|
|
44
|
+
res.setHeader('Location', '/loop');
|
|
45
|
+
res.end();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
|
|
49
|
+
const destPath = path.join(tempDir, 'file.txt');
|
|
50
|
+
const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
success: false,
|
|
54
|
+
size: 0,
|
|
55
|
+
error: 'Too many redirects (> 2)',
|
|
56
|
+
});
|
|
57
|
+
expect(fs.existsSync(destPath)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not forward cookies across cross-domain redirects', async () => {
|
|
61
|
+
let forwardedCookie: string | undefined;
|
|
62
|
+
const targetUrl = await startServer((req, res) => {
|
|
63
|
+
forwardedCookie = req.headers.cookie;
|
|
64
|
+
res.statusCode = 200;
|
|
65
|
+
res.end('ok');
|
|
66
|
+
}, 'localhost');
|
|
67
|
+
|
|
68
|
+
const redirectUrl = await startServer((_req, res) => {
|
|
69
|
+
res.statusCode = 302;
|
|
70
|
+
res.setHeader('Location', targetUrl);
|
|
71
|
+
res.end();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
|
|
75
|
+
const destPath = path.join(tempDir, 'redirect.txt');
|
|
76
|
+
const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({ success: true, size: 2 });
|
|
79
|
+
expect(forwardedCookie).toBeUndefined();
|
|
80
|
+
expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not forward cookie headers across cross-domain redirects', async () => {
|
|
84
|
+
let forwardedCookie: string | undefined;
|
|
85
|
+
const targetUrl = await startServer((req, res) => {
|
|
86
|
+
forwardedCookie = req.headers.cookie;
|
|
87
|
+
res.statusCode = 200;
|
|
88
|
+
res.end('ok');
|
|
89
|
+
}, 'localhost');
|
|
90
|
+
|
|
91
|
+
const redirectUrl = await startServer((_req, res) => {
|
|
92
|
+
res.statusCode = 302;
|
|
93
|
+
res.setHeader('Location', targetUrl);
|
|
94
|
+
res.end();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
|
|
98
|
+
const destPath = path.join(tempDir, 'redirect-header.txt');
|
|
99
|
+
const result = await httpDownload(`${redirectUrl}/start`, destPath, {
|
|
100
|
+
headers: { Cookie: 'sid=header-cookie' },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result).toEqual({ success: true, size: 2 });
|
|
104
|
+
expect(forwardedCookie).toBeUndefined();
|
|
105
|
+
expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
|
|
106
|
+
});
|
|
16
107
|
});
|