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 +1 @@
1
- import{ArgumentError as W,CommandExecutionError as O,EmptyResultError as X}from"@jackwener/opencli/errors";export const SE_API="https://api.stackexchange.com/2.3",SE_SITE="stackoverflow";const Z="opencli-stackoverflow (+https://github.com/jackwener/opencli)";export function normalizeLimit(q,z,B,G="limit"){const F=Number(q??z);if(!Number.isInteger(F)||F<=0)throw new W(`${G} must be a positive integer`);if(F>B)throw new W(`${G} must be <= ${B}`);return F}export function requireString(q,z){const B=String(q??"").trim();if(!B)throw new W(`${z} cannot be empty`);return B}export async function seFetch(q,{searchParams:z}={}){const B=new URL(q.startsWith("http")?q:`${SE_API}${q.startsWith("/")?"":"/"}${q}`);if(z)for(const[F,Q]of Object.entries(z)){if(Q==null||Q==="")continue;B.searchParams.set(F,String(Q))}if(!B.searchParams.has("site"))B.searchParams.set("site",SE_SITE);let G;try{G=await fetch(B,{headers:{Accept:"application/json","Accept-Encoding":"gzip","User-Agent":Z}})}catch(F){throw new O(`stack exchange request failed: ${F?.message||F}`)}if(G.status===429)throw new O("stack exchange returned HTTP 429 (rate limited)","Wait a few seconds and retry, or lower --limit.");if(!G.ok){let F="";try{F=(await G.json())?.error_message||""}catch{}throw new O(`stack exchange HTTP ${G.status}: ${F||G.statusText}`)}let K;try{K=await G.json()}catch(F){throw new O(`stack exchange returned malformed JSON: ${F?.message||F}`)}if(K?.error_id)throw new O(`stack exchange API error: ${K.error_message||K.error_name}`,"Inspect the URL in a browser for the canonical error context.");return K}export function epochToDate(q){if(q==null||q==="")return"";const z=Number(q);if(!Number.isFinite(z)||z<=0)return"";return new Date(z*1000).toISOString().slice(0,10)}export function ensureItems(q,z){const B=Array.isArray(q?.items)?q.items:[];if(B.length===0)throw new X(z,`${z} returned no items.`);return B}const $={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'","&apos;":"'","&nbsp;":" "};export function decodeHtmlEntities(q){if(q==null)return"";return String(q).replace(/&#x([0-9a-fA-F]+);/g,(z,B)=>String.fromCodePoint(parseInt(B,16))).replace(/&#(\d+);/g,(z,B)=>String.fromCodePoint(parseInt(B,10))).replace(/&(amp|lt|gt|quot|#39|apos|nbsp);/g,(z)=>$[z]||z)}
1
+ import{ArgumentError as W,CommandExecutionError as O,EmptyResultError as X}from"@jackwener/opencli/errors";export const SE_API="https://api.stackexchange.com/2.3",SE_SITE="stackoverflow";const Z="opencli-stackoverflow (+https://github.com/jackwener/OpenCLI)";export function normalizeLimit(q,z,B,G="limit"){const F=Number(q??z);if(!Number.isInteger(F)||F<=0)throw new W(`${G} must be a positive integer`);if(F>B)throw new W(`${G} must be <= ${B}`);return F}export function requireString(q,z){const B=String(q??"").trim();if(!B)throw new W(`${z} cannot be empty`);return B}export async function seFetch(q,{searchParams:z}={}){const B=new URL(q.startsWith("http")?q:`${SE_API}${q.startsWith("/")?"":"/"}${q}`);if(z)for(const[F,Q]of Object.entries(z)){if(Q==null||Q==="")continue;B.searchParams.set(F,String(Q))}if(!B.searchParams.has("site"))B.searchParams.set("site",SE_SITE);let G;try{G=await fetch(B,{headers:{Accept:"application/json","Accept-Encoding":"gzip","User-Agent":Z}})}catch(F){throw new O(`stack exchange request failed: ${F?.message||F}`)}if(G.status===429)throw new O("stack exchange returned HTTP 429 (rate limited)","Wait a few seconds and retry, or lower --limit.");if(!G.ok){let F="";try{F=(await G.json())?.error_message||""}catch{}throw new O(`stack exchange HTTP ${G.status}: ${F||G.statusText}`)}let K;try{K=await G.json()}catch(F){throw new O(`stack exchange returned malformed JSON: ${F?.message||F}`)}if(K?.error_id)throw new O(`stack exchange API error: ${K.error_message||K.error_name}`,"Inspect the URL in a browser for the canonical error context.");return K}export function epochToDate(q){if(q==null||q==="")return"";const z=Number(q);if(!Number.isFinite(z)||z<=0)return"";return new Date(z*1000).toISOString().slice(0,10)}export function ensureItems(q,z){const B=Array.isArray(q?.items)?q.items:[];if(B.length===0)throw new X(z,`${z} returned no items.`);return B}const $={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'","&apos;":"'","&nbsp;":" "};export function decodeHtmlEntities(q){if(q==null)return"";return String(q).replace(/&#x([0-9a-fA-F]+);/g,(z,B)=>String.fromCodePoint(parseInt(B,16))).replace(/&#(\d+);/g,(z,B)=>String.fromCodePoint(parseInt(B,10))).replace(/&(amp|lt|gt|quot|#39|apos|nbsp);/g,(z)=>$[z]||z)}
@@ -1 +1 @@
1
- import{ArgumentError as s,CommandExecutionError as a,EmptyResultError as u}from"@jackwener/opencli/errors";export const STEAM_STORE="https://store.steampowered.com";const c="opencli-steam-adapter (+https://github.com/jackwener/opencli)";export function requireString(e,t){const r=String(e??"").trim();if(!r)throw new s(`steam ${t} cannot be empty`);return r}export function requireCountryCode(e,t="us"){const o=String(e===void 0||e===null?t:e).trim().toLowerCase();if(!/^[a-z]{2}$/.test(o))throw new s(`steam currency must be a two-letter storefront country code (got "${e}")`,"Examples: us, cn, jp, de. This controls Steam regional pricing and availability.");return o}export function requireBoundedInt(e,t,r,o="limit"){const n=e??t,i=typeof n==="number"?n:Number(n);if(!Number.isInteger(i)||i<=0)throw new s(`steam ${o} must be a positive integer`);if(i>r)throw new s(`steam ${o} must be <= ${r}`);return i}export function requireAppId(e){const t=String(e??"").trim();if(!t)throw new s('steam app id is required (e.g. "620" for Portal 2)');if(!/^\d+$/.test(t))throw new s(`steam app id "${e}" must be a positive integer`,"Copy the numeric id from `steam search` or the URL `store.steampowered.com/app/<id>/`.");return t}export async function steamFetch(e,t){let r;try{r=await fetch(e,{headers:{"user-agent":c,accept:"application/json"}})}catch(n){throw new a(`${t} request failed: ${n?.message??n}`,"Check that store.steampowered.com is reachable from this network.")}if(r.status===429)throw new a(`${t} returned HTTP 429 (rate limited)`,"Steam throttles bursty traffic; wait a few seconds and retry.");if(r.status===404)throw new u(t,"Steam returned 404 — the resource does not exist.");if(!r.ok)throw new a(`${t} returned HTTP ${r.status}`);let o;try{o=await r.json()}catch(n){throw new a(`${t} returned malformed JSON: ${n?.message??n}`)}return o}export function asString(e){return e==null?"":String(e)}const m={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&apos;":"'","&#39;":"'","&nbsp;":" "};export function decodeHtmlEntities(e){return String(e??"").replace(/&#x([0-9a-fA-F]+);/g,(t,r)=>String.fromCodePoint(parseInt(r,16))).replace(/&#(\d+);/g,(t,r)=>String.fromCodePoint(parseInt(r,10))).replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g,(t)=>m[t]||t)}export function priceCents(e){if(e==null||e==="")return null;const t=Number(e);if(!Number.isFinite(t))return null;return Number((t/100).toFixed(2))}
1
+ import{ArgumentError as s,CommandExecutionError as a,EmptyResultError as u}from"@jackwener/opencli/errors";export const STEAM_STORE="https://store.steampowered.com";const c="opencli-steam-adapter (+https://github.com/jackwener/OpenCLI)";export function requireString(e,t){const r=String(e??"").trim();if(!r)throw new s(`steam ${t} cannot be empty`);return r}export function requireCountryCode(e,t="us"){const o=String(e===void 0||e===null?t:e).trim().toLowerCase();if(!/^[a-z]{2}$/.test(o))throw new s(`steam currency must be a two-letter storefront country code (got "${e}")`,"Examples: us, cn, jp, de. This controls Steam regional pricing and availability.");return o}export function requireBoundedInt(e,t,r,o="limit"){const n=e??t,i=typeof n==="number"?n:Number(n);if(!Number.isInteger(i)||i<=0)throw new s(`steam ${o} must be a positive integer`);if(i>r)throw new s(`steam ${o} must be <= ${r}`);return i}export function requireAppId(e){const t=String(e??"").trim();if(!t)throw new s('steam app id is required (e.g. "620" for Portal 2)');if(!/^\d+$/.test(t))throw new s(`steam app id "${e}" must be a positive integer`,"Copy the numeric id from `steam search` or the URL `store.steampowered.com/app/<id>/`.");return t}export async function steamFetch(e,t){let r;try{r=await fetch(e,{headers:{"user-agent":c,accept:"application/json"}})}catch(n){throw new a(`${t} request failed: ${n?.message??n}`,"Check that store.steampowered.com is reachable from this network.")}if(r.status===429)throw new a(`${t} returned HTTP 429 (rate limited)`,"Steam throttles bursty traffic; wait a few seconds and retry.");if(r.status===404)throw new u(t,"Steam returned 404 — the resource does not exist.");if(!r.ok)throw new a(`${t} returned HTTP ${r.status}`);let o;try{o=await r.json()}catch(n){throw new a(`${t} returned malformed JSON: ${n?.message??n}`)}return o}export function asString(e){return e==null?"":String(e)}const m={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&apos;":"'","&#39;":"'","&nbsp;":" "};export function decodeHtmlEntities(e){return String(e??"").replace(/&#x([0-9a-fA-F]+);/g,(t,r)=>String.fromCodePoint(parseInt(r,16))).replace(/&#(\d+);/g,(t,r)=>String.fromCodePoint(parseInt(r,10))).replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g,(t)=>m[t]||t)}export function priceCents(e){if(e==null||e==="")return null;const t=Number(e);if(!Number.isFinite(t))return null;return Number((t/100).toFixed(2))}
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as i,CommandExecutionError as o}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as n}from"../_shared/site-auth.js";import{publicationContext as c}from"./post-utils.js";async function a(e){return(await e.getCookies({url:"https://substack.com"})).some((s)=>s.name==="substack.sid"||s.name==="connect.sid")}async function r(e){if(!await a(e))throw new i("substack.com","Substack session cookie missing");await e.goto("https://substack.com/");const t=await e.evaluate(`(async () => {
1
+ import{AuthRequiredError as i,CommandExecutionError as o}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as n}from"../_shared/site-auth.js";import{publicationContext as c}from"./post-utils.js";async function a(s){return(await s.getCookies({url:"https://substack.com"})).some((e)=>e.name==="substack.sid"||e.name==="connect.sid")}async function r(s){if(!await a(s))throw new i("substack.com","Substack session cookie missing");await s.goto("https://substack.com/");const t=await s.evaluate(`(async () => {
2
2
  try {
3
3
  const res = await fetch('/api/v1/user/profile/self', { credentials: 'include', headers: { 'Accept': 'application/json' } });
4
4
  if (res.status === 401 || res.status === 403) return { kind: 'auth', detail: 'profile/self HTTP ' + res.status };
@@ -8,4 +8,4 @@ import{AuthRequiredError as i,CommandExecutionError as o}from"@jackwener/opencli
8
8
  if (!data || data.id == null) return { kind: 'auth', detail: 'profile/self returned no user — anonymous' };
9
9
  return { ok: true, profile: data };
10
10
  } catch (e) { return { kind: 'exception', detail: String(e && e.message || e) }; }
11
- })()`);if(t?.kind==="auth")throw new i("substack.com",t.detail);if(t?.kind==="http")throw new o(`HTTP ${t.httpStatus} from profile/self`);if(t?.kind==="exception")throw new o(`Substack whoami failed: ${t.detail}`);if(!t?.ok)throw new o(`Unexpected Substack probe: ${JSON.stringify(t)}`);const s=c(t.profile);return{user_id:String(s.userId),publication:s.publication,url:s.host}}n({site:"substack",domain:"substack.com",loginUrl:"https://substack.com/sign-in",columns:["user_id","publication","url"],quickCheck:a,verify:r,poll:async(e)=>{if(!await a(e))throw new i("substack.com","Waiting for Substack session cookie");return r(e)}});
11
+ })()`);if(t?.kind==="auth")throw new i("substack.com",t.detail);if(t?.kind==="http")throw new o(`HTTP ${t.httpStatus} from profile/self`);if(t?.kind==="exception")throw new o(`Substack whoami failed: ${t.detail}`);if(!t?.ok)throw new o(`Unexpected Substack probe: ${JSON.stringify(t)}`);const e=c(t.profile);return{user_id:String(e.userId),publication:e.publication,url:e.host}}n({site:"substack",domain:"substack.com",loginUrl:"https://substack.com/sign-in",columns:["user_id","publication","url"],quickCheck:a,verify:r,poll:async(s)=>{if(!await a(s))throw new i("substack.com","Waiting for Substack session cookie");return r(s)}});
@@ -1,4 +1,4 @@
1
1
  import{cli as c,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as z,CommandExecutionError as V}from"@jackwener/opencli/errors";import{DEFAULT_SUNO_MODEL as y,SUNO_DOMAIN as t,SUNO_MODELS as v,SUNO_URL as Z,checkSunoCaptcha as o,clampSlider as _,downloadSunoClip as i,ensureSunoSession as e,normalizeBooleanFlag as A,parseFormats as a,pollSunoClips as s,requirePositiveInt as r,resolveSunoOutputDir as HH,submitSunoGeneration as JH}from"./utils.js";import*as C from"node:crypto";import*as KH from"node:os";function QH(K){if(!K)return"-";const J=KH.homedir();return K.startsWith(J)?`~${K.slice(J.length)}`:K}export const generateCommand=c({site:"suno",name:"generate",access:"write",description:"Generate music with Suno (V5.5 chirp-fenix by default) and download clips locally",domain:t,strategy:n.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",positional:!0,required:!1,help:"Simple-mode description (ignored when --lyrics is provided)"},{name:"lyrics",help:"Custom-mode lyrics (with [Verse]/[Chorus] metatags). Triggers Custom mode."},{name:"tags",help:"Custom-mode style tags (genre, BPM, instruments...). Used with --lyrics."},{name:"negative-tags",help:'Custom-mode style exclusions (e.g. "no vocals, no autotune"). Used with --lyrics.'},{name:"title",help:"Song title (default: auto-derived from prompt)"},{name:"instrumental",type:"boolean",default:!1,help:"No vocals"},{name:"model",help:`Model id: ${v.join(", ")}. Default: ${y}`},{name:"weirdness",help:"Creative weirdness slider (0..1). Default: 0.5"},{name:"style-weight",help:"Style adherence slider (0..1). Default: 0.5"},{name:"formats",help:"Comma-separated download formats: mp3, m4a, wav, video, cover, metadata. Default: mp3,metadata"},{name:"op",help:"Output directory (default: ~/Music/suno)"},{name:"timeout",type:"int",default:300,help:"Max seconds to wait for clips to finish (default: 300)"},{name:"sd",type:"boolean",default:!1,help:"Skip download; only print clip ids and Suno URLs"},{name:"confirm-paid",type:"boolean",default:!1,help:"Required to allow paid downloads (wav). Without it, paid formats are skipped with a warning."}],columns:["status","clip","title","files","link"],func:async(K,J)=>{const B=J.lyrics?String(J.lyrics):"",M=J.tags?String(J.tags):"",D=J["negative-tags"]?String(J["negative-tags"]):"",R=J.prompt?String(J.prompt):"",P=J.title?String(J.title):"",T=J.model?String(J.model).trim():y;if(!v.includes(T))throw new z(`Unsupported --model "${T}"`,`Choices: ${v.join(", ")}`);const $=B.trim()!=="";if(!$&&!R.trim())throw new z("Either provide a Simple-mode prompt as the positional argument, or pass --lyrics for Custom mode.",`Examples:
2
- opencli suno generate "lo-fi study beat, 80 bpm"
3
- opencli suno generate --lyrics "[Verse]\\n..." --tags "synthwave, 120 bpm"`);if(!$&&(M||D))throw new z("--tags and --negative-tags only apply in Custom mode (alongside --lyrics).");const f=a(J.formats),h=A(J["confirm-paid"]),I=A(J.sd),O=new Set(["wav"]),b=[],L=f.filter((H)=>{if(O.has(H)&&!h){b.push(H);return!1}return!0});if(!I&&!L.length)throw new z("All requested formats require --confirm-paid true","Add --confirm-paid true or include a free format such as mp3 or metadata.");const U=HH(J.op),E=r(J.timeout,"--timeout"),S=A(J.instrumental),u=_(J.weirdness,"--weirdness",0.5),m=_(J["style-weight"],"--style-weight",0.5),d=(P||($?M||B.split(`
2
+ ppcli suno generate "lo-fi study beat, 80 bpm"
3
+ ppcli suno generate --lyrics "[Verse]\\n..." --tags "synthwave, 120 bpm"`);if(!$&&(M||D))throw new z("--tags and --negative-tags only apply in Custom mode (alongside --lyrics).");const f=a(J.formats),h=A(J["confirm-paid"]),I=A(J.sd),O=new Set(["wav"]),b=[],L=f.filter((H)=>{if(O.has(H)&&!h){b.push(H);return!1}return!0});if(!I&&!L.length)throw new z("All requested formats require --confirm-paid true","Add --confirm-paid true or include a free format such as mp3 or metadata.");const U=HH(J.op),E=r(J.timeout,"--timeout"),S=A(J.instrumental),u=_(J.weirdness,"--weirdness",0.5),m=_(J["style-weight"],"--style-weight",0.5),d=(P||($?M||B.split(`
4
4
  `)[0]:R)).replace(/\s+/g," ").trim().slice(0,60)||"Untitled",X=await e(K);if(!X.planId)throw new V(`Suno generation needs a resolved plan id for the user_tier field, but billing/info did not surface one for this account (subscription_type=${X.planKey}). Verify the account is active at ${Z}/account, then retry.`);const G=X.deviceId,W=await o(K,G);if(!W?.ok)throw new V(`Suno captcha pre-flight failed${W?.status?` (HTTP ${W.status})`:""}.`,`Open ${Z}/create in Chrome and verify the account is ready, then retry.`);if(W?.required)throw new V("Suno requires a CAPTCHA challenge for this account/IP right now.",`Open ${Z}/create in Chrome, solve a Create challenge once, then retry.`);if(X.totalCreditsAvailable<10){const H=X.breakdown;throw new V(`Suno generation needs ~10 credits; you have ${X.totalCreditsAvailable} (monthly ${H.monthlyRemaining}/${H.monthlyLimit} + packs ${H.purchasedPacks} + leftover ${H.pack}). Top up at ${Z}/account.`)}const k=C.randomUUID(),p=C.randomUUID(),N=await JH(K,{mode:$?"custom":"simple",model:T,title:d,lyrics:B,tags:M,negativeTags:D,description:R,makeInstrumental:S,weirdness:u,styleWeight:m,userTier:X.planId,createSessionToken:p,transactionUuid:k,deviceId:G});if(!Array.isArray(N.clips))throw new V("Suno generation returned malformed clips payload.");const j=N.clips.map((H)=>H?.id);if(j.some((H)=>!H))throw new V("Suno generation returned malformed clip identity.");if(!j.length)throw new V("Suno accepted the request but returned no clip ids.");const x=await s(K,j,E,G);if(!x.filter((H)=>H.status==="complete").length){const H=x.map((Y)=>`${Y.id.slice(0,8)}:${Y.status}`).join(", ");throw new V(`All Suno clips failed (${H}). Open ${Z}/song/${j[0]} to inspect.`)}const q=[];for(const H of x){const Y=`${Z}/song/${H.id}`;if(H.status!=="complete"){q.push({status:`❌ ${H.status}`,clip:H.id.slice(0,8),title:H.title||"(untitled)",files:"-",link:`\uD83D\uDD17 ${Y}`});continue}if(I){q.push({status:"\uD83C\uDFB5 generated",clip:H.id.slice(0,8),title:H.title||"(untitled)",files:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${Y}`});continue}const F=await i(K,H,U,L,G);if(!F.written.some((Q)=>Q.ok))throw new V(`Suno download wrote no files for clip ${H.id}`);const w=F.written.map((Q)=>Q.ok?`${Q.format}:${QH(Q.file)}`:`${Q.format}:✗(${Q.reason})`).join(" | "),g=b.length?` | skipped(needs --confirm-paid):${b.join(",")}`:"",l=F.written.some((Q)=>!Q.ok);q.push({status:l?"⚠ partial":"✅ saved",clip:H.id.slice(0,8),title:H.title||"(untitled)",files:`\uD83D\uDCC1 ${w}${g}`,link:`\uD83D\uDD17 ${Y}`})}return q}});
