publishport-opencli 1.0.0 → 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.
Files changed (333) hide show
  1. package/README.md +61 -61
  2. package/README.zh-CN.md +68 -68
  3. package/cli-manifest.json +954 -61
  4. package/clis/1688/shared.js +2 -2
  5. package/clis/36kr/news.js +1 -1
  6. package/clis/_atlassian/shared.js +1 -1
  7. package/clis/_shared/article/auth.js +1 -1
  8. package/clis/_shared/article/publish.js +1 -1
  9. package/clis/_shared/content-guard.js +1 -1
  10. package/clis/_shared/self-hosted-sites.js +2 -0
  11. package/clis/_shared/site-auth.js +1 -1
  12. package/clis/_shared/token-auth.js +2 -2
  13. package/clis/_shared/video-publish.js +1 -1
  14. package/clis/aibase/news.js +1 -1
  15. package/clis/amazon/shared.js +2 -2
  16. package/clis/antigravity/SKILL.md +12 -12
  17. package/clis/antigravity/storage.js +1 -1
  18. package/clis/apple-podcasts/episodes.js +1 -1
  19. package/clis/archive/item.js +1 -1
  20. package/clis/archive/search.js +1 -1
  21. package/clis/archive/snapshots.js +1 -1
  22. package/clis/archive/wayback.js +1 -1
  23. package/clis/arxiv/author.js +1 -1
  24. package/clis/baijiahao/publish.js +1 -1
  25. package/clis/bbc/utils.js +1 -1
  26. package/clis/bloomberg/businessweek.js +1 -1
  27. package/clis/bloomberg/utils.js +1 -1
  28. package/clis/booking/search.js +1 -1
  29. package/clis/boss/utils.js +1 -1
  30. package/clis/chatgpt/ask.js +1 -1
  31. package/clis/chatgpt/detail.js +1 -1
  32. package/clis/chatgpt/history.js +1 -1
  33. package/clis/chatgpt/project-file-add.js +1 -1
  34. package/clis/chatgpt/project-list.js +1 -1
  35. package/clis/chatgpt/utils.js +2 -2
  36. package/clis/chess/utils.js +1 -1
  37. package/clis/claude/ask.js +1 -1
  38. package/clis/claude/history.js +1 -1
  39. package/clis/claude/utils.js +1 -1
  40. package/clis/codex/extract-diff.js +1 -1
  41. package/clis/coingecko/coin.js +1 -1
  42. package/clis/crates/utils.js +1 -1
  43. package/clis/csdn/stats.js +1 -1
  44. package/clis/dblp/utils.js +1 -1
  45. package/clis/defillama/utils.js +1 -1
  46. package/clis/devto/publish.js +1 -1
  47. package/clis/discord-app/utils.js +1 -1
  48. package/clis/dockerhub/utils.js +1 -1
  49. package/clis/douban/utils.js +1 -1
  50. package/clis/douyin/_shared/tos-upload.js +1 -1
  51. package/clis/douyin/hashtag.js +1 -1
  52. package/clis/douyin/publish-image.js +1 -1
  53. package/clis/douyin/publish.js +1 -1
  54. package/clis/eastmoney/announcement.js +1 -1
  55. package/clis/eastmoney/rank.js +1 -1
  56. package/clis/eastmoney/sectors.js +1 -1
  57. package/clis/endoflife/utils.js +1 -1
  58. package/clis/flathub/utils.js +1 -1
  59. package/clis/gemini/detail.js +1 -1
  60. package/clis/gemini/read.js +1 -1
  61. package/clis/geogebra/add-circle.js +1 -1
  62. package/clis/geogebra/add-line.js +1 -1
  63. package/clis/geogebra/add-point.js +1 -1
  64. package/clis/geogebra/add-polygon.js +1 -1
  65. package/clis/geogebra/eval.js +1 -1
  66. package/clis/geogebra/hexagon.js +1 -1
  67. package/clis/geogebra/info.js +1 -1
  68. package/clis/geogebra/triangle.js +1 -1
  69. package/clis/ghost/login.js +1 -0
  70. package/clis/ghost/publish.js +1 -1
  71. package/clis/ghost/shared.js +24 -1
  72. package/clis/ghost/sites.js +1 -0
  73. package/clis/ghost/whoami.js +1 -1
  74. package/clis/gitee/user.js +2 -2
  75. package/clis/github-trending/repos.js +1 -1
  76. package/clis/goproxy/utils.js +1 -1
  77. package/clis/grok/export-all.js +1 -1
  78. package/clis/grok/export.js +1 -1
  79. package/clis/hashnode/publish.js +1 -1
  80. package/clis/hf/datasets.js +1 -1
  81. package/clis/hf/models.js +1 -1
  82. package/clis/hf/paper.js +1 -1
  83. package/clis/hf/spaces.js +1 -1
  84. package/clis/homebrew/utils.js +1 -1
  85. package/clis/instagram/note.js +1 -1
  86. package/clis/jimeng/generate.js +1 -1
  87. package/clis/juejin/utils.js +1 -1
  88. package/clis/kuaishou/publish.js +1 -1
  89. package/clis/lesswrong/tag.js +1 -1
  90. package/clis/lichess/utils.js +1 -1
  91. package/clis/linkedin/search.js +2 -2
  92. package/clis/linux-do/feed.js +2 -2
  93. package/clis/lobsters/domain.js +1 -1
  94. package/clis/mastodon/login.js +1 -0
  95. package/clis/mastodon/post.js +1 -1
  96. package/clis/mastodon/shared.js +10 -10
  97. package/clis/mastodon/sites.js +1 -0
  98. package/clis/mastodon/whoami.js +1 -1
  99. package/clis/maven/utils.js +1 -1
  100. package/clis/mdn/search.js +1 -1
  101. package/clis/medium/tag.js +1 -1
  102. package/clis/notebooklm/current.js +1 -1
  103. package/clis/notebooklm/get.js +1 -1
  104. package/clis/notebooklm/history.js +1 -1
  105. package/clis/notebooklm/note-list.js +1 -1
  106. package/clis/notebooklm/notes-get.js +1 -1
  107. package/clis/notebooklm/open.js +1 -1
  108. package/clis/notebooklm/source-fulltext.js +1 -1
  109. package/clis/notebooklm/source-get.js +1 -1
  110. package/clis/notebooklm/source-guide.js +1 -1
  111. package/clis/notebooklm/source-list.js +1 -1
  112. package/clis/notebooklm/summary.js +1 -1
  113. package/clis/notebooklm/utils.js +1 -1
  114. package/clis/npm/utils.js +1 -1
  115. package/clis/nuget/utils.js +1 -1
  116. package/clis/nvd/cve.js +1 -1
  117. package/clis/oeis/utils.js +1 -1
  118. package/clis/ones/common.js +2 -2
  119. package/clis/ones/me.js +1 -1
  120. package/clis/ones/tasks.js +1 -1
  121. package/clis/ones/token-info.js +1 -1
  122. package/clis/ones/worklog.js +1 -1
  123. package/clis/openalex/utils.js +1 -1
  124. package/clis/oschina/stats.js +1 -1
  125. package/clis/osv/utils.js +1 -1
  126. package/clis/packagist/utils.js +1 -1
  127. package/clis/pixiv/detail.js +1 -1
  128. package/clis/pixiv/user.js +1 -1
  129. package/clis/producthunt/utils.js +1 -1
  130. package/clis/pypi/utils.js +1 -1
  131. package/clis/qiita/gql.js +2 -2
  132. package/clis/quora/__fixtures__/dom.html +39 -0
  133. package/clis/quora/answer-comments.js +9 -0
  134. package/clis/quora/answer-detail.js +1 -0
  135. package/clis/quora/answer-queue.js +1 -0
  136. package/clis/quora/comment.js +23 -0
  137. package/clis/quora/create-answer.js +4 -4
  138. package/clis/quora/create-question.js +3 -3
  139. package/clis/quora/follow.js +15 -0
  140. package/clis/quora/followers.js +1 -0
  141. package/clis/quora/question.js +1 -0
  142. package/clis/quora/search.js +1 -0
  143. package/clis/quora/shared.js +26 -11
  144. package/clis/quora/stats.js +1 -0
  145. package/clis/quora/upvote.js +14 -0
  146. package/clis/quora/user-answers.js +1 -0
  147. package/clis/quora/user.js +1 -0
  148. package/clis/rest-countries/utils.js +1 -1
  149. package/clis/rfc/utils.js +1 -1
  150. package/clis/rubygems/utils.js +1 -1
  151. package/clis/semanticscholar/utils.js +1 -1
  152. package/clis/slock/errors.js +1 -1
  153. package/clis/spotify/spotify.js +1 -1
  154. package/clis/spotify/utils.js +1 -1
  155. package/clis/stackoverflow/utils.js +1 -1
  156. package/clis/steam/utils.js +1 -1
  157. package/clis/substack/auth.js +2 -2
  158. package/clis/suno/generate.js +2 -2
  159. package/clis/suno/list.js +3 -3
  160. package/clis/telegram/send.js +1 -1
  161. package/clis/tiktok/creator-videos.js +1 -1
  162. package/clis/tiktok/publish.js +1 -1
  163. package/clis/tiktok/utils.js +1 -1
  164. package/clis/trae-cn/activity.js +1 -1
  165. package/clis/trae-cn/approve.js +1 -1
  166. package/clis/trae-cn/ask.js +1 -1
  167. package/clis/trae-cn/dump.js +1 -1
  168. package/clis/trae-cn/export.js +1 -1
  169. package/clis/trae-cn/model.js +1 -1
  170. package/clis/trae-cn/new.js +1 -1
  171. package/clis/trae-cn/read.js +1 -1
  172. package/clis/trae-cn/screenshot.js +1 -1
  173. package/clis/trae-cn/select-model.js +1 -1
  174. package/clis/trae-cn/send.js +1 -1
  175. package/clis/trae-cn/setup.js +1 -1
  176. package/clis/trae-cn/status.js +1 -1
  177. package/clis/trae-cn/targets.js +1 -1
  178. package/clis/trae-cn/watch.js +1 -1
  179. package/clis/trae-solo/state-fs.js +1 -1
  180. package/clis/tumblr/publish.js +1 -1
  181. package/clis/tvmaze/utils.js +1 -1
  182. package/clis/twitter/bookmark-folder.js +1 -1
  183. package/clis/twitter/download.js +1 -1
  184. package/clis/twitter/followers.js +2 -2
  185. package/clis/twitter/following.js +2 -2
  186. package/clis/twitter/likes.js +1 -1
  187. package/clis/twitter/list-add-batch.js +1 -1
  188. package/clis/twitter/list-add-core.js +1 -1
  189. package/clis/twitter/list-add.js +1 -1
  190. package/clis/twitter/list-create.js +1 -1
  191. package/clis/twitter/list-delete.js +3 -3
  192. package/clis/twitter/list-remove-batch.js +1 -1
  193. package/clis/twitter/list-remove.js +1 -1
  194. package/clis/twitter/list-tweets.js +1 -1
  195. package/clis/twitter/profile.js +1 -1
  196. package/clis/twitter/search.js +1 -1
  197. package/clis/twitter/shared.js +1 -1
  198. package/clis/twitter/tweets.js +1 -1
  199. package/clis/uisdc/news.js +1 -1
  200. package/clis/v2ex/daily.js +2 -2
  201. package/clis/v2ex/me.js +2 -2
  202. package/clis/v2ex/notifications.js +1 -1
  203. package/clis/wikidata/utils.js +1 -1
  204. package/clis/wikipedia/page.js +2 -2
  205. package/clis/wikipedia/summary.js +1 -1
  206. package/clis/wikipedia/utils.js +1 -1
  207. package/clis/wordpress/login.js +1 -1
  208. package/clis/wordpress/publish.js +1 -1
  209. package/clis/wordpress/shared.js +3 -3
  210. package/clis/wordpress/sites.js +1 -0
  211. package/clis/wordpress/whoami.js +1 -1
  212. package/clis/woshipm/stats.js +1 -1
  213. package/clis/xiaoe/content.js +1 -1
  214. package/clis/xiaohongshu/draft-delete.js +1 -1
  215. package/clis/xiaohongshu/draft-open.js +1 -1
  216. package/clis/xiaohongshu/draft-utils.js +2 -2
  217. package/clis/xiaohongshu/publish-video.js +1 -1
  218. package/clis/xiaohongshu/publish.js +1 -1
  219. package/clis/xiaoyuzhou/auth.js +1 -1
  220. package/clis/xiaoyuzhou/transcript.js +1 -1
  221. package/clis/yollomi/background.js +1 -1
  222. package/clis/yollomi/edit.js +1 -1
  223. package/clis/yollomi/generate.js +1 -1
  224. package/clis/yollomi/try-on.js +1 -1
  225. package/clis/youtube/publish.js +1 -1
  226. package/clis/zhihu/answer-comments.js +1 -1
  227. package/clis/zhihu/answer-detail.js +1 -1
  228. package/clis/zhihu/collection.js +2 -2
  229. package/clis/zhihu/collections.js +1 -1
  230. package/clis/zhihu/question.js +1 -1
  231. package/clis/zhihu/search.js +1 -1
  232. package/clis/zhihu/target.js +1 -1
  233. package/clis/zhihu/user-arg.js +1 -1
  234. package/dist/src/adapter-shadow.js +1 -1
  235. package/dist/src/browser/analyze.js +1 -1
  236. package/dist/src/browser/article-extract.d.ts +1 -1
  237. package/dist/src/browser/base-page.js +3 -3
  238. package/dist/src/browser/cdp.js +1 -1
  239. package/dist/src/browser/daemon-client.d.ts +1 -1
  240. package/dist/src/browser/daemon-lifecycle.js +6 -6
  241. package/dist/src/browser/daemon-version.js +1 -1
  242. package/dist/src/browser/dom-snapshot.d.ts +1 -1
  243. package/dist/src/browser/dom-snapshot.js +2 -2
  244. package/dist/src/browser/errors.js +2 -2
  245. package/dist/src/browser/network-cache.js +1 -1
  246. package/dist/src/browser/profile.js +1 -1
  247. package/dist/src/browser/target-resolver.js +4 -4
  248. package/dist/src/browser/verify-fixture.js +1 -1
  249. package/dist/src/browser-session-lock.js +1 -1
  250. package/dist/src/build-manifest.d.ts +1 -1
  251. package/dist/src/build-manifest.js +1 -1
  252. package/dist/src/cli-argv-preprocess.d.ts +4 -4
  253. package/dist/src/cli-argv-preprocess.js +1 -1
  254. package/dist/src/cli.d.ts +1 -1
  255. package/dist/src/cli.js +19 -19
  256. package/dist/src/commanderAdapter.js +1 -1
  257. package/dist/src/commands/auth.js +2 -2
  258. package/dist/src/commands/daemon.d.ts +4 -4
  259. package/dist/src/commands/daemon.js +1 -1
  260. package/dist/src/completion-shared.js +12 -12
  261. package/dist/src/completion.d.ts +2 -2
  262. package/dist/src/constants.d.ts +1 -1
  263. package/dist/src/constants.js +1 -1
  264. package/dist/src/daemon-utils.d.ts +1 -1
  265. package/dist/src/daemon-utils.js +1 -1
  266. package/dist/src/daemon.d.ts +2 -2
  267. package/dist/src/daemon.js +1 -1
  268. package/dist/src/discovery.d.ts +7 -7
  269. package/dist/src/discovery.js +2 -2
  270. package/dist/src/doctor.d.ts +1 -1
  271. package/dist/src/doctor.js +6 -6
  272. package/dist/src/electron-apps.d.ts +1 -1
  273. package/dist/src/electron-apps.js +1 -1
  274. package/dist/src/errors.d.ts +2 -2
  275. package/dist/src/errors.js +1 -1
  276. package/dist/src/execution.js +1 -1
  277. package/dist/src/external.d.ts +1 -1
  278. package/dist/src/external.js +1 -1
  279. package/dist/src/help.d.ts +1 -1
  280. package/dist/src/help.js +3 -3
  281. package/dist/src/hooks.d.ts +1 -1
  282. package/dist/src/launcher.js +2 -2
  283. package/dist/src/logger.d.ts +1 -1
  284. package/dist/src/main.d.ts +1 -1
  285. package/dist/src/main.js +1 -1
  286. package/dist/src/observation/artifact.js +2 -2
  287. package/dist/src/package-paths.js +1 -1
  288. package/dist/src/package-paths.test.d.ts +1 -0
  289. package/dist/src/plugin-manifest.d.ts +12 -3
  290. package/dist/src/plugin-manifest.js +1 -1
  291. package/dist/src/plugin-scaffold.d.ts +2 -2
  292. package/dist/src/plugin-scaffold.js +9 -9
  293. package/dist/src/plugin.d.ts +4 -4
  294. package/dist/src/plugin.js +10 -10
  295. package/dist/src/rate-limit.js +1 -1
  296. package/dist/src/registry-api.d.ts +1 -1
  297. package/dist/src/runtime-detect.d.ts +1 -1
  298. package/dist/src/serialization.js +1 -1
  299. package/dist/src/skills.js +6 -6
  300. package/dist/src/user-runtime.d.ts +8 -0
  301. package/dist/src/user-runtime.js +1 -0
  302. package/dist/src/user-runtime.test.d.ts +1 -0
  303. package/dist/src/validate.js +1 -1
  304. package/dist/src/validate.test.d.ts +1 -1
  305. package/docs/guide/browser-bridge.md +23 -23
  306. package/docs/guide/electron-app-cli.md +3 -3
  307. package/docs/guide/exit-codes.md +3 -3
  308. package/docs/guide/extending-opencli.md +37 -37
  309. package/docs/guide/getting-started.md +19 -19
  310. package/docs/guide/installation.md +6 -6
  311. package/docs/guide/plugins.md +32 -32
  312. package/docs/guide/remote-orchestration.md +5 -5
  313. package/docs/guide/troubleshooting.md +9 -9
  314. package/package.json +3 -2
  315. package/scripts/fetch-adapters.js +6 -6
  316. package/scripts/postinstall.js +14 -14
  317. package/skills/opencli-adapter-author/SKILL.md +24 -24
  318. package/skills/opencli-adapter-author/references/adapter-template.md +13 -13
  319. package/skills/opencli-adapter-author/references/api-discovery.md +25 -25
  320. package/skills/opencli-adapter-author/references/coverage-matrix.md +3 -3
  321. package/skills/opencli-adapter-author/references/field-decode-playbook.md +10 -10
  322. package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +2 -2
  323. package/skills/opencli-adapter-author/references/site-memory.md +11 -11
  324. package/skills/opencli-adapter-author/references/site-recon.md +7 -7
  325. package/skills/opencli-adapter-author/references/strategy-selection.md +3 -3
  326. package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +4 -4
  327. package/skills/opencli-adapter-author/references/typed-errors.md +2 -2
  328. package/skills/opencli-autofix/SKILL.md +29 -29
  329. package/skills/opencli-browser/SKILL.md +55 -55
  330. package/skills/opencli-browser-sitemap/SKILL.md +7 -7
  331. package/skills/opencli-sitemap-author/SKILL.md +11 -11
  332. package/skills/opencli-sitemap-author/references/sitemap-schema.md +19 -19
  333. package/skills/opencli-usage/SKILL.md +43 -43
