publishport-opencli 1.0.1 → 1.0.2
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/README.md +61 -61
- package/README.zh-CN.md +68 -68
- package/cli-manifest.json +502 -59
- package/clis/1688/shared.js +2 -2
- package/clis/36kr/news.js +1 -1
- package/clis/_atlassian/shared.js +1 -1
- package/clis/_shared/article/auth.js +1 -1
- package/clis/_shared/article/publish.js +1 -1
- package/clis/_shared/content-guard.js +1 -1
- package/clis/_shared/self-hosted-sites.js +2 -0
- package/clis/_shared/site-auth.js +1 -1
- package/clis/_shared/token-auth.js +2 -2
- package/clis/_shared/video-publish.js +1 -1
- package/clis/aibase/news.js +1 -1
- package/clis/amazon/shared.js +2 -2
- package/clis/antigravity/SKILL.md +12 -12
- package/clis/antigravity/storage.js +1 -1
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/archive/item.js +1 -1
- package/clis/archive/search.js +1 -1
- package/clis/archive/snapshots.js +1 -1
- package/clis/archive/wayback.js +1 -1
- package/clis/arxiv/author.js +1 -1
- package/clis/baijiahao/publish.js +1 -1
- package/clis/bbc/utils.js +1 -1
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/utils.js +1 -1
- package/clis/booking/search.js +1 -1
- package/clis/boss/utils.js +1 -1
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/detail.js +1 -1
- package/clis/chatgpt/history.js +1 -1
- package/clis/chatgpt/project-file-add.js +1 -1
- package/clis/chatgpt/project-list.js +1 -1
- package/clis/chatgpt/utils.js +2 -2
- package/clis/chess/utils.js +1 -1
- package/clis/claude/ask.js +1 -1
- package/clis/claude/history.js +1 -1
- package/clis/claude/utils.js +1 -1
- package/clis/codex/extract-diff.js +1 -1
- package/clis/coingecko/coin.js +1 -1
- package/clis/crates/utils.js +1 -1
- package/clis/csdn/stats.js +1 -1
- package/clis/dblp/utils.js +1 -1
- package/clis/defillama/utils.js +1 -1
- package/clis/devto/publish.js +1 -1
- package/clis/discord-app/utils.js +1 -1
- package/clis/dockerhub/utils.js +1 -1
- package/clis/douban/utils.js +1 -1
- package/clis/douyin/_shared/tos-upload.js +1 -1
- package/clis/douyin/hashtag.js +1 -1
- package/clis/douyin/publish-image.js +1 -1
- package/clis/douyin/publish.js +1 -1
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/endoflife/utils.js +1 -1
- package/clis/flathub/utils.js +1 -1
- package/clis/gemini/detail.js +1 -1
- package/clis/gemini/read.js +1 -1
- package/clis/geogebra/add-circle.js +1 -1
- package/clis/geogebra/add-line.js +1 -1
- package/clis/geogebra/add-point.js +1 -1
- package/clis/geogebra/add-polygon.js +1 -1
- package/clis/geogebra/eval.js +1 -1
- package/clis/geogebra/hexagon.js +1 -1
- package/clis/geogebra/info.js +1 -1
- package/clis/geogebra/triangle.js +1 -1
- package/clis/ghost/login.js +1 -0
- package/clis/ghost/publish.js +1 -1
- package/clis/ghost/shared.js +24 -1
- package/clis/ghost/sites.js +1 -0
- package/clis/ghost/whoami.js +1 -1
- package/clis/gitee/user.js +2 -2
- package/clis/github-trending/repos.js +1 -1
- package/clis/goproxy/utils.js +1 -1
- package/clis/grok/export-all.js +1 -1
- package/clis/grok/export.js +1 -1
- package/clis/hashnode/publish.js +1 -1
- package/clis/hf/datasets.js +1 -1
- package/clis/hf/models.js +1 -1
- package/clis/hf/paper.js +1 -1
- package/clis/hf/spaces.js +1 -1
- package/clis/homebrew/utils.js +1 -1
- package/clis/instagram/note.js +1 -1
- package/clis/jimeng/generate.js +1 -1
- package/clis/juejin/utils.js +1 -1
- package/clis/kuaishou/publish.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lichess/utils.js +1 -1
- package/clis/linkedin/search.js +2 -2
- package/clis/linux-do/feed.js +2 -2
- package/clis/lobsters/domain.js +1 -1
- package/clis/mastodon/login.js +1 -0
- package/clis/mastodon/post.js +1 -1
- package/clis/mastodon/shared.js +10 -10
- package/clis/mastodon/sites.js +1 -0
- package/clis/mastodon/whoami.js +1 -1
- package/clis/maven/utils.js +1 -1
- package/clis/mdn/search.js +1 -1
- package/clis/medium/tag.js +1 -1
- package/clis/notebooklm/current.js +1 -1
- package/clis/notebooklm/get.js +1 -1
- package/clis/notebooklm/history.js +1 -1
- package/clis/notebooklm/note-list.js +1 -1
- package/clis/notebooklm/notes-get.js +1 -1
- package/clis/notebooklm/open.js +1 -1
- package/clis/notebooklm/source-fulltext.js +1 -1
- package/clis/notebooklm/source-get.js +1 -1
- package/clis/notebooklm/source-guide.js +1 -1
- package/clis/notebooklm/source-list.js +1 -1
- package/clis/notebooklm/summary.js +1 -1
- package/clis/notebooklm/utils.js +1 -1
- package/clis/npm/utils.js +1 -1
- package/clis/nuget/utils.js +1 -1
- package/clis/nvd/cve.js +1 -1
- package/clis/oeis/utils.js +1 -1
- package/clis/ones/common.js +2 -2
- package/clis/ones/me.js +1 -1
- package/clis/ones/tasks.js +1 -1
- package/clis/ones/token-info.js +1 -1
- package/clis/ones/worklog.js +1 -1
- package/clis/openalex/utils.js +1 -1
- package/clis/oschina/stats.js +1 -1
- package/clis/osv/utils.js +1 -1
- package/clis/packagist/utils.js +1 -1
- package/clis/pixiv/detail.js +1 -1
- package/clis/pixiv/user.js +1 -1
- package/clis/producthunt/utils.js +1 -1
- package/clis/pypi/utils.js +1 -1
- package/clis/qiita/gql.js +2 -2
- package/clis/rest-countries/utils.js +1 -1
- package/clis/rfc/utils.js +1 -1
- package/clis/rubygems/utils.js +1 -1
- package/clis/semanticscholar/utils.js +1 -1
- package/clis/slock/errors.js +1 -1
- package/clis/spotify/spotify.js +1 -1
- package/clis/spotify/utils.js +1 -1
- package/clis/stackoverflow/utils.js +1 -1
- package/clis/steam/utils.js +1 -1
- package/clis/substack/auth.js +2 -2
- package/clis/suno/generate.js +2 -2
- package/clis/suno/list.js +3 -3
- package/clis/telegram/send.js +1 -1
- package/clis/tiktok/creator-videos.js +1 -1
- package/clis/tiktok/publish.js +1 -1
- package/clis/tiktok/utils.js +1 -1
- package/clis/trae-cn/activity.js +1 -1
- package/clis/trae-cn/approve.js +1 -1
- package/clis/trae-cn/ask.js +1 -1
- package/clis/trae-cn/dump.js +1 -1
- package/clis/trae-cn/export.js +1 -1
- package/clis/trae-cn/model.js +1 -1
- package/clis/trae-cn/new.js +1 -1
- package/clis/trae-cn/read.js +1 -1
- package/clis/trae-cn/screenshot.js +1 -1
- package/clis/trae-cn/select-model.js +1 -1
- package/clis/trae-cn/send.js +1 -1
- package/clis/trae-cn/setup.js +1 -1
- package/clis/trae-cn/status.js +1 -1
- package/clis/trae-cn/targets.js +1 -1
- package/clis/trae-cn/watch.js +1 -1
- package/clis/trae-solo/state-fs.js +1 -1
- package/clis/tumblr/publish.js +1 -1
- package/clis/tvmaze/utils.js +1 -1
- package/clis/twitter/bookmark-folder.js +1 -1
- package/clis/twitter/download.js +1 -1
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +2 -2
- package/clis/twitter/likes.js +1 -1
- package/clis/twitter/list-add-batch.js +1 -1
- package/clis/twitter/list-add-core.js +1 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +1 -1
- package/clis/twitter/list-delete.js +3 -3
- package/clis/twitter/list-remove-batch.js +1 -1
- package/clis/twitter/list-remove.js +1 -1
- package/clis/twitter/list-tweets.js +1 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/search.js +1 -1
- package/clis/twitter/shared.js +1 -1
- package/clis/twitter/tweets.js +1 -1
- package/clis/uisdc/news.js +1 -1
- package/clis/v2ex/daily.js +2 -2
- package/clis/v2ex/me.js +2 -2
- package/clis/v2ex/notifications.js +1 -1
- package/clis/wikidata/utils.js +1 -1
- package/clis/wikipedia/page.js +2 -2
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/utils.js +1 -1
- package/clis/wordpress/login.js +1 -1
- package/clis/wordpress/publish.js +1 -1
- package/clis/wordpress/shared.js +3 -3
- package/clis/wordpress/sites.js +1 -0
- package/clis/wordpress/whoami.js +1 -1
- package/clis/woshipm/stats.js +1 -1
- package/clis/xiaoe/content.js +1 -1
- package/clis/xiaohongshu/draft-delete.js +1 -1
- package/clis/xiaohongshu/draft-open.js +1 -1
- package/clis/xiaohongshu/draft-utils.js +2 -2
- package/clis/xiaohongshu/publish-video.js +1 -1
- package/clis/xiaohongshu/publish.js +1 -1
- package/clis/xiaoyuzhou/auth.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/yollomi/background.js +1 -1
- package/clis/yollomi/edit.js +1 -1
- package/clis/yollomi/generate.js +1 -1
- package/clis/yollomi/try-on.js +1 -1
- package/clis/youtube/publish.js +1 -1
- package/clis/zhihu/answer-comments.js +1 -1
- package/clis/zhihu/answer-detail.js +1 -1
- package/clis/zhihu/collection.js +2 -2
- package/clis/zhihu/collections.js +1 -1
- package/clis/zhihu/question.js +1 -1
- package/clis/zhihu/search.js +1 -1
- package/clis/zhihu/target.js +1 -1
- package/clis/zhihu/user-arg.js +1 -1
- package/dist/src/adapter-shadow.js +1 -1
- package/dist/src/browser/analyze.js +1 -1
- package/dist/src/browser/article-extract.d.ts +1 -1
- package/dist/src/browser/base-page.js +3 -3
- package/dist/src/browser/cdp.js +1 -1
- package/dist/src/browser/daemon-client.d.ts +1 -1
- package/dist/src/browser/daemon-lifecycle.js +6 -6
- package/dist/src/browser/daemon-version.js +1 -1
- package/dist/src/browser/dom-snapshot.d.ts +1 -1
- package/dist/src/browser/dom-snapshot.js +2 -2
- package/dist/src/browser/errors.js +2 -2
- package/dist/src/browser/network-cache.js +1 -1
- package/dist/src/browser/profile.js +1 -1
- package/dist/src/browser/target-resolver.js +4 -4
- package/dist/src/browser/verify-fixture.js +1 -1
- package/dist/src/browser-session-lock.js +1 -1
- package/dist/src/build-manifest.d.ts +1 -1
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +4 -4
- package/dist/src/cli-argv-preprocess.js +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +19 -19
- package/dist/src/commanderAdapter.js +1 -1
- package/dist/src/commands/auth.js +2 -2
- package/dist/src/commands/daemon.d.ts +4 -4
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/completion-shared.js +12 -12
- package/dist/src/completion.d.ts +2 -2
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.js +1 -1
- package/dist/src/daemon-utils.d.ts +1 -1
- package/dist/src/daemon-utils.js +1 -1
- package/dist/src/daemon.d.ts +2 -2
- package/dist/src/daemon.js +1 -1
- package/dist/src/discovery.d.ts +7 -7
- package/dist/src/discovery.js +2 -2
- package/dist/src/doctor.d.ts +1 -1
- package/dist/src/doctor.js +6 -6
- package/dist/src/electron-apps.d.ts +1 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/errors.d.ts +2 -2
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.js +1 -1
- package/dist/src/external.d.ts +1 -1
- package/dist/src/external.js +1 -1
- package/dist/src/help.d.ts +1 -1
- package/dist/src/help.js +3 -3
- package/dist/src/hooks.d.ts +1 -1
- package/dist/src/launcher.js +2 -2
- package/dist/src/logger.d.ts +1 -1
- package/dist/src/main.d.ts +1 -1
- package/dist/src/main.js +1 -1
- package/dist/src/observation/artifact.js +2 -2
- package/dist/src/package-paths.js +1 -1
- package/dist/src/package-paths.test.d.ts +1 -0
- package/dist/src/plugin-manifest.d.ts +12 -3
- package/dist/src/plugin-manifest.js +1 -1
- package/dist/src/plugin-scaffold.d.ts +2 -2
- package/dist/src/plugin-scaffold.js +9 -9
- package/dist/src/plugin.d.ts +4 -4
- package/dist/src/plugin.js +10 -10
- package/dist/src/rate-limit.js +1 -1
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/runtime-detect.d.ts +1 -1
- package/dist/src/serialization.js +1 -1
- package/dist/src/skills.js +6 -6
- package/dist/src/user-runtime.d.ts +8 -0
- package/dist/src/user-runtime.js +1 -0
- package/dist/src/user-runtime.test.d.ts +1 -0
- package/dist/src/validate.js +1 -1
- package/dist/src/validate.test.d.ts +1 -1
- package/docs/guide/browser-bridge.md +23 -23
- package/docs/guide/electron-app-cli.md +3 -3
- package/docs/guide/exit-codes.md +3 -3
- package/docs/guide/extending-opencli.md +37 -37
- package/docs/guide/getting-started.md +19 -19
- package/docs/guide/installation.md +6 -6
- package/docs/guide/plugins.md +32 -32
- package/docs/guide/remote-orchestration.md +5 -5
- package/docs/guide/troubleshooting.md +9 -9
- package/package.json +3 -2
- package/scripts/fetch-adapters.js +6 -6
- package/scripts/postinstall.js +14 -14
- package/skills/opencli-adapter-author/SKILL.md +24 -24
- package/skills/opencli-adapter-author/references/adapter-template.md +13 -13
- package/skills/opencli-adapter-author/references/api-discovery.md +25 -25
- package/skills/opencli-adapter-author/references/coverage-matrix.md +3 -3
- package/skills/opencli-adapter-author/references/field-decode-playbook.md +10 -10
- package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +2 -2
- package/skills/opencli-adapter-author/references/site-memory.md +11 -11
- package/skills/opencli-adapter-author/references/site-recon.md +7 -7
- package/skills/opencli-adapter-author/references/strategy-selection.md +3 -3
- package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +4 -4
- package/skills/opencli-adapter-author/references/typed-errors.md +2 -2
- package/skills/opencli-autofix/SKILL.md +29 -29
- package/skills/opencli-browser/SKILL.md +55 -55
- package/skills/opencli-browser-sitemap/SKILL.md +7 -7
- package/skills/opencli-sitemap-author/SKILL.md +11 -11
- package/skills/opencli-sitemap-author/references/sitemap-schema.md +19 -19
- package/skills/opencli-usage/SKILL.md +43 -43
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as $ from"node:fs";import*as h from"node:path";import{cli as U,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as Y,EmptyResultError as Z}from"@jackwener/opencli/errors";import{TRAE_GLOBAL_STATE_DB as Q,TRAE_WORKSPACE_STORAGE as V,listKeys as j,getValue as P}from"./_state.js";function W(q){const H=q.workspace?String(q.workspace).trim():"";if(!H)return Q;const I=h.join(V,H,"state.vscdb");if(!$.existsSync(I))throw new Y(`Workspace state.vscdb not found: ${I}`,"List valid workspace ids with `
|
|
1
|
+
import*as $ from"node:fs";import*as h from"node:path";import{cli as U,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as Y,EmptyResultError as Z}from"@jackwener/opencli/errors";import{TRAE_GLOBAL_STATE_DB as Q,TRAE_WORKSPACE_STORAGE as V,listKeys as j,getValue as P}from"./_state.js";function W(q){const H=q.workspace?String(q.workspace).trim():"";if(!H)return Q;const I=h.join(V,H,"state.vscdb");if(!$.existsSync(I))throw new Y(`Workspace state.vscdb not found: ${I}`,"List valid workspace ids with `ppcli trae-solo workspaces-list`.");return I}U({site:"trae-solo",name:"state-keys",access:"read",description:"List all keys present in Trae SOLO's globalStorage state.vscdb (VSCode-style UI/agent state). Pass --workspace <ws-id> to query a per-workspace DB instead. Use state-get to read a specific value. (See renderer storage-keys for browser-side LS/SS.)",domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"filter",required:!1,help:"Case-insensitive substring filter over keys"},{name:"workspace",required:!1,help:"Workspace id (from workspaces-list) to query a per-workspace DB"},{name:"limit",type:"int",required:!1,default:200}],columns:["Index","Key","Kind","Path"],func:async(q)=>{const H=W(q),I=j(H),z=q.filter?String(q.filter).toLowerCase():null,F=z?I.filter((J)=>J.toLowerCase().includes(z)):I;if(!F.length)throw new Z("trae-solo storage-keys",z?`No keys match "${z}".`:"No keys.");const M=Number.isInteger(q.limit)&&q.limit>0?q.limit:200;return F.slice(0,M).map((J,N)=>({Index:N+1,Key:J,Kind:"",Path:""}))}});U({site:"trae-solo",name:"state-get",access:"read",description:"Read a single key from Trae SOLO's globalStorage state.vscdb. Pass --workspace <ws-id> to query a per-workspace DB instead. Returns parsed JSON if the value is JSON.",domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"key",positional:!0,required:!0,help:"State key (use state-keys to discover)"},{name:"workspace",required:!1,help:"Workspace id (from workspaces-list) to query a per-workspace DB"},{name:"max-bytes",type:"int",required:!1,default:8000,help:"Truncate value to this many bytes"}],columns:["Field","Value"],func:async(q)=>{const H=String(q.key||"").trim();if(!H)throw new K("key required");const I=W(q),z=P(I,H);if(z===null)throw new Y(`Key not found: ${H}`,"List available keys with `ppcli trae-solo state-keys`.");const F=Number.isInteger(q["max-bytes"])&&q["max-bytes"]>0?q["max-bytes"]:8000,M=typeof z==="string"?z:JSON.stringify(z,null,2),J=M.length>F;return[{Field:"Key",Value:H},{Field:"Type",Value:typeof z==="string"?"string":Array.isArray(z)?"array":typeof z},{Field:"Size",Value:`${M.length} chars${J?" (truncated)":""}`},{Field:"Value",Value:J?M.slice(0,F)+`
|
|
2
2
|
...(truncated, use --max-bytes to read more)`:M}]}});U({site:"trae-solo",name:"recent-workspaces",access:"read",description:`Show Trae SOLO's recently-opened workspaces (the File → Open Recent menu, stored under key "history.recentlyOpenedPathsList" in state.vscdb).`,domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"limit",type:"int",required:!1,default:20}],columns:["Index","Key","Kind","Path"],func:async(q)=>{if(!$.existsSync(Q))throw new Y(`state.vscdb not found: ${Q}`,"");const H=P(Q,"history.recentlyOpenedPathsList");if(!H)throw new Z("trae-solo recent-workspaces","No recent workspaces recorded.");const I=H.entries||[];if(!I.length)throw new Z("trae-solo recent-workspaces","history.recentlyOpenedPathsList has no entries.");const z=Number.isInteger(q.limit)&&q.limit>0?q.limit:20;return I.slice(0,z).map((F,M)=>{let J="other",N=JSON.stringify(F).slice(0,200);if(F.folderUri){J="folder";N=decodeURI(String(F.folderUri).replace(/^file:\/\//,""))}else if(F.workspace&&F.workspace.configPath){J="workspace";N=decodeURI(String(F.workspace.configPath).replace(/^file:\/\//,""))}else if(F.fileUri){J="file";N=decodeURI(String(F.fileUri).replace(/^file:\/\//,""))}return{Index:M+1,Key:"",Kind:J,Path:N}})}});
|
package/clis/tumblr/publish.js
CHANGED
|
@@ -11,4 +11,4 @@ import*as M from"node:fs";import{cli as _,Strategy as L}from"@jackwener/opencli/
|
|
|
11
11
|
if (status === 401 || status === 403) return { kind: 'auth', detail: 'HTTP ' + status + ' ' + msg };
|
|
12
12
|
return { kind: 'http', status: status, detail: String(msg).slice(0, 300) };
|
|
13
13
|
}
|
|
14
|
-
})()`)}async function F(z,G){if(G.blog)return blogHost(G.blog);const I=await Z(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new X("tumblr.com",`Not logged in (${I.detail}). Run \`
|
|
14
|
+
})()`)}async function F(z,G){if(G.blog)return blogHost(G.blog);const I=await Z(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new X("tumblr.com",`Not logged in (${I.detail}). Run \`ppcli tumblr login\`.`);if(I?.kind!=="ok"||!I.response||!I.response.user)throw new P(`Failed to resolve default blog: ${JSON.stringify(I)}`);const N=I.response.user.blogs||[],J=N.find((Q)=>Q.primary)||N[0];if(!J||!J.name)throw new P("No blog found on this account; pass --blog explicitly.");return blogHost(J.name)}_({site:"tumblr",name:"publish",access:"write",description:"Publish a Tumblr post (NPF: title/text/image/link). Draft by default; pass --state published to go live.",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Title (rendered as a heading1 text block)"},{name:"text",type:"string",required:!1,help:"Body text (blank lines split paragraphs); alias of --body"},{name:"body",type:"string",required:!1,help:"Body text (blank lines split paragraphs)"},{name:"body-file",type:"string",required:!1,help:"Path to a text file for the body"},{name:"image-url",type:"string",required:!1,help:"Image URL (NPF image block)"},{name:"link",type:"string",required:!1,help:"Link URL (NPF link block)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags"},{name:"state",type:"string",required:!1,default:"draft",choices:Y,help:"draft | published | queue | private (default: draft)"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog","state","url"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr publish");const I=String(G.state??"draft");if(!Y.includes(I))throw new U(`Invalid --state "${I}". One of: ${Y.join(", ")}`);const N=resolveBody(G),J=buildContent(G,N),Q=resolveTags(G.tags);await z.goto(q);const V=await F(z,G),$={content:J,state:I};if(Q)$.tags=Q;const K=await Z(z,`/v2/blog/${V}/posts`,{method:"POST",body:$});if(K?.kind==="auth")throw new X("tumblr.com",`Not logged in (${K.detail}). Run \`ppcli tumblr login\`.`);if(K?.kind==="http")throw new P(`Tumblr publish failed: HTTP ${K.status} ${K.detail}`);if(K?.kind!=="ok"||!K.response)throw new P(`Unexpected Tumblr response: ${JSON.stringify(K)}`);const W=K.response.id_string??(K.response.id!=null?String(K.response.id):"");if(!W)throw new P("Tumblr post response missing id");return[{status:"success",id:W,blog:V,state:I,url:I==="published"?`${O}/${V}/${W}`:""}]}});_({site:"tumblr",name:"delete",access:"write",description:"Delete a Tumblr post by id (POST /v2/blog/{blog}/post/delete).",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,help:"Post id to delete"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr delete");const I=String(G.id??"").trim();if(!I)throw new U("--id is required");await z.goto(q);const N=await F(z,G),J=await Z(z,`/v2/blog/${N}/post/delete`,{method:"POST",body:{id:I}});if(J?.kind==="auth")throw new X("tumblr.com",`Not logged in (${J.detail}). Run \`ppcli tumblr login\`.`);if(J?.kind==="http")throw new P(`Tumblr delete failed: HTTP ${J.status} ${J.detail}`);if(J?.kind!=="ok")throw new P(`Unexpected Tumblr delete response: ${JSON.stringify(J)}`);return[{status:"deleted",id:I,blog:N}]}});export const __test__={blogHost,resolveBody,buildContent,resolveTags};
|
package/clis/tvmaze/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ArgumentError as J,CommandExecutionError as K,EmptyResultError as O}from"@jackwener/opencli/errors";export const TVMAZE_BASE="https://api.tvmaze.com";const Q="opencli-tvmaze-adapter (+https://github.com/jackwener/
|
|
1
|
+
import{ArgumentError as J,CommandExecutionError as K,EmptyResultError as O}from"@jackwener/opencli/errors";export const TVMAZE_BASE="https://api.tvmaze.com";const Q="opencli-tvmaze-adapter (+https://github.com/jackwener/OpenCLI)";export function requireString(D,z){const q=String(D??"").trim();if(!q)throw new J(`tvmaze ${z} cannot be empty`);return q}export function requireShowId(D){const z=D,q=typeof z==="number"?z:Number(String(z??"").trim());if(!Number.isInteger(q)||q<=0)throw new J("tvmaze show id is required and must be a positive integer","TVmaze show ids appear in the URL: https://www.tvmaze.com/shows/<id>/<slug>.");return q}export function requireBoundedInt(D,z,q,F="limit"){const B=D??z,G=typeof B==="number"?B:Number(B);if(!Number.isInteger(G)||G<=0)throw new J(`tvmaze ${F} must be a positive integer`);if(G>q)throw new J(`tvmaze ${F} must be <= ${q}`);return G}export async function tvmazeFetch(D,z){let q;try{q=await fetch(D,{headers:{"user-agent":Q,accept:"application/json"}})}catch(B){throw new K(`${z} request failed: ${B?.message??B}`,"Check that api.tvmaze.com is reachable from this network.")}if(q.status===404)throw new O(z,`TVmaze returned 404 for ${D}.`);if(q.status===429)throw new K(`${z} returned HTTP 429 (rate limited)`,"TVmaze caps unauthenticated traffic at ~20 req/10s; wait a few seconds and retry.");if(!q.ok)throw new K(`${z} returned HTTP ${q.status}`);let F;try{F=await q.json()}catch(B){throw new K(`${z} returned malformed JSON: ${B?.message??B}`)}return F}const S={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" ",rsquo:"’",lsquo:"‘",rdquo:"”",ldquo:"“",hellip:"…",ndash:"–",mdash:"—"};export function stripHtml(D){if(D==null)return"";let z=String(D);z=z.replace(/<[^>]+>/g,"");z=z.replace(/&#x([0-9a-fA-F]+);/g,(q,F)=>{const B=parseInt(F,16);return Number.isFinite(B)?String.fromCodePoint(B):""}).replace(/&#(\d+);/g,(q,F)=>{const B=parseInt(F,10);return Number.isFinite(B)?String.fromCodePoint(B):""}).replace(/&([a-zA-Z]+);/g,(q,F)=>S[F]??q);return z.replace(/\s+/g," ").trim()}export function joinList(D){return Array.isArray(D)?D.filter(Boolean).join(", "):""}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as U,Strategy as P}from"@jackwener/opencli/registry";import{ArgumentError as S,AuthRequiredError as f,CommandExecutionError as O}from"@jackwener/opencli/errors";import{TWITTER_BEARER_TOKEN as C,applyTopByEngagement as F}from"./utils.js";import{extractMedia as N,resolveTwitterQueryId as _,describeTwitterApiError as R}from"./shared.js";const j="BookmarkFolderTimeline",x="13H7EUATwethsj_jZ6QQAQ",q=/^[A-Za-z0-9_-]+$/,k=100,p={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_enhance_cards_enabled:!1};function L(J,V,z,H){const $={bookmark_collection_id:String(V),count:z,includePromotedContent:!1};if(H)$.cursor=H;return`/i/api/graphql/${J}/${j}?variables=${encodeURIComponent(JSON.stringify($))}&features=${encodeURIComponent(JSON.stringify(p))}`}export function extractFolderTweet(J,V){if(!J)return null;const z=J.tweet||J,H=z.legacy||{};if(!z.rest_id||V.has(z.rest_id))return null;V.add(z.rest_id);const $=z.core?.user_results?.result,W=$?.legacy?.screen_name||$?.core?.screen_name||"",Y=z.note_tweet?.note_tweet_results?.result?.text;return{id:z.rest_id,author:W,text:Y||H.full_text||"",likes:H.favorite_count||0,retweets:H.retweet_count||0,bookmarks:H.bookmark_count||0,created_at:H.created_at||"",url:W?`https://x.com/${W}/status/${z.rest_id}`:`https://x.com/i/status/${z.rest_id}`,...N(H)}}export function parseBookmarkFolderTimeline(J,V){const z=[];let H=null;const $=J?.data?.bookmark_collection_timeline?.timeline?.instructions||J?.data?.bookmark_timeline_v2?.timeline?.instructions||J?.data?.bookmark_timeline?.timeline?.instructions||[];for(const W of $)for(const Y of W.entries||[]){const Z=Y.content;if(Z?.entryType==="TimelineTimelineCursor"||Z?.__typename==="TimelineTimelineCursor"){if(Z.cursorType==="Bottom"||Z.cursorType==="ShowMore")H=Z.value;continue}if(Y.entryId?.startsWith("cursor-bottom-")||Y.entryId?.startsWith("cursor-showMore-")){H=Z?.value||Z?.itemContent?.value||H;continue}const X=extractFolderTweet(Z?.itemContent?.tweet_results?.result,V);if(X){z.push(X);continue}for(const Q of Z?.items||[]){const G=extractFolderTweet(Q.item?.itemContent?.tweet_results?.result,V);if(G)z.push(G)}}return{tweets:z,nextCursor:H}}U({site:"twitter",name:"bookmark-folder",access:"read",description:"Read the tweets inside a single Twitter/X bookmark folder. Get the folder id from `
|
|
1
|
+
import{cli as U,Strategy as P}from"@jackwener/opencli/registry";import{ArgumentError as S,AuthRequiredError as f,CommandExecutionError as O}from"@jackwener/opencli/errors";import{TWITTER_BEARER_TOKEN as C,applyTopByEngagement as F}from"./utils.js";import{extractMedia as N,resolveTwitterQueryId as _,describeTwitterApiError as R}from"./shared.js";const j="BookmarkFolderTimeline",x="13H7EUATwethsj_jZ6QQAQ",q=/^[A-Za-z0-9_-]+$/,k=100,p={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_enhance_cards_enabled:!1};function L(J,V,z,H){const $={bookmark_collection_id:String(V),count:z,includePromotedContent:!1};if(H)$.cursor=H;return`/i/api/graphql/${J}/${j}?variables=${encodeURIComponent(JSON.stringify($))}&features=${encodeURIComponent(JSON.stringify(p))}`}export function extractFolderTweet(J,V){if(!J)return null;const z=J.tweet||J,H=z.legacy||{};if(!z.rest_id||V.has(z.rest_id))return null;V.add(z.rest_id);const $=z.core?.user_results?.result,W=$?.legacy?.screen_name||$?.core?.screen_name||"",Y=z.note_tweet?.note_tweet_results?.result?.text;return{id:z.rest_id,author:W,text:Y||H.full_text||"",likes:H.favorite_count||0,retweets:H.retweet_count||0,bookmarks:H.bookmark_count||0,created_at:H.created_at||"",url:W?`https://x.com/${W}/status/${z.rest_id}`:`https://x.com/i/status/${z.rest_id}`,...N(H)}}export function parseBookmarkFolderTimeline(J,V){const z=[];let H=null;const $=J?.data?.bookmark_collection_timeline?.timeline?.instructions||J?.data?.bookmark_timeline_v2?.timeline?.instructions||J?.data?.bookmark_timeline?.timeline?.instructions||[];for(const W of $)for(const Y of W.entries||[]){const Z=Y.content;if(Z?.entryType==="TimelineTimelineCursor"||Z?.__typename==="TimelineTimelineCursor"){if(Z.cursorType==="Bottom"||Z.cursorType==="ShowMore")H=Z.value;continue}if(Y.entryId?.startsWith("cursor-bottom-")||Y.entryId?.startsWith("cursor-showMore-")){H=Z?.value||Z?.itemContent?.value||H;continue}const X=extractFolderTweet(Z?.itemContent?.tweet_results?.result,V);if(X){z.push(X);continue}for(const Q of Z?.items||[]){const G=extractFolderTweet(Q.item?.itemContent?.tweet_results?.result,V);if(G)z.push(G)}}return{tweets:z,nextCursor:H}}U({site:"twitter",name:"bookmark-folder",access:"read",description:"Read the tweets inside a single Twitter/X bookmark folder. Get the folder id from `ppcli twitter bookmark-folders`.",domain:"x.com",strategy:P.COOKIE,browser:!0,args:[{name:"folder-id",positional:!0,type:"string",required:!0,help:"Folder id from `ppcli twitter bookmark-folders`."},{name:"limit",type:"int",default:20,help:"Maximum number of bookmarks to return (default 20)."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the folder by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API's native (saved-time) ordering."}],columns:["id","author","text","likes","retweets","bookmarks","created_at","url","has_media","media_urls","media_posters"],func:async(J,V)=>{const z=String(V["folder-id"]||"").trim();if(!z||!q.test(z))throw new S(`Invalid folder-id: ${JSON.stringify(V["folder-id"])}. Expected a safe folder ID from \`ppcli twitter bookmark-folders\`.`);const H=Number(V.limit??20);if(!Number.isInteger(H)||H<1)throw new S(`Invalid --limit: ${JSON.stringify(V.limit)}. Expected a positive integer.`);const W=(await J.getCookies({url:"https://x.com"})).find((K)=>K.name==="ct0")?.value||null;if(!W)throw new f("x.com","Not logged into x.com (no ct0 cookie)");const Y=await _(J,j,x),Z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(C)}`,"X-Csrf-Token":W,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),X=[],Q=new Set;let G=null;for(let K=0;K<k&&X.length<H;K++){const h=Math.min(100,H-X.length+10),B=L(Y,z,h,G),b=await J.evaluate(`async () => {
|
|
2
2
|
const r = await fetch(${JSON.stringify(B)}, { headers: ${Z}, credentials: 'include' });
|
|
3
3
|
return r.ok ? await r.json() : { error: r.status };
|
|
4
4
|
}`);if(b?.error){if(X.length===0)throw new O(R("BookmarkFolderTimeline",b.error,`folder=${z}`));break}const{tweets:D,nextCursor:v}=parseBookmarkFolderTimeline(b,Q);X.push(...D);if(!v||v===G)break;G=v}const M=X.slice(0,H);return F(M,V["top-by-engagement"])}});export const __test__={parseBookmarkFolderTimeline,extractFolderTweet,buildFolderTimelineUrl:L,FOLDER_ID_PATTERN:q};
|
package/clis/twitter/download.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{ArgumentError as D,AuthRequiredError as h,CliError as t,CommandExecutionError as X,EmptyResultError as T}from"@jackwener/opencli/errors";import{formatCookieHeader as C}from"@jackwener/opencli/download";import{downloadMedia as a}from"@jackwener/opencli/download/media-download";import{resolveTwitterOperationMetadata as R,normalizeTwitterGraphqlPayload as _,unwrapBrowserResult as y,normalizeTwitterScreenName as e,extractMedia as jj,parseTweetUrl as Fj}from"./shared.js";import{TWITTER_BEARER_TOKEN as Gj}from"./utils.js";const I="9EovraBTXJYGSEQXZqlLmQ",k="IGgvgiOx4QZndDHuD3x9TQ",E=100,O={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},f={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},S={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},N={withPayments:!0,withAuxiliaryUserLabels:!0},Hj={queryId:I,features:O,fieldToggles:f},Jj={queryId:k,features:S,fieldToggles:N};function U(j){const F=Number(j??10);if(!Number.isInteger(F)||F<1||F>1000)throw new D("--limit must be an integer between 1 and 1000");return F}function x(j,F){const G=j-F;if(G<=0)return 0;const H=G+10;if(H>100)return 100;return H}async function w(j,F){return(await a(j,F)).map((H,K)=>{const V=j[K]||{};return{index:H.index,tweet_id:V.tweet_id||"",url:V.url||"",type:H.type,status:H.status,size:H.size}})}function Kj(j){if(typeof j==="string")return{queryId:j,features:O,fieldToggles:f};return{queryId:j?.queryId||I,features:j?.features||O,fieldToggles:j?.fieldToggles||f}}function Vj(j){if(typeof j==="string")return{queryId:j,features:S,fieldToggles:N};return{queryId:j?.queryId||k,features:j?.features||S,fieldToggles:j?.fieldToggles||N}}function c(j,F,G){const H=G.fieldToggles||{},K=[`variables=${encodeURIComponent(JSON.stringify(F))}`,`features=${encodeURIComponent(JSON.stringify(G.features||{}))}`];if(Object.keys(H).length>0)K.push(`fieldToggles=${encodeURIComponent(JSON.stringify(H))}`);return`${j}?${K.join("&")}`}function v(j,F,G,H){const K=Kj(j),V={userId:F,count:G,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0};if(H)V.cursor=H;return c(`/i/api/graphql/${K.queryId}/UserMedia`,V,K)}function d(j,F){const G=Vj(j),H={screen_name:F,withSafetyModeUserFields:!0};return c(`/i/api/graphql/${G.queryId}/UserByScreenName`,H,G)}function g(j){if(!j)return"unknown";if(/video\.twimg\.com|\.mp4(\?|$)|\.m3u8(\?|$)/.test(j))return"video";return"image"}function p(j,F){if(!j||typeof j!=="object"||Array.isArray(j))throw new X(`Twitter ${F} returned malformed payload`);return j}function Wj(j,F,G){if(F===401||F===403)throw new h("x.com",`Twitter ${j} requires an authenticated x.com session`);if(F===404)throw new T(`twitter download ${j}`,G||"Twitter returned not found");const H=F?`HTTP ${F}`:"fetch failed";throw new X(`Twitter ${j} fetch failed: ${H}${G?` - ${G}`:""}`)}function m(j,F){const G=p(y(j),F);if(G.ok===!0)return G.payload;if(G.ok===!1)Wj(F,Number(G.status)||0,typeof G.error==="string"?G.error:"");throw new X(`Twitter ${F} returned malformed fetch result`)}function Xj(j){const F=p(j,"UserMedia");if(Array.isArray(F.errors)&&F.errors.length>0)throw new X(`Twitter UserMedia returned GraphQL errors: ${JSON.stringify(F.errors).slice(0,200)}`);const G=F.data?.user?.result;if(!G||typeof G!=="object")throw new X("Twitter UserMedia returned malformed user result");const H=G.timeline_v2?.timeline?.instructions||G.timeline?.timeline?.instructions;if(!Array.isArray(H))throw new X("Twitter UserMedia returned malformed timeline instructions");return F}function n(j,F){const G=[];let H=null;const K=Xj(j).data.user.result,Z=[K.timeline_v2?.timeline?.instructions,K.timeline?.timeline?.instructions].filter(Array.isArray).flat(),Q=(J)=>{if(!J||typeof J!=="object")return;if(J.type==="TimelinePinEntry")return;if(J.tweet_results?.result){const W=J.tweet_results.result,b=W.__typename==="TweetWithVisibilityResults"&&W.tweet?W.tweet:W.tweet||W,$=typeof b.rest_id==="string"||typeof b.rest_id==="number"?String(b.rest_id):"";if(!$)throw new X("Twitter UserMedia returned a tweet without rest_id");if(!F.has($)){F.add($);const{media_urls:B}=jj(b.legacy||{});for(const P of B)G.push({tweet_id:$,url:P,type:g(P)})}}if((J.entryType==="TimelineTimelineCursor"||J.__typename==="TimelineTimelineCursor")&&(J.cursorType==="Bottom"||J.cursorType==="ShowMore")&&J.value)H=J.value;if(Array.isArray(J)){for(const W of J)Q(W);return}for(const W of Object.values(J))if(W&&typeof W==="object")Q(W)};Q(Z);return{items:G,nextCursor:H}}o({site:"twitter",name:"download",access:"read",description:"Download Twitter/X media (images and videos). Provide either <username> to fetch every media item from their profile via the GraphQL UserMedia endpoint with cursor pagination, or --tweet-url to download a single tweet.",domain:"x.com",strategy:r.COOKIE,browser:!0,args:[{name:"username",positional:!0,help:"Twitter username (with or without @) to scan their profile media. Either <username> or --tweet-url is required."},{name:"tweet-url",help:"Single tweet URL to download. Use this OR <username>, not both required at once."},{name:"limit",type:"int",default:10,help:"Maximum number of media items to download when scanning a profile (default 10). Ignored when --tweet-url is used."},{name:"output",default:"./twitter-downloads",help:"Output directory (default ./twitter-downloads). A per-source subdir is created inside."}],columns:["index","tweet_id","url","type","status","size"],func:async(j,F)=>{try{const G=String(F.username??"").trim(),H=String(F["tweet-url"]??"").trim(),K=F.output;if(!G&&!H)throw new D("twitter download requires either <username> or --tweet-url");if(G&&H)throw new D("Use either <username> or --tweet-url, not both");if(H)return $j(j,H,K);const V=U(F.limit),Z=e(G);if(!Z)throw new D("twitter download username must be a valid Twitter/X handle","Example:
|
|
1
|
+
import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{ArgumentError as D,AuthRequiredError as h,CliError as t,CommandExecutionError as X,EmptyResultError as T}from"@jackwener/opencli/errors";import{formatCookieHeader as C}from"@jackwener/opencli/download";import{downloadMedia as a}from"@jackwener/opencli/download/media-download";import{resolveTwitterOperationMetadata as R,normalizeTwitterGraphqlPayload as _,unwrapBrowserResult as y,normalizeTwitterScreenName as e,extractMedia as jj,parseTweetUrl as Fj}from"./shared.js";import{TWITTER_BEARER_TOKEN as Gj}from"./utils.js";const I="9EovraBTXJYGSEQXZqlLmQ",k="IGgvgiOx4QZndDHuD3x9TQ",E=100,O={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},f={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},S={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},N={withPayments:!0,withAuxiliaryUserLabels:!0},Hj={queryId:I,features:O,fieldToggles:f},Jj={queryId:k,features:S,fieldToggles:N};function U(j){const F=Number(j??10);if(!Number.isInteger(F)||F<1||F>1000)throw new D("--limit must be an integer between 1 and 1000");return F}function x(j,F){const G=j-F;if(G<=0)return 0;const H=G+10;if(H>100)return 100;return H}async function w(j,F){return(await a(j,F)).map((H,K)=>{const V=j[K]||{};return{index:H.index,tweet_id:V.tweet_id||"",url:V.url||"",type:H.type,status:H.status,size:H.size}})}function Kj(j){if(typeof j==="string")return{queryId:j,features:O,fieldToggles:f};return{queryId:j?.queryId||I,features:j?.features||O,fieldToggles:j?.fieldToggles||f}}function Vj(j){if(typeof j==="string")return{queryId:j,features:S,fieldToggles:N};return{queryId:j?.queryId||k,features:j?.features||S,fieldToggles:j?.fieldToggles||N}}function c(j,F,G){const H=G.fieldToggles||{},K=[`variables=${encodeURIComponent(JSON.stringify(F))}`,`features=${encodeURIComponent(JSON.stringify(G.features||{}))}`];if(Object.keys(H).length>0)K.push(`fieldToggles=${encodeURIComponent(JSON.stringify(H))}`);return`${j}?${K.join("&")}`}function v(j,F,G,H){const K=Kj(j),V={userId:F,count:G,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0};if(H)V.cursor=H;return c(`/i/api/graphql/${K.queryId}/UserMedia`,V,K)}function d(j,F){const G=Vj(j),H={screen_name:F,withSafetyModeUserFields:!0};return c(`/i/api/graphql/${G.queryId}/UserByScreenName`,H,G)}function g(j){if(!j)return"unknown";if(/video\.twimg\.com|\.mp4(\?|$)|\.m3u8(\?|$)/.test(j))return"video";return"image"}function p(j,F){if(!j||typeof j!=="object"||Array.isArray(j))throw new X(`Twitter ${F} returned malformed payload`);return j}function Wj(j,F,G){if(F===401||F===403)throw new h("x.com",`Twitter ${j} requires an authenticated x.com session`);if(F===404)throw new T(`twitter download ${j}`,G||"Twitter returned not found");const H=F?`HTTP ${F}`:"fetch failed";throw new X(`Twitter ${j} fetch failed: ${H}${G?` - ${G}`:""}`)}function m(j,F){const G=p(y(j),F);if(G.ok===!0)return G.payload;if(G.ok===!1)Wj(F,Number(G.status)||0,typeof G.error==="string"?G.error:"");throw new X(`Twitter ${F} returned malformed fetch result`)}function Xj(j){const F=p(j,"UserMedia");if(Array.isArray(F.errors)&&F.errors.length>0)throw new X(`Twitter UserMedia returned GraphQL errors: ${JSON.stringify(F.errors).slice(0,200)}`);const G=F.data?.user?.result;if(!G||typeof G!=="object")throw new X("Twitter UserMedia returned malformed user result");const H=G.timeline_v2?.timeline?.instructions||G.timeline?.timeline?.instructions;if(!Array.isArray(H))throw new X("Twitter UserMedia returned malformed timeline instructions");return F}function n(j,F){const G=[];let H=null;const K=Xj(j).data.user.result,Z=[K.timeline_v2?.timeline?.instructions,K.timeline?.timeline?.instructions].filter(Array.isArray).flat(),Q=(J)=>{if(!J||typeof J!=="object")return;if(J.type==="TimelinePinEntry")return;if(J.tweet_results?.result){const W=J.tweet_results.result,b=W.__typename==="TweetWithVisibilityResults"&&W.tweet?W.tweet:W.tweet||W,$=typeof b.rest_id==="string"||typeof b.rest_id==="number"?String(b.rest_id):"";if(!$)throw new X("Twitter UserMedia returned a tweet without rest_id");if(!F.has($)){F.add($);const{media_urls:B}=jj(b.legacy||{});for(const P of B)G.push({tweet_id:$,url:P,type:g(P)})}}if((J.entryType==="TimelineTimelineCursor"||J.__typename==="TimelineTimelineCursor")&&(J.cursorType==="Bottom"||J.cursorType==="ShowMore")&&J.value)H=J.value;if(Array.isArray(J)){for(const W of J)Q(W);return}for(const W of Object.values(J))if(W&&typeof W==="object")Q(W)};Q(Z);return{items:G,nextCursor:H}}o({site:"twitter",name:"download",access:"read",description:"Download Twitter/X media (images and videos). Provide either <username> to fetch every media item from their profile via the GraphQL UserMedia endpoint with cursor pagination, or --tweet-url to download a single tweet.",domain:"x.com",strategy:r.COOKIE,browser:!0,args:[{name:"username",positional:!0,help:"Twitter username (with or without @) to scan their profile media. Either <username> or --tweet-url is required."},{name:"tweet-url",help:"Single tweet URL to download. Use this OR <username>, not both required at once."},{name:"limit",type:"int",default:10,help:"Maximum number of media items to download when scanning a profile (default 10). Ignored when --tweet-url is used."},{name:"output",default:"./twitter-downloads",help:"Output directory (default ./twitter-downloads). A per-source subdir is created inside."}],columns:["index","tweet_id","url","type","status","size"],func:async(j,F)=>{try{const G=String(F.username??"").trim(),H=String(F["tweet-url"]??"").trim(),K=F.output;if(!G&&!H)throw new D("twitter download requires either <username> or --tweet-url");if(G&&H)throw new D("Use either <username> or --tweet-url, not both");if(H)return $j(j,H,K);const V=U(F.limit),Z=e(G);if(!Z)throw new D("twitter download username must be a valid Twitter/X handle","Example: ppcli twitter download @jack --limit 20");return Zj(j,Z,V,K)}catch(G){if(G instanceof t)throw G;throw new X(`twitter download failed: ${G?.message??String(G)}`)}}});async function Zj(j,F,G,H){await j.goto(`https://x.com/${F}`);await j.wait({selector:'[data-testid="primaryColumn"]'});const K=await j.getCookies({url:"https://x.com"}),V=K.find((Y)=>Y.name==="ct0")?.value||null;if(!V)throw new h("x.com","Not logged into x.com (no ct0 cookie)");const Z=await R(j,"UserMedia",Hj),Q=await R(j,"UserByScreenName",Jj),J=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(Gj)}`,"X-Csrf-Token":V,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),W=d(Q,F),b=m(await j.evaluate(`async () => {
|
|
2
2
|
try {
|
|
3
3
|
const resp = await fetch("${W}", { headers: ${J}, credentials: 'include' });
|
|
4
4
|
if (!resp.ok) return { ok: false, status: resp.status };
|
|
@@ -42,10 +42,10 @@ import{ArgumentError as V,AuthRequiredError as $,selectorError as T,EmptyResultE
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
return out;
|
|
45
|
-
}`;return G.evaluate(L)}function W(G){return D(G)}v({site:"twitter",name:"followers",access:"read",description:"Get accounts following a Twitter/X user (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:j.UI,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch followers of the currently logged-in account."},{name:"limit",type:"int",default:50,help:"Maximum number of follower rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio"],func:async(G,L)=>{const J=L.limit;if(!Number.isInteger(J)||J<=0)throw new V("limit must be a positive integer");const X=String(L.user??"").trim();let H=W(X);if(X&&!H)throw new V("twitter followers user must be a valid Twitter/X handle","Example:
|
|
45
|
+
}`;return G.evaluate(L)}function W(G){return D(G)}v({site:"twitter",name:"followers",access:"read",description:"Get accounts following a Twitter/X user (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:j.UI,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch followers of the currently logged-in account."},{name:"limit",type:"int",default:50,help:"Maximum number of follower rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio"],func:async(G,L)=>{const J=L.limit;if(!Number.isInteger(J)||J<=0)throw new V("limit must be a positive integer");const X=String(L.user??"").trim();let H=W(X);if(X&&!H)throw new V("twitter followers user must be a valid Twitter/X handle","Example: ppcli twitter followers @elonmusk --limit 100");if(!H){await G.goto("https://x.com/home");await G.wait({selector:'[data-testid="primaryColumn"]'});const I=M(await G.evaluate(`() => {
|
|
46
46
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
47
47
|
return link ? link.getAttribute('href') : null;
|
|
48
|
-
}`));if(!I||typeof I!=="string")throw new $("x.com","Could not find logged-in user profile link. Are you logged in?");H=W(I);if(!H)throw new $("x.com","Could not find logged-in user profile link. Are you logged in?")}if(!H)throw new V("twitter followers user cannot be empty","Example:
|
|
48
|
+
}`));if(!I||typeof I!=="string")throw new $("x.com","Could not find logged-in user profile link. Are you logged in?");H=W(I);if(!H)throw new $("x.com","Could not find logged-in user profile link. Are you logged in?")}if(!H)throw new V("twitter followers user cannot be empty","Example: ppcli twitter followers @elonmusk --limit 100");await G.goto(`https://x.com/${H}`);await G.wait(3);const B=JSON.stringify(H);if(!await G.evaluate(`() => {
|
|
49
49
|
const target = ${B};
|
|
50
50
|
const selectors = [
|
|
51
51
|
'a[href="/' + target + '/followers"]',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as U,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as v,AuthRequiredError as j,CommandExecutionError as b,EmptyResultError as C}from"@jackwener/opencli/errors";import{looksLikePrivateTwitterTimeline as E,normalizeTwitterScreenName as m,resolveTwitterQueryId as x,sanitizeQueryId as p,unwrapBrowserResult as B,describeTwitterApiError as d}from"./shared.js";import{TWITTER_BEARER_TOKEN as g}from"./utils.js";const c="F42cDX8PDFxkbjjq6JrM2w",i="IGgvgiOx4QZndDHuD3x9TQ",l=100,n={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1};function A(H,Z,J,$){const V={userId:Z,count:J,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0,withV2Timeline:!0};if($)V.cursor=$;return`/i/api/graphql/${H}/Following?variables=${encodeURIComponent(JSON.stringify(V))}&features=${encodeURIComponent(JSON.stringify(n))}`}function h(H,Z){const J=JSON.stringify({screen_name:Z,withSafetyModeUserFields:!0}),$=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${H}/UserByScreenName?variables=${encodeURIComponent(J)}&features=${encodeURIComponent($)}`}function N(H){if(!H||H.__typename!=="User")return null;const Z=H.core||{},J=H.legacy||{},$=Z.screen_name||J.screen_name||"";if(!$)throw new b("Malformed Twitter following user: missing screen_name");return{screen_name:$,name:Z.name||J.name||"",bio:J.description||H.profile_bio?.description||"",followers:J.followers_count||J.normal_followers_count||0}}function k(H){const Z=[];let J=null;const $=H?.data?.user?.result?.timeline_v2?.timeline?.instructions||H?.data?.user?.result?.timeline?.timeline?.instructions||[];for(const V of $)for(const W of V.entries||[]){const K=W.content;if(K?.entryType==="TimelineTimelineCursor"||K?.__typename==="TimelineTimelineCursor"){if(K.cursorType==="Bottom"||K.cursorType==="ShowMore")J=K.value;continue}if(W.entryId?.startsWith("cursor-bottom-")||W.entryId?.startsWith("cursor-showMore-")){J=K?.value||K?.itemContent?.value||J;continue}if(W.entryId?.startsWith("user-")){const O=N(K?.itemContent?.user_results?.result);if(O)Z.push(O)}}return{users:Z,nextCursor:J}}function q(H){return m(H)}U({site:"twitter",name:"following",access:"read",description:"Get accounts a Twitter/X user is following (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:w.COOKIE,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch the accounts the currently logged-in user follows."},{name:"limit",type:"int",default:50,help:"Maximum number of following rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio","followers"],func:async(H,Z)=>{const J=Z.limit===void 0||Z.limit===null?50:Number(Z.limit);if(!Number.isInteger(J)||J<=0)throw new v("twitter following --limit must be a positive integer","Example:
|
|
1
|
+
import{cli as U,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as v,AuthRequiredError as j,CommandExecutionError as b,EmptyResultError as C}from"@jackwener/opencli/errors";import{looksLikePrivateTwitterTimeline as E,normalizeTwitterScreenName as m,resolveTwitterQueryId as x,sanitizeQueryId as p,unwrapBrowserResult as B,describeTwitterApiError as d}from"./shared.js";import{TWITTER_BEARER_TOKEN as g}from"./utils.js";const c="F42cDX8PDFxkbjjq6JrM2w",i="IGgvgiOx4QZndDHuD3x9TQ",l=100,n={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1};function A(H,Z,J,$){const V={userId:Z,count:J,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0,withV2Timeline:!0};if($)V.cursor=$;return`/i/api/graphql/${H}/Following?variables=${encodeURIComponent(JSON.stringify(V))}&features=${encodeURIComponent(JSON.stringify(n))}`}function h(H,Z){const J=JSON.stringify({screen_name:Z,withSafetyModeUserFields:!0}),$=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${H}/UserByScreenName?variables=${encodeURIComponent(J)}&features=${encodeURIComponent($)}`}function N(H){if(!H||H.__typename!=="User")return null;const Z=H.core||{},J=H.legacy||{},$=Z.screen_name||J.screen_name||"";if(!$)throw new b("Malformed Twitter following user: missing screen_name");return{screen_name:$,name:Z.name||J.name||"",bio:J.description||H.profile_bio?.description||"",followers:J.followers_count||J.normal_followers_count||0}}function k(H){const Z=[];let J=null;const $=H?.data?.user?.result?.timeline_v2?.timeline?.instructions||H?.data?.user?.result?.timeline?.timeline?.instructions||[];for(const V of $)for(const W of V.entries||[]){const K=W.content;if(K?.entryType==="TimelineTimelineCursor"||K?.__typename==="TimelineTimelineCursor"){if(K.cursorType==="Bottom"||K.cursorType==="ShowMore")J=K.value;continue}if(W.entryId?.startsWith("cursor-bottom-")||W.entryId?.startsWith("cursor-showMore-")){J=K?.value||K?.itemContent?.value||J;continue}if(W.entryId?.startsWith("user-")){const O=N(K?.itemContent?.user_results?.result);if(O)Z.push(O)}}return{users:Z,nextCursor:J}}function q(H){return m(H)}U({site:"twitter",name:"following",access:"read",description:"Get accounts a Twitter/X user is following (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:w.COOKIE,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch the accounts the currently logged-in user follows."},{name:"limit",type:"int",default:50,help:"Maximum number of following rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio","followers"],func:async(H,Z)=>{const J=Z.limit===void 0||Z.limit===null?50:Number(Z.limit);if(!Number.isInteger(J)||J<=0)throw new v("twitter following --limit must be a positive integer","Example: ppcli twitter following @elonmusk --limit 200");const $=String(Z.user??"").trim();let V=q($);if($&&!V)throw new v("twitter following user must be a valid Twitter/X handle","Example: ppcli twitter following @elonmusk --limit 200");const K=(await H.getCookies({url:"https://x.com"})).find((X)=>X.name==="ct0")?.value||null;if(!K)throw new j("x.com","Not logged into x.com (no ct0 cookie)");if(!V){await H.goto("https://x.com/home");await H.wait({selector:'[data-testid="primaryColumn"]'});const X=B(await H.evaluate(`() => {
|
|
2
2
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
3
3
|
return link ? link.getAttribute('href') : null;
|
|
4
|
-
}`));if(!X||typeof X!=="string")throw new j("x.com","Could not detect logged-in user. Are you logged in?");V=q(X);if(!V)throw new j("x.com","Could not detect logged-in user. Are you logged in?")}if(!V)throw new v("twitter following user cannot be empty","Example:
|
|
4
|
+
}`));if(!X||typeof X!=="string")throw new j("x.com","Could not detect logged-in user. Are you logged in?");V=q(X);if(!V)throw new j("x.com","Could not detect logged-in user. Are you logged in?")}if(!V)throw new v("twitter following user cannot be empty","Example: ppcli twitter following @elonmusk --limit 200");const O=await x(H,"Following",c),I=await x(H,"UserByScreenName",i),L={Authorization:`Bearer ${decodeURIComponent(g)}`,"X-Csrf-Token":K,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"},M=B(await H.evaluate(async(X,f)=>{const P=await fetch(X,{headers:f,credentials:"include"});if(!P.ok)return{error:P.status};return{userId:(await P.json()).data?.user?.result?.rest_id||null}},h(I,V),L));if(M?.error===401||M?.error===403)throw new j("x.com",`Twitter user lookup failed (HTTP ${M.error})`);if(M?.error)throw new b(`HTTP ${M.error}: Failed to resolve Twitter user @${V}`);const _=M?.userId||null;if(!_)throw new b(`Could not find user @${V}`);const G=[],S=new Set;let z=null,T=null;for(let X=0;X<l&&G.length<J;X++){const f=Math.min(50,J-G.length+10),P=A(O,_,f,z),D=B(await H.evaluate(async(Y,y)=>{const Q=await fetch(Y,{headers:y,credentials:"include"});return Q.ok?await Q.json():{error:Q.status}},P,L));if(D?.error){if(D.error===401||D.error===403)throw new j("x.com",`Twitter following request failed (HTTP ${D.error})`);throw new b(d("Following",D.error))}T=D;const{users:R,nextCursor:F}=k(D);for(const Y of R)if(!S.has(Y.screen_name)){S.add(Y.screen_name);G.push(Y)}if(!F||F===z)break;z=F}if(G.length===0){if(E(T))throw new C("twitter following",`No following data returned for @${V} (the target account may have set their following list to private)`);throw new C("twitter following",`No following accounts found for @${V}`)}return G.slice(0,J)}});export const __test__={sanitizeQueryId:p,buildFollowingUrl:A,buildUserByScreenNameUrl:h,extractUser:N,normalizeScreenName:q,parseFollowing:k};
|
package/clis/twitter/likes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as k,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as R,AuthRequiredError as j,CommandExecutionError as v,EmptyResultError as q}from"@jackwener/opencli/errors";import{looksLikePrivateTwitterTimeline as T,normalizeTwitterScreenName as Q,resolveTwitterQueryId as L,sanitizeQueryId as E,extractMedia as y,unwrapBrowserResult as b,describeTwitterApiError as g}from"./shared.js";import{TWITTER_BEARER_TOKEN as p,applyTopByEngagement as m}from"./utils.js";const d="CDWHmpZeSdIJ3HGeRbNm0w",w="IGgvgiOx4QZndDHuD3x9TQ",c=100,l={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1};function B(J,$,H,Z){const V={userId:$,count:H,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0};if(Z)V.cursor=Z;return`/i/api/graphql/${J}/Likes?variables=${encodeURIComponent(JSON.stringify(V))}&features=${encodeURIComponent(JSON.stringify(l))}`}function h(J,$){const H=JSON.stringify({screen_name:$,withSafetyModeUserFields:!0}),Z=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${J}/UserByScreenName?variables=${encodeURIComponent(H)}&features=${encodeURIComponent(Z)}`}function C(J,$){if(!J)return null;const H=J.tweet||J,Z=H.legacy||{};if(!H.rest_id||$.has(H.rest_id))return null;$.add(H.rest_id);const V=H.core?.user_results?.result,G=V?.legacy?.screen_name||V?.core?.screen_name||"unknown",W=V?.legacy?.name||V?.core?.name||"",F=H.note_tweet?.note_tweet_results?.result?.text;return{id:H.rest_id,author:G,name:W,text:F||Z.full_text||"",likes:Z.favorite_count||0,retweets:Z.retweet_count||0,created_at:Z.created_at||"",url:`https://x.com/${G}/status/${H.rest_id}`,...y(Z)}}function S(J,$){const H=[];let Z=null;const V=J?.data?.user?.result?.timeline_v2?.timeline?.instructions||J?.data?.user?.result?.timeline?.timeline?.instructions||[];for(const G of V)for(const W of G.entries||[]){const F=W.content;if(F?.entryType==="TimelineTimelineCursor"||F?.__typename==="TimelineTimelineCursor"){if(F.cursorType==="Bottom"||F.cursorType==="ShowMore")Z=F.value;continue}if(W.entryId?.startsWith("cursor-bottom-")||W.entryId?.startsWith("cursor-showMore-")){Z=F?.value||F?.itemContent?.value||Z;continue}const O=C(F?.itemContent?.tweet_results?.result,$);if(O){H.push(O);continue}for(const z of F?.items||[]){const K=C(z.item?.itemContent?.tweet_results?.result,$);if(K)H.push(K)}}return{tweets:H,nextCursor:Z}}k({site:"twitter",name:"likes",access:"read",description:"Fetch liked tweets of a Twitter user (defaults to the logged-in user when no username is given)",domain:"x.com",strategy:I.COOKIE,browser:!0,args:[{name:"username",type:"string",positional:!0,help:"Twitter screen name (with or without @). Defaults to the logged-in user when omitted."},{name:"limit",type:"int",default:20,help:"Maximum number of liked tweets to return (default 20)."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the liked tweets by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API's native (recency) ordering."}],columns:["id","author","name","text","likes","retweets","created_at","url","has_media","media_urls","media_posters"],func:async(J,$)=>{const H=$.limit||20,Z=String($.username??"").trim();let V=Q(Z);if(Z&&!V)throw new R("twitter likes username must be a valid Twitter/X handle","Example:
|
|
1
|
+
import{cli as k,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as R,AuthRequiredError as j,CommandExecutionError as v,EmptyResultError as q}from"@jackwener/opencli/errors";import{looksLikePrivateTwitterTimeline as T,normalizeTwitterScreenName as Q,resolveTwitterQueryId as L,sanitizeQueryId as E,extractMedia as y,unwrapBrowserResult as b,describeTwitterApiError as g}from"./shared.js";import{TWITTER_BEARER_TOKEN as p,applyTopByEngagement as m}from"./utils.js";const d="CDWHmpZeSdIJ3HGeRbNm0w",w="IGgvgiOx4QZndDHuD3x9TQ",c=100,l={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1};function B(J,$,H,Z){const V={userId:$,count:H,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0};if(Z)V.cursor=Z;return`/i/api/graphql/${J}/Likes?variables=${encodeURIComponent(JSON.stringify(V))}&features=${encodeURIComponent(JSON.stringify(l))}`}function h(J,$){const H=JSON.stringify({screen_name:$,withSafetyModeUserFields:!0}),Z=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${J}/UserByScreenName?variables=${encodeURIComponent(H)}&features=${encodeURIComponent(Z)}`}function C(J,$){if(!J)return null;const H=J.tweet||J,Z=H.legacy||{};if(!H.rest_id||$.has(H.rest_id))return null;$.add(H.rest_id);const V=H.core?.user_results?.result,G=V?.legacy?.screen_name||V?.core?.screen_name||"unknown",W=V?.legacy?.name||V?.core?.name||"",F=H.note_tweet?.note_tweet_results?.result?.text;return{id:H.rest_id,author:G,name:W,text:F||Z.full_text||"",likes:Z.favorite_count||0,retweets:Z.retweet_count||0,created_at:Z.created_at||"",url:`https://x.com/${G}/status/${H.rest_id}`,...y(Z)}}function S(J,$){const H=[];let Z=null;const V=J?.data?.user?.result?.timeline_v2?.timeline?.instructions||J?.data?.user?.result?.timeline?.timeline?.instructions||[];for(const G of V)for(const W of G.entries||[]){const F=W.content;if(F?.entryType==="TimelineTimelineCursor"||F?.__typename==="TimelineTimelineCursor"){if(F.cursorType==="Bottom"||F.cursorType==="ShowMore")Z=F.value;continue}if(W.entryId?.startsWith("cursor-bottom-")||W.entryId?.startsWith("cursor-showMore-")){Z=F?.value||F?.itemContent?.value||Z;continue}const O=C(F?.itemContent?.tweet_results?.result,$);if(O){H.push(O);continue}for(const z of F?.items||[]){const K=C(z.item?.itemContent?.tweet_results?.result,$);if(K)H.push(K)}}return{tweets:H,nextCursor:Z}}k({site:"twitter",name:"likes",access:"read",description:"Fetch liked tweets of a Twitter user (defaults to the logged-in user when no username is given)",domain:"x.com",strategy:I.COOKIE,browser:!0,args:[{name:"username",type:"string",positional:!0,help:"Twitter screen name (with or without @). Defaults to the logged-in user when omitted."},{name:"limit",type:"int",default:20,help:"Maximum number of liked tweets to return (default 20)."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the liked tweets by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API's native (recency) ordering."}],columns:["id","author","name","text","likes","retweets","created_at","url","has_media","media_urls","media_posters"],func:async(J,$)=>{const H=$.limit||20,Z=String($.username??"").trim();let V=Q(Z);if(Z&&!V)throw new R("twitter likes username must be a valid Twitter/X handle","Example: ppcli twitter likes @jack --limit 20");const W=(await J.getCookies({url:"https://x.com"})).find((X)=>X.name==="ct0")?.value||null;if(!W)throw new j("x.com","Not logged into x.com (no ct0 cookie)");if(!V){await J.goto("https://x.com/home");await J.wait({selector:'[data-testid="primaryColumn"]'});const X=b(await J.evaluate(`() => {
|
|
2
2
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
3
3
|
return link ? link.getAttribute('href') : null;
|
|
4
4
|
}`));if(!X||typeof X!=="string")throw new j("x.com","Could not detect logged-in user. Are you logged in?");V=Q(X);if(!V)throw new j("x.com","Could not detect logged-in user. Are you logged in?")}const F=await L(J,"Likes",d),O=await L(J,"UserByScreenName",w),z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(p)}`,"X-Csrf-Token":W,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),K=b(await J.evaluate(`async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as i,Strategy as o}from"@jackwener/opencli/registry";import{listAddUser as n}from"./list-add-core.js";import{parseBatchIntervalSeconds as l,parseCommaSeparatedUsernames as m,runListBatch as d}from"./list-batch-utils.js";const u='Example:
|
|
1
|
+
import{cli as i,Strategy as o}from"@jackwener/opencli/registry";import{listAddUser as n}from"./list-add-core.js";import{parseBatchIntervalSeconds as l,parseCommaSeparatedUsernames as m,runListBatch as d}from"./list-batch-utils.js";const u='Example: ppcli twitter list-add-batch 123456789 "@alice,@bob" --interval 5';i({site:"twitter",name:"list-add-batch",access:"write",description:"Add multiple users to a Twitter/X list you own from a comma-separated username list",domain:"x.com",strategy:o.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `ppcli twitter lists`)"},{name:"usernames",positional:!0,type:"string",required:!0,help:"Comma-separated Twitter/X handles to add (with or without @)"},{name:"interval",type:"int",default:5,help:"Seconds to wait between account additions (default: 5)"},{name:"timeout",type:"int",default:600,help:"Max seconds for the overall batch command (default: 600)"}],columns:["listId","username","userId","status","message"],func:async(e,t)=>{const a=String(t.listId||"").trim(),r=m(t.usernames,u),s=l(t.interval);return d({page:e,listId:a,usernames:r,interval:s,operation:n})}});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as U,AuthRequiredError as w,CommandExecutionError as X}from"@jackwener/opencli/errors";import{resolveTwitterQueryId as Y,unwrapBrowserResult as x}from"./shared.js";import{parseListsManagement as B}from"./lists.js";import{TWITTER_BEARER_TOKEN as A}from"./utils.js";const c="IGgvgiOx4QZndDHuD3x9TQ",f="78UbkyXwXBD98IgUWXOy9g",T="vWPi0CTMoPFsjsL6W4IynQ",b={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!1,responsive_web_grok_share_attachment_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!1,creator_subscriptions_quote_tweet_preview_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_enhance_cards_enabled:!1};function C(z,V){const H=JSON.stringify({screen_name:V,withSafetyModeUserFields:!0}),P=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${z}/UserByScreenName?variables=${encodeURIComponent(H)}&features=${encodeURIComponent(P)}`}function g(z){return(Array.isArray(z)?z:[]).filter((H)=>!(H?.path||[]).join(".").includes("default_banner_media_results")&&!/decode/i.test(H?.message||""))}export function buildListAddMemberRow({addResult:z,memberCountBefore:V,listId:H,username:P,userId:v}){if(!z?.httpOk)throw new X(`Failed to add @${P} to list ${H}: HTTP ${z?.status??0}${z?.fetchError?" ("+z.fetchError+")":""}${z?.raw?" — "+z.raw:""}`);const O=z.mc!==null&&z.mc!==void 0,G=g(z.errors);if(!O&&G.length){const h=G.map((j)=>j.message||JSON.stringify(j)).join("; ");throw new X(`Failed to add @${P} to list ${H}: ${h.slice(0,300)}`)}if(!O)throw new X(`Failed to add @${P} to list ${H}: no member_count in response`);const Z=Number(z.mc);if(!Number.isFinite(Z))throw new X(`Failed to add @${P} to list ${H}: invalid member_count in response`);if(Z<V)throw new X(`Failed to add @${P} to list ${H}: member_count decreased unexpectedly (${V} → ${Z})`);const W=!(Z>V);if(W&&z.isMember!==!0)throw new X(`Failed to add @${P} to list ${H}: member_count unchanged and membership was not confirmed`);const J=`member_count ${V} → ${Z}`;return{listId:H,username:P,userId:String(v),status:W?"noop":"success",message:W?`@${P} is already a member of list ${H}`:`Added @${P} to list ${H} (verified via ${J})`}}export async function listAddUser(z,V){const H=String(V.listId||"").trim(),P=String(V.username||"").replace(/^@/,"").trim();if(!H||!/^\d+$/.test(H))throw new U(`Invalid listId: ${JSON.stringify(V.listId)}. Expected numeric ID.`,"Example:
|
|
1
|
+
import{ArgumentError as U,AuthRequiredError as w,CommandExecutionError as X}from"@jackwener/opencli/errors";import{resolveTwitterQueryId as Y,unwrapBrowserResult as x}from"./shared.js";import{parseListsManagement as B}from"./lists.js";import{TWITTER_BEARER_TOKEN as A}from"./utils.js";const c="IGgvgiOx4QZndDHuD3x9TQ",f="78UbkyXwXBD98IgUWXOy9g",T="vWPi0CTMoPFsjsL6W4IynQ",b={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!1,responsive_web_grok_share_attachment_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!1,creator_subscriptions_quote_tweet_preview_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_enhance_cards_enabled:!1};function C(z,V){const H=JSON.stringify({screen_name:V,withSafetyModeUserFields:!0}),P=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${z}/UserByScreenName?variables=${encodeURIComponent(H)}&features=${encodeURIComponent(P)}`}function g(z){return(Array.isArray(z)?z:[]).filter((H)=>!(H?.path||[]).join(".").includes("default_banner_media_results")&&!/decode/i.test(H?.message||""))}export function buildListAddMemberRow({addResult:z,memberCountBefore:V,listId:H,username:P,userId:v}){if(!z?.httpOk)throw new X(`Failed to add @${P} to list ${H}: HTTP ${z?.status??0}${z?.fetchError?" ("+z.fetchError+")":""}${z?.raw?" — "+z.raw:""}`);const O=z.mc!==null&&z.mc!==void 0,G=g(z.errors);if(!O&&G.length){const h=G.map((j)=>j.message||JSON.stringify(j)).join("; ");throw new X(`Failed to add @${P} to list ${H}: ${h.slice(0,300)}`)}if(!O)throw new X(`Failed to add @${P} to list ${H}: no member_count in response`);const Z=Number(z.mc);if(!Number.isFinite(Z))throw new X(`Failed to add @${P} to list ${H}: invalid member_count in response`);if(Z<V)throw new X(`Failed to add @${P} to list ${H}: member_count decreased unexpectedly (${V} → ${Z})`);const W=!(Z>V);if(W&&z.isMember!==!0)throw new X(`Failed to add @${P} to list ${H}: member_count unchanged and membership was not confirmed`);const J=`member_count ${V} → ${Z}`;return{listId:H,username:P,userId:String(v),status:W?"noop":"success",message:W?`@${P} is already a member of list ${H}`:`Added @${P} to list ${H} (verified via ${J})`}}export async function listAddUser(z,V){const H=String(V.listId||"").trim(),P=String(V.username||"").replace(/^@/,"").trim();if(!H||!/^\d+$/.test(H))throw new U(`Invalid listId: ${JSON.stringify(V.listId)}. Expected numeric ID.`,"Example: ppcli twitter list-add 123456789 alice");if(!P)throw new U("twitter list-add username is required","Example: ppcli twitter list-add 123456789 alice");await z.goto("https://x.com");await z.wait(3);const O=(await z.getCookies({url:"https://x.com"})).find((q)=>q.name==="ct0")?.value||null;if(!O)throw new w("x.com","Not logged into x.com (no ct0 cookie)");const G=await Y(z,"UserByScreenName",c),Z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(A)}`,"X-Csrf-Token":O,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),k=C(G,P),W=await z.evaluate(`async () => {
|
|
2
2
|
const resp = await fetch(${JSON.stringify(k)}, { headers: ${Z}, credentials: 'include' });
|
|
3
3
|
if (!resp.ok) return null;
|
|
4
4
|
const d = await resp.json();
|
package/clis/twitter/list-add.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as t,Strategy as e}from"@jackwener/opencli/registry";import{buildListAddMemberRow,listAddUser}from"./list-add-core.js";t({site:"twitter",name:"list-add",access:"write",description:"Add a user to a Twitter/X list you own (no-op if already a member)",domain:"x.com",strategy:e.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `ppcli twitter lists`)"},{name:"username",positional:!0,type:"string",required:!0,help:"Twitter/X handle to add (with or without @)"}],columns:["listId","username","userId","status","message"],func:listAddUser});export{buildListAddMemberRow,listAddUser};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as D,Strategy as O}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as B,CommandExecutionError as Z}from"@jackwener/opencli/errors";import{unwrapBrowserResult as P}from"./shared.js";import{TWITTER_BEARER_TOKEN as q}from"./utils.js";const y="UQRa0jJ9doxGEIQRea1Y0w",W=25,Y=100,S={profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0};export function parseListCreateArgs(z){const H=String(z.name||"").trim(),J=String(z.description||"").trim(),G=String(z.mode||"public").trim().toLowerCase();if(!H)throw new Q("List name is required",'Example:
|
|
1
|
+
import{cli as D,Strategy as O}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as B,CommandExecutionError as Z}from"@jackwener/opencli/errors";import{unwrapBrowserResult as P}from"./shared.js";import{TWITTER_BEARER_TOKEN as q}from"./utils.js";const y="UQRa0jJ9doxGEIQRea1Y0w",W=25,Y=100,S={profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0};export function parseListCreateArgs(z){const H=String(z.name||"").trim(),J=String(z.description||"").trim(),G=String(z.mode||"public").trim().toLowerCase();if(!H)throw new Q("List name is required",'Example: ppcli twitter list-create "My List"');if(H.length>W)throw new Q(`List name too long: ${H.length} chars (max ${W})`);if(J.length>Y)throw new Q(`Description too long: ${J.length} chars (max ${Y})`);if(G!=="public"&&G!=="private")throw new Q(`Invalid mode: ${JSON.stringify(z.mode)}. Expected "public" or "private".`);return{listName:H,listDescription:J,listMode:G,privateFlag:G==="private"}}function X(z,H,J){if(!z||typeof z!=="object")throw new Z(`Unexpected result from twitter list-create: ${JSON.stringify(z)}`);if(z.httpStatus===401||z.httpStatus===403)throw new B("x.com",`Twitter CreateList returned HTTP ${z.httpStatus}`);if(!z.ok){const $=String(z.bodyText||"").slice(0,300);throw new Z(`HTTP ${z.httpStatus} from CreateList: ${$}`)}if(!z.bodyJson||typeof z.bodyJson!=="object")throw new Z(`CreateList returned malformed JSON payload. Body: ${String(z.bodyText||"").slice(0,300)}`);const G=z.bodyJson?.data?.list;if(!G||typeof G!=="object"){const $=z.bodyJson?.errors;if(Array.isArray($)&&$.length>0)throw new Z(`CreateList failed: ${$[0].message||JSON.stringify($[0])}`);throw new Z(`CreateList returned no list payload. Body: ${String(z.bodyText||"").slice(0,300)}`)}const v=String(G.id_str||G.id||"");if(!/^\d+$/.test(v))throw new Z("CreateList returned a list payload without a numeric list id.");if(typeof G.name!=="string"||!G.name.trim())throw new Z("CreateList returned a list payload without a list name.");if(G.name.trim()!==H)throw new Z(`CreateList returned name ${JSON.stringify(G.name)}, expected ${JSON.stringify(H)}.`);const F=typeof G.mode==="string"?G.mode:"";if(!F)throw new Z("CreateList returned a list payload without list mode.");const K=/private/i.test(F)?"private":"public";if(K!==J)throw new Z(`CreateList returned mode ${K}, expected ${J}.`);return{createdList:G,listId:v,listMode:K}}export function buildListCreateRow({result:z,name:H,description:J,mode:G}){const{createdList:v,listId:F,listMode:K}=X(z,H,G);return{id:F,name:v.name,description:typeof v.description==="string"?v.description:J,mode:K,status:"success"}}D({site:"twitter",name:"list-create",description:"Create a new Twitter/X list (returns the new list id)",access:"write",domain:"x.com",strategy:O.COOKIE,browser:!0,args:[{name:"name",positional:!0,type:"string",required:!0,help:`List name (max ${W} chars)`},{name:"description",type:"string",default:"",help:`Optional list description (max ${Y} chars)`},{name:"mode",type:"string",default:"public",help:"public | private"}],columns:["id","name","description","mode","status"],func:async(z,H)=>{const{listName:J,listDescription:G,listMode:v,privateFlag:F}=parseListCreateArgs(H);await z.goto("https://x.com");await z.wait(3);const $=(await z.getCookies({url:"https://x.com"})).find((j)=>j.name==="ct0")?.value||null;if(!$)throw new B("x.com","Not logged into x.com (no ct0 cookie)");const f=y,V=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(q)}`,"X-Csrf-Token":$,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),b=JSON.stringify({variables:{isPrivate:F,name:J,description:G},features:S,queryId:f}),U=`/i/api/graphql/${f}/CreateList`,h=P(await z.evaluate(`async () => {
|
|
2
2
|
const r = await fetch(${JSON.stringify(U)}, {
|
|
3
3
|
method: 'POST',
|
|
4
4
|
headers: ${V},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{cli as f,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as d,AuthRequiredError as p,CommandExecutionError as
|
|
1
|
+
import{cli as f,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as d,AuthRequiredError as p,CommandExecutionError as o}from"@jackwener/opencli/errors";import{resolveTwitterQueryId as b,unwrapBrowserResult as c}from"./shared.js";import{parseListsManagement as g}from"./lists.js";import{TWITTER_BEARER_TOKEN as h}from"./utils.js";const y="78UbkyXwXBD98IgUWXOy9g",v={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!1,responsive_web_grok_share_attachment_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!1,creator_subscriptions_quote_tweet_preview_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_enhance_cards_enabled:!1};function k(e){return e===!0||e==="true"}async function m(e,t){const s=`/i/api/graphql/${await b(e,"ListsManagementPageTimeline",y)}/ListsManagementPageTimeline?features=${encodeURIComponent(JSON.stringify(v))}`,i=c(await e.evaluate(`async () => {
|
|
2
2
|
const r = await fetch(${JSON.stringify(s)}, { headers: ${t}, credentials: 'include' });
|
|
3
3
|
if (!r.ok) return { __error: 'HTTP ' + r.status };
|
|
4
4
|
return await r.json();
|
|
5
|
-
}`));if(i&&i.__error)throw new
|
|
5
|
+
}`));if(i&&i.__error)throw new o(`Could not fetch lists: ${i.__error}`);return g(i,new Set)}export function buildListDeleteRow({listId:e,targetList:t}){return{listId:e,name:t.name,members:String(t.members??"0"),status:"success",message:`Deleted list ${t.name} (${t.members??"0"} members)`}}f({site:"twitter",name:"list-delete",access:"write",description:"Delete a Twitter/X list you own after explicit confirmation",domain:"x.com",strategy:w.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `ppcli twitter lists`)"},{name:"confirm",type:"boolean",default:!1,help:"Required. Set --confirm true to delete the list."},{name:"timeout",type:"int",default:300,help:"Max seconds for the overall delete command (default: 300)"}],columns:["listId","name","members","status","message"],func:async(e,t)=>{const n=String(t.listId||"").trim();if(!n||!/^\d+$/.test(n))throw new d(`Invalid listId: ${JSON.stringify(t.listId)}. Expected numeric ID.`,"Example: ppcli twitter list-delete 123456789 --confirm true");if(!k(t.confirm))throw new d("Refusing to delete list without --confirm true","Example: ppcli twitter list-delete 123456789 --confirm true");await e.goto("https://x.com");await e.wait(3);const i=(await e.getCookies({url:"https://x.com"})).find((r)=>r.name==="ct0")?.value||null;if(!i)throw new p("x.com","Not logged into x.com (no ct0 cookie)");const a=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(h)}`,"X-Csrf-Token":i,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),l=await m(e,a),_=l.find((r)=>r.id===n);if(!_)throw new o(`List ${n} not found among your lists (${l.length} lists fetched).`);await e.goto(`https://x.com/i/lists/${n}`);await e.wait({selector:'[data-testid="primaryColumn"]'});const u=c(await e.evaluate(`(async () => {
|
|
6
6
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
7
|
const visible = (el) => !!el && el.offsetParent !== null;
|
|
8
8
|
const buttonText = (el) => (el.innerText || el.textContent || '').trim();
|
|
@@ -33,4 +33,4 @@ import{cli as f,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
33
33
|
confirmButton.click();
|
|
34
34
|
await sleep(2500);
|
|
35
35
|
return { ok: true, url: location.href };
|
|
36
|
-
})()`));if(!u?.ok)throw new
|
|
36
|
+
})()`));if(!u?.ok)throw new o(`Failed to delete list ${n}: ${u?.message||"unknown UI failure"}`);if((await m(e,a)).some((r)=>r.id===n))throw new o(`Failed to delete list ${n}: list still appears in managed lists.`);return[buildListDeleteRow({listId:n,targetList:_})]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as i,Strategy as o}from"@jackwener/opencli/registry";import{listRemoveUser as n}from"./list-remove-core.js";import{parseBatchIntervalSeconds as m,parseCommaSeparatedUsernames as l,runListBatch as u}from"./list-batch-utils.js";const c='Example: ppcli twitter list-remove-batch 123456789 "@alice,@bob" --interval 5';i({site:"twitter",name:"list-remove-batch",access:"write",description:"Remove multiple users from a Twitter/X list you own from a comma-separated username list",domain:"x.com",strategy:o.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `ppcli twitter lists`)"},{name:"usernames",positional:!0,type:"string",required:!0,help:"Comma-separated Twitter/X handles to remove (with or without @)"},{name:"interval",type:"int",default:5,help:"Seconds to wait between account removals (default: 5)"},{name:"timeout",type:"int",default:600,help:"Max seconds for the overall batch command (default: 600)"}],columns:["listId","username","userId","status","message"],func:async(t,e)=>{const r=String(e.listId||"").trim(),a=l(e.usernames,c),s=m(e.interval);return u({page:t,listId:r,usernames:a,interval:s,operation:n})}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as e,Strategy as t}from"@jackwener/opencli/registry";import{interpretRemoveResponse,listRemoveUser}from"./list-remove-core.js";e({site:"twitter",name:"list-remove",access:"write",description:"Remove a user from a Twitter/X list you own (toggles via UI; no-op if not currently a member)",domain:"x.com",strategy:t.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `
|
|
1
|
+
import{cli as e,Strategy as t}from"@jackwener/opencli/registry";import{interpretRemoveResponse,listRemoveUser}from"./list-remove-core.js";e({site:"twitter",name:"list-remove",access:"write",description:"Remove a user from a Twitter/X list you own (toggles via UI; no-op if not currently a member)",domain:"x.com",strategy:t.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `ppcli twitter lists`)"},{name:"username",positional:!0,type:"string",required:!0,help:"Twitter/X handle to remove (with or without @)"}],columns:["listId","username","userId","status","message"],func:listRemoveUser});export{interpretRemoveResponse,listRemoveUser};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as U,Strategy as h}from"@jackwener/opencli/registry";import{AuthRequiredError as S,CommandExecutionError as j}from"@jackwener/opencli/errors";import{BROWSER_JSON_SNIFF_FN as O,throwIfLoginWall as k}from"@jackwener/opencli/utils";import{TWITTER_BEARER_TOKEN as C,applyTopByEngagement as R}from"./utils.js";import{extractCard as _,extractQuotedTweet as v,extractMedia as A,describeTwitterApiError as N}from"./shared.js";const x="RlZzktZY_9wJynoepm8ZsA",L="ListLatestTweetsTimeline",m=100,p={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!1,responsive_web_grok_share_attachment_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!1,creator_subscriptions_quote_tweet_preview_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_enhance_cards_enabled:!1};function I($,Z,z,H){const D={listId:String(Z),count:z};if(H)D.cursor=H;return`/i/api/graphql/${$}/${L}?variables=${encodeURIComponent(JSON.stringify(D))}&features=${encodeURIComponent(JSON.stringify(p))}`}export function extractTimelineTweet($,Z){if(!$)return null;const z=$.tweet||$,H=z.legacy||{};if(!z.rest_id||Z.has(z.rest_id))return null;Z.add(z.rest_id);const D=z.core?.user_results?.result,X=D?.legacy?.screen_name||D?.core?.screen_name||"unknown",Y=D?.legacy?.name||D?.core?.name||"",V=D?.legacy?.description||"",f=z.note_tweet?.note_tweet_results?.result?.text;return{id:z.rest_id,author:X,name:Y,bio:V,text:f||H.full_text||"",likes:H.favorite_count||0,retweets:H.retweet_count||0,replies:H.reply_count||0,created_at:H.created_at||"",url:`https://x.com/${X}/status/${z.rest_id}`,...A(H),card:_(z),quoted_tweet:v(z)}}export function parseListTimeline($,Z){const z=[];let H=null;const D=$?.data?.list?.tweets_timeline?.timeline?.instructions||[];for(const X of D)for(const Y of X.entries||[]){const V=Y.content;if(V?.entryType==="TimelineTimelineCursor"||V?.__typename==="TimelineTimelineCursor"){if(V.cursorType==="Bottom"||V.cursorType==="ShowMore")H=V.value;continue}if(Y.entryId?.startsWith("cursor-bottom-")||Y.entryId?.startsWith("cursor-showMore-")){H=V?.value||V?.itemContent?.value||H;continue}const f=extractTimelineTweet(V?.itemContent?.tweet_results?.result,Z);if(f){z.push(f);continue}for(const G of V?.items||[]){const K=extractTimelineTweet(G.item?.itemContent?.tweet_results?.result,Z);if(K)z.push(K)}}return{tweets:z,nextCursor:H}}U({site:"twitter",name:"list-tweets",access:"read",description:"Fetch tweets from a Twitter/X list timeline",domain:"x.com",strategy:h.COOKIE,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of a Twitter/X list (e.g. from `
|
|
1
|
+
import{cli as U,Strategy as h}from"@jackwener/opencli/registry";import{AuthRequiredError as S,CommandExecutionError as j}from"@jackwener/opencli/errors";import{BROWSER_JSON_SNIFF_FN as O,throwIfLoginWall as k}from"@jackwener/opencli/utils";import{TWITTER_BEARER_TOKEN as C,applyTopByEngagement as R}from"./utils.js";import{extractCard as _,extractQuotedTweet as v,extractMedia as A,describeTwitterApiError as N}from"./shared.js";const x="RlZzktZY_9wJynoepm8ZsA",L="ListLatestTweetsTimeline",m=100,p={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!1,responsive_web_grok_share_attachment_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!1,creator_subscriptions_quote_tweet_preview_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_enhance_cards_enabled:!1};function I($,Z,z,H){const D={listId:String(Z),count:z};if(H)D.cursor=H;return`/i/api/graphql/${$}/${L}?variables=${encodeURIComponent(JSON.stringify(D))}&features=${encodeURIComponent(JSON.stringify(p))}`}export function extractTimelineTweet($,Z){if(!$)return null;const z=$.tweet||$,H=z.legacy||{};if(!z.rest_id||Z.has(z.rest_id))return null;Z.add(z.rest_id);const D=z.core?.user_results?.result,X=D?.legacy?.screen_name||D?.core?.screen_name||"unknown",Y=D?.legacy?.name||D?.core?.name||"",V=D?.legacy?.description||"",f=z.note_tweet?.note_tweet_results?.result?.text;return{id:z.rest_id,author:X,name:Y,bio:V,text:f||H.full_text||"",likes:H.favorite_count||0,retweets:H.retweet_count||0,replies:H.reply_count||0,created_at:H.created_at||"",url:`https://x.com/${X}/status/${z.rest_id}`,...A(H),card:_(z),quoted_tweet:v(z)}}export function parseListTimeline($,Z){const z=[];let H=null;const D=$?.data?.list?.tweets_timeline?.timeline?.instructions||[];for(const X of D)for(const Y of X.entries||[]){const V=Y.content;if(V?.entryType==="TimelineTimelineCursor"||V?.__typename==="TimelineTimelineCursor"){if(V.cursorType==="Bottom"||V.cursorType==="ShowMore")H=V.value;continue}if(Y.entryId?.startsWith("cursor-bottom-")||Y.entryId?.startsWith("cursor-showMore-")){H=V?.value||V?.itemContent?.value||H;continue}const f=extractTimelineTweet(V?.itemContent?.tweet_results?.result,Z);if(f){z.push(f);continue}for(const G of V?.items||[]){const K=extractTimelineTweet(G.item?.itemContent?.tweet_results?.result,Z);if(K)z.push(K)}}return{tweets:z,nextCursor:H}}U({site:"twitter",name:"list-tweets",access:"read",description:"Fetch tweets from a Twitter/X list timeline",domain:"x.com",strategy:h.COOKIE,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of a Twitter/X list (e.g. from `ppcli twitter lists`)"},{name:"limit",type:"int",default:50},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the list timeline by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the list's native (recency) ordering."}],columns:["id","author","bio","text","likes","retweets","replies","created_at","url","has_media","media_urls","media_posters","card","quoted_tweet"],func:async($,Z)=>{const z=String(Z.listId||"").trim();if(!z||!/^\d+$/.test(z))throw new j(`Invalid listId: ${JSON.stringify(Z.listId)}. Expected a numeric ID (see \`ppcli twitter lists\`).`);const H=Z.limit||50,X=(await $.getCookies({url:"https://x.com"})).find((J)=>J.name==="ct0")?.value||null;if(!X)throw new S("x.com","Not logged into x.com (no ct0 cookie)");const Y=(J)=>J&&typeof J==="object"&&("session"in J)&&("data"in J)?J.data:J,V=await $.evaluate(`async () => {
|
|
2
2
|
try {
|
|
3
3
|
const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
|
|
4
4
|
if (ghResp.ok) {
|
package/clis/twitter/profile.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as k,AuthRequiredError as z,CommandExecutionError as M,EmptyResultError as B}from"@jackwener/opencli/errors";import{cli as I,Strategy as q}from"@jackwener/opencli/registry";import{describeTwitterApiError as U,normalizeTwitterScreenName as Q,resolveTwitterQueryId as b,unwrapBrowserResult as Y}from"./shared.js";import{TWITTER_BEARER_TOKEN as x}from"./utils.js";const S="IGgvgiOx4QZndDHuD3x9TQ";function W(G){return G!=null&&typeof G==="object"&&!Array.isArray(G)}function H(G){return typeof G==="string"?G:""}export function mapTwitterProfileResult(G,J){if(!W(G))throw new M(`Twitter profile response for @${J} is malformed`);const K=W(G.legacy),Z=W(G.core);if(!K&&!Z)throw new M(`Twitter profile response for @${J} is missing profile fields`);const V=K?G.legacy:{},$=Z?G.core:{};if(!H($.screen_name)&&!H(V.screen_name)&&!H($.name)&&!H(V.name)&&!H($.created_at)&&!H(V.created_at))throw new M(`Twitter profile response for @${J} is missing profile identity fields`);const L=W(G.location)?G.location:{},X=V.entities?.url?.urls?.[0]?.expanded_url||"";return[{screen_name:H($.screen_name)||H(V.screen_name)||J,name:H($.name)||H(V.name),bio:H(V.description),location:H(L.location)||H(V.location),url:H(X),followers:V.followers_count||0,following:V.friends_count||0,tweets:V.statuses_count||0,likes:V.favourites_count||0,verified:Boolean(G.is_blue_verified||V.verified),created_at:H($.created_at)||H(V.created_at)}]}I({site:"twitter",name:"profile",access:"read",description:"Fetch a Twitter user profile — bio, stats, etc. (defaults to the logged-in user when no username is given)",domain:"x.com",strategy:q.COOKIE,browser:!0,args:[{name:"username",type:"string",positional:!0,help:"Twitter screen name (with or without @). Defaults to the logged-in user when omitted."}],columns:["screen_name","name","bio","location","url","followers","following","tweets","likes","verified","created_at"],func:async(G,J)=>{const K=String(J.username??"").trim();let Z=Q(K);if(K&&!Z)throw new k("twitter profile username must be a valid Twitter/X handle","Example:
|
|
1
|
+
import{ArgumentError as k,AuthRequiredError as z,CommandExecutionError as M,EmptyResultError as B}from"@jackwener/opencli/errors";import{cli as I,Strategy as q}from"@jackwener/opencli/registry";import{describeTwitterApiError as U,normalizeTwitterScreenName as Q,resolveTwitterQueryId as b,unwrapBrowserResult as Y}from"./shared.js";import{TWITTER_BEARER_TOKEN as x}from"./utils.js";const S="IGgvgiOx4QZndDHuD3x9TQ";function W(G){return G!=null&&typeof G==="object"&&!Array.isArray(G)}function H(G){return typeof G==="string"?G:""}export function mapTwitterProfileResult(G,J){if(!W(G))throw new M(`Twitter profile response for @${J} is malformed`);const K=W(G.legacy),Z=W(G.core);if(!K&&!Z)throw new M(`Twitter profile response for @${J} is missing profile fields`);const V=K?G.legacy:{},$=Z?G.core:{};if(!H($.screen_name)&&!H(V.screen_name)&&!H($.name)&&!H(V.name)&&!H($.created_at)&&!H(V.created_at))throw new M(`Twitter profile response for @${J} is missing profile identity fields`);const L=W(G.location)?G.location:{},X=V.entities?.url?.urls?.[0]?.expanded_url||"";return[{screen_name:H($.screen_name)||H(V.screen_name)||J,name:H($.name)||H(V.name),bio:H(V.description),location:H(L.location)||H(V.location),url:H(X),followers:V.followers_count||0,following:V.friends_count||0,tweets:V.statuses_count||0,likes:V.favourites_count||0,verified:Boolean(G.is_blue_verified||V.verified),created_at:H($.created_at)||H(V.created_at)}]}I({site:"twitter",name:"profile",access:"read",description:"Fetch a Twitter user profile — bio, stats, etc. (defaults to the logged-in user when no username is given)",domain:"x.com",strategy:q.COOKIE,browser:!0,args:[{name:"username",type:"string",positional:!0,help:"Twitter screen name (with or without @). Defaults to the logged-in user when omitted."}],columns:["screen_name","name","bio","location","url","followers","following","tweets","likes","verified","created_at"],func:async(G,J)=>{const K=String(J.username??"").trim();let Z=Q(K);if(K&&!Z)throw new k("twitter profile username must be a valid Twitter/X handle","Example: ppcli twitter profile @jack");if(!Z){await G.goto("https://x.com/home");await G.wait({selector:'[data-testid="primaryColumn"]'});const D=Y(await G.evaluate(`() => {
|
|
2
2
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
3
3
|
return link ? link.getAttribute('href') : null;
|
|
4
4
|
}`));if(!D||typeof D!=="string")throw new z("x.com","Could not detect logged-in user. Are you logged in?");Z=Q(D);if(!Z)throw new z("x.com","Could not detect logged-in user. Are you logged in?")}await G.goto(`https://x.com/${Z}`);await G.wait(3);const $=(await G.getCookies({url:"https://x.com"})).find((D)=>D.name==="ct0")?.value||null;if(!$)throw new z("x.com","Not logged into x.com (no ct0 cookie)");const L=await b(G,"UserByScreenName",S),X=Y(await G.evaluate(`
|
package/clis/twitter/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as f,AuthRequiredError as C,CommandExecutionError as d}from"@jackwener/opencli/errors";import{cli as O,Strategy as _}from"@jackwener/opencli/registry";import{extractMedia as R,extractCard as k,extractQuotedTweet as c,normalizeTwitterGraphqlPayload as T,resolveTwitterOperationMetadata as p,describeTwitterApiError as E}from"./shared.js";import{TWITTER_BEARER_TOKEN as g,applyTopByEngagement as n}from"./utils.js";const q=Object.freeze(["media","images","videos","links","replies"]),x=Object.freeze(["replies","retweets","media","links"]),D=Object.freeze(["top","live","photos","videos"]),Q=Object.freeze({top:"top",live:"live",photos:"image",videos:"video"}),i=Object.freeze({top:"Top",live:"Latest",photos:"Photos",videos:"Videos"}),w=100,j={queryId:"Yw6L66Pw54NHKuq4Dp7b4Q",features:{rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},fieldToggles:{withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0}},L=/^[A-Za-z0-9_]{1,15}$/,M=Object.freeze({replies:"-filter:replies",retweets:"-filter:nativeretweets",media:"-filter:media",links:"-filter:links"});function H(J,V){const Y=[String(J??"").trim()];if(V.from){const $=String(V.from).trim().replace(/^@+/,"");if($&&!L.test($))throw new f(`Invalid --from username: ${JSON.stringify(V.from)}`,"Use a Twitter/X handle with 1-15 letters, numbers, or underscores; omit @ or pass @handle.");if($)Y.push(`from:${$}`)}if(V.has)Y.push(`filter:${V.has}`);if(V.exclude){const $=M[V.exclude];if($)Y.push($)}return Y.filter(Boolean).join(" ")}function l(J){if(J.product){const V=Q[J.product];if(V)return V}return J.filter==="live"?"live":"top"}function y(J){const V=J.product||(J.filter==="live"?"live":"top");return i[V]||"Top"}function o(J){if(typeof J==="string")return{queryId:J,features:j.features,fieldToggles:j.fieldToggles};return{queryId:J?.queryId||j.queryId,features:J?.features||j.features,fieldToggles:J?.fieldToggles||j.fieldToggles}}function N(J,V,Y,$,W){const B=o(J),Z={rawQuery:V,count:$,querySource:"typed_query",product:Y};if(W)Z.cursor=W;return[`/i/api/graphql/${B.queryId}/SearchTimeline`,{variables:Z,features:B.features,fieldToggles:B.fieldToggles}]}function u(J){if(!J)return null;if(J.__typename==="TweetWithVisibilityResults"&&J.tweet)return J.tweet;if(J.tweet)return J.tweet;return J}function s(J,V){const Y=u(J);if(!Y?.rest_id||V.has(Y.rest_id))return null;V.add(Y.rest_id);const $=Y.core?.user_results?.result,W=$?.legacy?.description||"";return{id:Y.rest_id,author:$?.core?.screen_name||$?.legacy?.screen_name||"",bio:W,text:Y.note_tweet?.note_tweet_results?.result?.text||Y.legacy?.full_text||"",created_at:Y.legacy?.created_at||"",likes:Y.legacy?.favorite_count||0,views:Y.views?.count||"0",url:`https://x.com/i/status/${Y.rest_id}`,...R(Y.legacy),card:k(Y),quoted_tweet:c(Y)}}function h(J,V){const Y=[];let $=null;const W=J?.data?.search_by_raw_query?.search_timeline?.timeline?.instructions||[],B=(Z)=>{if(!Z||typeof Z!=="object")return;if(Z.tweet_results?.result){const K=s(Z.tweet_results.result,V);if(K)Y.push(K)}if((Z.entryType==="TimelineTimelineCursor"||Z.__typename==="TimelineTimelineCursor")&&(Z.cursorType==="Bottom"||Z.cursorType==="ShowMore")&&Z.value)$=Z.value;if(Array.isArray(Z)){for(const K of Z)B(K);return}for(const K of Object.values(Z))if(K&&typeof K==="object")B(K)};B(W);return{rows:Y,nextCursor:$}}O({site:"twitter",name:"search",access:"read",description:"Search Twitter/X for tweets, with optional --from / --has / --exclude / --product filters mapped to X's search operators",domain:"x.com",strategy:_.COOKIE,browser:!0,args:[{name:"query",type:"string",required:!0,positional:!0,help:'Search query. Raw X operators (e.g. "exact phrase", #tag, OR, lang:en, since:YYYY-MM-DD, from:, since:) are passed through unchanged.'},{name:"filter",type:"string",default:"top",choices:["top","live"],help:"Legacy alias for --product. Kept for backwards compatibility; if --product is set it wins."},{name:"product",type:"string",choices:D,help:"Which X search tab to read: top (default), live (Latest), photos, videos. Maps to the f= URL param."},{name:"from",type:"string",help:"Restrict to tweets authored by <user>. Leading @ is stripped. Equivalent to appending `from:<user>` to the query."},{name:"has",type:"string",choices:q,help:"Restrict to tweets that have media|images|videos|links|replies. Maps to X's `filter:<has>` operator."},{name:"exclude",type:"string",choices:x,help:"Exclude tweets matching <type>: replies|retweets|media|links. Maps to X's `-filter:<x>` operator (retweets → -filter:nativeretweets)."},{name:"limit",type:"int",default:15,help:"Maximum number of tweets to return (default 15). Result count after server-side filtering."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the results by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps X's native ordering."}],columns:["id","author","bio","text","created_at","likes","views","url","has_media","media_urls","media_posters","card","quoted_tweet"],func:async(J,V)=>{const Y=H(V.query,V);if(!Y)throw new f("twitter search query is empty","Provide a non-empty <query>, or use at least one of --from / --has / --exclude.");if(!Number.isInteger(Number(V.limit))||Number(V.limit)<=0)throw new f("twitter search --limit must be a positive integer","Example:
|
|
1
|
+
import{ArgumentError as f,AuthRequiredError as C,CommandExecutionError as d}from"@jackwener/opencli/errors";import{cli as O,Strategy as _}from"@jackwener/opencli/registry";import{extractMedia as R,extractCard as k,extractQuotedTweet as c,normalizeTwitterGraphqlPayload as T,resolveTwitterOperationMetadata as p,describeTwitterApiError as E}from"./shared.js";import{TWITTER_BEARER_TOKEN as g,applyTopByEngagement as n}from"./utils.js";const q=Object.freeze(["media","images","videos","links","replies"]),x=Object.freeze(["replies","retweets","media","links"]),D=Object.freeze(["top","live","photos","videos"]),Q=Object.freeze({top:"top",live:"live",photos:"image",videos:"video"}),i=Object.freeze({top:"Top",live:"Latest",photos:"Photos",videos:"Videos"}),w=100,j={queryId:"Yw6L66Pw54NHKuq4Dp7b4Q",features:{rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},fieldToggles:{withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0}},L=/^[A-Za-z0-9_]{1,15}$/,M=Object.freeze({replies:"-filter:replies",retweets:"-filter:nativeretweets",media:"-filter:media",links:"-filter:links"});function H(J,V){const Y=[String(J??"").trim()];if(V.from){const $=String(V.from).trim().replace(/^@+/,"");if($&&!L.test($))throw new f(`Invalid --from username: ${JSON.stringify(V.from)}`,"Use a Twitter/X handle with 1-15 letters, numbers, or underscores; omit @ or pass @handle.");if($)Y.push(`from:${$}`)}if(V.has)Y.push(`filter:${V.has}`);if(V.exclude){const $=M[V.exclude];if($)Y.push($)}return Y.filter(Boolean).join(" ")}function l(J){if(J.product){const V=Q[J.product];if(V)return V}return J.filter==="live"?"live":"top"}function y(J){const V=J.product||(J.filter==="live"?"live":"top");return i[V]||"Top"}function o(J){if(typeof J==="string")return{queryId:J,features:j.features,fieldToggles:j.fieldToggles};return{queryId:J?.queryId||j.queryId,features:J?.features||j.features,fieldToggles:J?.fieldToggles||j.fieldToggles}}function N(J,V,Y,$,W){const B=o(J),Z={rawQuery:V,count:$,querySource:"typed_query",product:Y};if(W)Z.cursor=W;return[`/i/api/graphql/${B.queryId}/SearchTimeline`,{variables:Z,features:B.features,fieldToggles:B.fieldToggles}]}function u(J){if(!J)return null;if(J.__typename==="TweetWithVisibilityResults"&&J.tweet)return J.tweet;if(J.tweet)return J.tweet;return J}function s(J,V){const Y=u(J);if(!Y?.rest_id||V.has(Y.rest_id))return null;V.add(Y.rest_id);const $=Y.core?.user_results?.result,W=$?.legacy?.description||"";return{id:Y.rest_id,author:$?.core?.screen_name||$?.legacy?.screen_name||"",bio:W,text:Y.note_tweet?.note_tweet_results?.result?.text||Y.legacy?.full_text||"",created_at:Y.legacy?.created_at||"",likes:Y.legacy?.favorite_count||0,views:Y.views?.count||"0",url:`https://x.com/i/status/${Y.rest_id}`,...R(Y.legacy),card:k(Y),quoted_tweet:c(Y)}}function h(J,V){const Y=[];let $=null;const W=J?.data?.search_by_raw_query?.search_timeline?.timeline?.instructions||[],B=(Z)=>{if(!Z||typeof Z!=="object")return;if(Z.tweet_results?.result){const K=s(Z.tweet_results.result,V);if(K)Y.push(K)}if((Z.entryType==="TimelineTimelineCursor"||Z.__typename==="TimelineTimelineCursor")&&(Z.cursorType==="Bottom"||Z.cursorType==="ShowMore")&&Z.value)$=Z.value;if(Array.isArray(Z)){for(const K of Z)B(K);return}for(const K of Object.values(Z))if(K&&typeof K==="object")B(K)};B(W);return{rows:Y,nextCursor:$}}O({site:"twitter",name:"search",access:"read",description:"Search Twitter/X for tweets, with optional --from / --has / --exclude / --product filters mapped to X's search operators",domain:"x.com",strategy:_.COOKIE,browser:!0,args:[{name:"query",type:"string",required:!0,positional:!0,help:'Search query. Raw X operators (e.g. "exact phrase", #tag, OR, lang:en, since:YYYY-MM-DD, from:, since:) are passed through unchanged.'},{name:"filter",type:"string",default:"top",choices:["top","live"],help:"Legacy alias for --product. Kept for backwards compatibility; if --product is set it wins."},{name:"product",type:"string",choices:D,help:"Which X search tab to read: top (default), live (Latest), photos, videos. Maps to the f= URL param."},{name:"from",type:"string",help:"Restrict to tweets authored by <user>. Leading @ is stripped. Equivalent to appending `from:<user>` to the query."},{name:"has",type:"string",choices:q,help:"Restrict to tweets that have media|images|videos|links|replies. Maps to X's `filter:<has>` operator."},{name:"exclude",type:"string",choices:x,help:"Exclude tweets matching <type>: replies|retweets|media|links. Maps to X's `-filter:<x>` operator (retweets → -filter:nativeretweets)."},{name:"limit",type:"int",default:15,help:"Maximum number of tweets to return (default 15). Result count after server-side filtering."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the results by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps X's native ordering."}],columns:["id","author","bio","text","created_at","likes","views","url","has_media","media_urls","media_posters","card","quoted_tweet"],func:async(J,V)=>{const Y=H(V.query,V);if(!Y)throw new f("twitter search query is empty","Provide a non-empty <query>, or use at least one of --from / --has / --exclude.");if(!Number.isInteger(Number(V.limit))||Number(V.limit)<=0)throw new f("twitter search --limit must be a positive integer","Example: ppcli twitter search ppcli --limit 15");const W=(await J.getCookies({url:"https://x.com"})).find((z)=>z.name==="ct0")?.value||null;if(!W)throw new C("x.com","Not logged into x.com (no ct0 cookie)");await J.goto("https://x.com/home",{waitUntil:"load",settleMs:1000});const B=await p(J,"SearchTimeline",j),Z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(g)}`,"X-Csrf-Token":W,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),K=y(V),G=[],U=new Set;let F=null;for(let z=0;z<w&&G.length<V.limit;z++){const m=Number(V.limit)-G.length+10,[I,S]=N(B,Y,K,m,F),A=JSON.stringify(S),X=T(await J.evaluate(`async () => {
|
|
2
2
|
const options = {
|
|
3
3
|
method: 'POST',
|
|
4
4
|
headers: ${Z},
|
package/clis/twitter/shared.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as A}from"@jackwener/opencli/errors";const S=/^[A-Za-z0-9_-]+$/,E=/^[A-Za-z0-9_]{1,15}$/,z=/^\/(?:[^/]+|i)\/status\/(\d+)\/?$/,L=new Set(["x.com","twitter.com"]),U=new Set(["x.com","twitter.com","mobile.twitter.com"]),h=new Set(["compose","explore","help","home","i","intent","jobs","login","logout","messages","notifications","privacy","search","settings","signup","tos"]);function I(j){return L.has(j)||j.endsWith(".x.com")||j.endsWith(".twitter.com")}export function parseTweetUrl(j){const G=String(j??"").trim();if(!G)throw new A("twitter tweet URL cannot be empty","Example:
|
|
1
|
+
import{ArgumentError as A}from"@jackwener/opencli/errors";const S=/^[A-Za-z0-9_-]+$/,E=/^[A-Za-z0-9_]{1,15}$/,z=/^\/(?:[^/]+|i)\/status\/(\d+)\/?$/,L=new Set(["x.com","twitter.com"]),U=new Set(["x.com","twitter.com","mobile.twitter.com"]),h=new Set(["compose","explore","help","home","i","intent","jobs","login","logout","messages","notifications","privacy","search","settings","signup","tos"]);function I(j){return L.has(j)||j.endsWith(".x.com")||j.endsWith(".twitter.com")}export function parseTweetUrl(j){const G=String(j??"").trim();if(!G)throw new A("twitter tweet URL cannot be empty","Example: ppcli twitter retweet https://x.com/jack/status/20");let V;try{V=new URL(G)}catch{throw new A(`Invalid tweet URL: ${G}`,"Use a full https://x.com/<user>/status/<id> URL")}const J=V.hostname.toLowerCase();if(V.protocol!=="https:"||!I(J))throw new A(`Invalid tweet URL host: ${G}`,"Use a full https://x.com/<user>/status/<id> URL");const X=V.pathname.match(z);if(!X?.[1])throw new A(`Could not extract tweet ID from URL: ${G}`,"Use a full https://x.com/<user>/status/<id> URL");return{id:X[1],url:V.toString()}}export function buildTwitterArticleScopeSource(j){return`
|
|
2
2
|
const tweetId = ${JSON.stringify(j)};
|
|
3
3
|
const __twTweetPathRe = /^\\/(?:[^/]+|i)\\/status\\/(\\d+)\\/?$/;
|
|
4
4
|
const __twIsTwitterHost = (hostname) => hostname === 'x.com'
|
package/clis/twitter/tweets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as u,Strategy as o}from"@jackwener/opencli/registry";import{ArgumentError as A,AuthRequiredError as L,CommandExecutionError as O,EmptyResultError as s}from"@jackwener/opencli/errors";import{resolveTwitterOperationMetadata as R,sanitizeQueryId as l,extractMedia as t,extractQuotedTweet as r,normalizeTwitterGraphqlPayload as a,unwrapBrowserResult as T,normalizeTwitterScreenName as U,describeTwitterApiError as e}from"./shared.js";import{TWITTER_BEARER_TOKEN as jj,applyTopByEngagement as qj}from"./utils.js";const f="lrMzG9qPQHpqJdP3AbM-bQ",x="IGgvgiOx4QZndDHuD3x9TQ",S=100,C=100,$=S*C,E=2,b={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,payments_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},B={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},D={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},_={withPayments:!0,withAuxiliaryUserLabels:!0},Fj={queryId:f,features:b,fieldToggles:B},Gj={queryId:x,features:D,fieldToggles:_};function Hj(j){if(typeof j==="string")return{queryId:j,features:b,fieldToggles:B};return{queryId:j?.queryId||f,features:j?.features||b,fieldToggles:j?.fieldToggles||B}}function Jj(j){if(typeof j==="string")return{queryId:j,features:D,fieldToggles:_};return{queryId:j?.queryId||x,features:j?.features||D,fieldToggles:j?.fieldToggles||_}}function k(j,q,F){const G=F.fieldToggles||{},J=[`variables=${encodeURIComponent(JSON.stringify(q))}`,`features=${encodeURIComponent(JSON.stringify(F.features||{}))}`];if(Object.keys(G).length>0)J.push(`fieldToggles=${encodeURIComponent(JSON.stringify(G))}`);return`${j}?${J.join("&")}`}function N(j,q,F,G){const J=Hj(j),K={userId:q,count:F,includePromotedContent:!1,withQuickPromoteEligibilityTweetFields:!0,withVoice:!0};if(G)K.cursor=G;return k(`/i/api/graphql/${J.queryId}/UserTweets`,K,J)}function h(j,q){const F=Jj(j),G={screen_name:q,withSafetyModeUserFields:!0};return k(`/i/api/graphql/${F.queryId}/UserByScreenName`,G,F)}function y(j,q){if(!j)return null;const F=j.__typename==="TweetWithVisibilityResults"&&j.tweet?j.tweet:j.tweet||j,G=F.legacy||{};if(!F.rest_id||q.has(F.rest_id))return null;q.add(F.rest_id);const J=F.core?.user_results?.result,K=J?.legacy?.screen_name||J?.core?.screen_name||"unknown",Z=J?.legacy?.name||J?.core?.name||"",W=F.note_tweet?.note_tweet_results?.result?.text,H=Boolean(G.retweeted_status_result||G.full_text?.startsWith("RT @"));return{id:F.rest_id,author:K,name:Z,text:W||G.full_text||"",likes:G.favorite_count||0,retweets:G.retweet_count||0,replies:G.reply_count||0,views:Number(F.views?.count)||0,is_retweet:H,created_at:G.created_at||"",url:`https://x.com/${K}/status/${F.rest_id}`,...t(G),quoted_tweet:r(F)}}function d(j,q){const F=[];let G=null;const J=j?.data?.user?.result||{},Z=[J.timeline_v2?.timeline?.instructions,J.timeline?.timeline?.instructions].filter(Array.isArray).flat(),W=(H)=>{if(!H||typeof H!=="object")return;if(H.type==="TimelinePinEntry")return;if(H.tweet_results?.result){const Q=y(H.tweet_results.result,q);if(Q)F.push(Q)}if((H.entryType==="TimelineTimelineCursor"||H.__typename==="TimelineTimelineCursor")&&(H.cursorType==="Bottom"||H.cursorType==="ShowMore")&&H.value)G=H.value;if(Array.isArray(H)){for(const Q of H)W(Q);return}for(const Q of Object.values(H))if(Q&&typeof Q==="object")W(Q)};W(Z);return{tweets:F,nextCursor:G}}function w(j){const q=j??20;if(!Number.isInteger(q)||q<1||q>$)throw new A(`twitter tweets --limit must be an integer between 1 and ${$}`,"Example:
|
|
1
|
+
import{cli as u,Strategy as o}from"@jackwener/opencli/registry";import{ArgumentError as A,AuthRequiredError as L,CommandExecutionError as O,EmptyResultError as s}from"@jackwener/opencli/errors";import{resolveTwitterOperationMetadata as R,sanitizeQueryId as l,extractMedia as t,extractQuotedTweet as r,normalizeTwitterGraphqlPayload as a,unwrapBrowserResult as T,normalizeTwitterScreenName as U,describeTwitterApiError as e}from"./shared.js";import{TWITTER_BEARER_TOKEN as jj,applyTopByEngagement as qj}from"./utils.js";const f="lrMzG9qPQHpqJdP3AbM-bQ",x="IGgvgiOx4QZndDHuD3x9TQ",S=100,C=100,$=S*C,E=2,b={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,payments_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},B={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},D={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},_={withPayments:!0,withAuxiliaryUserLabels:!0},Fj={queryId:f,features:b,fieldToggles:B},Gj={queryId:x,features:D,fieldToggles:_};function Hj(j){if(typeof j==="string")return{queryId:j,features:b,fieldToggles:B};return{queryId:j?.queryId||f,features:j?.features||b,fieldToggles:j?.fieldToggles||B}}function Jj(j){if(typeof j==="string")return{queryId:j,features:D,fieldToggles:_};return{queryId:j?.queryId||x,features:j?.features||D,fieldToggles:j?.fieldToggles||_}}function k(j,q,F){const G=F.fieldToggles||{},J=[`variables=${encodeURIComponent(JSON.stringify(q))}`,`features=${encodeURIComponent(JSON.stringify(F.features||{}))}`];if(Object.keys(G).length>0)J.push(`fieldToggles=${encodeURIComponent(JSON.stringify(G))}`);return`${j}?${J.join("&")}`}function N(j,q,F,G){const J=Hj(j),K={userId:q,count:F,includePromotedContent:!1,withQuickPromoteEligibilityTweetFields:!0,withVoice:!0};if(G)K.cursor=G;return k(`/i/api/graphql/${J.queryId}/UserTweets`,K,J)}function h(j,q){const F=Jj(j),G={screen_name:q,withSafetyModeUserFields:!0};return k(`/i/api/graphql/${F.queryId}/UserByScreenName`,G,F)}function y(j,q){if(!j)return null;const F=j.__typename==="TweetWithVisibilityResults"&&j.tweet?j.tweet:j.tweet||j,G=F.legacy||{};if(!F.rest_id||q.has(F.rest_id))return null;q.add(F.rest_id);const J=F.core?.user_results?.result,K=J?.legacy?.screen_name||J?.core?.screen_name||"unknown",Z=J?.legacy?.name||J?.core?.name||"",W=F.note_tweet?.note_tweet_results?.result?.text,H=Boolean(G.retweeted_status_result||G.full_text?.startsWith("RT @"));return{id:F.rest_id,author:K,name:Z,text:W||G.full_text||"",likes:G.favorite_count||0,retweets:G.retweet_count||0,replies:G.reply_count||0,views:Number(F.views?.count)||0,is_retweet:H,created_at:G.created_at||"",url:`https://x.com/${K}/status/${F.rest_id}`,...t(G),quoted_tweet:r(F)}}function d(j,q){const F=[];let G=null;const J=j?.data?.user?.result||{},Z=[J.timeline_v2?.timeline?.instructions,J.timeline?.timeline?.instructions].filter(Array.isArray).flat(),W=(H)=>{if(!H||typeof H!=="object")return;if(H.type==="TimelinePinEntry")return;if(H.tweet_results?.result){const Q=y(H.tweet_results.result,q);if(Q)F.push(Q)}if((H.entryType==="TimelineTimelineCursor"||H.__typename==="TimelineTimelineCursor")&&(H.cursorType==="Bottom"||H.cursorType==="ShowMore")&&H.value)G=H.value;if(Array.isArray(H)){for(const Q of H)W(Q);return}for(const Q of Object.values(H))if(Q&&typeof Q==="object")W(Q)};W(Z);return{tweets:F,nextCursor:G}}function w(j){const q=j??20;if(!Number.isInteger(q)||q<1||q>$)throw new A(`twitter tweets --limit must be an integer between 1 and ${$}`,"Example: ppcli twitter tweets @jack --limit 250");return q}function c(j){const q=j??E;if(!Number.isInteger(q)||q<0||q>60)throw new A("twitter tweets --page-delay must be an integer between 0 and 60 seconds","Example: ppcli twitter tweets @jack --limit 250 --page-delay 2");return q}u({site:"twitter",name:"tweets",access:"read",description:"Fetch a Twitter user's most recent tweets (chronological, excludes pinned; defaults to the logged-in user when no username is given)",domain:"x.com",strategy:o.COOKIE,browser:!0,args:[{name:"username",type:"string",positional:!0,help:"Twitter screen name (with or without @). Defaults to the logged-in user when omitted."},{name:"limit",type:"int",default:20,help:`Max tweets to return (1-${$}; fetched across cursor pages)`},{name:"page-delay",type:"int",default:E,help:"Seconds to wait between paginated timeline requests to reduce rate-limit risk. Use 0 to disable."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the tweets by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the chronological ordering."}],columns:["id","author","created_at","is_retweet","text","likes","retweets","replies","views","url","has_media","media_urls","media_posters","quoted_tweet"],func:async(j,q)=>{const F=w(q.limit),G=c(q["page-delay"]),J=String(q.username??"").trim();let K=U(J);if(J&&!K)throw new A("twitter tweets username must be a valid Twitter/X handle","Example: ppcli twitter tweets @jack --limit 20");if(!K){await j.goto("https://x.com/home");await j.wait({selector:'[data-testid="primaryColumn"]'});const V=T(await j.evaluate(`() => {
|
|
2
2
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
3
3
|
return link ? link.getAttribute('href') : null;
|
|
4
4
|
}`));if(!V||typeof V!=="string")throw new L("x.com","Could not detect logged-in user. Are you logged in?");K=U(V);if(!K)throw new L("x.com","Could not detect logged-in user. Are you logged in?")}const W=(await j.getCookies({url:"https://x.com"})).find((V)=>V.name==="ct0")?.value||null;if(!W)throw new L("x.com","Not logged into x.com (no ct0 cookie)");const H=await R(j,"UserTweets",Fj),Q=await R(j,"UserByScreenName",Gj),z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(jj)}`,"X-Csrf-Token":W,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),m=h(Q,K),I=T(await j.evaluate(`async () => {
|
package/clis/uisdc/news.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as d,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as s,EmptyResultError as h,getErrorMessage as b}from"@jackwener/opencli/errors";const g="https://www.uisdc.com/news",c=20,o=50;function l(t){const r=Number(t??c);if(!Number.isInteger(r)||r<=0)throw new a("limit must be a positive integer",`Example:
|
|
1
|
+
import{cli as d,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as s,EmptyResultError as h,getErrorMessage as b}from"@jackwener/opencli/errors";const g="https://www.uisdc.com/news",c=20,o=50;function l(t){const r=Number(t??c);if(!Number.isInteger(r)||r<=0)throw new a("limit must be a positive integer",`Example: ppcli uisdc news --limit ${c}`);if(r>o)throw new a(`limit must be <= ${o}`,`Example: ppcli uisdc news --limit ${o}`);return r}function u(t){return String(t??"").replace(/\s+/g," ").trim()}function m(){return`
|
|
2
2
|
(() => {
|
|
3
3
|
const cards = Array.from(document.querySelectorAll(
|
|
4
4
|
'.news-list > .news-item:first-child > .item-content > .dubao-items > .dubao-item'
|
package/clis/v2ex/daily.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{CommandExecutionError as B}from"@jackwener/opencli/errors";import{cli as G,Strategy as H}from"@jackwener/opencli/registry";G({site:"v2ex",name:"daily",access:"write",description:"V2EX 每日签到并领取铜币",domain:"www.v2ex.com",strategy:H.COOKIE,browser:!0,args:[],columns:["status","message"],func:async(q)=>{if(!q)throw new B("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[
|
|
1
|
+
import{CommandExecutionError as B}from"@jackwener/opencli/errors";import{cli as G,Strategy as H}from"@jackwener/opencli/registry";G({site:"v2ex",name:"daily",access:"write",description:"V2EX 每日签到并领取铜币",domain:"www.v2ex.com",strategy:H.COOKIE,browser:!0,args:[],columns:["status","message"],func:async(q)=>{if(!q)throw new B("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Navigating to /mission/daily");await q.goto("https://www.v2ex.com/mission/daily");for(let z=0;z<5;z++){await new Promise((F)=>setTimeout(F,1500));if(!(await q.evaluate("() => document.title"))?.includes("Just a moment"))break;if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Waiting for Cloudflare...")}const b=await q.evaluate(`
|
|
2
2
|
async () => {
|
|
3
3
|
const btn = document.querySelector('input.super.normal.button');
|
|
4
4
|
if (!btn || !btn.value.includes('领取')) {
|
|
@@ -20,7 +20,7 @@ import{CommandExecutionError as B}from"@jackwener/opencli/errors";import{cli as
|
|
|
20
20
|
debug_body: document.body.innerText.substring(0, 200).replace(/\\n/g, ' ')
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
`);if(b.error){if(process.env.OPENCLI_VERBOSE){console.error(`[
|
|
23
|
+
`);if(b.error){if(process.env.OPENCLI_VERBOSE){console.error(`[ppcli:v2ex:debug] Page Title: ${b.debug_title}`);console.error(`[ppcli:v2ex:debug] Page Body: ${b.debug_body}`)}throw new B(b.error)}if(b.claimed)return[{status:"✅ 已签到",message:b.message}];if(process.env.OPENCLI_VERBOSE)console.error(`[ppcli:v2ex] Found check-in token: once=${b.once}. Checking in...`);await q.goto(`https://www.v2ex.com/mission/daily/redeem?once=${b.once}`);await new Promise((z)=>setTimeout(z,3000));const A=await q.evaluate(`
|
|
24
24
|
async () => {
|
|
25
25
|
const btn = document.querySelector('input.super.normal.button');
|
|
26
26
|
if (!btn || !btn.value.includes('领取')) {
|