package/clis/suno/list.js CHANGED
@@ -1,6 +1,6 @@
1
- import{cli as d,Strategy as u}from"@jackwener/opencli/registry";import{CommandExecutionError as i,EmptyResultError as c}from"@jackwener/opencli/errors";import{STUDIO_API as p,SUNO_DOMAIN as f,SUNO_URL as m,ensureSunoSession as y,requireNonNegativeInt as w,requirePositiveInt as g,unwrapEvaluateResult as k}from"./utils.js";export const listCommand=d({site:"suno",name:"list",access:"read",description:"List recent Suno clips in your library (id, title, status, created_at, link)",domain:f,strategy:u.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"limit",type:"int",default:20,help:"Max clips to list (default: 20)"},{name:"page",type:"int",default:0,help:"Pagination offset, 0-based (default: 0)"}],columns:["rank","clip","title","status","created","link"],func:async(r,s)=>{const a=g(s.limit,"--limit"),o=w(s.page??0,"--page"),n=(await y(r)).deviceId,t=k(await r.evaluate(`(async () => {
1
+ import{cli as d,Strategy as u}from"@jackwener/opencli/registry";import{CommandExecutionError as i,EmptyResultError as p}from"@jackwener/opencli/errors";import{STUDIO_API as c,SUNO_DOMAIN as f,SUNO_URL as m,ensureSunoSession as y,requireNonNegativeInt as w,requirePositiveInt as g,unwrapEvaluateResult as k}from"./utils.js";export const listCommand=d({site:"suno",name:"list",access:"read",description:"List recent Suno clips in your library (id, title, status, created_at, link)",domain:f,strategy:u.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"limit",type:"int",default:20,help:"Max clips to list (default: 20)"},{name:"page",type:"int",default:0,help:"Pagination offset, 0-based (default: 0)"}],columns:["rank","clip","title","status","created","link"],func:async(r,s)=>{const a=g(s.limit,"--limit"),o=w(s.page??0,"--page"),n=(await y(r)).deviceId,t=k(await r.evaluate(`(async () => {
2
2
  const browserToken = JSON.stringify({ token: btoa(JSON.stringify({ timestamp: Date.now() })) });
3
- const res = await fetch('${p}/api/feed/v2?page=${o}', {
3
+ const res = await fetch('${c}/api/feed/v2?page=${o}', {
4
4
  headers: {
5
5
  'Authorization': 'Bearer ' + (await window.Clerk.session.getToken()),
6
6
  'browser-token': browserToken,
@@ -11,4 +11,4 @@ import{cli as d,Strategy as u}from"@jackwener/opencli/registry";import{CommandEx
11
11
  const data = await res.json().catch(() => null);
12
12
  if (!data || !Array.isArray(data.clips)) return { ok: false, error: 'malformed clips payload' };
13
13
  return { ok: true, clips: data.clips };
14
- })()`));if(!t?.ok)throw new i(t?.error||`Suno feed lookup failed (HTTP ${t?.status||"?"}).`);if(!Array.isArray(t.clips))throw new i("Suno feed lookup returned malformed clips payload");if(t.clips.length===0)throw new c("suno list","No Suno clips found in your library.");return t.clips.slice(0,a).map((e,l)=>{if(!e||typeof e.id!=="string"||!e.id)throw new i("Suno feed lookup returned malformed clip identity");return{rank:l+1+o*a,clip:e.id.slice(0,8),title:e.title||"(untitled)",status:e.status||"?",created:(e.created_at||"").replace("T"," ").replace(/\..*$/,"").replace(/Z$/,""),link:`${m}/song/${e.id}`}})}});
14
+ })()`));if(!t?.ok)throw new i(t?.error||`Suno feed lookup failed (HTTP ${t?.status||"?"}).`);if(!Array.isArray(t.clips))throw new i("Suno feed lookup returned malformed clips payload");if(t.clips.length===0)throw new p("suno list","No Suno clips found in your library.");return t.clips.slice(0,a).map((e,l)=>{if(!e||typeof e.id!=="string"||!e.id)throw new i("Suno feed lookup returned malformed clip identity");return{rank:l+1+o*a,clip:e.id.slice(0,8),title:e.title||"(untitled)",status:e.status||"?",created:(e.created_at||"").replace("T"," ").replace(/\..*$/,"").replace(/Z$/,""),link:`${m}/song/${e.id}`}})}});
@@ -8,7 +8,7 @@ import{ArgumentError as X,AuthRequiredError as G,CommandExecutionError as Z}from
8
8
  })()`)}async function W(b){return b.evaluate(`(() => {
9
9
  const ed = document.querySelector(${JSON.stringify(Y)});
10
10
  return ed ? (ed.textContent || '').replace(/\\uFEFF/g, '') : null;
11
- })()`)}j({site:"telegram",name:"send",access:"write",description:'Send a text message to a Telegram chat via web.telegram.org/k/. Pass a channel @username / t.me link, or "saved" for your own Saved Messages. You must have permission to post in the target.',domain:"web.telegram.org",strategy:v.UI,browser:!0,args:[{name:"peer",type:"string",required:!0,positional:!0,help:'Target: channel @username, t.me/<name> link, or "saved" (your Saved Messages)'},{name:"text",type:"string",required:!0,positional:!0,help:"Message text (multi-line allowed)"}],columns:["status","peer","message"],func:async(b,z)=>{if(!b)throw new Z("Browser session required for telegram send");const J=resolvePeer(z.peer),F=String(z.text??"");y(F,{label:"消息内容"});if(!F.trim())throw new X("消息内容不能为空。");await b.goto(J.url);let V=!1;for(let H=0;H<B;H++){const q=await U(b);if(q==="auth")throw new G("web.telegram.org","Telegram Web 停在登录页 —— 未登录。先 `opencli telegram login`。");if(q==="ready"){V=!0;break}if(q==="no-chat"&&J.savedMessages)await I(b);await b.wait(1)}if(!V)throw new Z(`未能在 ${J.label} 打开消息输入框 —— 会话可能不存在、你没有发帖权限,或页面未加载完。`);await b.click(Y);await b.pressKey("Control+A");await b.pressKey("Delete");const Q=F.split(`
11
+ })()`)}j({site:"telegram",name:"send",access:"write",description:'Send a text message to a Telegram chat via web.telegram.org/k/. Pass a channel @username / t.me link, or "saved" for your own Saved Messages. You must have permission to post in the target.',domain:"web.telegram.org",strategy:v.UI,browser:!0,args:[{name:"peer",type:"string",required:!0,positional:!0,help:'Target: channel @username, t.me/<name> link, or "saved" (your Saved Messages)'},{name:"text",type:"string",required:!0,positional:!0,help:"Message text (multi-line allowed)"}],columns:["status","peer","message"],func:async(b,z)=>{if(!b)throw new Z("Browser session required for telegram send");const J=resolvePeer(z.peer),F=String(z.text??"");y(F,{label:"消息内容"});if(!F.trim())throw new X("消息内容不能为空。");await b.goto(J.url);let V=!1;for(let H=0;H<B;H++){const q=await U(b);if(q==="auth")throw new G("web.telegram.org","Telegram Web 停在登录页 —— 未登录。先 `ppcli telegram login`。");if(q==="ready"){V=!0;break}if(q==="no-chat"&&J.savedMessages)await I(b);await b.wait(1)}if(!V)throw new Z(`未能在 ${J.label} 打开消息输入框 —— 会话可能不存在、你没有发帖权限,或页面未加载完。`);await b.click(Y);await b.pressKey("Control+A");await b.pressKey("Delete");const Q=F.split(`
12
12
  `);for(let H=0;H<Q.length;H++){if(H>0)await b.pressKey("Shift+Enter");if(Q[H].length)await b.typeText(Y,Q[H])}const $=await W(b),f=(H)=>String(H||"").replace(/\s+/g," ").trim();if($===null||f($)!==f(F))throw new Z(`发送前组文失败(输入框内容为 ${JSON.stringify(($||"").slice(0,60))})。`);if(!(await b.evaluate(`(() => {
13
13
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
14
14
  const btn = Array.from(document.querySelectorAll(${JSON.stringify(M)})).find(visible);
@@ -1,4 +1,4 @@
1
- import{cli as V,Strategy as R}from"@jackwener/opencli/registry";import{ArgumentError as j,AuthRequiredError as G,CommandExecutionError as $,EmptyResultError as T,getErrorMessage as _}from"@jackwener/opencli/errors";const F="https://www.tiktok.com/tiktokstudio/content",A="/tiktok/creator/manage/item_list/v1/",y=20,N=250,P=50;function U(B,J,W,Q){const Z=Number(B??W);if(!Number.isInteger(Z)||Z<=0)throw new j(`${J} must be a positive integer`,`Example: opencli tiktok creator-videos --${J} ${W}`);if(Z>Q)throw new j(`${J} must be <= ${Q}`,`Example: opencli tiktok creator-videos --${J} ${Q}`);return Z}function O(B){const W=String(B??"0").trim();if(!/^\d+$/.test(W))throw new j("cursor must be a non-negative integer string","Example: opencli tiktok creator-videos --cursor 0");const Q=Number(W);if(!Number.isSafeInteger(Q))throw new j("cursor must be a safe integer","Example: opencli tiktok creator-videos --cursor 0");return Q}function h(B,J){return{cursor:B,size:J,query:{conditions:[],sort_orders:[{field_name:"create_time",order:2}]}}}function q(B){const J={url:`${A}?aid=1988`,body:B};return`
1
+ import{cli as V,Strategy as R}from"@jackwener/opencli/registry";import{ArgumentError as j,AuthRequiredError as G,CommandExecutionError as $,EmptyResultError as T,getErrorMessage as _}from"@jackwener/opencli/errors";const F="https://www.tiktok.com/tiktokstudio/content",A="/tiktok/creator/manage/item_list/v1/",y=20,N=250,P=50;function U(B,J,W,Q){const Z=Number(B??W);if(!Number.isInteger(Z)||Z<=0)throw new j(`${J} must be a positive integer`,`Example: ppcli tiktok creator-videos --${J} ${W}`);if(Z>Q)throw new j(`${J} must be <= ${Q}`,`Example: ppcli tiktok creator-videos --${J} ${Q}`);return Z}function O(B){const W=String(B??"0").trim();if(!/^\d+$/.test(W))throw new j("cursor must be a non-negative integer string","Example: ppcli tiktok creator-videos --cursor 0");const Q=Number(W);if(!Number.isSafeInteger(Q))throw new j("cursor must be a safe integer","Example: ppcli tiktok creator-videos --cursor 0");return Q}function h(B,J){return{cursor:B,size:J,query:{conditions:[],sort_orders:[{field_name:"create_time",order:2}]}}}function q(B){const J={url:`${A}?aid=1988`,body:B};return`
2
2
  (async () => {
3
3
  const request = ${JSON.stringify(J)};
4
4
  try {
@@ -79,4 +79,4 @@ import{cli as V,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentE
79
79
  }
80
80
  return false;
81
81
  })()
82
- `)}async function k(q){const z=Date.now()+300000;while(Date.now()<z){if(await f(q,b))await q.evaluate(`(() => { const b = document.querySelector('${b}'); if (b) b.click(); })()`);await q.wait({time:1});if(await f(q,M))return;await q.wait({time:0.5})}if(await f(q,M))return;throw new Q("TikTok 发布未确认成功(未出现成功标识)")}V({site:"tiktok",name:"publish",access:"write",description:"Publish a video to TikTok (one command: upload + caption + publish; optional schedule)",domain:"www.tiktok.com",strategy:v.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"Video file path"},{name:"title",required:!0,help:"Caption / title text"},{name:"tags",default:"",help:"Comma-separated hashtags (without #)"},{name:"schedule",default:"",help:"Scheduled publish time (ISO8601 or Unix seconds); omit to post now"}],columns:["status","title","url"],func:async(q,z)=>{const H=String(z.title??"").trim();if(!H)throw new N("--title is required");const J=F(z.video),$=z.tags?String(z.tags).split(",").map((W)=>W.trim()).filter(Boolean):[],K=I(z.schedule);await q.goto(D);if(!await x(q,"tiktokstudio/upload",30000))throw new Q("未能进入 TikTok Studio 上传页(登录态可能失效,请先 opencli tiktok login)");await G(q,'iframe[data-tt="Upload_index_iframe"], div.upload-container',1e4);await G(q,'input[type="file"]',15000);await j(q,A,[J]);if(!await G(q,B,60000))throw new Q("TikTok 文案编辑器未出现");await P(q,H,$);await U(q,J);if(K)await L(q,K);await k(q);return[{status:K?"✅ Scheduled":"✅ Published",title:H,url:await q.evaluate("() => location.href").then(Z)}]}});
82
+ `)}async function k(q){const z=Date.now()+300000;while(Date.now()<z){if(await f(q,b))await q.evaluate(`(() => { const b = document.querySelector('${b}'); if (b) b.click(); })()`);await q.wait({time:1});if(await f(q,M))return;await q.wait({time:0.5})}if(await f(q,M))return;throw new Q("TikTok 发布未确认成功(未出现成功标识)")}V({site:"tiktok",name:"publish",access:"write",description:"Publish a video to TikTok (one command: upload + caption + publish; optional schedule)",domain:"www.tiktok.com",strategy:v.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"Video file path"},{name:"title",required:!0,help:"Caption / title text"},{name:"tags",default:"",help:"Comma-separated hashtags (without #)"},{name:"schedule",default:"",help:"Scheduled publish time (ISO8601 or Unix seconds); omit to post now"}],columns:["status","title","url"],func:async(q,z)=>{const H=String(z.title??"").trim();if(!H)throw new N("--title is required");const J=F(z.video),$=z.tags?String(z.tags).split(",").map((W)=>W.trim()).filter(Boolean):[],K=I(z.schedule);await q.goto(D);if(!await x(q,"tiktokstudio/upload",30000))throw new Q("未能进入 TikTok Studio 上传页(登录态可能失效,请先 ppcli tiktok login)");await G(q,'iframe[data-tt="Upload_index_iframe"], div.upload-container',1e4);await G(q,'input[type="file"]',15000);await j(q,A,[J]);if(!await G(q,B,60000))throw new Q("TikTok 文案编辑器未出现");await P(q,H,$);await U(q,J);if(K)await L(q,K);await k(q);return[{status:K?"✅ Scheduled":"✅ Published",title:H,url:await q.evaluate("() => location.href").then(Z)}]}});
@@ -1,4 +1,4 @@
1
- import{ArgumentError as L,AuthRequiredError as W,CommandExecutionError as V,EmptyResultError as $,getErrorMessage as Z}from"@jackwener/opencli/errors";export const TIKTOK_AID="1988",TIKTOK_HOST="https://www.tiktok.com",SERVER_PAGE_MAX=30,MAX_PAGES=4;export function requireLimit(B,{fallback:z,max:D,name:G="limit"}){const J=Number(B??z);if(!Number.isInteger(J)||J<=0)throw new L(`${G} must be a positive integer`,`Example: --${G} ${z}`);if(J>D)throw new L(`${G} must be <= ${D}`,`Example: --${G} ${D}`);return J}export function normalizeUsername(B){const z=String(B??"").trim().replace(/^@+/,"");if(!z)throw new L("username is required","Example: opencli tiktok following <username>");if(!/^[A-Za-z0-9._-]+$/.test(z))throw new L("username contains unsupported characters","Pass the TikTok handle without @, for example: dictogo");return z}export const NOTIFICATION_TYPES={all:{code:0,label:"all"},likes:{code:3,label:"likes"},comments:{code:7,label:"comments"},mentions:{code:6,label:"mentions"},followers:{code:4,label:"followers"}};export function requireNotificationType(B){const z=String(B??"all").trim().toLowerCase();if(!Object.prototype.hasOwnProperty.call(NOTIFICATION_TYPES,z))throw new L(`unsupported notification type: ${B}`,`Allowed: ${Object.keys(NOTIFICATION_TYPES).join(", ")}`);return z}export const COMMENT_TEXT_MAX=150;export function requireCommentText(B){const z=String(B??"").trim();if(!z)throw new L("comment text is required",'Example: opencli tiktok comment <url> "great video"');if(z.length>COMMENT_TEXT_MAX)throw new L(`comment text must be <= ${COMMENT_TEXT_MAX} characters (got ${z.length})`,"TikTok rejects long comments at submit time; trim before retrying");return z}export function parseTikTokVideoUrl(B){const z=String(B??"").trim();if(!z)throw new L("video URL is required",'Example: opencli tiktok comment https://www.tiktok.com/@user/video/1234567890 "..."');let D;try{D=new URL(z)}catch{throw new L(`invalid video URL: ${z}`,"Example: https://www.tiktok.com/@user/video/1234567890")}if(!/(^|\.)tiktok\.com$/i.test(D.hostname))throw new L(`URL must be on tiktok.com (got ${D.hostname})`,"Example: https://www.tiktok.com/@user/video/1234567890");const G=D.pathname.match(/^\/@([A-Za-z0-9._-]+)\/video\/(\d+)\/?$/);if(!G)throw new L("URL must follow /@<username>/video/<id> format","Example: https://www.tiktok.com/@user/video/1234567890");return{url:D.toString(),username:G[1],videoId:G[2]}}export function looksTikTokAuthFailure(B){return/\bAUTH_REQUIRED\b|\b(auth|captcha|login|log in|permission|unauthori[sz]ed|forbidden)\b|HTTP\s+(401|403)\b/i.test(String(B||""))}export function looksTikTokUpstreamFailure(B){return/\b(API failed|HTTP\s+\d+|invalid JSON|Failed to fetch|network|fetch)\b/i.test(String(B||""))}export function throwTikTokPageContextError(B,{authMessage:z,emptyPattern:D,emptyTarget:G,failureMessage:Q}){const J=Z(B);if(looksTikTokAuthFailure(J))throw new W("tiktok.com",z);if(looksTikTokUpstreamFailure(J))throw new V(`${Q}: ${J}`);if(D.test(J))throw new $(G,J);throw new V(`${Q}: ${J}`)}export const BUTTON_WALKER_SENTINELS={AUTH_REQUIRED:"AUTH_REQUIRED",BUTTON_NOT_FOUND:"BUTTON_NOT_FOUND",STATE_VERIFY_FAIL:"STATE_VERIFY_FAIL",RATE_LIMITED:"RATE_LIMITED"},RETRYABLE_HINTS={commentFailure:"retryable=false reason=server-fan-out — TikTok may have accepted the comment but state verification timed out; do NOT auto-retry",relationFailure:"retryable=true reason=idempotent — TikTok dedupes follow/unfollow on retry; safe to re-run"};export function throwButtonWalkerError(B,{authMessage:z,failureMessage:D,retryableHint:G}){const Q=Z(B);if(/\bRATE_LIMITED\b|\b(captcha|too many requests|rate limit|try again later|slow down)\b/i.test(Q))throw new V(`${D}: ${Q}`,G);if(looksTikTokAuthFailure(Q))throw new W("tiktok.com",z);throw new V(`${D}: ${Q}`,G)}export const BROWSER_HELPERS=`
1
+ import{ArgumentError as L,AuthRequiredError as W,CommandExecutionError as V,EmptyResultError as $,getErrorMessage as Z}from"@jackwener/opencli/errors";export const TIKTOK_AID="1988",TIKTOK_HOST="https://www.tiktok.com",SERVER_PAGE_MAX=30,MAX_PAGES=4;export function requireLimit(B,{fallback:z,max:D,name:G="limit"}){const J=Number(B??z);if(!Number.isInteger(J)||J<=0)throw new L(`${G} must be a positive integer`,`Example: --${G} ${z}`);if(J>D)throw new L(`${G} must be <= ${D}`,`Example: --${G} ${D}`);return J}export function normalizeUsername(B){const z=String(B??"").trim().replace(/^@+/,"");if(!z)throw new L("username is required","Example: ppcli tiktok following <username>");if(!/^[A-Za-z0-9._-]+$/.test(z))throw new L("username contains unsupported characters","Pass the TikTok handle without @, for example: dictogo");return z}export const NOTIFICATION_TYPES={all:{code:0,label:"all"},likes:{code:3,label:"likes"},comments:{code:7,label:"comments"},mentions:{code:6,label:"mentions"},followers:{code:4,label:"followers"}};export function requireNotificationType(B){const z=String(B??"all").trim().toLowerCase();if(!Object.prototype.hasOwnProperty.call(NOTIFICATION_TYPES,z))throw new L(`unsupported notification type: ${B}`,`Allowed: ${Object.keys(NOTIFICATION_TYPES).join(", ")}`);return z}export const COMMENT_TEXT_MAX=150;export function requireCommentText(B){const z=String(B??"").trim();if(!z)throw new L("comment text is required",'Example: ppcli tiktok comment <url> "great video"');if(z.length>COMMENT_TEXT_MAX)throw new L(`comment text must be <= ${COMMENT_TEXT_MAX} characters (got ${z.length})`,"TikTok rejects long comments at submit time; trim before retrying");return z}export function parseTikTokVideoUrl(B){const z=String(B??"").trim();if(!z)throw new L("video URL is required",'Example: ppcli tiktok comment https://www.tiktok.com/@user/video/1234567890 "..."');let D;try{D=new URL(z)}catch{throw new L(`invalid video URL: ${z}`,"Example: https://www.tiktok.com/@user/video/1234567890")}if(!/(^|\.)tiktok\.com$/i.test(D.hostname))throw new L(`URL must be on tiktok.com (got ${D.hostname})`,"Example: https://www.tiktok.com/@user/video/1234567890");const G=D.pathname.match(/^\/@([A-Za-z0-9._-]+)\/video\/(\d+)\/?$/);if(!G)throw new L("URL must follow /@<username>/video/<id> format","Example: https://www.tiktok.com/@user/video/1234567890");return{url:D.toString(),username:G[1],videoId:G[2]}}export function looksTikTokAuthFailure(B){return/\bAUTH_REQUIRED\b|\b(auth|captcha|login|log in|permission|unauthori[sz]ed|forbidden)\b|HTTP\s+(401|403)\b/i.test(String(B||""))}export function looksTikTokUpstreamFailure(B){return/\b(API failed|HTTP\s+\d+|invalid JSON|Failed to fetch|network|fetch)\b/i.test(String(B||""))}export function throwTikTokPageContextError(B,{authMessage:z,emptyPattern:D,emptyTarget:G,failureMessage:Q}){const J=Z(B);if(looksTikTokAuthFailure(J))throw new W("tiktok.com",z);if(looksTikTokUpstreamFailure(J))throw new V(`${Q}: ${J}`);if(D.test(J))throw new $(G,J);throw new V(`${Q}: ${J}`)}export const BUTTON_WALKER_SENTINELS={AUTH_REQUIRED:"AUTH_REQUIRED",BUTTON_NOT_FOUND:"BUTTON_NOT_FOUND",STATE_VERIFY_FAIL:"STATE_VERIFY_FAIL",RATE_LIMITED:"RATE_LIMITED"},RETRYABLE_HINTS={commentFailure:"retryable=false reason=server-fan-out — TikTok may have accepted the comment but state verification timed out; do NOT auto-retry",relationFailure:"retryable=true reason=idempotent — TikTok dedupes follow/unfollow on retry; safe to re-run"};export function throwButtonWalkerError(B,{authMessage:z,failureMessage:D,retryableHint:G}){const Q=Z(B);if(/\bRATE_LIMITED\b|\b(captcha|too many requests|rate limit|try again later|slow down)\b/i.test(Q))throw new V(`${D}: ${Q}`,G);if(looksTikTokAuthFailure(Q))throw new W("tiktok.com",z);throw new V(`${D}: ${Q}`,G)}export const BROWSER_HELPERS=`
2
2
  function asNumber(value) {
3
3
  if (value === null || value === undefined || value === '') return null;
4
4
  const n = Number(value);
@@ -1 +1 @@
1
- import{cli as r,Strategy as s}from"@jackwener/opencli/registry";import{activityScript as i,normalizeMaxChars as n}from"./utils.js";export const activityCommand=r({site:"trae-cn",name:"activity",access:"read",description:"Read the current Trae CN task/activity state, including in-progress steps when visible",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn activity --max-chars 1200 -f json",domain:"localhost",strategy:s.UI,browser:!0,args:[{name:"max-chars",type:"int",required:!1,help:"Max chars per text field; 0 returns full text (default: 1200)",default:1200}],columns:["Status","Workspace","Model","Agent","LatestRole","TurnIndex","MessageId","Progress","ActiveStep","CompletedSteps","PendingSteps","TotalSteps","ApprovalPending","ApprovalKind","ApprovalButton","Thinking","LatestText","TextChars","UpdatedAt"],func:async(t,e)=>{const a=n(e["max-chars"],1200);return[await t.evaluate(i(a))]}});
1
+ import{cli as r,Strategy as s}from"@jackwener/opencli/registry";import{activityScript as i,normalizeMaxChars as n}from"./utils.js";export const activityCommand=r({site:"trae-cn",name:"activity",access:"read",description:"Read the current Trae CN task/activity state, including in-progress steps when visible",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn activity --max-chars 1200 -f json",domain:"localhost",strategy:s.UI,browser:!0,args:[{name:"max-chars",type:"int",required:!1,help:"Max chars per text field; 0 returns full text (default: 1200)",default:1200}],columns:["Status","Workspace","Model","Agent","LatestRole","TurnIndex","MessageId","Progress","ActiveStep","CompletedSteps","PendingSteps","TotalSteps","ApprovalPending","ApprovalKind","ApprovalButton","Thinking","LatestText","TextChars","UpdatedAt"],func:async(t,e)=>{const a=n(e["max-chars"],1200);return[await t.evaluate(i(a))]}});
@@ -1 +1 @@
1
- import{cli as s,Strategy as m}from"@jackwener/opencli/registry";import{CommandExecutionError as c}from"@jackwener/opencli/errors";import{approveTraePrompts as d,normalizeApprovalKinds as u,normalizeApprovalLimit as f,normalizeMaxChars as h}from"./utils.js";function v(e){return e===!0||String(e).toLowerCase()==="true"}export const approveCommand=s({site:"trae-cn",name:"approve",access:"write",description:"Approve visible Trae CN permission prompts such as terminal-run, high-risk command, or delete confirmations",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn approve --approve-kinds terminal,delete -f json",domain:"localhost",strategy:m.UI,browser:!0,args:[{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"},{name:"limit",type:"int",required:!1,help:"Max visible prompts to approve in this pass (default: 1)",default:1},{name:"max-chars",type:"int",required:!1,help:"Max chars to return from prompt text; 0 returns full text (default: 600)",default:600},{name:"dry-run",type:"boolean",required:!1,help:"Detect matching prompts without clicking them (default: false)",default:!1}],columns:["Status","Kind","Button","Prompt","Selector","Action"],func:async(e,t)=>{const o=u(t["approve-kinds"]),l=f(t.limit,1),p=h(t["max-chars"],600),i=v(t["dry-run"]),r=await d(e,o,{click:!i,limit:l,maxChars:p});if(!i&&r.some((a)=>a.Action&&String(a.Action).startsWith("click-failed"))){const a=r.find((n)=>n.Action&&String(n.Action).startsWith("click-failed"));throw new c(`Trae CN approval click failed: ${a?.Action||"unknown error"}`)}if(r.length===0)return[{Status:"NoPrompt",Kind:o.join(","),Button:"",Prompt:"No matching visible Trae CN approval prompt found",Selector:"",Action:i?"dry-run":"none"}];return r}});
1
+ import{cli as s,Strategy as m}from"@jackwener/opencli/registry";import{CommandExecutionError as c}from"@jackwener/opencli/errors";import{approveTraePrompts as d,normalizeApprovalKinds as u,normalizeApprovalLimit as f,normalizeMaxChars as h}from"./utils.js";function v(e){return e===!0||String(e).toLowerCase()==="true"}export const approveCommand=s({site:"trae-cn",name:"approve",access:"write",description:"Approve visible Trae CN permission prompts such as terminal-run, high-risk command, or delete confirmations",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn approve --approve-kinds terminal,delete -f json",domain:"localhost",strategy:m.UI,browser:!0,args:[{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"},{name:"limit",type:"int",required:!1,help:"Max visible prompts to approve in this pass (default: 1)",default:1},{name:"max-chars",type:"int",required:!1,help:"Max chars to return from prompt text; 0 returns full text (default: 600)",default:600},{name:"dry-run",type:"boolean",required:!1,help:"Detect matching prompts without clicking them (default: false)",default:!1}],columns:["Status","Kind","Button","Prompt","Selector","Action"],func:async(e,t)=>{const o=u(t["approve-kinds"]),l=f(t.limit,1),p=h(t["max-chars"],600),i=v(t["dry-run"]),r=await d(e,o,{click:!i,limit:l,maxChars:p});if(!i&&r.some((a)=>a.Action&&String(a.Action).startsWith("click-failed"))){const a=r.find((n)=>n.Action&&String(n.Action).startsWith("click-failed"));throw new c(`Trae CN approval click failed: ${a?.Action||"unknown error"}`)}if(r.length===0)return[{Status:"NoPrompt",Kind:o.join(","),Button:"",Prompt:"No matching visible Trae CN approval prompt found",Selector:"",Action:i?"dry-run":"none"}];return r}});
@@ -1 +1 @@
1
- import{cli as w,Strategy as C}from"@jackwener/opencli/registry";import{TimeoutError as c}from"@jackwener/opencli/errors";import{activityScript as y,approveTraePrompts as g,isActivityComplete as A,isLikelyBlockingActivity as d,latestAssistantScript as m,normalizeApprovalKinds as b,normalizeMaxChars as k,normalizeTimeout as P,sendTraePrompt as I}from"./utils.js";function N(e){return e===!0||String(e).toLowerCase()==="true"}export const askCommand=w({site:"trae-cn",name:"ask",access:"write",description:"Send a prompt to Trae CN, wait for the assistant result without treating approval cards as final, and return it",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn ask "请只回复 OK" --timeout 120 -f json',domain:"localhost",strategy:C.UI,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Prompt to send into Trae CN"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for response (default: 60)",default:60},{name:"max-chars",type:"int",required:!1,help:"Max chars to return from the assistant response; 0 returns full text (default: 12000)",default:12000},{name:"auto-approve",type:"boolean",required:!1,help:"While waiting, approve visible terminal/delete prompts, including high-risk terminal confirmation layers (default: false)",default:!1},{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories for --auto-approve: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"}],columns:["Role","Text","TextChars","Truncated","TurnIndex","MessageId"],func:async(e,r)=>{const o=P(r.timeout,60),f=k(r["max-chars"],12000),h=N(r["auto-approve"]),v=b(r["approve-kinds"]),i=await e.evaluate(m(0)),T=[i?.MessageId||"",i?.TurnIndex||"",i?.Text||""].join("|");await I(e,r.text);const x=Date.now()+o*1000;let s="",l=0,t=null,a=null;while(Date.now()<x){await e.wait(2);if(h)await g(e,v,{click:!0,limit:1,maxChars:600});t=await e.evaluate(m(f));a=await e.evaluate(y(600));const n=t?.Text?.trim()||"";if(!n)continue;if([t?.MessageId||"",t?.TurnIndex||"",t?.Text||""].join("|")===T)continue;if(n!==s){s=n;l=Date.now();continue}const p=Date.now()-l,u=d(a);if(A(a)&&!u&&p>=1000)return[t];if(!u&&p>=3000)return[t]}if(t?.Text){if(d(a))throw new c("trae-cn ask",o,`Current status=${a?.Status??""}, approvalPending=${a?.ApprovalPending??""}, approvalKind=${a?.ApprovalKind??""}, approvalButton=${a?.ApprovalButton??""}. Use "opencli trae-cn targets -f json" to find the waiting target, then run "OPENCLI_CDP_TARGET=<target> opencli trae-cn watch --stream true --duration 300" or "opencli trae-cn approve".`);return[t]}throw new c("trae-cn ask",o,"No Trae CN response was visible before the timeout. The agent may still be generating.")}});
1
+ import{cli as w,Strategy as C}from"@jackwener/opencli/registry";import{TimeoutError as c}from"@jackwener/opencli/errors";import{activityScript as y,approveTraePrompts as g,isActivityComplete as A,isLikelyBlockingActivity as d,latestAssistantScript as m,normalizeApprovalKinds as b,normalizeMaxChars as k,normalizeTimeout as P,sendTraePrompt as I}from"./utils.js";function N(e){return e===!0||String(e).toLowerCase()==="true"}export const askCommand=w({site:"trae-cn",name:"ask",access:"write",description:"Send a prompt to Trae CN, wait for the assistant result without treating approval cards as final, and return it",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn ask "请只回复 OK" --timeout 120 -f json',domain:"localhost",strategy:C.UI,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Prompt to send into Trae CN"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for response (default: 60)",default:60},{name:"max-chars",type:"int",required:!1,help:"Max chars to return from the assistant response; 0 returns full text (default: 12000)",default:12000},{name:"auto-approve",type:"boolean",required:!1,help:"While waiting, approve visible terminal/delete prompts, including high-risk terminal confirmation layers (default: false)",default:!1},{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories for --auto-approve: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"}],columns:["Role","Text","TextChars","Truncated","TurnIndex","MessageId"],func:async(e,r)=>{const o=P(r.timeout,60),f=k(r["max-chars"],12000),h=N(r["auto-approve"]),v=b(r["approve-kinds"]),i=await e.evaluate(m(0)),T=[i?.MessageId||"",i?.TurnIndex||"",i?.Text||""].join("|");await I(e,r.text);const x=Date.now()+o*1000;let s="",l=0,t=null,a=null;while(Date.now()<x){await e.wait(2);if(h)await g(e,v,{click:!0,limit:1,maxChars:600});t=await e.evaluate(m(f));a=await e.evaluate(y(600));const n=t?.Text?.trim()||"";if(!n)continue;if([t?.MessageId||"",t?.TurnIndex||"",t?.Text||""].join("|")===T)continue;if(n!==s){s=n;l=Date.now();continue}const p=Date.now()-l,u=d(a);if(A(a)&&!u&&p>=1000)return[t];if(!u&&p>=3000)return[t]}if(t?.Text){if(d(a))throw new c("trae-cn ask",o,`Current status=${a?.Status??""}, approvalPending=${a?.ApprovalPending??""}, approvalKind=${a?.ApprovalKind??""}, approvalButton=${a?.ApprovalButton??""}. Use "ppcli trae-cn targets -f json" to find the waiting target, then run "OPENCLI_CDP_TARGET=<target> ppcli trae-cn watch --stream true --duration 300" or "ppcli trae-cn approve".`);return[t]}throw new c("trae-cn ask",o,"No Trae CN response was visible before the timeout. The agent may still be generating.")}});
@@ -1 +1 @@
1
- import{makeDumpCommand as m}from"../_shared/desktop-commands.js";export const dumpCommand=m("trae-cn",{example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn dump -f json"});
1
+ import{makeDumpCommand as m}from"../_shared/desktop-commands.js";export const dumpCommand=m("trae-cn",{example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn dump -f json"});
@@ -1,3 +1,3 @@
1
- import*as p from"node:fs";import{cli as m,Strategy as c}from"@jackwener/opencli/registry";import{normalizeLimit as l,readTraeMessages as u}from"./utils.js";export const exportCommand=m({site:"trae-cn",name:"export",access:"read",description:"Export the current Trae CN conversation to Markdown",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn export --limit 20 --output /tmp/trae-cn-export.md",domain:"localhost",strategy:c.UI,browser:!0,args:[{name:"output",required:!1,help:"Output file (default: /tmp/trae-cn-export.md)"},{name:"limit",type:"int",required:!1,help:"Max recent turns to export (default: 50)",default:50}],columns:["Status","File","Messages"],func:async(a,t)=>{const e=t.output||"/tmp/trae-cn-export.md",n=l(t.limit,50),r=await u(a,n),s=["# Trae CN Conversation Export","",...r.map((o,i)=>[`## ${i+1}. ${o.Role}`,"",o.Text,""].join(`
1
+ import*as p from"node:fs";import{cli as m,Strategy as c}from"@jackwener/opencli/registry";import{normalizeLimit as l,readTraeMessages as u}from"./utils.js";export const exportCommand=m({site:"trae-cn",name:"export",access:"read",description:"Export the current Trae CN conversation to Markdown",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn export --limit 20 --output /tmp/trae-cn-export.md",domain:"localhost",strategy:c.UI,browser:!0,args:[{name:"output",required:!1,help:"Output file (default: /tmp/trae-cn-export.md)"},{name:"limit",type:"int",required:!1,help:"Max recent turns to export (default: 50)",default:50}],columns:["Status","File","Messages"],func:async(a,t)=>{const e=t.output||"/tmp/trae-cn-export.md",n=l(t.limit,50),r=await u(a,n),s=["# Trae CN Conversation Export","",...r.map((o,i)=>[`## ${i+1}. ${o.Role}`,"",o.Text,""].join(`
2
2
  `))].join(`
3
3
  `);p.writeFileSync(e,s);return[{Status:"Success",File:e,Messages:r.length}]}});
@@ -1 +1 @@
1
- import{cli as o,Strategy as a}from"@jackwener/opencli/registry";import{inspectTraeShellScript as r}from"./utils.js";export const modelCommand=o({site:"trae-cn",name:"model",access:"read",description:"Read the model label currently shown in the Trae CN chat input",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn model -f json",domain:"localhost",strategy:a.UI,browser:!0,columns:["Model","Agent","Workspace"],func:async(t)=>{const e=await t.evaluate(r());return[{Model:e.model||"",Agent:e.agent||"",Workspace:e.workspace||""}]}});
1
+ import{cli as o,Strategy as a}from"@jackwener/opencli/registry";import{inspectTraeShellScript as r}from"./utils.js";export const modelCommand=o({site:"trae-cn",name:"model",access:"read",description:"Read the model label currently shown in the Trae CN chat input",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn model -f json",domain:"localhost",strategy:a.UI,browser:!0,columns:["Model","Agent","Workspace"],func:async(t)=>{const e=await t.evaluate(r());return[{Model:e.model||"",Agent:e.agent||"",Workspace:e.workspace||""}]}});
@@ -1 +1 @@
1
- import{cli as c,Strategy as u}from"@jackwener/opencli/registry";import{CommandExecutionError as p,selectorError as d}from"@jackwener/opencli/errors";import{clickNewTaskScript as l,currentTaskStateScript as s,ensurePrompt as f,normalizeTimeout as w,sendTraePrompt as h}from"./utils.js";export const newCommand=c({site:"trae-cn",name:"new",access:"write",description:"Start a new Trae CN task in the current workspace, optionally sending the first prompt",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn new "请执行你的任务" -f json',domain:"localhost",strategy:u.UI,browser:!0,args:[{name:"prompt",required:!1,positional:!0,help:"Optional first prompt to send after creating the task"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for a fresh task composer (default: 10)",default:10}],columns:["Status","Action","Workspace","Model","Agent","FreshTaskConfirmed","TurnsBeforeSubmit","Turns","ComposerReady","SubmitMode"],func:async(t,r)=>{const n=w(r.timeout,10),o=await t.evaluate(l());if(!o?.ok)throw d("Trae CN new task button");const i=Date.now();let e=null;while(Date.now()-i<n*1000){await t.wait(0.5);e=await t.evaluate(s());if(e?.composerReady&&e.turns===0)break}if(!e?.composerReady||e.turns!==0)throw new p(`Clicked Trae CN new task via ${o.method}; fresh empty composer was not confirmed`,`Observed composerReady=${e?.composerReady?"yes":"no"}, turns=${e?.turns??"unavailable"}. Verify the current window is a Trae CN chat workspace and retry.`);const m=e.turns;let a="";if(r.prompt!==void 0&&r.prompt!==null&&String(r.prompt).trim()){a=(await h(t,f(r.prompt))).mode;await t.wait(0.5);e=await t.evaluate(s())}return[{Status:"Success",Action:r.prompt?`New task created via ${o.method}; prompt submitted`:`New task created via ${o.method}`,Workspace:e?.workspace||"",Model:e?.model||"",Agent:e?.agent||"",FreshTaskConfirmed:"yes",TurnsBeforeSubmit:m,Turns:e?.turns??0,ComposerReady:e?.composerReady?"yes":"no",SubmitMode:a}]}});
1
+ import{cli as c,Strategy as u}from"@jackwener/opencli/registry";import{CommandExecutionError as p,selectorError as d}from"@jackwener/opencli/errors";import{clickNewTaskScript as l,currentTaskStateScript as s,ensurePrompt as f,normalizeTimeout as w,sendTraePrompt as h}from"./utils.js";export const newCommand=c({site:"trae-cn",name:"new",access:"write",description:"Start a new Trae CN task in the current workspace, optionally sending the first prompt",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn new "请执行你的任务" -f json',domain:"localhost",strategy:u.UI,browser:!0,args:[{name:"prompt",required:!1,positional:!0,help:"Optional first prompt to send after creating the task"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for a fresh task composer (default: 10)",default:10}],columns:["Status","Action","Workspace","Model","Agent","FreshTaskConfirmed","TurnsBeforeSubmit","Turns","ComposerReady","SubmitMode"],func:async(t,r)=>{const n=w(r.timeout,10),o=await t.evaluate(l());if(!o?.ok)throw d("Trae CN new task button");const i=Date.now();let e=null;while(Date.now()-i<n*1000){await t.wait(0.5);e=await t.evaluate(s());if(e?.composerReady&&e.turns===0)break}if(!e?.composerReady||e.turns!==0)throw new p(`Clicked Trae CN new task via ${o.method}; fresh empty composer was not confirmed`,`Observed composerReady=${e?.composerReady?"yes":"no"}, turns=${e?.turns??"unavailable"}. Verify the current window is a Trae CN chat workspace and retry.`);const m=e.turns;let a="";if(r.prompt!==void 0&&r.prompt!==null&&String(r.prompt).trim()){a=(await h(t,f(r.prompt))).mode;await t.wait(0.5);e=await t.evaluate(s())}return[{Status:"Success",Action:r.prompt?`New task created via ${o.method}; prompt submitted`:`New task created via ${o.method}`,Workspace:e?.workspace||"",Model:e?.model||"",Agent:e?.agent||"",FreshTaskConfirmed:"yes",TurnsBeforeSubmit:m,Turns:e?.turns??0,ComposerReady:e?.composerReady?"yes":"no",SubmitMode:a}]}});
@@ -1 +1 @@
1
- import{cli as n,Strategy as s}from"@jackwener/opencli/registry";import{normalizeLimit as i,normalizeMaxChars as l,readTraeMessages as c}from"./utils.js";export const readCommand=n({site:"trae-cn",name:"read",access:"read",description:"Read the current Trae CN chat conversation",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn read --limit 5 --max-chars 12000 -f json",domain:"localhost",strategy:s.UI,browser:!0,args:[{name:"limit",type:"int",required:!1,help:"Max recent turns to return (default: 20)",default:20},{name:"max-chars",type:"int",required:!1,help:"Max chars per turn; 0 returns full text (default: 6000)",default:6000}],columns:["Role","Text","TextChars","Truncated","TurnIndex","MessageId"],func:async(a,e)=>{const r=i(e.limit,20),t=l(e["max-chars"],6000);return c(a,r,t)}});
1
+ import{cli as n,Strategy as s}from"@jackwener/opencli/registry";import{normalizeLimit as i,normalizeMaxChars as l,readTraeMessages as c}from"./utils.js";export const readCommand=n({site:"trae-cn",name:"read",access:"read",description:"Read the current Trae CN chat conversation",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn read --limit 5 --max-chars 12000 -f json",domain:"localhost",strategy:s.UI,browser:!0,args:[{name:"limit",type:"int",required:!1,help:"Max recent turns to return (default: 20)",default:20},{name:"max-chars",type:"int",required:!1,help:"Max chars per turn; 0 returns full text (default: 6000)",default:6000}],columns:["Role","Text","TextChars","Truncated","TurnIndex","MessageId"],func:async(a,e)=>{const r=i(e.limit,20),t=l(e["max-chars"],6000);return c(a,r,t)}});
@@ -1 +1 @@
1
- import{makeScreenshotCommand as t}from"../_shared/desktop-commands.js";export const screenshotTraeCn=t("trae-cn","Trae CN",{example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn screenshot --output /tmp/trae-cn-snapshot.txt"});
1
+ import{makeScreenshotCommand as t}from"../_shared/desktop-commands.js";export const screenshotTraeCn=t("trae-cn","Trae CN",{example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn screenshot --output /tmp/trae-cn-snapshot.txt"});
@@ -1 +1 @@
1
- import{cli as a,Strategy as s}from"@jackwener/opencli/registry";import{selectTraeModel as o}from"./utils.js";export const selectModelCommand=a({site:"trae-cn",name:"select-model",access:"write",description:"Select a model in the current Trae CN chat input",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn select-model "GPT 5.4" -f json',domain:"localhost",strategy:s.UI,browser:!0,args:[{name:"name",required:!0,positional:!0,help:"Model label, for example GPT 5.4 or GPT-5.4"}],columns:["Status","Requested","Selected","Workspace","Agent"],func:async(t,r)=>{const e=await o(t,r.name);return[{Status:"Success",Requested:e.requested,Selected:e.selected,Workspace:e.workspace,Agent:e.agent}]}});
1
+ import{cli as a,Strategy as s}from"@jackwener/opencli/registry";import{selectTraeModel as c}from"./utils.js";export const selectModelCommand=a({site:"trae-cn",name:"select-model",access:"write",description:"Select a model in the current Trae CN chat input",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn select-model "GPT 5.4" -f json',domain:"localhost",strategy:s.UI,browser:!0,args:[{name:"name",required:!0,positional:!0,help:"Model label, for example GPT 5.4 or GPT-5.4"}],columns:["Status","Requested","Selected","Workspace","Agent"],func:async(t,r)=>{const e=await c(t,r.name);return[{Status:"Success",Requested:e.requested,Selected:e.selected,Workspace:e.workspace,Agent:e.agent}]}});
@@ -1 +1 @@
1
- import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{sendTraePrompt as a}from"./utils.js";export const sendCommand=n({site:"trae-cn",name:"send",access:"write",description:"Send a prompt into the current Trae CN chat input",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn send "请执行你的任务" -f json',domain:"localhost",strategy:o.UI,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Text to send into Trae CN"}],columns:["Status","InjectedText","SubmitMode"],func:async(e,r)=>{const t=await a(e,r.text);return[{Status:"Success",InjectedText:t.prompt,SubmitMode:t.mode}]}});
1
+ import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{sendTraePrompt as a}from"./utils.js";export const sendCommand=n({site:"trae-cn",name:"send",access:"write",description:"Send a prompt into the current Trae CN chat input",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn send "请执行你的任务" -f json',domain:"localhost",strategy:o.UI,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Text to send into Trae CN"}],columns:["Status","InjectedText","SubmitMode"],func:async(e,r)=>{const t=await a(e,r.text);return[{Status:"Success",InjectedText:t.prompt,SubmitMode:t.mode}]}});
@@ -1 +1 @@
1
- import{cli as e,Strategy as t}from"@jackwener/opencli/registry";export const setupCommand=e({site:"trae-cn",name:"setup",access:"read",description:"Show local setup commands for controlling Trae CN with OpenCLI",example:"opencli trae-cn setup -f table",strategy:t.LOCAL,browser:!1,args:[],columns:["Step","Command","Purpose"],func:async()=>[{Step:"1. Launch Trae CN with CDP",Command:'open -a "Trae CN" --args --remote-debugging-port=39240',Purpose:"Start Trae CN with a Chrome DevTools Protocol endpoint"},{Step:"2. Point OpenCLI at Trae",Command:'export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:39240"',Purpose:"Tell OpenCLI which local Trae CDP endpoint to use"},{Step:"3. Select a workspace target",Command:'export OPENCLI_CDP_TARGET="talk"',Purpose:"Choose the Trae workspace/window title when multiple targets are open"},{Step:"4. List Trae targets",Command:"opencli trae-cn targets -f table",Purpose:"Find the right workspace target and spot windows with ApprovalPending=yes"},{Step:"5. Verify connection",Command:"opencli trae-cn status -f json",Purpose:"Confirm workspace, model, agent, turn count, and composer readiness"},{Step:"6. Start a fresh task",Command:'opencli trae-cn new "请执行你的任务" -f json',Purpose:"Create a new task and send the first prompt"},{Step:"7. Monitor progress",Command:"opencli trae-cn watch --stream true --duration 120",Purpose:"Read in-app running/completed state as JSONL without approving terminal/delete confirmations"},{Step:"8. Approve blockers when needed",Command:"opencli trae-cn approve --approve-kinds terminal,delete -f json",Purpose:"Click visible Trae prompts for terminal command or delete confirmations"},{Step:"9. Opt in to auto-approve when needed",Command:"opencli trae-cn watch --stream true --duration 120 --auto-approve true",Purpose:"Explicitly opt in when you want watch to approve terminal/delete prompts while monitoring"},{Step:"10. Read result",Command:"opencli trae-cn read --limit 5 --max-chars 12000 -f json",Purpose:"Fetch recent user/assistant turns from the current task"},{Step:"Auto-run boundary",Command:"rm, mv, chmod, dd, truncate, kill, destructive git/database commands",Purpose:"Trae CN may still stop these as high-risk even when command mode is 自动运行; OpenCLI approves them only after explicit --auto-approve true or approve"},{Step:"Help",Command:"opencli trae-cn --help -f yaml",Purpose:"Get all Trae CN commands, args, examples, and output columns in structured form"}]});
1
+ import{cli as e,Strategy as t}from"@jackwener/opencli/registry";export const setupCommand=e({site:"trae-cn",name:"setup",access:"read",description:"Show local setup commands for controlling Trae CN with ppcli",example:"ppcli trae-cn setup -f table",strategy:t.LOCAL,browser:!1,args:[],columns:["Step","Command","Purpose"],func:async()=>[{Step:"1. Launch Trae CN with CDP",Command:'open -a "Trae CN" --args --remote-debugging-port=39240',Purpose:"Start Trae CN with a Chrome DevTools Protocol endpoint"},{Step:"2. Point ppcli at Trae",Command:'export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:39240"',Purpose:"Tell ppcli which local Trae CDP endpoint to use"},{Step:"3. Select a workspace target",Command:'export OPENCLI_CDP_TARGET="talk"',Purpose:"Choose the Trae workspace/window title when multiple targets are open"},{Step:"4. List Trae targets",Command:"ppcli trae-cn targets -f table",Purpose:"Find the right workspace target and spot windows with ApprovalPending=yes"},{Step:"5. Verify connection",Command:"ppcli trae-cn status -f json",Purpose:"Confirm workspace, model, agent, turn count, and composer readiness"},{Step:"6. Start a fresh task",Command:'ppcli trae-cn new "请执行你的任务" -f json',Purpose:"Create a new task and send the first prompt"},{Step:"7. Monitor progress",Command:"ppcli trae-cn watch --stream true --duration 120",Purpose:"Read in-app running/completed state as JSONL without approving terminal/delete confirmations"},{Step:"8. Approve blockers when needed",Command:"ppcli trae-cn approve --approve-kinds terminal,delete -f json",Purpose:"Click visible Trae prompts for terminal command or delete confirmations"},{Step:"9. Opt in to auto-approve when needed",Command:"ppcli trae-cn watch --stream true --duration 120 --auto-approve true",Purpose:"Explicitly opt in when you want watch to approve terminal/delete prompts while monitoring"},{Step:"10. Read result",Command:"ppcli trae-cn read --limit 5 --max-chars 12000 -f json",Purpose:"Fetch recent user/assistant turns from the current task"},{Step:"Auto-run boundary",Command:"rm, mv, chmod, dd, truncate, kill, destructive git/database commands",Purpose:"Trae CN may still stop these as high-risk even when command mode is 自动运行; ppcli approves them only after explicit --auto-approve true or approve"},{Step:"Help",Command:"ppcli trae-cn --help -f yaml",Purpose:"Get all Trae CN commands, args, examples, and output columns in structured form"}]});
@@ -1 +1 @@
1
- import{cli as o,Strategy as a}from"@jackwener/opencli/registry";import{inspectTraeShellScript as r}from"./utils.js";export const statusCommand=o({site:"trae-cn",name:"status",access:"read",description:"Check active CDP connection to Trae CN and summarize the current workspace/model",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn status -f json",domain:"localhost",strategy:a.UI,browser:!0,columns:["Status","Title","Workspace","Model","Agent","Turns","ComposerReady","Url"],func:async(t)=>{const e=await t.evaluate(r());return[{Status:"Connected",Title:e.title||"",Workspace:e.workspace||"",Model:e.model||"",Agent:e.agent||"",Turns:e.turns??0,ComposerReady:e.composerReady?"yes":"no",Url:e.url||""}]}});
1
+ import{cli as a,Strategy as o}from"@jackwener/opencli/registry";import{inspectTraeShellScript as r}from"./utils.js";export const statusCommand=a({site:"trae-cn",name:"status",access:"read",description:"Check active CDP connection to Trae CN and summarize the current workspace/model",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn status -f json",domain:"localhost",strategy:o.UI,browser:!0,columns:["Status","Title","Workspace","Model","Agent","Turns","ComposerReady","Url"],func:async(t)=>{const e=await t.evaluate(r());return[{Status:"Connected",Title:e.title||"",Workspace:e.workspace||"",Model:e.model||"",Agent:e.agent||"",Turns:e.turns??0,ComposerReady:e.composerReady?"yes":"no",Url:e.url||""}]}});
@@ -1 +1 @@
1
- import{CDPBridge as l}from"@jackwener/opencli/browser/cdp";import{ArgumentError as u}from"@jackwener/opencli/errors";import{cli as d,Strategy as m}from"@jackwener/opencli/registry";import{activityScript as f}from"./utils.js";function g(t){const e=String(t||process.env.OPENCLI_CDP_ENDPOINT||"http://127.0.0.1:39240").trim();if(!e)throw new u("Set OPENCLI_CDP_ENDPOINT, for example http://127.0.0.1:39240");return e.replace(/\/$/,"")}function h(t,e=!0){if(t===void 0||t===null)return e;return t===!0||String(t).toLowerCase()==="true"}function w(t){const e=String(t?.title||"").trim();return e.match(/—\s*(.+)$/)?.[1]?.trim()||e||String(t?.url||"").trim()}function y(t,e){if(!e)return!1;const r=String(e).toLowerCase();return`${t.title||""} ${t.url||""}`.toLowerCase().includes(r)}async function P(t){const e=await fetch(`${t}/json`,{signal:AbortSignal.timeout(5000)});if(!e.ok)throw Error(`Failed to fetch CDP targets: HTTP ${e.status}`);const r=await e.json();return Array.isArray(r)?r:[]}async function C(t,e){if(!t.webSocketDebuggerUrl)return null;const r=new l;try{return await(await r.connect({cdpEndpoint:t.webSocketDebuggerUrl,timeout:3,surface:"adapter"})).evaluate(f(e))}catch{return null}finally{await r.close().catch(()=>{})}}export const targetsCommand=d({site:"trae-cn",name:"targets",access:"read",description:"List Trae CN CDP targets and show which workspace/window is waiting",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 opencli trae-cn targets -f table",strategy:m.LOCAL,browser:!1,args:[{name:"endpoint",type:"string",required:!1,help:"CDP endpoint (default: OPENCLI_CDP_ENDPOINT)"},{name:"probe-activity",type:"boolean",required:!1,help:"Probe each target for Trae activity state (default: true)",default:!0},{name:"max-chars",type:"int",required:!1,help:"Max chars for approval prompt/activity snippets (default: 240)",default:240}],columns:["Index","SelectedHint","RecommendedTarget","Title","Workspace","Status","ApprovalPending","ApprovalKind","ApprovalButton","Model","Agent","Type","Url"],func:async(t)=>{const e=g(t.endpoint),r=h(t["probe-activity"],!0),i=Number.isInteger(Number(t["max-chars"]))?Number(t["max-chars"]):240,s=process.env.OPENCLI_CDP_TARGET||"",p=(await P(e)).filter((o)=>o.webSocketDebuggerUrl&&o.type!=="worker"),c=[];for(const[o,n]of p.entries()){const a=r?await C(n,i):null;c.push({Index:o,SelectedHint:s?y(n,s)?"yes":"no":o===0?"default":"",RecommendedTarget:w(n),Title:n.title||"",Workspace:a?.Workspace||n.title?.match(/—\s*(.+)$/)?.[1]?.trim()||"",Status:a?.Status||"",ApprovalPending:a?.ApprovalPending||"",ApprovalKind:a?.ApprovalKind||"",ApprovalButton:a?.ApprovalButton||"",Model:a?.Model||"",Agent:a?.Agent||"",Type:n.type||"",Url:n.url||""})}return c}});
1
+ import{CDPBridge as l}from"@jackwener/opencli/browser/cdp";import{ArgumentError as u}from"@jackwener/opencli/errors";import{cli as d,Strategy as m}from"@jackwener/opencli/registry";import{activityScript as f}from"./utils.js";function g(t){const e=String(t||process.env.OPENCLI_CDP_ENDPOINT||"http://127.0.0.1:39240").trim();if(!e)throw new u("Set OPENCLI_CDP_ENDPOINT, for example http://127.0.0.1:39240");return e.replace(/\/$/,"")}function h(t,e=!0){if(t===void 0||t===null)return e;return t===!0||String(t).toLowerCase()==="true"}function w(t){const e=String(t?.title||"").trim();return e.match(/—\s*(.+)$/)?.[1]?.trim()||e||String(t?.url||"").trim()}function y(t,e){if(!e)return!1;const r=String(e).toLowerCase();return`${t.title||""} ${t.url||""}`.toLowerCase().includes(r)}async function P(t){const e=await fetch(`${t}/json`,{signal:AbortSignal.timeout(5000)});if(!e.ok)throw Error(`Failed to fetch CDP targets: HTTP ${e.status}`);const r=await e.json();return Array.isArray(r)?r:[]}async function C(t,e){if(!t.webSocketDebuggerUrl)return null;const r=new l;try{return await(await r.connect({cdpEndpoint:t.webSocketDebuggerUrl,timeout:3,surface:"adapter"})).evaluate(f(e))}catch{return null}finally{await r.close().catch(()=>{})}}export const targetsCommand=d({site:"trae-cn",name:"targets",access:"read",description:"List Trae CN CDP targets and show which workspace/window is waiting",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 ppcli trae-cn targets -f table",strategy:m.LOCAL,browser:!1,args:[{name:"endpoint",type:"string",required:!1,help:"CDP endpoint (default: OPENCLI_CDP_ENDPOINT)"},{name:"probe-activity",type:"boolean",required:!1,help:"Probe each target for Trae activity state (default: true)",default:!0},{name:"max-chars",type:"int",required:!1,help:"Max chars for approval prompt/activity snippets (default: 240)",default:240}],columns:["Index","SelectedHint","RecommendedTarget","Title","Workspace","Status","ApprovalPending","ApprovalKind","ApprovalButton","Model","Agent","Type","Url"],func:async(t)=>{const e=g(t.endpoint),r=h(t["probe-activity"],!0),i=Number.isInteger(Number(t["max-chars"]))?Number(t["max-chars"]):240,s=process.env.OPENCLI_CDP_TARGET||"",p=(await P(e)).filter((o)=>o.webSocketDebuggerUrl&&o.type!=="worker"),c=[];for(const[o,n]of p.entries()){const a=r?await C(n,i):null;c.push({Index:o,SelectedHint:s?y(n,s)?"yes":"no":o===0?"default":"",RecommendedTarget:w(n),Title:n.title||"",Workspace:a?.Workspace||n.title?.match(/—\s*(.+)$/)?.[1]?.trim()||"",Status:a?.Status||"",ApprovalPending:a?.ApprovalPending||"",ApprovalKind:a?.ApprovalKind||"",ApprovalButton:a?.ApprovalButton||"",Model:a?.Model||"",Agent:a?.Agent||"",Type:n.type||"",Url:n.url||""})}return c}});
@@ -1,3 +1,3 @@
1
- import{cli as x,Strategy as C}from"@jackwener/opencli/registry";import{activityScript as y,approveTraePrompts as T,normalizeApprovalKinds as P,normalizeDuration as I,normalizeInterval as L,normalizeMaxChars as b}from"./utils.js";function w(e){return[e.Status||"",e.MessageId||"",e.TurnIndex||"",e.Progress||"",e.ActiveStep||"",e.ApprovalPending||"",e.ApprovalKind||"",e.ApprovalButton||"",e.TextChars??"",e.LatestText||""].join("|")}function f(e){return new Promise((a)=>setTimeout(a,e*1000))}function q(e){return e===!0||String(e).toLowerCase()==="true"}function D(e){return e===void 0||e===!0||String(e).toLowerCase()==="true"}function M(e){return e===!0||String(e).toLowerCase()==="true"}export const watchCommand=x({site:"trae-cn",name:"watch",access:"write",description:"Sample Trae CN activity over time to monitor long-running tasks; optionally approve visible terminal/delete prompts",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk opencli trae-cn watch --stream true --duration 120",domain:"localhost",strategy:C.UI,browser:!0,args:[{name:"duration",type:"int",required:!1,help:"Seconds to observe (default: 30)",default:30},{name:"interval",type:"int",required:!1,help:"Seconds between samples (default: 2)",default:2},{name:"max-chars",type:"int",required:!1,help:"Max chars per text field; 0 returns full text (default: 600)",default:600},{name:"timeout",type:"int",required:!1,help:"Max seconds for the overall watch command (default: 86400)",default:86400},{name:"stream",type:"boolean",required:!1,help:"Write each sample immediately as JSONL and skip result rendering (default: false)",default:!1},{name:"stop-on-complete",type:"boolean",required:!1,help:"Stop watching after the first completed sample (default: true)",default:!0},{name:"auto-approve",type:"boolean",required:!1,help:"Approve visible terminal/delete prompts before each sample, including high-risk terminal confirmation layers (default: false)",default:!1},{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories for --auto-approve: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"}],columns:["Sample","ElapsedSec","Changed","Status","Progress","ActiveStep","ApprovalPending","ApprovalKind","ApprovalButton","AutoApproved","LatestRole","TurnIndex","MessageId","TextChars","LatestText","UpdatedAt"],func:async(e,a)=>{const v=I(a.duration,30),l=L(a.interval,2),h=b(a["max-chars"],600),s=q(a.stream),A=D(a["stop-on-complete"]),S=M(a["auto-approve"]),g=P(a["approve-kinds"]),i=Date.now(),p=i+v*1000,d=[];let n="",o=0;while(Date.now()<=p||o===0){const u=S?await T(e,g,{click:!0,limit:1,maxChars:300}):[];if(u.length>0)await f(0.5);const t=await e.evaluate(y(h)),c=w(t),m={Sample:++o,ElapsedSec:Math.round((Date.now()-i)/1000),Changed:n&&n!==c?"yes":o===1?"initial":"no",Status:t.Status,Progress:t.Progress,ActiveStep:t.ActiveStep,ApprovalPending:t.ApprovalPending,ApprovalKind:t.ApprovalKind,ApprovalButton:t.ApprovalButton,AutoApproved:u.filter((r)=>r.Status==="Approved"||r.Action==="clicked").map((r)=>`${r.Kind}:${r.Button}`).join(`
1
+ import{cli as x,Strategy as C}from"@jackwener/opencli/registry";import{activityScript as y,approveTraePrompts as T,normalizeApprovalKinds as P,normalizeDuration as I,normalizeInterval as L,normalizeMaxChars as b}from"./utils.js";function w(e){return[e.Status||"",e.MessageId||"",e.TurnIndex||"",e.Progress||"",e.ActiveStep||"",e.ApprovalPending||"",e.ApprovalKind||"",e.ApprovalButton||"",e.TextChars??"",e.LatestText||""].join("|")}function f(e){return new Promise((a)=>setTimeout(a,e*1000))}function q(e){return e===!0||String(e).toLowerCase()==="true"}function D(e){return e===void 0||e===!0||String(e).toLowerCase()==="true"}function M(e){return e===!0||String(e).toLowerCase()==="true"}export const watchCommand=x({site:"trae-cn",name:"watch",access:"write",description:"Sample Trae CN activity over time to monitor long-running tasks; optionally approve visible terminal/delete prompts",example:"OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn watch --stream true --duration 120",domain:"localhost",strategy:C.UI,browser:!0,args:[{name:"duration",type:"int",required:!1,help:"Seconds to observe (default: 30)",default:30},{name:"interval",type:"int",required:!1,help:"Seconds between samples (default: 2)",default:2},{name:"max-chars",type:"int",required:!1,help:"Max chars per text field; 0 returns full text (default: 600)",default:600},{name:"timeout",type:"int",required:!1,help:"Max seconds for the overall watch command (default: 86400)",default:86400},{name:"stream",type:"boolean",required:!1,help:"Write each sample immediately as JSONL and skip result rendering (default: false)",default:!1},{name:"stop-on-complete",type:"boolean",required:!1,help:"Stop watching after the first completed sample (default: true)",default:!0},{name:"auto-approve",type:"boolean",required:!1,help:"Approve visible terminal/delete prompts before each sample, including high-risk terminal confirmation layers (default: false)",default:!1},{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories for --auto-approve: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"}],columns:["Sample","ElapsedSec","Changed","Status","Progress","ActiveStep","ApprovalPending","ApprovalKind","ApprovalButton","AutoApproved","LatestRole","TurnIndex","MessageId","TextChars","LatestText","UpdatedAt"],func:async(e,a)=>{const v=I(a.duration,30),l=L(a.interval,2),h=b(a["max-chars"],600),s=q(a.stream),A=D(a["stop-on-complete"]),S=M(a["auto-approve"]),g=P(a["approve-kinds"]),i=Date.now(),p=i+v*1000,d=[];let n="",o=0;while(Date.now()<=p||o===0){const u=S?await T(e,g,{click:!0,limit:1,maxChars:300}):[];if(u.length>0)await f(0.5);const t=await e.evaluate(y(h)),c=w(t),m={Sample:++o,ElapsedSec:Math.round((Date.now()-i)/1000),Changed:n&&n!==c?"yes":o===1?"initial":"no",Status:t.Status,Progress:t.Progress,ActiveStep:t.ActiveStep,ApprovalPending:t.ApprovalPending,ApprovalKind:t.ApprovalKind,ApprovalButton:t.ApprovalButton,AutoApproved:u.filter((r)=>r.Status==="Approved"||r.Action==="clicked").map((r)=>`${r.Kind}:${r.Button}`).join(`
2
2
  `),LatestRole:t.LatestRole,TurnIndex:t.TurnIndex,MessageId:t.MessageId,TextChars:t.TextChars,LatestText:t.LatestText,UpdatedAt:t.UpdatedAt};if(s)process.stdout.write(`${JSON.stringify(m)}
3
3
  `);else d.push(m);n=c;if(A&&t.Status==="completed")break;if(Date.now()+l*1000>p)break;await f(l)}return s?null:d}});
@@ -1,2 +1,2 @@
1
- import*as $ from"node:fs";import*as h from"node:path";import{cli as U,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as Y,EmptyResultError as Z}from"@jackwener/opencli/errors";import{TRAE_GLOBAL_STATE_DB as Q,TRAE_WORKSPACE_STORAGE as V,listKeys as j,getValue as P}from"./_state.js";function W(q){const H=q.workspace?String(q.workspace).trim():"";if(!H)return Q;const I=h.join(V,H,"state.vscdb");if(!$.existsSync(I))throw new Y(`Workspace state.vscdb not found: ${I}`,"List valid workspace ids with `opencli trae-solo workspaces-list`.");return I}U({site:"trae-solo",name:"state-keys",access:"read",description:"List all keys present in Trae SOLO's globalStorage state.vscdb (VSCode-style UI/agent state). Pass --workspace <ws-id> to query a per-workspace DB instead. Use state-get to read a specific value. (See renderer storage-keys for browser-side LS/SS.)",domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"filter",required:!1,help:"Case-insensitive substring filter over keys"},{name:"workspace",required:!1,help:"Workspace id (from workspaces-list) to query a per-workspace DB"},{name:"limit",type:"int",required:!1,default:200}],columns:["Index","Key","Kind","Path"],func:async(q)=>{const H=W(q),I=j(H),z=q.filter?String(q.filter).toLowerCase():null,F=z?I.filter((J)=>J.toLowerCase().includes(z)):I;if(!F.length)throw new Z("trae-solo storage-keys",z?`No keys match "${z}".`:"No keys.");const M=Number.isInteger(q.limit)&&q.limit>0?q.limit:200;return F.slice(0,M).map((J,N)=>({Index:N+1,Key:J,Kind:"",Path:""}))}});U({site:"trae-solo",name:"state-get",access:"read",description:"Read a single key from Trae SOLO's globalStorage state.vscdb. Pass --workspace <ws-id> to query a per-workspace DB instead. Returns parsed JSON if the value is JSON.",domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"key",positional:!0,required:!0,help:"State key (use state-keys to discover)"},{name:"workspace",required:!1,help:"Workspace id (from workspaces-list) to query a per-workspace DB"},{name:"max-bytes",type:"int",required:!1,default:8000,help:"Truncate value to this many bytes"}],columns:["Field","Value"],func:async(q)=>{const H=String(q.key||"").trim();if(!H)throw new K("key required");const I=W(q),z=P(I,H);if(z===null)throw new Y(`Key not found: ${H}`,"List available keys with `opencli trae-solo state-keys`.");const F=Number.isInteger(q["max-bytes"])&&q["max-bytes"]>0?q["max-bytes"]:8000,M=typeof z==="string"?z:JSON.stringify(z,null,2),J=M.length>F;return[{Field:"Key",Value:H},{Field:"Type",Value:typeof z==="string"?"string":Array.isArray(z)?"array":typeof z},{Field:"Size",Value:`${M.length} chars${J?" (truncated)":""}`},{Field:"Value",Value:J?M.slice(0,F)+`
1
+ import*as $ from"node:fs";import*as h from"node:path";import{cli as U,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as Y,EmptyResultError as Z}from"@jackwener/opencli/errors";import{TRAE_GLOBAL_STATE_DB as Q,TRAE_WORKSPACE_STORAGE as V,listKeys as j,getValue as P}from"./_state.js";function W(q){const H=q.workspace?String(q.workspace).trim():"";if(!H)return Q;const I=h.join(V,H,"state.vscdb");if(!$.existsSync(I))throw new Y(`Workspace state.vscdb not found: ${I}`,"List valid workspace ids with `ppcli trae-solo workspaces-list`.");return I}U({site:"trae-solo",name:"state-keys",access:"read",description:"List all keys present in Trae SOLO's globalStorage state.vscdb (VSCode-style UI/agent state). Pass --workspace <ws-id> to query a per-workspace DB instead. Use state-get to read a specific value. (See renderer storage-keys for browser-side LS/SS.)",domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"filter",required:!1,help:"Case-insensitive substring filter over keys"},{name:"workspace",required:!1,help:"Workspace id (from workspaces-list) to query a per-workspace DB"},{name:"limit",type:"int",required:!1,default:200}],columns:["Index","Key","Kind","Path"],func:async(q)=>{const H=W(q),I=j(H),z=q.filter?String(q.filter).toLowerCase():null,F=z?I.filter((J)=>J.toLowerCase().includes(z)):I;if(!F.length)throw new Z("trae-solo storage-keys",z?`No keys match "${z}".`:"No keys.");const M=Number.isInteger(q.limit)&&q.limit>0?q.limit:200;return F.slice(0,M).map((J,N)=>({Index:N+1,Key:J,Kind:"",Path:""}))}});U({site:"trae-solo",name:"state-get",access:"read",description:"Read a single key from Trae SOLO's globalStorage state.vscdb. Pass --workspace <ws-id> to query a per-workspace DB instead. Returns parsed JSON if the value is JSON.",domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"key",positional:!0,required:!0,help:"State key (use state-keys to discover)"},{name:"workspace",required:!1,help:"Workspace id (from workspaces-list) to query a per-workspace DB"},{name:"max-bytes",type:"int",required:!1,default:8000,help:"Truncate value to this many bytes"}],columns:["Field","Value"],func:async(q)=>{const H=String(q.key||"").trim();if(!H)throw new K("key required");const I=W(q),z=P(I,H);if(z===null)throw new Y(`Key not found: ${H}`,"List available keys with `ppcli trae-solo state-keys`.");const F=Number.isInteger(q["max-bytes"])&&q["max-bytes"]>0?q["max-bytes"]:8000,M=typeof z==="string"?z:JSON.stringify(z,null,2),J=M.length>F;return[{Field:"Key",Value:H},{Field:"Type",Value:typeof z==="string"?"string":Array.isArray(z)?"array":typeof z},{Field:"Size",Value:`${M.length} chars${J?" (truncated)":""}`},{Field:"Value",Value:J?M.slice(0,F)+`
2
2
  ...(truncated, use --max-bytes to read more)`:M}]}});U({site:"trae-solo",name:"recent-workspaces",access:"read",description:`Show Trae SOLO's recently-opened workspaces (the File → Open Recent menu, stored under key "history.recentlyOpenedPathsList" in state.vscdb).`,domain:"localhost",browser:!1,strategy:X.LOCAL,args:[{name:"limit",type:"int",required:!1,default:20}],columns:["Index","Key","Kind","Path"],func:async(q)=>{if(!$.existsSync(Q))throw new Y(`state.vscdb not found: ${Q}`,"");const H=P(Q,"history.recentlyOpenedPathsList");if(!H)throw new Z("trae-solo recent-workspaces","No recent workspaces recorded.");const I=H.entries||[];if(!I.length)throw new Z("trae-solo recent-workspaces","history.recentlyOpenedPathsList has no entries.");const z=Number.isInteger(q.limit)&&q.limit>0?q.limit:20;return I.slice(0,z).map((F,M)=>{let J="other",N=JSON.stringify(F).slice(0,200);if(F.folderUri){J="folder";N=decodeURI(String(F.folderUri).replace(/^file:\/\//,""))}else if(F.workspace&&F.workspace.configPath){J="workspace";N=decodeURI(String(F.workspace.configPath).replace(/^file:\/\//,""))}else if(F.fileUri){J="file";N=decodeURI(String(F.fileUri).replace(/^file:\/\//,""))}return{Index:M+1,Key:"",Kind:J,Path:N}})}});
@@ -11,4 +11,4 @@ import*as M from"node:fs";import{cli as _,Strategy as L}from"@jackwener/opencli/
11
11
  if (status === 401 || status === 403) return { kind: 'auth', detail: 'HTTP ' + status + ' ' + msg };
12
12
  return { kind: 'http', status: status, detail: String(msg).slice(0, 300) };
13
13
  }
14
- })()`)}async function F(z,G){if(G.blog)return blogHost(G.blog);const I=await Z(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new X("tumblr.com",`Not logged in (${I.detail}). Run \`opencli tumblr login\`.`);if(I?.kind!=="ok"||!I.response||!I.response.user)throw new P(`Failed to resolve default blog: ${JSON.stringify(I)}`);const N=I.response.user.blogs||[],J=N.find((Q)=>Q.primary)||N[0];if(!J||!J.name)throw new P("No blog found on this account; pass --blog explicitly.");return blogHost(J.name)}_({site:"tumblr",name:"publish",access:"write",description:"Publish a Tumblr post (NPF: title/text/image/link). Draft by default; pass --state published to go live.",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Title (rendered as a heading1 text block)"},{name:"text",type:"string",required:!1,help:"Body text (blank lines split paragraphs); alias of --body"},{name:"body",type:"string",required:!1,help:"Body text (blank lines split paragraphs)"},{name:"body-file",type:"string",required:!1,help:"Path to a text file for the body"},{name:"image-url",type:"string",required:!1,help:"Image URL (NPF image block)"},{name:"link",type:"string",required:!1,help:"Link URL (NPF link block)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags"},{name:"state",type:"string",required:!1,default:"draft",choices:Y,help:"draft | published | queue | private (default: draft)"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog","state","url"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr publish");const I=String(G.state??"draft");if(!Y.includes(I))throw new U(`Invalid --state "${I}". One of: ${Y.join(", ")}`);const N=resolveBody(G),J=buildContent(G,N),Q=resolveTags(G.tags);await z.goto(q);const V=await F(z,G),$={content:J,state:I};if(Q)$.tags=Q;const K=await Z(z,`/v2/blog/${V}/posts`,{method:"POST",body:$});if(K?.kind==="auth")throw new X("tumblr.com",`Not logged in (${K.detail}). Run \`opencli tumblr login\`.`);if(K?.kind==="http")throw new P(`Tumblr publish failed: HTTP ${K.status} ${K.detail}`);if(K?.kind!=="ok"||!K.response)throw new P(`Unexpected Tumblr response: ${JSON.stringify(K)}`);const W=K.response.id_string??(K.response.id!=null?String(K.response.id):"");if(!W)throw new P("Tumblr post response missing id");return[{status:"success",id:W,blog:V,state:I,url:I==="published"?`${O}/${V}/${W}`:""}]}});_({site:"tumblr",name:"delete",access:"write",description:"Delete a Tumblr post by id (POST /v2/blog/{blog}/post/delete).",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,help:"Post id to delete"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr delete");const I=String(G.id??"").trim();if(!I)throw new U("--id is required");await z.goto(q);const N=await F(z,G),J=await Z(z,`/v2/blog/${N}/post/delete`,{method:"POST",body:{id:I}});if(J?.kind==="auth")throw new X("tumblr.com",`Not logged in (${J.detail}). Run \`opencli tumblr login\`.`);if(J?.kind==="http")throw new P(`Tumblr delete failed: HTTP ${J.status} ${J.detail}`);if(J?.kind!=="ok")throw new P(`Unexpected Tumblr delete response: ${JSON.stringify(J)}`);return[{status:"deleted",id:I,blog:N}]}});export const __test__={blogHost,resolveBody,buildContent,resolveTags};
14
+ })()`)}async function F(z,G){if(G.blog)return blogHost(G.blog);const I=await Z(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new X("tumblr.com",`Not logged in (${I.detail}). Run \`ppcli tumblr login\`.`);if(I?.kind!=="ok"||!I.response||!I.response.user)throw new P(`Failed to resolve default blog: ${JSON.stringify(I)}`);const N=I.response.user.blogs||[],J=N.find((Q)=>Q.primary)||N[0];if(!J||!J.name)throw new P("No blog found on this account; pass --blog explicitly.");return blogHost(J.name)}_({site:"tumblr",name:"publish",access:"write",description:"Publish a Tumblr post (NPF: title/text/image/link). Draft by default; pass --state published to go live.",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Title (rendered as a heading1 text block)"},{name:"text",type:"string",required:!1,help:"Body text (blank lines split paragraphs); alias of --body"},{name:"body",type:"string",required:!1,help:"Body text (blank lines split paragraphs)"},{name:"body-file",type:"string",required:!1,help:"Path to a text file for the body"},{name:"image-url",type:"string",required:!1,help:"Image URL (NPF image block)"},{name:"link",type:"string",required:!1,help:"Link URL (NPF link block)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags"},{name:"state",type:"string",required:!1,default:"draft",choices:Y,help:"draft | published | queue | private (default: draft)"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog","state","url"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr publish");const I=String(G.state??"draft");if(!Y.includes(I))throw new U(`Invalid --state "${I}". One of: ${Y.join(", ")}`);const N=resolveBody(G),J=buildContent(G,N),Q=resolveTags(G.tags);await z.goto(q);const V=await F(z,G),$={content:J,state:I};if(Q)$.tags=Q;const K=await Z(z,`/v2/blog/${V}/posts`,{method:"POST",body:$});if(K?.kind==="auth")throw new X("tumblr.com",`Not logged in (${K.detail}). Run \`ppcli tumblr login\`.`);if(K?.kind==="http")throw new P(`Tumblr publish failed: HTTP ${K.status} ${K.detail}`);if(K?.kind!=="ok"||!K.response)throw new P(`Unexpected Tumblr response: ${JSON.stringify(K)}`);const W=K.response.id_string??(K.response.id!=null?String(K.response.id):"");if(!W)throw new P("Tumblr post response missing id");return[{status:"success",id:W,blog:V,state:I,url:I==="published"?`${O}/${V}/${W}`:""}]}});_({site:"tumblr",name:"delete",access:"write",description:"Delete a Tumblr post by id (POST /v2/blog/{blog}/post/delete).",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,help:"Post id to delete"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr delete");const I=String(G.id??"").trim();if(!I)throw new U("--id is required");await z.goto(q);const N=await F(z,G),J=await Z(z,`/v2/blog/${N}/post/delete`,{method:"POST",body:{id:I}});if(J?.kind==="auth")throw new X("tumblr.com",`Not logged in (${J.detail}). Run \`ppcli tumblr login\`.`);if(J?.kind==="http")throw new P(`Tumblr delete failed: HTTP ${J.status} ${J.detail}`);if(J?.kind!=="ok")throw new P(`Unexpected Tumblr delete response: ${JSON.stringify(J)}`);return[{status:"deleted",id:I,blog:N}]}});export const __test__={blogHost,resolveBody,buildContent,resolveTags};
@@ -1 +1 @@
1
- import{ArgumentError as J,CommandExecutionError as K,EmptyResultError as O}from"@jackwener/opencli/errors";export const TVMAZE_BASE="https://api.tvmaze.com";const Q="opencli-tvmaze-adapter (+https://github.com/jackwener/opencli)";export function requireString(D,z){const q=String(D??"").trim();if(!q)throw new J(`tvmaze ${z} cannot be empty`);return q}export function requireShowId(D){const z=D,q=typeof z==="number"?z:Number(String(z??"").trim());if(!Number.isInteger(q)||q<=0)throw new J("tvmaze show id is required and must be a positive integer","TVmaze show ids appear in the URL: https://www.tvmaze.com/shows/<id>/<slug>.");return q}export function requireBoundedInt(D,z,q,F="limit"){const B=D??z,G=typeof B==="number"?B:Number(B);if(!Number.isInteger(G)||G<=0)throw new J(`tvmaze ${F} must be a positive integer`);if(G>q)throw new J(`tvmaze ${F} must be <= ${q}`);return G}export async function tvmazeFetch(D,z){let q;try{q=await fetch(D,{headers:{"user-agent":Q,accept:"application/json"}})}catch(B){throw new K(`${z} request failed: ${B?.message??B}`,"Check that api.tvmaze.com is reachable from this network.")}if(q.status===404)throw new O(z,`TVmaze returned 404 for ${D}.`);if(q.status===429)throw new K(`${z} returned HTTP 429 (rate limited)`,"TVmaze caps unauthenticated traffic at ~20 req/10s; wait a few seconds and retry.");if(!q.ok)throw new K(`${z} returned HTTP ${q.status}`);let F;try{F=await q.json()}catch(B){throw new K(`${z} returned malformed JSON: ${B?.message??B}`)}return F}const S={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" ",rsquo:"’",lsquo:"‘",rdquo:"”",ldquo:"“",hellip:"…",ndash:"–",mdash:"—"};export function stripHtml(D){if(D==null)return"";let z=String(D);z=z.replace(/<[^>]+>/g,"");z=z.replace(/&#x([0-9a-fA-F]+);/g,(q,F)=>{const B=parseInt(F,16);return Number.isFinite(B)?String.fromCodePoint(B):""}).replace(/&#(\d+);/g,(q,F)=>{const B=parseInt(F,10);return Number.isFinite(B)?String.fromCodePoint(B):""}).replace(/&([a-zA-Z]+);/g,(q,F)=>S[F]??q);return z.replace(/\s+/g," ").trim()}export function joinList(D){return Array.isArray(D)?D.filter(Boolean).join(", "):""}
1
+ import{ArgumentError as J,CommandExecutionError as K,EmptyResultError as O}from"@jackwener/opencli/errors";export const TVMAZE_BASE="https://api.tvmaze.com";const Q="opencli-tvmaze-adapter (+https://github.com/jackwener/OpenCLI)";export function requireString(D,z){const q=String(D??"").trim();if(!q)throw new J(`tvmaze ${z} cannot be empty`);return q}export function requireShowId(D){const z=D,q=typeof z==="number"?z:Number(String(z??"").trim());if(!Number.isInteger(q)||q<=0)throw new J("tvmaze show id is required and must be a positive integer","TVmaze show ids appear in the URL: https://www.tvmaze.com/shows/<id>/<slug>.");return q}export function requireBoundedInt(D,z,q,F="limit"){const B=D??z,G=typeof B==="number"?B:Number(B);if(!Number.isInteger(G)||G<=0)throw new J(`tvmaze ${F} must be a positive integer`);if(G>q)throw new J(`tvmaze ${F} must be <= ${q}`);return G}export async function tvmazeFetch(D,z){let q;try{q=await fetch(D,{headers:{"user-agent":Q,accept:"application/json"}})}catch(B){throw new K(`${z} request failed: ${B?.message??B}`,"Check that api.tvmaze.com is reachable from this network.")}if(q.status===404)throw new O(z,`TVmaze returned 404 for ${D}.`);if(q.status===429)throw new K(`${z} returned HTTP 429 (rate limited)`,"TVmaze caps unauthenticated traffic at ~20 req/10s; wait a few seconds and retry.");if(!q.ok)throw new K(`${z} returned HTTP ${q.status}`);let F;try{F=await q.json()}catch(B){throw new K(`${z} returned malformed JSON: ${B?.message??B}`)}return F}const S={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" ",rsquo:"’",lsquo:"‘",rdquo:"”",ldquo:"“",hellip:"…",ndash:"–",mdash:"—"};export function stripHtml(D){if(D==null)return"";let z=String(D);z=z.replace(/<[^>]+>/g,"");z=z.replace(/&#x([0-9a-fA-F]+);/g,(q,F)=>{const B=parseInt(F,16);return Number.isFinite(B)?String.fromCodePoint(B):""}).replace(/&#(\d+);/g,(q,F)=>{const B=parseInt(F,10);return Number.isFinite(B)?String.fromCodePoint(B):""}).replace(/&([a-zA-Z]+);/g,(q,F)=>S[F]??q);return z.replace(/\s+/g," ").trim()}export function joinList(D){return Array.isArray(D)?D.filter(Boolean).join(", "):""}
@@ -1,4 +1,4 @@
1
- import{cli as U,Strategy as P}from"@jackwener/opencli/registry";import{ArgumentError as S,AuthRequiredError as f,CommandExecutionError as O}from"@jackwener/opencli/errors";import{TWITTER_BEARER_TOKEN as C,applyTopByEngagement as F}from"./utils.js";import{extractMedia as N,resolveTwitterQueryId as _,describeTwitterApiError as R}from"./shared.js";const j="BookmarkFolderTimeline",x="13H7EUATwethsj_jZ6QQAQ",q=/^[A-Za-z0-9_-]+$/,k=100,p={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_enhance_cards_enabled:!1};function L(J,V,z,H){const $={bookmark_collection_id:String(V),count:z,includePromotedContent:!1};if(H)$.cursor=H;return`/i/api/graphql/${J}/${j}?variables=${encodeURIComponent(JSON.stringify($))}&features=${encodeURIComponent(JSON.stringify(p))}`}export function extractFolderTweet(J,V){if(!J)return null;const z=J.tweet||J,H=z.legacy||{};if(!z.rest_id||V.has(z.rest_id))return null;V.add(z.rest_id);const $=z.core?.user_results?.result,W=$?.legacy?.screen_name||$?.core?.screen_name||"",Y=z.note_tweet?.note_tweet_results?.result?.text;return{id:z.rest_id,author:W,text:Y||H.full_text||"",likes:H.favorite_count||0,retweets:H.retweet_count||0,bookmarks:H.bookmark_count||0,created_at:H.created_at||"",url:W?`https://x.com/${W}/status/${z.rest_id}`:`https://x.com/i/status/${z.rest_id}`,...N(H)}}export function parseBookmarkFolderTimeline(J,V){const z=[];let H=null;const $=J?.data?.bookmark_collection_timeline?.timeline?.instructions||J?.data?.bookmark_timeline_v2?.timeline?.instructions||J?.data?.bookmark_timeline?.timeline?.instructions||[];for(const W of $)for(const Y of W.entries||[]){const Z=Y.content;if(Z?.entryType==="TimelineTimelineCursor"||Z?.__typename==="TimelineTimelineCursor"){if(Z.cursorType==="Bottom"||Z.cursorType==="ShowMore")H=Z.value;continue}if(Y.entryId?.startsWith("cursor-bottom-")||Y.entryId?.startsWith("cursor-showMore-")){H=Z?.value||Z?.itemContent?.value||H;continue}const X=extractFolderTweet(Z?.itemContent?.tweet_results?.result,V);if(X){z.push(X);continue}for(const Q of Z?.items||[]){const G=extractFolderTweet(Q.item?.itemContent?.tweet_results?.result,V);if(G)z.push(G)}}return{tweets:z,nextCursor:H}}U({site:"twitter",name:"bookmark-folder",access:"read",description:"Read the tweets inside a single Twitter/X bookmark folder. Get the folder id from `opencli twitter bookmark-folders`.",domain:"x.com",strategy:P.COOKIE,browser:!0,args:[{name:"folder-id",positional:!0,type:"string",required:!0,help:"Folder id from `opencli twitter bookmark-folders`."},{name:"limit",type:"int",default:20,help:"Maximum number of bookmarks to return (default 20)."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the folder by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API's native (saved-time) ordering."}],columns:["id","author","text","likes","retweets","bookmarks","created_at","url","has_media","media_urls","media_posters"],func:async(J,V)=>{const z=String(V["folder-id"]||"").trim();if(!z||!q.test(z))throw new S(`Invalid folder-id: ${JSON.stringify(V["folder-id"])}. Expected a safe folder ID from \`opencli twitter bookmark-folders\`.`);const H=Number(V.limit??20);if(!Number.isInteger(H)||H<1)throw new S(`Invalid --limit: ${JSON.stringify(V.limit)}. Expected a positive integer.`);const W=(await J.getCookies({url:"https://x.com"})).find((K)=>K.name==="ct0")?.value||null;if(!W)throw new f("x.com","Not logged into x.com (no ct0 cookie)");const Y=await _(J,j,x),Z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(C)}`,"X-Csrf-Token":W,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),X=[],Q=new Set;let G=null;for(let K=0;K<k&&X.length<H;K++){const h=Math.min(100,H-X.length+10),B=L(Y,z,h,G),b=await J.evaluate(`async () => {
1
+ import{cli as U,Strategy as P}from"@jackwener/opencli/registry";import{ArgumentError as S,AuthRequiredError as f,CommandExecutionError as O}from"@jackwener/opencli/errors";import{TWITTER_BEARER_TOKEN as C,applyTopByEngagement as F}from"./utils.js";import{extractMedia as N,resolveTwitterQueryId as _,describeTwitterApiError as R}from"./shared.js";const j="BookmarkFolderTimeline",x="13H7EUATwethsj_jZ6QQAQ",q=/^[A-Za-z0-9_-]+$/,k=100,p={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_enhance_cards_enabled:!1};function L(J,V,z,H){const $={bookmark_collection_id:String(V),count:z,includePromotedContent:!1};if(H)$.cursor=H;return`/i/api/graphql/${J}/${j}?variables=${encodeURIComponent(JSON.stringify($))}&features=${encodeURIComponent(JSON.stringify(p))}`}export function extractFolderTweet(J,V){if(!J)return null;const z=J.tweet||J,H=z.legacy||{};if(!z.rest_id||V.has(z.rest_id))return null;V.add(z.rest_id);const $=z.core?.user_results?.result,W=$?.legacy?.screen_name||$?.core?.screen_name||"",Y=z.note_tweet?.note_tweet_results?.result?.text;return{id:z.rest_id,author:W,text:Y||H.full_text||"",likes:H.favorite_count||0,retweets:H.retweet_count||0,bookmarks:H.bookmark_count||0,created_at:H.created_at||"",url:W?`https://x.com/${W}/status/${z.rest_id}`:`https://x.com/i/status/${z.rest_id}`,...N(H)}}export function parseBookmarkFolderTimeline(J,V){const z=[];let H=null;const $=J?.data?.bookmark_collection_timeline?.timeline?.instructions||J?.data?.bookmark_timeline_v2?.timeline?.instructions||J?.data?.bookmark_timeline?.timeline?.instructions||[];for(const W of $)for(const Y of W.entries||[]){const Z=Y.content;if(Z?.entryType==="TimelineTimelineCursor"||Z?.__typename==="TimelineTimelineCursor"){if(Z.cursorType==="Bottom"||Z.cursorType==="ShowMore")H=Z.value;continue}if(Y.entryId?.startsWith("cursor-bottom-")||Y.entryId?.startsWith("cursor-showMore-")){H=Z?.value||Z?.itemContent?.value||H;continue}const X=extractFolderTweet(Z?.itemContent?.tweet_results?.result,V);if(X){z.push(X);continue}for(const Q of Z?.items||[]){const G=extractFolderTweet(Q.item?.itemContent?.tweet_results?.result,V);if(G)z.push(G)}}return{tweets:z,nextCursor:H}}U({site:"twitter",name:"bookmark-folder",access:"read",description:"Read the tweets inside a single Twitter/X bookmark folder. Get the folder id from `ppcli twitter bookmark-folders`.",domain:"x.com",strategy:P.COOKIE,browser:!0,args:[{name:"folder-id",positional:!0,type:"string",required:!0,help:"Folder id from `ppcli twitter bookmark-folders`."},{name:"limit",type:"int",default:20,help:"Maximum number of bookmarks to return (default 20)."},{name:"top-by-engagement",type:"int",default:0,help:"When set to N>0, re-rank the folder by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the API's native (saved-time) ordering."}],columns:["id","author","text","likes","retweets","bookmarks","created_at","url","has_media","media_urls","media_posters"],func:async(J,V)=>{const z=String(V["folder-id"]||"").trim();if(!z||!q.test(z))throw new S(`Invalid folder-id: ${JSON.stringify(V["folder-id"])}. Expected a safe folder ID from \`ppcli twitter bookmark-folders\`.`);const H=Number(V.limit??20);if(!Number.isInteger(H)||H<1)throw new S(`Invalid --limit: ${JSON.stringify(V.limit)}. Expected a positive integer.`);const W=(await J.getCookies({url:"https://x.com"})).find((K)=>K.name==="ct0")?.value||null;if(!W)throw new f("x.com","Not logged into x.com (no ct0 cookie)");const Y=await _(J,j,x),Z=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(C)}`,"X-Csrf-Token":W,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),X=[],Q=new Set;let G=null;for(let K=0;K<k&&X.length<H;K++){const h=Math.min(100,H-X.length+10),B=L(Y,z,h,G),b=await J.evaluate(`async () => {
2
2
  const r = await fetch(${JSON.stringify(B)}, { headers: ${Z}, credentials: 'include' });
3
3
  return r.ok ? await r.json() : { error: r.status };
4
4
  }`);if(b?.error){if(X.length===0)throw new O(R("BookmarkFolderTimeline",b.error,`folder=${z}`));break}const{tweets:D,nextCursor:v}=parseBookmarkFolderTimeline(b,Q);X.push(...D);if(!v||v===G)break;G=v}const M=X.slice(0,H);return F(M,V["top-by-engagement"])}});export const __test__={parseBookmarkFolderTimeline,extractFolderTweet,buildFolderTimelineUrl:L,FOLDER_ID_PATTERN:q};
@@ -1,4 +1,4 @@
1
- import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{ArgumentError as D,AuthRequiredError as h,CliError as t,CommandExecutionError as X,EmptyResultError as T}from"@jackwener/opencli/errors";import{formatCookieHeader as C}from"@jackwener/opencli/download";import{downloadMedia as a}from"@jackwener/opencli/download/media-download";import{resolveTwitterOperationMetadata as R,normalizeTwitterGraphqlPayload as _,unwrapBrowserResult as y,normalizeTwitterScreenName as e,extractMedia as jj,parseTweetUrl as Fj}from"./shared.js";import{TWITTER_BEARER_TOKEN as Gj}from"./utils.js";const I="9EovraBTXJYGSEQXZqlLmQ",k="IGgvgiOx4QZndDHuD3x9TQ",E=100,O={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},f={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},S={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},N={withPayments:!0,withAuxiliaryUserLabels:!0},Hj={queryId:I,features:O,fieldToggles:f},Jj={queryId:k,features:S,fieldToggles:N};function U(j){const F=Number(j??10);if(!Number.isInteger(F)||F<1||F>1000)throw new D("--limit must be an integer between 1 and 1000");return F}function x(j,F){const G=j-F;if(G<=0)return 0;const H=G+10;if(H>100)return 100;return H}async function w(j,F){return(await a(j,F)).map((H,K)=>{const V=j[K]||{};return{index:H.index,tweet_id:V.tweet_id||"",url:V.url||"",type:H.type,status:H.status,size:H.size}})}function Kj(j){if(typeof j==="string")return{queryId:j,features:O,fieldToggles:f};return{queryId:j?.queryId||I,features:j?.features||O,fieldToggles:j?.fieldToggles||f}}function Vj(j){if(typeof j==="string")return{queryId:j,features:S,fieldToggles:N};return{queryId:j?.queryId||k,features:j?.features||S,fieldToggles:j?.fieldToggles||N}}function c(j,F,G){const H=G.fieldToggles||{},K=[`variables=${encodeURIComponent(JSON.stringify(F))}`,`features=${encodeURIComponent(JSON.stringify(G.features||{}))}`];if(Object.keys(H).length>0)K.push(`fieldToggles=${encodeURIComponent(JSON.stringify(H))}`);return`${j}?${K.join("&")}`}function v(j,F,G,H){const K=Kj(j),V={userId:F,count:G,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0};if(H)V.cursor=H;return c(`/i/api/graphql/${K.queryId}/UserMedia`,V,K)}function d(j,F){const G=Vj(j),H={screen_name:F,withSafetyModeUserFields:!0};return c(`/i/api/graphql/${G.queryId}/UserByScreenName`,H,G)}function g(j){if(!j)return"unknown";if(/video\.twimg\.com|\.mp4(\?|$)|\.m3u8(\?|$)/.test(j))return"video";return"image"}function p(j,F){if(!j||typeof j!=="object"||Array.isArray(j))throw new X(`Twitter ${F} returned malformed payload`);return j}function Wj(j,F,G){if(F===401||F===403)throw new h("x.com",`Twitter ${j} requires an authenticated x.com session`);if(F===404)throw new T(`twitter download ${j}`,G||"Twitter returned not found");const H=F?`HTTP ${F}`:"fetch failed";throw new X(`Twitter ${j} fetch failed: ${H}${G?` - ${G}`:""}`)}function m(j,F){const G=p(y(j),F);if(G.ok===!0)return G.payload;if(G.ok===!1)Wj(F,Number(G.status)||0,typeof G.error==="string"?G.error:"");throw new X(`Twitter ${F} returned malformed fetch result`)}function Xj(j){const F=p(j,"UserMedia");if(Array.isArray(F.errors)&&F.errors.length>0)throw new X(`Twitter UserMedia returned GraphQL errors: ${JSON.stringify(F.errors).slice(0,200)}`);const G=F.data?.user?.result;if(!G||typeof G!=="object")throw new X("Twitter UserMedia returned malformed user result");const H=G.timeline_v2?.timeline?.instructions||G.timeline?.timeline?.instructions;if(!Array.isArray(H))throw new X("Twitter UserMedia returned malformed timeline instructions");return F}function n(j,F){const G=[];let H=null;const K=Xj(j).data.user.result,Z=[K.timeline_v2?.timeline?.instructions,K.timeline?.timeline?.instructions].filter(Array.isArray).flat(),Q=(J)=>{if(!J||typeof J!=="object")return;if(J.type==="TimelinePinEntry")return;if(J.tweet_results?.result){const W=J.tweet_results.result,b=W.__typename==="TweetWithVisibilityResults"&&W.tweet?W.tweet:W.tweet||W,$=typeof b.rest_id==="string"||typeof b.rest_id==="number"?String(b.rest_id):"";if(!$)throw new X("Twitter UserMedia returned a tweet without rest_id");if(!F.has($)){F.add($);const{media_urls:B}=jj(b.legacy||{});for(const P of B)G.push({tweet_id:$,url:P,type:g(P)})}}if((J.entryType==="TimelineTimelineCursor"||J.__typename==="TimelineTimelineCursor")&&(J.cursorType==="Bottom"||J.cursorType==="ShowMore")&&J.value)H=J.value;if(Array.isArray(J)){for(const W of J)Q(W);return}for(const W of Object.values(J))if(W&&typeof W==="object")Q(W)};Q(Z);return{items:G,nextCursor:H}}o({site:"twitter",name:"download",access:"read",description:"Download Twitter/X media (images and videos). Provide either <username> to fetch every media item from their profile via the GraphQL UserMedia endpoint with cursor pagination, or --tweet-url to download a single tweet.",domain:"x.com",strategy:r.COOKIE,browser:!0,args:[{name:"username",positional:!0,help:"Twitter username (with or without @) to scan their profile media. Either <username> or --tweet-url is required."},{name:"tweet-url",help:"Single tweet URL to download. Use this OR <username>, not both required at once."},{name:"limit",type:"int",default:10,help:"Maximum number of media items to download when scanning a profile (default 10). Ignored when --tweet-url is used."},{name:"output",default:"./twitter-downloads",help:"Output directory (default ./twitter-downloads). A per-source subdir is created inside."}],columns:["index","tweet_id","url","type","status","size"],func:async(j,F)=>{try{const G=String(F.username??"").trim(),H=String(F["tweet-url"]??"").trim(),K=F.output;if(!G&&!H)throw new D("twitter download requires either <username> or --tweet-url");if(G&&H)throw new D("Use either <username> or --tweet-url, not both");if(H)return $j(j,H,K);const V=U(F.limit),Z=e(G);if(!Z)throw new D("twitter download username must be a valid Twitter/X handle","Example: opencli twitter download @jack --limit 20");return Zj(j,Z,V,K)}catch(G){if(G instanceof t)throw G;throw new X(`twitter download failed: ${G?.message??String(G)}`)}}});async function Zj(j,F,G,H){await j.goto(`https://x.com/${F}`);await j.wait({selector:'[data-testid="primaryColumn"]'});const K=await j.getCookies({url:"https://x.com"}),V=K.find((Y)=>Y.name==="ct0")?.value||null;if(!V)throw new h("x.com","Not logged into x.com (no ct0 cookie)");const Z=await R(j,"UserMedia",Hj),Q=await R(j,"UserByScreenName",Jj),J=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(Gj)}`,"X-Csrf-Token":V,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),W=d(Q,F),b=m(await j.evaluate(`async () => {
1
+ import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{ArgumentError as D,AuthRequiredError as h,CliError as t,CommandExecutionError as X,EmptyResultError as T}from"@jackwener/opencli/errors";import{formatCookieHeader as C}from"@jackwener/opencli/download";import{downloadMedia as a}from"@jackwener/opencli/download/media-download";import{resolveTwitterOperationMetadata as R,normalizeTwitterGraphqlPayload as _,unwrapBrowserResult as y,normalizeTwitterScreenName as e,extractMedia as jj,parseTweetUrl as Fj}from"./shared.js";import{TWITTER_BEARER_TOKEN as Gj}from"./utils.js";const I="9EovraBTXJYGSEQXZqlLmQ",k="IGgvgiOx4QZndDHuD3x9TQ",E=100,O={rweb_video_screen_enabled:!0,rweb_cashtags_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1},f={withPayments:!0,withAuxiliaryUserLabels:!0,withArticleRichContentState:!0,withArticlePlainText:!0,withArticleSummaryText:!0,withArticleVoiceOver:!0,withGrokAnalyze:!0,withDisallowedReplyControls:!0},S={hidden_profile_subscriptions_enabled:!0,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0},N={withPayments:!0,withAuxiliaryUserLabels:!0},Hj={queryId:I,features:O,fieldToggles:f},Jj={queryId:k,features:S,fieldToggles:N};function U(j){const F=Number(j??10);if(!Number.isInteger(F)||F<1||F>1000)throw new D("--limit must be an integer between 1 and 1000");return F}function x(j,F){const G=j-F;if(G<=0)return 0;const H=G+10;if(H>100)return 100;return H}async function w(j,F){return(await a(j,F)).map((H,K)=>{const V=j[K]||{};return{index:H.index,tweet_id:V.tweet_id||"",url:V.url||"",type:H.type,status:H.status,size:H.size}})}function Kj(j){if(typeof j==="string")return{queryId:j,features:O,fieldToggles:f};return{queryId:j?.queryId||I,features:j?.features||O,fieldToggles:j?.fieldToggles||f}}function Vj(j){if(typeof j==="string")return{queryId:j,features:S,fieldToggles:N};return{queryId:j?.queryId||k,features:j?.features||S,fieldToggles:j?.fieldToggles||N}}function c(j,F,G){const H=G.fieldToggles||{},K=[`variables=${encodeURIComponent(JSON.stringify(F))}`,`features=${encodeURIComponent(JSON.stringify(G.features||{}))}`];if(Object.keys(H).length>0)K.push(`fieldToggles=${encodeURIComponent(JSON.stringify(H))}`);return`${j}?${K.join("&")}`}function v(j,F,G,H){const K=Kj(j),V={userId:F,count:G,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0};if(H)V.cursor=H;return c(`/i/api/graphql/${K.queryId}/UserMedia`,V,K)}function d(j,F){const G=Vj(j),H={screen_name:F,withSafetyModeUserFields:!0};return c(`/i/api/graphql/${G.queryId}/UserByScreenName`,H,G)}function g(j){if(!j)return"unknown";if(/video\.twimg\.com|\.mp4(\?|$)|\.m3u8(\?|$)/.test(j))return"video";return"image"}function p(j,F){if(!j||typeof j!=="object"||Array.isArray(j))throw new X(`Twitter ${F} returned malformed payload`);return j}function Wj(j,F,G){if(F===401||F===403)throw new h("x.com",`Twitter ${j} requires an authenticated x.com session`);if(F===404)throw new T(`twitter download ${j}`,G||"Twitter returned not found");const H=F?`HTTP ${F}`:"fetch failed";throw new X(`Twitter ${j} fetch failed: ${H}${G?` - ${G}`:""}`)}function m(j,F){const G=p(y(j),F);if(G.ok===!0)return G.payload;if(G.ok===!1)Wj(F,Number(G.status)||0,typeof G.error==="string"?G.error:"");throw new X(`Twitter ${F} returned malformed fetch result`)}function Xj(j){const F=p(j,"UserMedia");if(Array.isArray(F.errors)&&F.errors.length>0)throw new X(`Twitter UserMedia returned GraphQL errors: ${JSON.stringify(F.errors).slice(0,200)}`);const G=F.data?.user?.result;if(!G||typeof G!=="object")throw new X("Twitter UserMedia returned malformed user result");const H=G.timeline_v2?.timeline?.instructions||G.timeline?.timeline?.instructions;if(!Array.isArray(H))throw new X("Twitter UserMedia returned malformed timeline instructions");return F}function n(j,F){const G=[];let H=null;const K=Xj(j).data.user.result,Z=[K.timeline_v2?.timeline?.instructions,K.timeline?.timeline?.instructions].filter(Array.isArray).flat(),Q=(J)=>{if(!J||typeof J!=="object")return;if(J.type==="TimelinePinEntry")return;if(J.tweet_results?.result){const W=J.tweet_results.result,b=W.__typename==="TweetWithVisibilityResults"&&W.tweet?W.tweet:W.tweet||W,$=typeof b.rest_id==="string"||typeof b.rest_id==="number"?String(b.rest_id):"";if(!$)throw new X("Twitter UserMedia returned a tweet without rest_id");if(!F.has($)){F.add($);const{media_urls:B}=jj(b.legacy||{});for(const P of B)G.push({tweet_id:$,url:P,type:g(P)})}}if((J.entryType==="TimelineTimelineCursor"||J.__typename==="TimelineTimelineCursor")&&(J.cursorType==="Bottom"||J.cursorType==="ShowMore")&&J.value)H=J.value;if(Array.isArray(J)){for(const W of J)Q(W);return}for(const W of Object.values(J))if(W&&typeof W==="object")Q(W)};Q(Z);return{items:G,nextCursor:H}}o({site:"twitter",name:"download",access:"read",description:"Download Twitter/X media (images and videos). Provide either <username> to fetch every media item from their profile via the GraphQL UserMedia endpoint with cursor pagination, or --tweet-url to download a single tweet.",domain:"x.com",strategy:r.COOKIE,browser:!0,args:[{name:"username",positional:!0,help:"Twitter username (with or without @) to scan their profile media. Either <username> or --tweet-url is required."},{name:"tweet-url",help:"Single tweet URL to download. Use this OR <username>, not both required at once."},{name:"limit",type:"int",default:10,help:"Maximum number of media items to download when scanning a profile (default 10). Ignored when --tweet-url is used."},{name:"output",default:"./twitter-downloads",help:"Output directory (default ./twitter-downloads). A per-source subdir is created inside."}],columns:["index","tweet_id","url","type","status","size"],func:async(j,F)=>{try{const G=String(F.username??"").trim(),H=String(F["tweet-url"]??"").trim(),K=F.output;if(!G&&!H)throw new D("twitter download requires either <username> or --tweet-url");if(G&&H)throw new D("Use either <username> or --tweet-url, not both");if(H)return $j(j,H,K);const V=U(F.limit),Z=e(G);if(!Z)throw new D("twitter download username must be a valid Twitter/X handle","Example: ppcli twitter download @jack --limit 20");return Zj(j,Z,V,K)}catch(G){if(G instanceof t)throw G;throw new X(`twitter download failed: ${G?.message??String(G)}`)}}});async function Zj(j,F,G,H){await j.goto(`https://x.com/${F}`);await j.wait({selector:'[data-testid="primaryColumn"]'});const K=await j.getCookies({url:"https://x.com"}),V=K.find((Y)=>Y.name==="ct0")?.value||null;if(!V)throw new h("x.com","Not logged into x.com (no ct0 cookie)");const Z=await R(j,"UserMedia",Hj),Q=await R(j,"UserByScreenName",Jj),J=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(Gj)}`,"X-Csrf-Token":V,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"}),W=d(Q,F),b=m(await j.evaluate(`async () => {
2
2
  try {
3
3
  const resp = await fetch("${W}", { headers: ${J}, credentials: 'include' });
4
4
  if (!resp.ok) return { ok: false, status: resp.status };
@@ -42,10 +42,10 @@ import{ArgumentError as V,AuthRequiredError as $,selectorError as T,EmptyResultE
42
42
  }
43
43
  }
44
44
  return out;
45
- }`;return G.evaluate(L)}function W(G){return D(G)}v({site:"twitter",name:"followers",access:"read",description:"Get accounts following a Twitter/X user (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:j.UI,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch followers of the currently logged-in account."},{name:"limit",type:"int",default:50,help:"Maximum number of follower rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio"],func:async(G,L)=>{const J=L.limit;if(!Number.isInteger(J)||J<=0)throw new V("limit must be a positive integer");const X=String(L.user??"").trim();let H=W(X);if(X&&!H)throw new V("twitter followers user must be a valid Twitter/X handle","Example: opencli twitter followers @elonmusk --limit 100");if(!H){await G.goto("https://x.com/home");await G.wait({selector:'[data-testid="primaryColumn"]'});const I=M(await G.evaluate(`() => {
45
+ }`;return G.evaluate(L)}function W(G){return D(G)}v({site:"twitter",name:"followers",access:"read",description:"Get accounts following a Twitter/X user (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:j.UI,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch followers of the currently logged-in account."},{name:"limit",type:"int",default:50,help:"Maximum number of follower rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio"],func:async(G,L)=>{const J=L.limit;if(!Number.isInteger(J)||J<=0)throw new V("limit must be a positive integer");const X=String(L.user??"").trim();let H=W(X);if(X&&!H)throw new V("twitter followers user must be a valid Twitter/X handle","Example: ppcli twitter followers @elonmusk --limit 100");if(!H){await G.goto("https://x.com/home");await G.wait({selector:'[data-testid="primaryColumn"]'});const I=M(await G.evaluate(`() => {
46
46
  const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
47
47
  return link ? link.getAttribute('href') : null;
48
- }`));if(!I||typeof I!=="string")throw new $("x.com","Could not find logged-in user profile link. Are you logged in?");H=W(I);if(!H)throw new $("x.com","Could not find logged-in user profile link. Are you logged in?")}if(!H)throw new V("twitter followers user cannot be empty","Example: opencli twitter followers @elonmusk --limit 100");await G.goto(`https://x.com/${H}`);await G.wait(3);const B=JSON.stringify(H);if(!await G.evaluate(`() => {
48
+ }`));if(!I||typeof I!=="string")throw new $("x.com","Could not find logged-in user profile link. Are you logged in?");H=W(I);if(!H)throw new $("x.com","Could not find logged-in user profile link. Are you logged in?")}if(!H)throw new V("twitter followers user cannot be empty","Example: ppcli twitter followers @elonmusk --limit 100");await G.goto(`https://x.com/${H}`);await G.wait(3);const B=JSON.stringify(H);if(!await G.evaluate(`() => {
49
49
  const target = ${B};
50
50
  const selectors = [
51
51
  'a[href="/' + target + '/followers"]',
@@ -1,4 +1,4 @@
1
- import{cli as U,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as v,AuthRequiredError as j,CommandExecutionError as b,EmptyResultError as C}from"@jackwener/opencli/errors";import{looksLikePrivateTwitterTimeline as E,normalizeTwitterScreenName as m,resolveTwitterQueryId as x,sanitizeQueryId as p,unwrapBrowserResult as B,describeTwitterApiError as d}from"./shared.js";import{TWITTER_BEARER_TOKEN as g}from"./utils.js";const c="F42cDX8PDFxkbjjq6JrM2w",i="IGgvgiOx4QZndDHuD3x9TQ",l=100,n={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1};function A(H,Z,J,$){const V={userId:Z,count:J,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0,withV2Timeline:!0};if($)V.cursor=$;return`/i/api/graphql/${H}/Following?variables=${encodeURIComponent(JSON.stringify(V))}&features=${encodeURIComponent(JSON.stringify(n))}`}function h(H,Z){const J=JSON.stringify({screen_name:Z,withSafetyModeUserFields:!0}),$=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${H}/UserByScreenName?variables=${encodeURIComponent(J)}&features=${encodeURIComponent($)}`}function N(H){if(!H||H.__typename!=="User")return null;const Z=H.core||{},J=H.legacy||{},$=Z.screen_name||J.screen_name||"";if(!$)throw new b("Malformed Twitter following user: missing screen_name");return{screen_name:$,name:Z.name||J.name||"",bio:J.description||H.profile_bio?.description||"",followers:J.followers_count||J.normal_followers_count||0}}function k(H){const Z=[];let J=null;const $=H?.data?.user?.result?.timeline_v2?.timeline?.instructions||H?.data?.user?.result?.timeline?.timeline?.instructions||[];for(const V of $)for(const W of V.entries||[]){const K=W.content;if(K?.entryType==="TimelineTimelineCursor"||K?.__typename==="TimelineTimelineCursor"){if(K.cursorType==="Bottom"||K.cursorType==="ShowMore")J=K.value;continue}if(W.entryId?.startsWith("cursor-bottom-")||W.entryId?.startsWith("cursor-showMore-")){J=K?.value||K?.itemContent?.value||J;continue}if(W.entryId?.startsWith("user-")){const O=N(K?.itemContent?.user_results?.result);if(O)Z.push(O)}}return{users:Z,nextCursor:J}}function q(H){return m(H)}U({site:"twitter",name:"following",access:"read",description:"Get accounts a Twitter/X user is following (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:w.COOKIE,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch the accounts the currently logged-in user follows."},{name:"limit",type:"int",default:50,help:"Maximum number of following rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio","followers"],func:async(H,Z)=>{const J=Z.limit===void 0||Z.limit===null?50:Number(Z.limit);if(!Number.isInteger(J)||J<=0)throw new v("twitter following --limit must be a positive integer","Example: opencli twitter following @elonmusk --limit 200");const $=String(Z.user??"").trim();let V=q($);if($&&!V)throw new v("twitter following user must be a valid Twitter/X handle","Example: opencli twitter following @elonmusk --limit 200");const K=(await H.getCookies({url:"https://x.com"})).find((X)=>X.name==="ct0")?.value||null;if(!K)throw new j("x.com","Not logged into x.com (no ct0 cookie)");if(!V){await H.goto("https://x.com/home");await H.wait({selector:'[data-testid="primaryColumn"]'});const X=B(await H.evaluate(`() => {
1
+ import{cli as U,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as v,AuthRequiredError as j,CommandExecutionError as b,EmptyResultError as C}from"@jackwener/opencli/errors";import{looksLikePrivateTwitterTimeline as E,normalizeTwitterScreenName as m,resolveTwitterQueryId as x,sanitizeQueryId as p,unwrapBrowserResult as B,describeTwitterApiError as d}from"./shared.js";import{TWITTER_BEARER_TOKEN as g}from"./utils.js";const c="F42cDX8PDFxkbjjq6JrM2w",i="IGgvgiOx4QZndDHuD3x9TQ",l=100,n={rweb_video_screen_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,articles_preview_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,tweet_awards_web_tipping_enabled:!1,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!1,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!1,responsive_web_enhance_cards_enabled:!1};function A(H,Z,J,$){const V={userId:Z,count:J,includePromotedContent:!1,withClientEventToken:!1,withBirdwatchNotes:!1,withVoice:!0,withV2Timeline:!0};if($)V.cursor=$;return`/i/api/graphql/${H}/Following?variables=${encodeURIComponent(JSON.stringify(V))}&features=${encodeURIComponent(JSON.stringify(n))}`}function h(H,Z){const J=JSON.stringify({screen_name:Z,withSafetyModeUserFields:!0}),$=JSON.stringify({hidden_profile_subscriptions_enabled:!0,rweb_tipjar_consumption_enabled:!0,responsive_web_graphql_exclude_directive_enabled:!0,verified_phone_label_enabled:!1,subscriptions_verification_info_is_identity_verified_enabled:!0,subscriptions_verification_info_verified_since_enabled:!0,highlights_tweets_tab_ui_enabled:!0,responsive_web_twitter_article_notes_tab_enabled:!0,subscriptions_feature_can_gift_premium:!0,creator_subscriptions_tweet_preview_api_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,responsive_web_graphql_timeline_navigation_enabled:!0});return`/i/api/graphql/${H}/UserByScreenName?variables=${encodeURIComponent(J)}&features=${encodeURIComponent($)}`}function N(H){if(!H||H.__typename!=="User")return null;const Z=H.core||{},J=H.legacy||{},$=Z.screen_name||J.screen_name||"";if(!$)throw new b("Malformed Twitter following user: missing screen_name");return{screen_name:$,name:Z.name||J.name||"",bio:J.description||H.profile_bio?.description||"",followers:J.followers_count||J.normal_followers_count||0}}function k(H){const Z=[];let J=null;const $=H?.data?.user?.result?.timeline_v2?.timeline?.instructions||H?.data?.user?.result?.timeline?.timeline?.instructions||[];for(const V of $)for(const W of V.entries||[]){const K=W.content;if(K?.entryType==="TimelineTimelineCursor"||K?.__typename==="TimelineTimelineCursor"){if(K.cursorType==="Bottom"||K.cursorType==="ShowMore")J=K.value;continue}if(W.entryId?.startsWith("cursor-bottom-")||W.entryId?.startsWith("cursor-showMore-")){J=K?.value||K?.itemContent?.value||J;continue}if(W.entryId?.startsWith("user-")){const O=N(K?.itemContent?.user_results?.result);if(O)Z.push(O)}}return{users:Z,nextCursor:J}}function q(H){return m(H)}U({site:"twitter",name:"following",access:"read",description:"Get accounts a Twitter/X user is following (defaults to the logged-in user when no user is given)",domain:"x.com",strategy:w.COOKIE,browser:!0,args:[{name:"user",positional:!0,type:"string",required:!1,help:"Twitter/X handle (with or without @). Omit to fetch the accounts the currently logged-in user follows."},{name:"limit",type:"int",default:50,help:"Maximum number of following rows to return (default 50). Must be a positive integer."}],columns:["screen_name","name","bio","followers"],func:async(H,Z)=>{const J=Z.limit===void 0||Z.limit===null?50:Number(Z.limit);if(!Number.isInteger(J)||J<=0)throw new v("twitter following --limit must be a positive integer","Example: ppcli twitter following @elonmusk --limit 200");const $=String(Z.user??"").trim();let V=q($);if($&&!V)throw new v("twitter following user must be a valid Twitter/X handle","Example: ppcli twitter following @elonmusk --limit 200");const K=(await H.getCookies({url:"https://x.com"})).find((X)=>X.name==="ct0")?.value||null;if(!K)throw new j("x.com","Not logged into x.com (no ct0 cookie)");if(!V){await H.goto("https://x.com/home");await H.wait({selector:'[data-testid="primaryColumn"]'});const X=B(await H.evaluate(`() => {
2
2
  const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
3
3
  return link ? link.getAttribute('href') : null;
4
- }`));if(!X||typeof X!=="string")throw new j("x.com","Could not detect logged-in user. Are you logged in?");V=q(X);if(!V)throw new j("x.com","Could not detect logged-in user. Are you logged in?")}if(!V)throw new v("twitter following user cannot be empty","Example: opencli twitter following @elonmusk --limit 200");const O=await x(H,"Following",c),I=await x(H,"UserByScreenName",i),L={Authorization:`Bearer ${decodeURIComponent(g)}`,"X-Csrf-Token":K,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"},M=B(await H.evaluate(async(X,f)=>{const P=await fetch(X,{headers:f,credentials:"include"});if(!P.ok)return{error:P.status};return{userId:(await P.json()).data?.user?.result?.rest_id||null}},h(I,V),L));if(M?.error===401||M?.error===403)throw new j("x.com",`Twitter user lookup failed (HTTP ${M.error})`);if(M?.error)throw new b(`HTTP ${M.error}: Failed to resolve Twitter user @${V}`);const _=M?.userId||null;if(!_)throw new b(`Could not find user @${V}`);const G=[],S=new Set;let z=null,T=null;for(let X=0;X<l&&G.length<J;X++){const f=Math.min(50,J-G.length+10),P=A(O,_,f,z),D=B(await H.evaluate(async(Y,y)=>{const Q=await fetch(Y,{headers:y,credentials:"include"});return Q.ok?await Q.json():{error:Q.status}},P,L));if(D?.error){if(D.error===401||D.error===403)throw new j("x.com",`Twitter following request failed (HTTP ${D.error})`);throw new b(d("Following",D.error))}T=D;const{users:R,nextCursor:F}=k(D);for(const Y of R)if(!S.has(Y.screen_name)){S.add(Y.screen_name);G.push(Y)}if(!F||F===z)break;z=F}if(G.length===0){if(E(T))throw new C("twitter following",`No following data returned for @${V} (the target account may have set their following list to private)`);throw new C("twitter following",`No following accounts found for @${V}`)}return G.slice(0,J)}});export const __test__={sanitizeQueryId:p,buildFollowingUrl:A,buildUserByScreenNameUrl:h,extractUser:N,normalizeScreenName:q,parseFollowing:k};
4
+ }`));if(!X||typeof X!=="string")throw new j("x.com","Could not detect logged-in user. Are you logged in?");V=q(X);if(!V)throw new j("x.com","Could not detect logged-in user. Are you logged in?")}if(!V)throw new v("twitter following user cannot be empty","Example: ppcli twitter following @elonmusk --limit 200");const O=await x(H,"Following",c),I=await x(H,"UserByScreenName",i),L={Authorization:`Bearer ${decodeURIComponent(g)}`,"X-Csrf-Token":K,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes"},M=B(await H.evaluate(async(X,f)=>{const P=await fetch(X,{headers:f,credentials:"include"});if(!P.ok)return{error:P.status};return{userId:(await P.json()).data?.user?.result?.rest_id||null}},h(I,V),L));if(M?.error===401||M?.error===403)throw new j("x.com",`Twitter user lookup failed (HTTP ${M.error})`);if(M?.error)throw new b(`HTTP ${M.error}: Failed to resolve Twitter user @${V}`);const _=M?.userId||null;if(!_)throw new b(`Could not find user @${V}`);const G=[],S=new Set;let z=null,T=null;for(let X=0;X<l&&G.length<J;X++){const f=Math.min(50,J-G.length+10),P=A(O,_,f,z),D=B(await H.evaluate(async(Y,y)=>{const Q=await fetch(Y,{headers:y,credentials:"include"});return Q.ok?await Q.json():{error:Q.status}},P,L));if(D?.error){if(D.error===401||D.error===403)throw new j("x.com",`Twitter following request failed (HTTP ${D.error})`);throw new b(d("Following",D.error))}T=D;const{users:R,nextCursor:F}=k(D);for(const Y of R)if(!S.has(Y.screen_name)){S.add(Y.screen_name);G.push(Y)}if(!F||F===z)break;z=F}if(G.length===0){if(E(T))throw new C("twitter following",`No following data returned for @${V} (the target account may have set their following list to private)`);throw new C("twitter following",`No following accounts found for @${V}`)}return G.slice(0,J)}});export const __test__={sanitizeQueryId:p,buildFollowingUrl:A,buildUserByScreenNameUrl:h,extractUser:N,normalizeScreenName:q,parseFollowing:k};