@@ -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: opencli 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(`() => {
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: opencli 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 `opencli 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
+ 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: opencli twitter list-add 123456789 alice");if(!P)throw new U("twitter list-add username is required","Example: opencli 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 () => {
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();
@@ -1 +1 @@
1
- import{cli as e,Strategy as t}from"@jackwener/opencli/registry";import{buildListAddMemberRow,listAddUser}from"./list-add-core.js";e({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:t.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `opencli 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
+ 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: opencli 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 () => {
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 r}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 () => {
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 r(`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 `opencli 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: opencli twitter list-delete 123456789 --confirm true");if(!k(t.confirm))throw new d("Refusing to delete list without --confirm true","Example: opencli 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((o)=>o.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((o)=>o.id===n);if(!_)throw new r(`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 () => {
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 r(`Failed to delete list ${n}: ${u?.message||"unknown UI failure"}`);if((await m(e,a)).some((o)=>o.id===n))throw new r(`Failed to delete list ${n}: list still appears in managed lists.`);return[buildListDeleteRow({listId:n,targetList:_})]}});
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 o,Strategy as i}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: opencli twitter list-remove-batch 123456789 "@alice,@bob" --interval 5';o({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:i.UI,browser:!0,args:[{name:"listId",positional:!0,type:"string",required:!0,help:"Numeric ID of the list you own (e.g. from `opencli 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
+ 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 `opencli 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
+ 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 `opencli 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 \`opencli 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 () => {
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) {
@@ -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: opencli 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(`() => {
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(`
@@ -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: opencli twitter search opencli --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 () => {
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},
@@ -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: opencli 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`
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'
@@ -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: opencli 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: opencli 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: opencli 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(`() => {
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 () => {
@@ -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: opencli uisdc news --limit ${c}`);if(r>o)throw new a(`limit must be <= ${o}`,`Example: opencli uisdc news --limit ${o}`);return r}function u(t){return String(t??"").replace(/\s+/g," ").trim()}function m(){return`
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'
@@ -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("[opencli: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("[opencli:v2ex] Waiting for Cloudflare...")}const b=await q.evaluate(`
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(`[opencli:v2ex:debug] Page Title: ${b.debug_title}`);console.error(`[opencli: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(`[opencli: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(`
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('领取')) {
package/clis/v2ex/me.js CHANGED
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as B,Strategy as F}from"@jackwener/opencli/registry";B({site:"v2ex",name:"me",access:"read",description:"V2EX 获取个人资料 (余额/未读提醒)",domain:"www.v2ex.com",strategy:F.COOKIE,browser:!0,args:[],columns:["username","balance","unread_notifications","daily_reward_ready"],func:async(h)=>{if(!h)throw new z("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[opencli:v2ex] Navigating to /");await h.goto("https://www.v2ex.com/");for(let q=0;q<5;q++){await new Promise((A)=>setTimeout(A,1500));if(!(await h.evaluate("() => document.title"))?.includes("Just a moment"))break;if(process.env.OPENCLI_VERBOSE)console.error("[opencli:v2ex] Waiting for Cloudflare...")}const b=await h.evaluate(`
1
+ import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as B,Strategy as F}from"@jackwener/opencli/registry";B({site:"v2ex",name:"me",access:"read",description:"V2EX 获取个人资料 (余额/未读提醒)",domain:"www.v2ex.com",strategy:F.COOKIE,browser:!0,args:[],columns:["username","balance","unread_notifications","daily_reward_ready"],func:async(h)=>{if(!h)throw new z("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Navigating to /");await h.goto("https://www.v2ex.com/");for(let q=0;q<5;q++){await new Promise((A)=>setTimeout(A,1500));if(!(await h.evaluate("() => document.title"))?.includes("Just a moment"))break;if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Waiting for Cloudflare...")}const b=await h.evaluate(`
2
2
  async () => {
3
3
  let username = 'Unknown';
4
4
  const navLinks = Array.from(document.querySelectorAll('a.top')).map(a => a.textContent?.trim());
@@ -55,4 +55,4 @@ import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as
55
55
  daily_reward_ready: daily_reward_ready ? '是' : '否'
56
56
  };
57
57
  }
58
- `);if(b.error){if(process.env.OPENCLI_VERBOSE){console.error(`[opencli:v2ex:debug] Page Title: ${b.debug_title}`);console.error(`[opencli:v2ex:debug] Page Body: ${b.debug_body}`)}throw new z(b.error)}return[b]}});
58
+ `);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 z(b.error)}return[b]}});
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as q}from"@jackwener/opencli/errors";import{cli as F,Strategy as G}from"@jackwener/opencli/registry";F({site:"v2ex",name:"notifications",access:"read",description:"V2EX 获取提醒 (回复/由于)",domain:"www.v2ex.com",strategy:G.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20,help:"Number of notifications"}],columns:["type","content","time"],func:async(b,z)=>{if(!b)throw new q("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[opencli:v2ex] Navigating to /notifications");await b.goto("https://www.v2ex.com/notifications");await new Promise((B)=>setTimeout(B,1500));const h=await b.evaluate(`
1
+ import{CommandExecutionError as q}from"@jackwener/opencli/errors";import{cli as F,Strategy as G}from"@jackwener/opencli/registry";F({site:"v2ex",name:"notifications",access:"read",description:"V2EX 获取提醒 (回复/由于)",domain:"www.v2ex.com",strategy:G.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20,help:"Number of notifications"}],columns:["type","content","time"],func:async(b,z)=>{if(!b)throw new q("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Navigating to /notifications");await b.goto("https://www.v2ex.com/notifications");await new Promise((B)=>setTimeout(B,1500));const h=await b.evaluate(`
2
2
  async () => {
3
3
  const items = Array.from(document.querySelectorAll('#Main .box .cell[id^="n_"]'));
4
4
  return items.map(item => {
@@ -1 +1 @@
1
- import{ArgumentError as K,CommandExecutionError as L,EmptyResultError as O}from"@jackwener/opencli/errors";export const WIKIDATA_BASE="https://www.wikidata.org";const Q="opencli-wikidata-adapter/1.0 (+https://github.com/jackwener/opencli; mailto:opencli@example.com)",S=/^[QPL]\d+$/;export function requireString(z,F){const B=String(z??"").trim();if(!B)throw new K(`wikidata ${F} cannot be empty`);return B}export function requireBoundedInt(z,F,B,H="limit"){const G=z??F,J=typeof G==="number"?G:Number(G);if(!Number.isInteger(J)||J<=0)throw new K(`wikidata ${H} must be a positive integer`);if(J>B)throw new K(`wikidata ${H} must be <= ${B}`);return J}export function requireEntityId(z){const F=String(z??"").trim().toUpperCase();if(!F)throw new K('wikidata entity id is required (e.g. "Q937")');const B=F.replace(/^HTTPS?:\/\/[^/]+\/WIKI\//i,"");if(!S.test(B))throw new K(`wikidata entity id "${z}" is not a valid Q/P/L identifier`,'Expected format: "Q<digits>" (item), "P<digits>" (property), or "L<digits>" (lexeme).');return B}export function requireLanguage(z,F="en"){const B=String(z??F).trim().toLowerCase();if(!/^[a-z]{2,3}(-[a-z]{2,8})?$/.test(B))throw new K(`wikidata language "${z}" is not a valid language code`,'Expected an ISO 639 language code such as "en", "fr", "zh", "zh-hans".');return B}export async function wikidataFetch(z,F){let B;try{B=await fetch(z,{headers:{"user-agent":Q,accept:"application/json"}})}catch(G){throw new L(`${F} request failed: ${G?.message??G}`,"Check that www.wikidata.org is reachable from this network.")}if(B.status===404)throw new O(F,`Wikidata returned 404 for ${z}.`);if(B.status===429)throw new L(`${F} returned HTTP 429 (rate limited)`,"Wikidata throttles anonymous traffic; back off and retry.");if(!B.ok)throw new L(`${F} returned HTTP ${B.status}`);let H;try{H=await B.json()}catch(G){throw new L(`${F} returned malformed JSON: ${G?.message??G}`)}return H}export function pickLocalised(z,F){if(!z||typeof z!=="object")return null;const B=z[F];if(B&&typeof B.value==="string"&&B.value.trim())return B.value;if(F!=="en"&&z.en&&typeof z.en.value==="string"&&z.en.value.trim())return z.en.value;return null}export function joinAliases(z,F,B=5){if(!z||typeof z!=="object")return"";let H=Array.isArray(z[F])?z[F]:[];if(H.length===0&&F!=="en"&&Array.isArray(z.en))H=z.en;const G=H.map((J)=>J&&typeof J.value==="string"?J.value:"").filter(Boolean);if(G.length===0)return"";if(G.length>B)return[...G.slice(0,B),`(+${G.length-B})`].join(", ");return G.join(", ")}
1
+ import{ArgumentError as K,CommandExecutionError as L,EmptyResultError as O}from"@jackwener/opencli/errors";export const WIKIDATA_BASE="https://www.wikidata.org";const Q="opencli-wikidata-adapter/1.0 (+https://github.com/jackwener/OpenCLI; mailto:ppcli@example.com)",S=/^[QPL]\d+$/;export function requireString(z,F){const B=String(z??"").trim();if(!B)throw new K(`wikidata ${F} cannot be empty`);return B}export function requireBoundedInt(z,F,B,H="limit"){const G=z??F,J=typeof G==="number"?G:Number(G);if(!Number.isInteger(J)||J<=0)throw new K(`wikidata ${H} must be a positive integer`);if(J>B)throw new K(`wikidata ${H} must be <= ${B}`);return J}export function requireEntityId(z){const F=String(z??"").trim().toUpperCase();if(!F)throw new K('wikidata entity id is required (e.g. "Q937")');const B=F.replace(/^HTTPS?:\/\/[^/]+\/WIKI\//i,"");if(!S.test(B))throw new K(`wikidata entity id "${z}" is not a valid Q/P/L identifier`,'Expected format: "Q<digits>" (item), "P<digits>" (property), or "L<digits>" (lexeme).');return B}export function requireLanguage(z,F="en"){const B=String(z??F).trim().toLowerCase();if(!/^[a-z]{2,3}(-[a-z]{2,8})?$/.test(B))throw new K(`wikidata language "${z}" is not a valid language code`,'Expected an ISO 639 language code such as "en", "fr", "zh", "zh-hans".');return B}export async function wikidataFetch(z,F){let B;try{B=await fetch(z,{headers:{"user-agent":Q,accept:"application/json"}})}catch(G){throw new L(`${F} request failed: ${G?.message??G}`,"Check that www.wikidata.org is reachable from this network.")}if(B.status===404)throw new O(F,`Wikidata returned 404 for ${z}.`);if(B.status===429)throw new L(`${F} returned HTTP 429 (rate limited)`,"Wikidata throttles anonymous traffic; back off and retry.");if(!B.ok)throw new L(`${F} returned HTTP ${B.status}`);let H;try{H=await B.json()}catch(G){throw new L(`${F} returned malformed JSON: ${G?.message??G}`)}return H}export function pickLocalised(z,F){if(!z||typeof z!=="object")return null;const B=z[F];if(B&&typeof B.value==="string"&&B.value.trim())return B.value;if(F!=="en"&&z.en&&typeof z.en.value==="string"&&z.en.value.trim())return z.en.value;return null}export function joinAliases(z,F,B=5){if(!z||typeof z!=="object")return"";let H=Array.isArray(z[F])?z[F]:[];if(H.length===0&&F!=="en"&&Array.isArray(z.en))H=z.en;const G=H.map((J)=>J&&typeof J.value==="string"?J.value:"").filter(Boolean);if(G.length===0)return"";if(G.length>B)return[...G.slice(0,B),`(+${G.length-B})`].join(", ");return G.join(", ")}
@@ -1,3 +1,3 @@
1
- import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as o,EmptyResultError as m}from"@jackwener/opencli/errors";u({site:"wikipedia",name:"page",access:"read",description:"Full plain-text extract of a Wikipedia article (optional paragraph cap).",domain:"wikipedia.org",strategy:w.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,type:"string",help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",type:"string",default:"en",help:"Language code (en, zh, ja, de, ...)."},{name:"paragraphs",type:"int",default:0,help:"Cap to first N paragraphs (0 = full article)."}],columns:["title","description","pageId","paragraphs","extract","url"],func:async(i)=>{const l=String(i.title??"").trim();if(!l)throw new c("wikipedia page title cannot be empty");const n=String(i.lang??"en").trim().toLowerCase();if(!/^[a-z]{2,3}(?:-[a-z0-9]+)?$/.test(n))throw new c(`wikipedia lang must be a language code like en, zh, ja (got "${i.lang}")`);const s=Number(i.paragraphs??0);if(!Number.isInteger(s)||s<0)throw new c("paragraphs must be a non-negative integer (0 = full article)");const e=new URL(`https://${n}.wikipedia.org/w/api.php`);e.searchParams.set("action","query");e.searchParams.set("format","json");e.searchParams.set("formatversion","2");e.searchParams.set("prop","extracts|info|description");e.searchParams.set("inprop","url");e.searchParams.set("explaintext","1");e.searchParams.set("redirects","1");e.searchParams.set("titles",l);let p;try{p=await fetch(e,{headers:{"User-Agent":"opencli/1.0 (+https://github.com/jackwener/opencli)",Accept:"application/json"}})}catch(t){throw new o(`wikipedia page request failed: ${t?.message||t}`)}if(!p.ok)throw new o(`wikipedia page failed: HTTP ${p.status}`);let r;try{r=await p.json()}catch(t){throw new o(`wikipedia returned malformed JSON: ${t?.message||t}`)}if(r?.error)throw new o(`wikipedia API error: ${r.error.info||r.error.code}`);const a=(Array.isArray(r?.query?.pages)?r.query.pages:[])[0];if(!a||a.missing)throw new m("wikipedia page",`No article "${l}" on ${n}.wikipedia.org. Try \`opencli wikipedia search\` first.`);const g=String(a.extract??"");if(!g.trim())throw new m("wikipedia page",`Article "${a.title}" exists but has no plain-text extract (likely a disambiguation/redirect page).`);const h=g.split(/\n{2,}/).map((t)=>t.trim()).filter(Boolean),d=s>0?h.slice(0,s):h;return[{title:a.title,description:a.description||"",pageId:a.pageid??null,paragraphs:d.length,extract:d.join(`
1
+ import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as o,EmptyResultError as m}from"@jackwener/opencli/errors";u({site:"wikipedia",name:"page",access:"read",description:"Full plain-text extract of a Wikipedia article (optional paragraph cap).",domain:"wikipedia.org",strategy:w.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,type:"string",help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",type:"string",default:"en",help:"Language code (en, zh, ja, de, ...)."},{name:"paragraphs",type:"int",default:0,help:"Cap to first N paragraphs (0 = full article)."}],columns:["title","description","pageId","paragraphs","extract","url"],func:async(i)=>{const l=String(i.title??"").trim();if(!l)throw new c("wikipedia page title cannot be empty");const s=String(i.lang??"en").trim().toLowerCase();if(!/^[a-z]{2,3}(?:-[a-z0-9]+)?$/.test(s))throw new c(`wikipedia lang must be a language code like en, zh, ja (got "${i.lang}")`);const n=Number(i.paragraphs??0);if(!Number.isInteger(n)||n<0)throw new c("paragraphs must be a non-negative integer (0 = full article)");const e=new URL(`https://${s}.wikipedia.org/w/api.php`);e.searchParams.set("action","query");e.searchParams.set("format","json");e.searchParams.set("formatversion","2");e.searchParams.set("prop","extracts|info|description");e.searchParams.set("inprop","url");e.searchParams.set("explaintext","1");e.searchParams.set("redirects","1");e.searchParams.set("titles",l);let p;try{p=await fetch(e,{headers:{"User-Agent":"ppcli/1.0 (+https://github.com/jackwener/OpenCLI)",Accept:"application/json"}})}catch(t){throw new o(`wikipedia page request failed: ${t?.message||t}`)}if(!p.ok)throw new o(`wikipedia page failed: HTTP ${p.status}`);let r;try{r=await p.json()}catch(t){throw new o(`wikipedia returned malformed JSON: ${t?.message||t}`)}if(r?.error)throw new o(`wikipedia API error: ${r.error.info||r.error.code}`);const a=(Array.isArray(r?.query?.pages)?r.query.pages:[])[0];if(!a||a.missing)throw new m("wikipedia page",`No article "${l}" on ${s}.wikipedia.org. Try \`ppcli wikipedia search\` first.`);const g=String(a.extract??"");if(!g.trim())throw new m("wikipedia page",`Article "${a.title}" exists but has no plain-text extract (likely a disambiguation/redirect page).`);const h=g.split(/\n{2,}/).map((t)=>t.trim()).filter(Boolean),d=n>0?h.slice(0,n):h;return[{title:a.title,description:a.description||"",pageId:a.pageid??null,paragraphs:d.length,extract:d.join(`
2
2
 
3
- `),url:a.fullurl||`https://${n}.wikipedia.org/wiki/${encodeURIComponent(a.title.replace(/ /g,"_"))}`}]}});
3
+ `),url:a.fullurl||`https://${s}.wikipedia.org/wiki/${encodeURIComponent(a.title.replace(/ /g,"_"))}`}]}});
@@ -1 +1 @@
1
- import{CliError as a}from"@jackwener/opencli/errors";import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{formatSummaryRow as l,wikiFetch as c}from"./utils.js";n({site:"wikipedia",name:"summary",access:"read",description:"Get Wikipedia article summary",strategy:o.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",default:"en",help:"Language code (e.g. en, zh, ja)"}],columns:["title","description","extract","url"],func:async(e)=>{const t=e.lang||"en",i=encodeURIComponent(e.title.replace(/ /g,"_")),r=await c(t,`/api/rest_v1/page/summary/${i}`);if(!r?.title)throw new a("NOT_FOUND",`Article "${e.title}" not found`,"Try searching first: opencli wikipedia search <keyword>");return[l(r,t)]}});
1
+ import{CliError as a}from"@jackwener/opencli/errors";import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{formatSummaryRow as l,wikiFetch as c}from"./utils.js";n({site:"wikipedia",name:"summary",access:"read",description:"Get Wikipedia article summary",strategy:o.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",default:"en",help:"Language code (e.g. en, zh, ja)"}],columns:["title","description","extract","url"],func:async(e)=>{const t=e.lang||"en",i=encodeURIComponent(e.title.replace(/ /g,"_")),r=await c(t,`/api/rest_v1/page/summary/${i}`);if(!r?.title)throw new a("NOT_FOUND",`Article "${e.title}" not found`,"Try searching first: ppcli wikipedia search <keyword>");return[l(r,t)]}});
@@ -1 +1 @@
1
- import{CliError as x}from"@jackwener/opencli/errors";export const EXTRACT_MAX_LEN=300,DESC_MAX_LEN=80;export async function wikiFetch(b,f){const v=`https://${b}.wikipedia.org${f}`,q=await fetch(v,{headers:{"User-Agent":"opencli/1.0 (https://github.com/jackwener/opencli)"}});if(!q.ok)throw new x("FETCH_ERROR",`Wikipedia API HTTP ${q.status}`,"Check your title or search term");return q.json()}export function formatSummaryRow(b,f){return{title:b.title,description:b.description??"-",extract:(b.extract??"").slice(0,EXTRACT_MAX_LEN),url:b.content_urls?.desktop?.page??`https://${f}.wikipedia.org`}}
1
+ import{CliError as x}from"@jackwener/opencli/errors";export const EXTRACT_MAX_LEN=300,DESC_MAX_LEN=80;export async function wikiFetch(b,f){const v=`https://${b}.wikipedia.org${f}`,q=await fetch(v,{headers:{"User-Agent":"ppcli/1.0 (https://github.com/jackwener/OpenCLI)"}});if(!q.ok)throw new x("FETCH_ERROR",`Wikipedia API HTTP ${q.status}`,"Check your title or search term");return q.json()}export function formatSummaryRow(b,f){return{title:b.title,description:b.description??"-",extract:(b.extract??"").slice(0,EXTRACT_MAX_LEN),url:b.content_urls?.desktop?.page??`https://${f}.wikipedia.org`}}
@@ -1 +1 @@
1
- import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{TimeoutError as $}from"@jackwener/opencli/errors";import{resolveBaseUrl as h,readCookieLogin as x,gotoWpAdmin as v,WP_ADMIN as B,WP_LOGIN as G}from"./shared.js";const Q=300,R=3000;async function z(f){if(typeof f?.closeWindow==="function")try{await f.closeWindow()}catch{}}Y({site:"wordpress",name:"login",access:"write",description:"打开自建 WordPress 站点的登录页并等待浏览器会话完成登录(加站模式:站点网址取自连接里的 WORDPRESS_BASE_URL,或用 --base-url)。",domain:"wordpress.org",strategy:Z.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"base-url",help:"站点根网址(默认读连接里的 WORDPRESS_BASE_URL)"},{name:"timeout",type:"int",default:Q,help:"等待用户完成登录的最长秒数"}],columns:["status","logged_in","site"],func:async(f,H)=>{const q=h(H);if(await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}if(!(await v(f,q,B)).onLogin&&await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}await f.goto(q+G);const J=Number(H.timeout??Q),K=Date.now()+J*1000;while(Date.now()<K){const X=K-Date.now();await f.wait({time:Math.min(R/1000,Math.max(0.2,X/1000))});if(await x(f,q)){await z(f);return[{status:"login_complete",logged_in:!0,site:q}]}}throw new $("wordpress login",J,`请在打开的窗口里完成 ${q} 的登录后重试。`)}});
1
+ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{TimeoutError as $}from"@jackwener/opencli/errors";import{resolveBaseUrl as h,readCookieLogin as x,gotoWpAdmin as v,WP_ADMIN as B,WP_LOGIN as G}from"./shared.js";const Q=300,R=3000;async function z(f){if(typeof f?.closeWindow==="function")try{await f.closeWindow()}catch{}}Y({site:"wordpress",name:"login",access:"write",description:"打开自建 WordPress 站点的登录页并等待浏览器会话完成登录(加站模式:站点网址取自连接里的 WORDPRESS_BASE_URL,或用 --base-url)。",domain:"wordpress.org",strategy:Z.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"site",help:"连接名称或站点网址;省略时使用默认站点"},{name:"base-url",help:"站点根网址(默认读连接里的 WORDPRESS_BASE_URL)"},{name:"timeout",type:"int",default:Q,help:"等待用户完成登录的最长秒数"}],columns:["status","logged_in","site"],func:async(f,H)=>{const q=h(H);if(await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}if(!(await v(f,q,B)).onLogin&&await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}await f.goto(q+G);const J=Number(H.timeout??Q),K=Date.now()+J*1000;while(Date.now()<K){const X=K-Date.now();await f.wait({time:Math.min(R/1000,Math.max(0.2,X/1000))});if(await x(f,q)){await z(f);return[{status:"login_complete",logged_in:!0,site:q}]}}throw new $("wordpress login",J,`请在打开的窗口里完成 ${q} 的登录后重试。`)}});
@@ -1 +1 @@
1
- import{cli as F,Strategy as H}from"@jackwener/opencli/registry";import{CliError as G,AuthRequiredError as M,CommandExecutionError as O}from"@jackwener/opencli/errors";import{readFileSync as L}from"node:fs";import{normalizeContent as N}from"../_shared/article/format.js";import{assertLiteralContent as f}from"../_shared/content-guard.js";import{resolveBaseUrl as k,getAppPasswordCreds as q,basicAuthHeader as R,wpRequest as W,gotoWpAdmin as I,buildPublishJs as T,WP_ADMIN_POST_NEW as j}from"./shared.js";const U=["draft","publish","pending","private"];function V(K){return String(K).split(",").map((Q)=>Number(Q.trim())).filter((Q)=>Number.isFinite(Q))}function w(K,Q){const Z={title:String(K.title),content:Q,status:String(K.status)};if(K.excerpt)Z.excerpt=String(K.excerpt);if(K.slug)Z.slug=String(K.slug);if(K.categories)Z.categories=V(K.categories);if(K.tags)Z.tags=V(K.tags);return Z}F({site:"wordpress",name:"publish",access:"write",description:"发布一篇文章到自建 WordPress 站点。默认走登录态 cookie(零配置:只需在「连接」里填站点网址并在浏览器里登录),通过同源页面 fetch + REST nonce 调 POST /wp/v2/posts。Markdown 正文自动转 HTML。为安全默认存草稿,--status publish 才真发。分类/标签是 WordPress 的 term ID(数字),不是名字。",domain:"wordpress.org",strategy:H.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"title",required:!0,help:"文章标题"},{name:"content",help:"正文(Markdown 或 HTML),或用 --file"},{name:"file",help:"从本地文件读正文,替代 --content"},{name:"markup",default:"auto",choices:["auto","markdown","html"],help:"正文格式:auto 自动识别(默认) / markdown / html"},{name:"status",default:"draft",choices:U,help:"文章状态(默认 draft)"},{name:"excerpt",help:"摘要(可选)"},{name:"slug",help:"URL 别名 slug(可选)"},{name:"categories",help:"分类 term ID,逗号分隔(数字,非名字)"},{name:"tags",help:"标签 term ID,逗号分隔(数字,非名字)"},{name:"base-url",help:"覆盖站点根网址(默认读连接里的 WORDPRESS_BASE_URL)"}],columns:["id","status","title","url"],func:async(K,Q)=>{const Z=k(Q);let $=typeof Q.content==="string"?Q.content:"";if(Q.file)try{$=L(String(Q.file),"utf8")}catch(Y){throw new G("CONFIG",`Cannot read --file ${Q.file}: ${Y?.message??Y}`,"Pass a readable UTF-8 text file.")}else f($,{fileFlag:"--file",label:"正文"});if(!$.trim())throw new G("CONFIG","Post body is empty",'Pass --content "..." or --file <path>.');const x=N($,{format:Q.markup}).html,v=w(Q,x),J=q();if(J){const Y=await W(Z,"/wp/v2/posts",{method:"POST",body:v,authorization:R(J),label:"WordPress publish"});return[{id:Y?.id??"",status:Y?.status??"",title:Y?.title?.rendered??v.title,url:Y?.link??""}]}if(!K)throw new O("WordPress publish 需要浏览器会话(默认走登录态;或配置 WORDPRESS_APP_PASSWORD 走 Basic auth)。");if((await I(K,Z,j)).onLogin)throw new M("wordpress",`未登录 ${Z}(打开 wp-admin 被重定向到登录页)。请先在浏览器里登录该站点(可用 wordpress login)。`);const X=await K.evaluate(T(v));if(!X||X.ok===!1){const Y=X?.stage||"publish",z=X?.httpStatus!=null?` (HTTP ${X.httpStatus})`:"",D=X?.bodyText?`;接口原始响应:${String(X.bodyText).slice(0,500)}`:"",B=X?.message||"发布失败";if(Y==="auth")throw new M("wordpress",`WordPress publish 未授权${z}:${B}`);if(Y==="not_found")throw new G("NOT_FOUND",`WordPress publish:REST 接口不可达${z}:${B}`,"部分主机/安全插件会禁用 /wp-json(REST API)。请确认站点开启了 REST API。");throw new O(`[${Y}] WordPress publish 失败${z}:${B}${D}`)}return[{id:X.id!=null?String(X.id):"",status:X.status??"",title:X.title??v.title,url:X.link??""}]}});
1
+ import{cli as F,Strategy as H}from"@jackwener/opencli/registry";import{CliError as G,AuthRequiredError as M,CommandExecutionError as O}from"@jackwener/opencli/errors";import{readFileSync as L}from"node:fs";import{normalizeContent as N}from"../_shared/article/format.js";import{assertLiteralContent as f}from"../_shared/content-guard.js";import{resolveBaseUrl as k,getAppPasswordCreds as q,basicAuthHeader as R,wpRequest as W,gotoWpAdmin as I,buildPublishJs as T,WP_ADMIN_POST_NEW as j}from"./shared.js";const U=["draft","publish","pending","private"];function V(K){return String(K).split(",").map((Q)=>Number(Q.trim())).filter((Q)=>Number.isFinite(Q))}function w(K,Q){const Z={title:String(K.title),content:Q,status:String(K.status)};if(K.excerpt)Z.excerpt=String(K.excerpt);if(K.slug)Z.slug=String(K.slug);if(K.categories)Z.categories=V(K.categories);if(K.tags)Z.tags=V(K.tags);return Z}F({site:"wordpress",name:"publish",access:"write",description:"发布一篇文章到自建 WordPress 站点。默认走登录态 cookie(零配置:只需在「连接」里填站点网址并在浏览器里登录),通过同源页面 fetch + REST nonce 调 POST /wp/v2/posts。Markdown 正文自动转 HTML。为安全默认存草稿,--status publish 才真发。分类/标签是 WordPress 的 term ID(数字),不是名字。",domain:"wordpress.org",strategy:H.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"site",help:"连接名称或站点网址;省略时使用默认站点"},{name:"title",required:!0,help:"文章标题"},{name:"content",help:"正文(Markdown 或 HTML),或用 --file"},{name:"file",help:"从本地文件读正文,替代 --content"},{name:"markup",default:"auto",choices:["auto","markdown","html"],help:"正文格式:auto 自动识别(默认) / markdown / html"},{name:"status",default:"draft",choices:U,help:"文章状态(默认 draft)"},{name:"excerpt",help:"摘要(可选)"},{name:"slug",help:"URL 别名 slug(可选)"},{name:"categories",help:"分类 term ID,逗号分隔(数字,非名字)"},{name:"tags",help:"标签 term ID,逗号分隔(数字,非名字)"},{name:"base-url",help:"兼容旧版:覆盖站点根网址(推荐改用 --site)"}],columns:["id","status","title","url"],func:async(K,Q)=>{const Z=k(Q);let $=typeof Q.content==="string"?Q.content:"";if(Q.file)try{$=L(String(Q.file),"utf8")}catch(Y){throw new G("CONFIG",`Cannot read --file ${Q.file}: ${Y?.message??Y}`,"Pass a readable UTF-8 text file.")}else f($,{fileFlag:"--file",label:"正文"});if(!$.trim())throw new G("CONFIG","Post body is empty",'Pass --content "..." or --file <path>.');const x=N($,{format:Q.markup}).html,v=w(Q,x),J=q();if(J){const Y=await W(Z,"/wp/v2/posts",{method:"POST",body:v,authorization:R(J),label:"WordPress publish"});return[{id:Y?.id??"",status:Y?.status??"",title:Y?.title?.rendered??v.title,url:Y?.link??""}]}if(!K)throw new O("WordPress publish 需要浏览器会话(默认走登录态;或配置 WORDPRESS_APP_PASSWORD 走 Basic auth)。");if((await I(K,Z,j)).onLogin)throw new M("wordpress",`未登录 ${Z}(打开 wp-admin 被重定向到登录页)。请先在浏览器里登录该站点(可用 wordpress login)。`);const X=await K.evaluate(T(v));if(!X||X.ok===!1){const Y=X?.stage||"publish",z=X?.httpStatus!=null?` (HTTP ${X.httpStatus})`:"",D=X?.bodyText?`;接口原始响应:${String(X.bodyText).slice(0,500)}`:"",B=X?.message||"发布失败";if(Y==="auth")throw new M("wordpress",`WordPress publish 未授权${z}:${B}`);if(Y==="not_found")throw new G("NOT_FOUND",`WordPress publish:REST 接口不可达${z}:${B}`,"部分主机/安全插件会禁用 /wp-json(REST API)。请确认站点开启了 REST API。");throw new O(`[${Y}] WordPress publish 失败${z}:${B}${D}`)}return[{id:X.id!=null?String(X.id):"",status:X.status??"",title:X.title??v.title,url:X.link??""}]}});
@@ -1,9 +1,9 @@
1
- import{CliError as $}from"@jackwener/opencli/errors";export const WP_LOGIN_COOKIE_PREFIX="wordpress_logged_in_",WP_ADMIN_POST_NEW="/wp-admin/post-new.php",WP_ADMIN="/wp-admin/",WP_LOGIN="/wp-login.php";export function normalizeBaseUrl(D,M){const q=D?.trim().replace(/\/+$/,"");if(!q)throw new $("CONFIG",`Missing ${M}`,`Set ${M}, e.g. https://your-site.com (your WordPress site root URL).`);let Q;try{Q=new URL(q)}catch{throw new $("CONFIG",`Invalid ${M}: ${q}`,"Use an absolute http(s) URL.")}if(Q.protocol!=="http:"&&Q.protocol!=="https:")throw new $("CONFIG",`Invalid ${M}: ${q}`,"Use an http(s) URL.");return q}export function getWordpressBaseUrl(D=process.env){return normalizeBaseUrl(D.WORDPRESS_BASE_URL,"WORDPRESS_BASE_URL")}export function resolveBaseUrl(D={},M=process.env){const q=D["base-url"]??D.baseUrl??D.base_url;if(q!=null&&String(q).trim())return normalizeBaseUrl(String(q),"--base-url");return getWordpressBaseUrl(M)}export function getAppPasswordCreds(D=process.env){const M=D.WORDPRESS_USER?.trim(),q=D.WORDPRESS_APP_PASSWORD?.replace(/\s+/g,"");if(!M||!q)return null;return{user:M,appPassword:q}}export function basicAuthHeader(D){return`Basic ${Buffer.from(`${D.user}:${D.appPassword}`,"utf8").toString("base64")}`}export async function wpRequest(D,M,q={}){const Q=M.startsWith("/")?M:`/${M}`,V=`${D}/wp-json${Q}`,Y=q.label||`WordPress ${q.method||"GET"} ${Q}`;let S;try{S=await fetch(V,{method:q.method||"GET",headers:{...q.authorization?{Authorization:q.authorization}:{},Accept:"application/json",...q.body!==void 0?{"Content-Type":"application/json"}:{},...q.headers||{}},body:q.body!==void 0?JSON.stringify(q.body):void 0})}catch(F){throw new $("FETCH_ERROR",`${Y} failed: ${F?.message??F}`,"Check the site URL is reachable and the REST API (/wp-json) is enabled.")}const Z=await S.text();let j=null;try{j=Z?JSON.parse(Z):null}catch{j=null}if(S.status===401||S.status===403)throw new $("AUTH_REQUIRED",`${Y} returned HTTP ${S.status} (unauthorized)`,"Check WORDPRESS_USER / WORDPRESS_APP_PASSWORD — the Application Password must belong to that user and not be revoked.");if(S.status===404)throw new $("NOT_FOUND",`${Y} returned HTTP 404`,"Check the site URL and that the WordPress REST API is enabled (some hosts/security plugins disable /wp-json).");if(!S.ok){const F=j?.message?`: ${j.message}`:S.statusText?`: ${S.statusText}`:"";throw new $("HTTP_ERROR",`${Y} returned HTTP ${S.status}${F}`)}return j}export async function readCookieLogin(D,M){if(typeof D?.getCookies!=="function")return!1;const q=await D.getCookies({url:M});return Array.isArray(q)&&q.some((Q)=>Q&&Q.value&&typeof Q.name==="string"&&Q.name.startsWith(WP_LOGIN_COOKIE_PREFIX))}export async function gotoWpAdmin(D,M,q){const Q=M+q;let V={};for(let Y=0;Y<6;Y++){await D.goto(Q);await D.wait({time:1});V=await D.evaluate(`(() => {
1
+ import{CliError as K}from"@jackwener/opencli/errors";import{loadSiteConnections as R,normalizeSiteUrl as T,resolveSiteConnection as j}from"../_shared/self-hosted-sites.js";export const WP_LOGIN_COOKIE_PREFIX="wordpress_logged_in_",WP_ADMIN_POST_NEW="/wp-admin/post-new.php",WP_ADMIN="/wp-admin/",WP_LOGIN="/wp-login.php";export function normalizeBaseUrl(q,D){return T(q,{label:D})}export function getWordpressBaseUrl(q=process.env){return normalizeBaseUrl(q.WORDPRESS_BASE_URL,"WORDPRESS_BASE_URL")}export function resolveBaseUrl(q={},D=process.env){return j("wordpress",q,{selector:"site",legacySelectors:["base-url","baseUrl","base_url"],envUrl:D.WORDPRESS_BASE_URL}).url}export function configuredWordpressSites(q=process.env){const D=R("wordpress").sites;if(D.length>0)return D;if(!q.WORDPRESS_BASE_URL)return[];return[{name:"",url:normalizeBaseUrl(q.WORDPRESS_BASE_URL,"WORDPRESS_BASE_URL")}]}export function getAppPasswordCreds(q=process.env){const D=q.WORDPRESS_USER?.trim(),M=q.WORDPRESS_APP_PASSWORD?.replace(/\s+/g,"");if(!D||!M)return null;return{user:D,appPassword:M}}export function basicAuthHeader(q){return`Basic ${Buffer.from(`${q.user}:${q.appPassword}`,"utf8").toString("base64")}`}export async function wpRequest(q,D,M={}){const V=D.startsWith("/")?D:`/${D}`,Y=`${q}/wp-json${V}`,Z=M.label||`WordPress ${M.method||"GET"} ${V}`;let Q;try{Q=await fetch(Y,{method:M.method||"GET",headers:{...M.authorization?{Authorization:M.authorization}:{},Accept:"application/json",...M.body!==void 0?{"Content-Type":"application/json"}:{},...M.headers||{}},body:M.body!==void 0?JSON.stringify(M.body):void 0})}catch(H){throw new K("FETCH_ERROR",`${Z} failed: ${H?.message??H}`,"Check the site URL is reachable and the REST API (/wp-json) is enabled.")}const $=await Q.text();let F=null;try{F=$?JSON.parse($):null}catch{F=null}if(Q.status===401||Q.status===403)throw new K("AUTH_REQUIRED",`${Z} returned HTTP ${Q.status} (unauthorized)`,"Check WORDPRESS_USER / WORDPRESS_APP_PASSWORD — the Application Password must belong to that user and not be revoked.");if(Q.status===404)throw new K("NOT_FOUND",`${Z} returned HTTP 404`,"Check the site URL and that the WordPress REST API is enabled (some hosts/security plugins disable /wp-json).");if(!Q.ok){const H=F?.message?`: ${F.message}`:Q.statusText?`: ${Q.statusText}`:"";throw new K("HTTP_ERROR",`${Z} returned HTTP ${Q.status}${H}`)}return F}export async function readCookieLogin(q,D){if(typeof q?.getCookies!=="function")return!1;const M=await q.getCookies({url:D});return Array.isArray(M)&&M.some((V)=>V&&V.value&&typeof V.name==="string"&&V.name.startsWith(WP_LOGIN_COOKIE_PREFIX))}export async function gotoWpAdmin(q,D,M){const V=D+M;let Y={};for(let Z=0;Z<6;Z++){await q.goto(V);await q.wait({time:1});Y=await q.evaluate(`(() => {
2
2
  var href = ''; try { href = location.href; } catch (e) {}
3
3
  var cookieOk = true; try { void document.cookie; } catch (e) { cookieOk = false; }
4
4
  var hasNonce = !!(window.wpApiSettings && window.wpApiSettings.nonce);
5
5
  return { href: href, cookieOk: cookieOk, hasNonce: hasNonce };
6
- })()`)||{};const Z=V.href||"";if(/wp-login\.php/i.test(Z))return{onLogin:!0,href:Z};if(V.cookieOk&&(V.hasNonce||/\/wp-admin\//i.test(Z)))return{onLogin:!1,href:Z};await D.wait({time:1})}return{onLogin:!1,href:V.href||"",uncertain:!0}}export function buildPublishJs(D){return`(async () => {
6
+ })()`)||{};const $=Y.href||"";if(/wp-login\.php/i.test($))return{onLogin:!0,href:$};if(Y.cookieOk&&(Y.hasNonce||/\/wp-admin\//i.test($)))return{onLogin:!1,href:$};await q.wait({time:1})}return{onLogin:!1,href:Y.href||"",uncertain:!0}}export function buildPublishJs(q){return`(async () => {
7
7
  var s = (window.wpApiSettings || {});
8
8
  var nonce = s.nonce || "";
9
9
  var root = s.root || (location.origin + "/wp-json/");
@@ -19,7 +19,7 @@ import{CliError as $}from"@jackwener/opencli/errors";export const WP_LOGIN_COOKI
19
19
  `+` if (!nonce) return { ok: false, stage: "nonce", message: "无法从 wp-admin 页面取到 REST nonce(window.wpApiSettings.nonce 缺失)。可能未登录、REST API 被禁用,或该页面未加载区块编辑器。" };
20
20
  `+` if (!/\\/$/.test(root)) root += "/";
21
21
  var url = root + "wp/v2/posts";
22
- var payload = `+JSON.stringify(D)+`;
22
+ var payload = `+JSON.stringify(q)+`;
23
23
  try {
24
24
  var r = await fetch(url, { method: "POST", headers: { "X-WP-Nonce": nonce, "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(payload) });
25
25
  var text = await r.text();
@@ -0,0 +1 @@
1
+ import{cli as m}from"@jackwener/opencli/registry";import{connectionCommandDefinitions as n}from"../_shared/self-hosted-sites.js";for(const o of n("wordpress"))m(o);
@@ -1 +1 @@
1
- import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{resolveBaseUrl as $,getWordpressBaseUrl as v,getAppPasswordCreds as V,basicAuthHeader as x,wpRequest as D,readCookieLogin as X,gotoWpAdmin as E,buildWhoamiJs as H,WP_ADMIN_POST_NEW as I}from"./shared.js";async function J(z){if(V())return{logged_in:!0};let K;try{K=v()}catch{return{logged_in:!1}}return{logged_in:await X(z,K)}}Y({site:"wordpress",name:"whoami",access:"read",description:"检测自建 WordPress 站点的登录状态(返回布尔)。默认走登录态 cookie / 页面上下文 GET /wp/v2/users/me?context=edit;配置了 WORDPRESS_APP_PASSWORD 时走 Basic auth。",domain:"wordpress.org",strategy:Z.COOKIE,browser:!0,navigateBefore:!1,columns:["logged_in","user_id","username","site"],authStatus:{quickCheck:J},func:async(z,K)=>{const f=$(K),Q=V();if(Q)try{const F=await D(f,"/wp/v2/users/me?context=edit",{authorization:x(Q),label:"WordPress whoami"});return[{logged_in:!!(F&&F.id!=null),user_id:F?.id!=null?String(F.id):"",username:F?.name??F?.slug??"",site:f}]}catch{return[{logged_in:!1,user_id:"",username:"",site:f}]}if(!z)return[{logged_in:!1,user_id:"",username:"",site:f}];if((await E(z,f,I)).onLogin)return[{logged_in:!1,user_id:"",username:"",site:f}];const G=await z.evaluate(H());if(G&&G.loggedIn)return[{logged_in:!0,user_id:G.id||"",username:G.name||G.slug||"",site:f}];return[{logged_in:await X(z,f),user_id:"",username:"",site:f}]}});
1
+ import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{resolveBaseUrl as f,configuredWordpressSites as v,getAppPasswordCreds as X,basicAuthHeader as x,wpRequest as B,readCookieLogin as Y,gotoWpAdmin as D,buildWhoamiJs as E,WP_ADMIN_POST_NEW as H}from"./shared.js";async function I(F){if(X())return{logged_in:!0};for(const Q of v())if(await Y(F,Q.url))return{logged_in:!0};return{logged_in:!1}}Z({site:"wordpress",name:"whoami",access:"read",description:"检测自建 WordPress 站点的登录状态(返回布尔)。默认走登录态 cookie / 页面上下文 GET /wp/v2/users/me?context=edit;配置了 WORDPRESS_APP_PASSWORD 时走 Basic auth。",domain:"wordpress.org",strategy:$.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"site",help:"连接名称或站点网址;省略时使用默认站点"},{name:"base-url",help:"兼容旧版:站点根网址(推荐改用 --site)"}],columns:["logged_in","user_id","username","site"],authStatus:{quickCheck:I},func:async(F,Q)=>{const z=f(Q),V=X();if(V)try{const G=await B(z,"/wp/v2/users/me?context=edit",{authorization:x(V),label:"WordPress whoami"});return[{logged_in:!!(G&&G.id!=null),user_id:G?.id!=null?String(G.id):"",username:G?.name??G?.slug??"",site:z}]}catch{return[{logged_in:!1,user_id:"",username:"",site:z}]}if(!F)return[{logged_in:!1,user_id:"",username:"",site:z}];if((await D(F,z,H)).onLogin)return[{logged_in:!1,user_id:"",username:"",site:z}];const K=await F.evaluate(E());if(K&&K.loggedIn)return[{logged_in:!0,user_id:K.id||"",username:K.name||K.slug||"",site:z}];return[{logged_in:await Y(F,z),user_id:"",username:"",site:z}]}});
@@ -13,7 +13,7 @@ import{cli as Q,Strategy as V}from"@jackwener/opencli/registry";import{CommandEx
13
13
  if (loose) nonce = loose[1];
14
14
  }
15
15
  if (!nonce) {
16
- return { err: '页面里找不到 X-WP-Nonce(PURE.nonce),可能未登录人人都是产品经理,请先 opencli woshipm login' };
16
+ return { err: '页面里找不到 X-WP-Nonce(PURE.nonce),可能未登录人人都是产品经理,请先 ppcli woshipm login' };
17
17
  }
18
18
 
19
19
  // ── 分页拉「全部文章」列表(post_status=all,与页面自发请求一致)──
@@ -1,4 +1,4 @@
1
- import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as q,EmptyResultError as Y}from"@jackwener/opencli/errors";export const CONTENT_SELECTORS=[".rich-text-wrap",".content-wrap",".article-content",".text-content",".course-detail",".detail-content",'[class*="richtext"]','[class*="rich-text"]',".ql-editor"],CONTENT_MIN_LENGTH=50;export function pickContentText(B,K,J=CONTENT_MIN_LENGTH){for(const F of K){const Q=B.querySelector(F);if(!Q)continue;const V=(Q.innerText||Q.textContent||"").trim();if(V.length>J)return V}const z=B.querySelector("main")||B.querySelector("#app")||B.body;if(!z)return"";return(z.innerText||z.textContent||"").trim()}export function countXiaoeImages(B){let K=0;const J=B.querySelectorAll("img");for(let z=0;z<J.length;z+=1){const F=J[z].getAttribute("src")||J[z].src||"";if(!F)continue;if(F.startsWith("data:"))continue;if(!F.includes("xiaoe"))continue;K+=1}return K}export function requireXiaoePageUrl(B,K){const J=typeof B==="string"?B.trim():"";if(!J)throw new W("url is required (positional)");let z;try{z=new URL(J)}catch{throw new W(`invalid xiaoe URL: ${J}`,`Example: opencli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`)}if(z.protocol!=="https:")throw new W(`xiaoe URL must use https (got ${z.protocol.replace(":","")})`,`Example: opencli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`);const F=z.hostname.toLowerCase();if(F!=="h5.xet.citv.cn"&&!F.endsWith(".h5.xet.citv.cn"))throw new W(`url must be on h5.xet.citv.cn or a shop subdomain (got ${z.hostname})`,`Example: opencli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`);return z.toString()}export function buildContentScript(){return`
1
+ import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as q,EmptyResultError as Y}from"@jackwener/opencli/errors";export const CONTENT_SELECTORS=[".rich-text-wrap",".content-wrap",".article-content",".text-content",".course-detail",".detail-content",'[class*="richtext"]','[class*="rich-text"]',".ql-editor"],CONTENT_MIN_LENGTH=50;export function pickContentText(B,K,J=CONTENT_MIN_LENGTH){for(const F of K){const Q=B.querySelector(F);if(!Q)continue;const V=(Q.innerText||Q.textContent||"").trim();if(V.length>J)return V}const z=B.querySelector("main")||B.querySelector("#app")||B.body;if(!z)return"";return(z.innerText||z.textContent||"").trim()}export function countXiaoeImages(B){let K=0;const J=B.querySelectorAll("img");for(let z=0;z<J.length;z+=1){const F=J[z].getAttribute("src")||J[z].src||"";if(!F)continue;if(F.startsWith("data:"))continue;if(!F.includes("xiaoe"))continue;K+=1}return K}export function requireXiaoePageUrl(B,K){const J=typeof B==="string"?B.trim():"";if(!J)throw new W("url is required (positional)");let z;try{z=new URL(J)}catch{throw new W(`invalid xiaoe URL: ${J}`,`Example: ppcli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`)}if(z.protocol!=="https:")throw new W(`xiaoe URL must use https (got ${z.protocol.replace(":","")})`,`Example: ppcli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`);const F=z.hostname.toLowerCase();if(F!=="h5.xet.citv.cn"&&!F.endsWith(".h5.xet.citv.cn"))throw new W(`url must be on h5.xet.citv.cn or a shop subdomain (got ${z.hostname})`,`Example: ppcli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`);return z.toString()}export function buildContentScript(){return`
2
2
  (() => {
3
3
  ${pickContentText.toString()}
4
4
  ${countXiaoeImages.toString()}