publishport-opencli 1.0.1 → 1.0.3

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 (317) hide show
  1. package/README.md +61 -61
  2. package/README.zh-CN.md +68 -68
  3. package/cli-manifest.json +502 -59
  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/rest-countries/utils.js +1 -1
  133. package/clis/rfc/utils.js +1 -1
  134. package/clis/rubygems/utils.js +1 -1
  135. package/clis/semanticscholar/utils.js +1 -1
  136. package/clis/slock/errors.js +1 -1
  137. package/clis/spotify/spotify.js +1 -1
  138. package/clis/spotify/utils.js +1 -1
  139. package/clis/stackoverflow/utils.js +1 -1
  140. package/clis/steam/utils.js +1 -1
  141. package/clis/substack/auth.js +2 -2
  142. package/clis/suno/generate.js +2 -2
  143. package/clis/suno/list.js +3 -3
  144. package/clis/telegram/send.js +1 -1
  145. package/clis/tiktok/creator-videos.js +1 -1
  146. package/clis/tiktok/publish.js +1 -1
  147. package/clis/tiktok/utils.js +1 -1
  148. package/clis/trae-cn/activity.js +1 -1
  149. package/clis/trae-cn/approve.js +1 -1
  150. package/clis/trae-cn/ask.js +1 -1
  151. package/clis/trae-cn/dump.js +1 -1
  152. package/clis/trae-cn/export.js +1 -1
  153. package/clis/trae-cn/model.js +1 -1
  154. package/clis/trae-cn/new.js +1 -1
  155. package/clis/trae-cn/read.js +1 -1
  156. package/clis/trae-cn/screenshot.js +1 -1
  157. package/clis/trae-cn/select-model.js +1 -1
  158. package/clis/trae-cn/send.js +1 -1
  159. package/clis/trae-cn/setup.js +1 -1
  160. package/clis/trae-cn/status.js +1 -1
  161. package/clis/trae-cn/targets.js +1 -1
  162. package/clis/trae-cn/watch.js +1 -1
  163. package/clis/trae-solo/state-fs.js +1 -1
  164. package/clis/tumblr/publish.js +1 -1
  165. package/clis/tvmaze/utils.js +1 -1
  166. package/clis/twitter/bookmark-folder.js +1 -1
  167. package/clis/twitter/download.js +1 -1
  168. package/clis/twitter/followers.js +2 -2
  169. package/clis/twitter/following.js +2 -2
  170. package/clis/twitter/likes.js +1 -1
  171. package/clis/twitter/list-add-batch.js +1 -1
  172. package/clis/twitter/list-add-core.js +1 -1
  173. package/clis/twitter/list-add.js +1 -1
  174. package/clis/twitter/list-create.js +1 -1
  175. package/clis/twitter/list-delete.js +3 -3
  176. package/clis/twitter/list-remove-batch.js +1 -1
  177. package/clis/twitter/list-remove.js +1 -1
  178. package/clis/twitter/list-tweets.js +1 -1
  179. package/clis/twitter/profile.js +1 -1
  180. package/clis/twitter/search.js +1 -1
  181. package/clis/twitter/shared.js +1 -1
  182. package/clis/twitter/tweets.js +1 -1
  183. package/clis/uisdc/news.js +1 -1
  184. package/clis/v2ex/daily.js +2 -2
  185. package/clis/v2ex/me.js +2 -2
  186. package/clis/v2ex/notifications.js +1 -1
  187. package/clis/wikidata/utils.js +1 -1
  188. package/clis/wikipedia/page.js +2 -2
  189. package/clis/wikipedia/summary.js +1 -1
  190. package/clis/wikipedia/utils.js +1 -1
  191. package/clis/wordpress/login.js +1 -1
  192. package/clis/wordpress/publish.js +1 -1
  193. package/clis/wordpress/shared.js +3 -3
  194. package/clis/wordpress/sites.js +1 -0
  195. package/clis/wordpress/whoami.js +1 -1
  196. package/clis/woshipm/stats.js +1 -1
  197. package/clis/xiaoe/content.js +1 -1
  198. package/clis/xiaohongshu/draft-delete.js +1 -1
  199. package/clis/xiaohongshu/draft-open.js +1 -1
  200. package/clis/xiaohongshu/draft-utils.js +2 -2
  201. package/clis/xiaohongshu/publish-video.js +1 -1
  202. package/clis/xiaohongshu/publish.js +1 -1
  203. package/clis/xiaoyuzhou/auth.js +1 -1
  204. package/clis/xiaoyuzhou/transcript.js +1 -1
  205. package/clis/yollomi/background.js +1 -1
  206. package/clis/yollomi/edit.js +1 -1
  207. package/clis/yollomi/generate.js +1 -1
  208. package/clis/yollomi/try-on.js +1 -1
  209. package/clis/youtube/publish.js +1 -1
  210. package/clis/zhihu/answer-comments.js +1 -1
  211. package/clis/zhihu/answer-detail.js +1 -1
  212. package/clis/zhihu/collection.js +2 -2
  213. package/clis/zhihu/collections.js +1 -1
  214. package/clis/zhihu/question.js +1 -1
  215. package/clis/zhihu/search.js +1 -1
  216. package/clis/zhihu/target.js +1 -1
  217. package/clis/zhihu/user-arg.js +1 -1
  218. package/dist/src/adapter-shadow.js +1 -1
  219. package/dist/src/browser/analyze.js +1 -1
  220. package/dist/src/browser/article-extract.d.ts +1 -1
  221. package/dist/src/browser/base-page.js +3 -3
  222. package/dist/src/browser/cdp.js +1 -1
  223. package/dist/src/browser/daemon-client.d.ts +1 -1
  224. package/dist/src/browser/daemon-lifecycle.js +6 -6
  225. package/dist/src/browser/daemon-version.js +1 -1
  226. package/dist/src/browser/dom-snapshot.d.ts +1 -1
  227. package/dist/src/browser/dom-snapshot.js +2 -2
  228. package/dist/src/browser/errors.js +2 -2
  229. package/dist/src/browser/network-cache.js +1 -1
  230. package/dist/src/browser/profile.js +1 -1
  231. package/dist/src/browser/target-resolver.js +4 -4
  232. package/dist/src/browser/verify-fixture.js +1 -1
  233. package/dist/src/browser-session-lock.js +1 -1
  234. package/dist/src/build-manifest.d.ts +1 -1
  235. package/dist/src/build-manifest.js +1 -1
  236. package/dist/src/cli-argv-preprocess.d.ts +4 -4
  237. package/dist/src/cli-argv-preprocess.js +1 -1
  238. package/dist/src/cli.d.ts +1 -1
  239. package/dist/src/cli.js +19 -19
  240. package/dist/src/commanderAdapter.js +1 -1
  241. package/dist/src/commands/auth.js +2 -2
  242. package/dist/src/commands/daemon.d.ts +4 -4
  243. package/dist/src/commands/daemon.js +1 -1
  244. package/dist/src/completion-shared.js +12 -12
  245. package/dist/src/completion.d.ts +2 -2
  246. package/dist/src/constants.d.ts +1 -1
  247. package/dist/src/constants.js +1 -1
  248. package/dist/src/daemon-utils.d.ts +1 -1
  249. package/dist/src/daemon-utils.js +1 -1
  250. package/dist/src/daemon.d.ts +2 -2
  251. package/dist/src/daemon.js +1 -1
  252. package/dist/src/discovery.d.ts +7 -7
  253. package/dist/src/discovery.js +2 -2
  254. package/dist/src/doctor.d.ts +1 -1
  255. package/dist/src/doctor.js +6 -6
  256. package/dist/src/electron-apps.d.ts +1 -1
  257. package/dist/src/electron-apps.js +1 -1
  258. package/dist/src/errors.d.ts +2 -2
  259. package/dist/src/errors.js +1 -1
  260. package/dist/src/execution.js +1 -1
  261. package/dist/src/external.d.ts +1 -1
  262. package/dist/src/external.js +1 -1
  263. package/dist/src/help.d.ts +1 -1
  264. package/dist/src/help.js +3 -3
  265. package/dist/src/hooks.d.ts +1 -1
  266. package/dist/src/launcher.js +2 -2
  267. package/dist/src/logger.d.ts +1 -1
  268. package/dist/src/main.d.ts +1 -1
  269. package/dist/src/main.js +1 -1
  270. package/dist/src/observation/artifact.js +2 -2
  271. package/dist/src/package-paths.js +1 -1
  272. package/dist/src/package-paths.test.d.ts +1 -0
  273. package/dist/src/plugin-manifest.d.ts +12 -3
  274. package/dist/src/plugin-manifest.js +1 -1
  275. package/dist/src/plugin-scaffold.d.ts +2 -2
  276. package/dist/src/plugin-scaffold.js +9 -9
  277. package/dist/src/plugin.d.ts +4 -4
  278. package/dist/src/plugin.js +10 -10
  279. package/dist/src/rate-limit.js +1 -1
  280. package/dist/src/registry-api.d.ts +1 -1
  281. package/dist/src/runtime-detect.d.ts +1 -1
  282. package/dist/src/serialization.js +1 -1
  283. package/dist/src/skills.js +6 -6
  284. package/dist/src/user-runtime.d.ts +8 -0
  285. package/dist/src/user-runtime.js +1 -0
  286. package/dist/src/user-runtime.test.d.ts +1 -0
  287. package/dist/src/validate.js +1 -1
  288. package/dist/src/validate.test.d.ts +1 -1
  289. package/docs/guide/browser-bridge.md +23 -23
  290. package/docs/guide/electron-app-cli.md +3 -3
  291. package/docs/guide/exit-codes.md +3 -3
  292. package/docs/guide/extending-opencli.md +37 -37
  293. package/docs/guide/getting-started.md +19 -19
  294. package/docs/guide/installation.md +6 -6
  295. package/docs/guide/plugins.md +32 -32
  296. package/docs/guide/remote-orchestration.md +5 -5
  297. package/docs/guide/troubleshooting.md +9 -9
  298. package/package.json +3 -2
  299. package/scripts/fetch-adapters.js +6 -6
  300. package/scripts/postinstall.js +14 -14
  301. package/skills/opencli-adapter-author/SKILL.md +24 -24
  302. package/skills/opencli-adapter-author/references/adapter-template.md +13 -13
  303. package/skills/opencli-adapter-author/references/api-discovery.md +25 -25
  304. package/skills/opencli-adapter-author/references/coverage-matrix.md +3 -3
  305. package/skills/opencli-adapter-author/references/field-decode-playbook.md +10 -10
  306. package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +2 -2
  307. package/skills/opencli-adapter-author/references/site-memory.md +11 -11
  308. package/skills/opencli-adapter-author/references/site-recon.md +7 -7
  309. package/skills/opencli-adapter-author/references/strategy-selection.md +3 -3
  310. package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +4 -4
  311. package/skills/opencli-adapter-author/references/typed-errors.md +2 -2
  312. package/skills/opencli-autofix/SKILL.md +29 -29
  313. package/skills/opencli-browser/SKILL.md +55 -55
  314. package/skills/opencli-browser-sitemap/SKILL.md +7 -7
  315. package/skills/opencli-sitemap-author/SKILL.md +11 -11
  316. package/skills/opencli-sitemap-author/references/sitemap-schema.md +19 -19
  317. package/skills/opencli-usage/SKILL.md +43 -43
@@ -1 +1 @@
1
- import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{ArgumentError as u,CommandExecutionError as a}from"@jackwener/opencli/errors";import{pixivFetch as d}from"./utils.js";function m(i,r){if(!i||Array.isArray(i)||typeof i!=="object")throw new a(`Pixiv illustration ${r} returned malformed detail payload`);const e=String(i.illustId??"").trim(),l=String(i.illustTitle??"").trim(),t=String(i.userName??"").trim(),s=String(i.userId??"").trim();if(!/^\d+$/.test(e)||e!==r||!l||!t||!/^\d+$/.test(s))throw new a(`Pixiv illustration ${r} returned malformed detail payload`);return{...i,illustId:e,illustTitle:l,userName:t,userId:s}}n({site:"pixiv",name:"detail",access:"read",description:"View illustration details (tags, stats, URLs)",domain:"www.pixiv.net",strategy:o.COOKIE,args:[{name:"id",required:!0,positional:!0,help:"Illustration ID"}],columns:["illust_id","title","author","type","pages","bookmarks","likes","views","tags","created","url"],func:async(i,r)=>{const e=String(r.id??"");if(!/^\d+$/.test(e))throw new u(`Invalid illustration ID: ${e}`,"Example: opencli pixiv detail 123456");const l=await d(i,`/ajax/illust/${e}`,{notFoundMsg:`Illustration not found: ${e}`}),t=m(l,e);return[{illust_id:t.illustId,title:t.illustTitle,author:t.userName,user_id:t.userId,type:t.illustType===0?"illust":t.illustType===1?"manga":t.illustType===2?"ugoira":String(t.illustType),pages:t.pageCount,bookmarks:t.bookmarkCount,likes:t.likeCount,views:t.viewCount,tags:(t.tags?.tags||[]).map((s)=>s.tag).join(", "),created:t.createDate?.split("T")[0]||"",url:`https://www.pixiv.net/artworks/${t.illustId}`}]}});
1
+ import{cli as u,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as o,CommandExecutionError as a}from"@jackwener/opencli/errors";import{pixivFetch as d}from"./utils.js";function m(i,r){if(!i||Array.isArray(i)||typeof i!=="object")throw new a(`Pixiv illustration ${r} returned malformed detail payload`);const e=String(i.illustId??"").trim(),l=String(i.illustTitle??"").trim(),t=String(i.userName??"").trim(),s=String(i.userId??"").trim();if(!/^\d+$/.test(e)||e!==r||!l||!t||!/^\d+$/.test(s))throw new a(`Pixiv illustration ${r} returned malformed detail payload`);return{...i,illustId:e,illustTitle:l,userName:t,userId:s}}u({site:"pixiv",name:"detail",access:"read",description:"View illustration details (tags, stats, URLs)",domain:"www.pixiv.net",strategy:n.COOKIE,args:[{name:"id",required:!0,positional:!0,help:"Illustration ID"}],columns:["illust_id","title","author","type","pages","bookmarks","likes","views","tags","created","url"],func:async(i,r)=>{const e=String(r.id??"");if(!/^\d+$/.test(e))throw new o(`Invalid illustration ID: ${e}`,"Example: ppcli pixiv detail 123456");const l=await d(i,`/ajax/illust/${e}`,{notFoundMsg:`Illustration not found: ${e}`}),t=m(l,e);return[{illust_id:t.illustId,title:t.illustTitle,author:t.userName,user_id:t.userId,type:t.illustType===0?"illust":t.illustType===1?"manga":t.illustType===2?"ugoira":String(t.illustType),pages:t.pageCount,bookmarks:t.bookmarkCount,likes:t.likeCount,views:t.viewCount,tags:(t.tags?.tags||[]).map((s)=>s.tag).join(", "),created:t.createDate?.split("T")[0]||"",url:`https://www.pixiv.net/artworks/${t.illustId}`}]}});
@@ -1 +1 @@
1
- import{cli as s,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as l,CommandExecutionError as n}from"@jackwener/opencli/errors";import{pixivFetch as m}from"./utils.js";function u(i,t){if(!i||Array.isArray(i)||typeof i!=="object")throw new n(`Pixiv user ${t} returned malformed profile payload`);const r=String(i.name??"").trim();if(!r)throw new n(`Pixiv user ${t} returned malformed profile payload`);return{...i,name:r}}s({site:"pixiv",name:"user",access:"read",description:"View Pixiv artist profile",domain:"www.pixiv.net",strategy:a.COOKIE,args:[{name:"uid",required:!0,positional:!0,help:"Pixiv user ID"}],columns:["user_id","name","premium","following","illusts","manga","novels","comment","url"],func:async(i,t)=>{const r=String(t.uid??"");if(!/^\d+$/.test(r))throw new l(`Invalid user ID: ${r}`,"Example: opencli pixiv user 123456");const o=await m(i,`/ajax/user/${r}`,{params:{full:1},notFoundMsg:`User not found: ${r}`}),e=u(o,r);return[{user_id:r,name:e.name,premium:e.premium?"Yes":"No",following:e.following,illusts:typeof e.illusts==="object"?Object.keys(e.illusts).length:e.illusts||0,manga:typeof e.manga==="object"?Object.keys(e.manga).length:e.manga||0,novels:typeof e.novels==="object"?Object.keys(e.novels).length:e.novels||0,comment:(e.comment||"").slice(0,80),url:`https://www.pixiv.net/users/${r}`}]}});
1
+ import{cli as s,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as l,CommandExecutionError as n}from"@jackwener/opencli/errors";import{pixivFetch as m}from"./utils.js";function u(i,t){if(!i||Array.isArray(i)||typeof i!=="object")throw new n(`Pixiv user ${t} returned malformed profile payload`);const r=String(i.name??"").trim();if(!r)throw new n(`Pixiv user ${t} returned malformed profile payload`);return{...i,name:r}}s({site:"pixiv",name:"user",access:"read",description:"View Pixiv artist profile",domain:"www.pixiv.net",strategy:a.COOKIE,args:[{name:"uid",required:!0,positional:!0,help:"Pixiv user ID"}],columns:["user_id","name","premium","following","illusts","manga","novels","comment","url"],func:async(i,t)=>{const r=String(t.uid??"");if(!/^\d+$/.test(r))throw new l(`Invalid user ID: ${r}`,"Example: ppcli pixiv user 123456");const o=await m(i,`/ajax/user/${r}`,{params:{full:1},notFoundMsg:`User not found: ${r}`}),e=u(o,r);return[{user_id:r,name:e.name,premium:e.premium?"Yes":"No",following:e.following,illusts:typeof e.illusts==="object"?Object.keys(e.illusts).length:e.illusts||0,manga:typeof e.manga==="object"?Object.keys(e.manga).length:e.manga||0,novels:typeof e.novels==="object"?Object.keys(e.novels).length:e.novels||0,comment:(e.comment||"").slice(0,80),url:`https://www.pixiv.net/users/${r}`}]}});
@@ -1 +1 @@
1
- export const PRODUCTHUNT_CATEGORY_SLUGS=["ai-agents","ai-coding-agents","ai-code-editors","ai-chatbots","ai-workflow-automation","vibe-coding","developer-tools","productivity","design-creative","marketing-sales","no-code-platforms","llms","finance","social-community","engineering-development"];const K="Mozilla/5.0 (compatible; opencli/1.0)";export async function fetchFeed(w){const B=w?`https://www.producthunt.com/feed?category=${encodeURIComponent(w)}`:"https://www.producthunt.com/feed",f=await fetch(B,{headers:{"User-Agent":K}});if(!f.ok)return[];const j=await f.text();return parseFeed(j)}export function parseFeed(w){const B=[],f=/<entry>([\s\S]*?)<\/entry>/g;let j,C=1;while(j=f.exec(w)){const q=j[1],z=q.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim()??"",E=q.match(/<name>([\s\S]*?)<\/name>/)?.[1]?.trim()??"",G=(q.match(/<published>(.*?)<\/published>/)?.[1]?.trim()??"").slice(0,10),H=q.match(/<link[^>]*href="([^"]+)"/)?.[1]?.trim()??"",J=(q.match(/<content[^>]*>([\s\S]*?)<\/content>/)?.[1]??"").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,"&").replace(/&quot;/g,'"').replace(/<[^>]+>/g," ").replace(/\s+/g," ").replace(/\s*Discussion\s*\|?\s*/gi,"").replace(/\s*\|?\s*Link\s*$/gi,"").trim().slice(0,120);if(z)B.push({rank:C++,name:z,tagline:J,author:E,date:G,url:H})}return B}export function pickVoteCount(w){return w.map((f)=>{const j=String(f.text??"").trim();if(!/^\d+$/.test(j))return null;if(f.inReviewLink)return null;const C=parseInt(j,10);if(!Number.isFinite(C)||C<=0)return null;const q=`${f.tagName??""} ${f.className??""} ${f.role??""}`.toLowerCase();let z=0;if(f.inButton)z+=4;if(q.includes("vote")||q.includes("upvote"))z+=3;if(q.includes("button"))z+=1;return{text:j,score:z,value:C}}).filter((f)=>Boolean(f)).sort((f,j)=>{if(j.score!==f.score)return j.score-f.score;if(j.value!==f.value)return j.value-f.value;return f.text.localeCompare(j.text)})[0]?.text??""}export function toDate(w){return w.slice(0,10)}
1
+ export const PRODUCTHUNT_CATEGORY_SLUGS=["ai-agents","ai-coding-agents","ai-code-editors","ai-chatbots","ai-workflow-automation","vibe-coding","developer-tools","productivity","design-creative","marketing-sales","no-code-platforms","llms","finance","social-community","engineering-development"];const K="Mozilla/5.0 (compatible; ppcli/1.0)";export async function fetchFeed(w){const B=w?`https://www.producthunt.com/feed?category=${encodeURIComponent(w)}`:"https://www.producthunt.com/feed",f=await fetch(B,{headers:{"User-Agent":K}});if(!f.ok)return[];const j=await f.text();return parseFeed(j)}export function parseFeed(w){const B=[],f=/<entry>([\s\S]*?)<\/entry>/g;let j,C=1;while(j=f.exec(w)){const q=j[1],z=q.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim()??"",E=q.match(/<name>([\s\S]*?)<\/name>/)?.[1]?.trim()??"",G=(q.match(/<published>(.*?)<\/published>/)?.[1]?.trim()??"").slice(0,10),H=q.match(/<link[^>]*href="([^"]+)"/)?.[1]?.trim()??"",J=(q.match(/<content[^>]*>([\s\S]*?)<\/content>/)?.[1]??"").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,"&").replace(/&quot;/g,'"').replace(/<[^>]+>/g," ").replace(/\s+/g," ").replace(/\s*Discussion\s*\|?\s*/gi,"").replace(/\s*\|?\s*Link\s*$/gi,"").trim().slice(0,120);if(z)B.push({rank:C++,name:z,tagline:J,author:E,date:G,url:H})}return B}export function pickVoteCount(w){return w.map((f)=>{const j=String(f.text??"").trim();if(!/^\d+$/.test(j))return null;if(f.inReviewLink)return null;const C=parseInt(j,10);if(!Number.isFinite(C)||C<=0)return null;const q=`${f.tagName??""} ${f.className??""} ${f.role??""}`.toLowerCase();let z=0;if(f.inButton)z+=4;if(q.includes("vote")||q.includes("upvote"))z+=3;if(q.includes("button"))z+=1;return{text:j,score:z,value:C}}).filter((f)=>Boolean(f)).sort((f,j)=>{if(j.score!==f.score)return j.score-f.score;if(j.value!==f.value)return j.value-f.value;return f.text.localeCompare(j.text)})[0]?.text??""}export function toDate(w){return w.slice(0,10)}
@@ -1 +1 @@
1
- import{ArgumentError as J,CommandExecutionError as F,EmptyResultError as L}from"@jackwener/opencli/errors";export const PYPI_BASE="https://pypi.org",PYPISTATS_BASE="https://pypistats.org";const O="opencli-pypi-adapter (+https://github.com/jackwener/opencli)",Q=/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;export function requirePackageName(B){const f=String(B??"").trim();if(!f)throw new J('pypi package name is required (e.g. "requests", "pandas")');if(!Q.test(f))throw new J(`pypi package name "${B}" is not a valid distribution name`,'PyPI accepts ASCII letters / digits / "._-" with no leading or trailing separator.');return f}export async function pypiFetch(B,f){let q;try{q=await fetch(B,{headers:{"user-agent":O,accept:"application/json"}})}catch(D){throw new F(`${f} request failed: ${D?.message??D}`,"Check that pypi.org / pypistats.org are reachable from this network.")}if(q.status===404)throw new L(f,`PyPI returned 404 for ${B}.`);if(q.status===429)throw new F(`${f} returned HTTP 429 (rate limited)`,"PyPI throttles unauthenticated bursts; wait a few seconds and retry.");if(!q.ok)throw new F(`${f} returned HTTP ${q.status}`);let H;try{H=await q.json()}catch(D){throw new F(`${f} returned malformed JSON: ${D?.message??D}`)}return H}
1
+ import{ArgumentError as J,CommandExecutionError as F,EmptyResultError as L}from"@jackwener/opencli/errors";export const PYPI_BASE="https://pypi.org",PYPISTATS_BASE="https://pypistats.org";const O="opencli-pypi-adapter (+https://github.com/jackwener/OpenCLI)",Q=/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;export function requirePackageName(B){const f=String(B??"").trim();if(!f)throw new J('pypi package name is required (e.g. "requests", "pandas")');if(!Q.test(f))throw new J(`pypi package name "${B}" is not a valid distribution name`,'PyPI accepts ASCII letters / digits / "._-" with no leading or trailing separator.');return f}export async function pypiFetch(B,f){let q;try{q=await fetch(B,{headers:{"user-agent":O,accept:"application/json"}})}catch(D){throw new F(`${f} request failed: ${D?.message??D}`,"Check that pypi.org / pypistats.org are reachable from this network.")}if(q.status===404)throw new L(f,`PyPI returned 404 for ${B}.`);if(q.status===429)throw new F(`${f} returned HTTP 429 (rate limited)`,"PyPI throttles unauthenticated bursts; wait a few seconds and retry.");if(!q.ok)throw new F(`${f} returned HTTP ${q.status}`);let H;try{H=await q.json()}catch(D){throw new F(`${f} returned malformed JSON: ${D?.message??D}`)}return H}
package/clis/qiita/gql.js CHANGED
@@ -14,7 +14,7 @@ import{AuthRequiredError as K,CommandExecutionError as H}from"@jackwener/opencli
14
14
  if (!r.ok) return { kind: 'http', status: r.status, detail: text.slice(0, 300) };
15
15
  return { kind: 'ok', body: data };
16
16
  } catch (e) { return { kind: 'exception', detail: String(e && e.message || e) }; }
17
- })()`);if(f?.kind==="auth")throw new K("qiita.com",`Not logged in (${f.detail}). Run \`opencli qiita login\`.`);if(f?.kind==="http")throw new H(`Qiita GraphQL failed: HTTP ${f.status} ${f.detail}`);if(f?.kind==="exception")throw new H(`Qiita GraphQL error: ${f.detail}`);if(f?.kind!=="ok"||!f.body)throw new H(`Unexpected Qiita response: ${JSON.stringify(f)}`);const J=f.body;if(Array.isArray(J.errors)&&J.errors.length){const L=J.errors.map((M)=>M.message).join("; ");if(/login required|unauthor|not authenticated/i.test(L))throw new K("qiita.com",`Qiita auth error: ${L}`);throw new H(`Qiita GraphQL error: ${L}`)}return J.data||{}}export async function qiitaDeleteByForm(F,z){const B=await F.evaluate(`(async () => {
17
+ })()`);if(f?.kind==="auth")throw new K("qiita.com",`Not logged in (${f.detail}). Run \`ppcli qiita login\`.`);if(f?.kind==="http")throw new H(`Qiita GraphQL failed: HTTP ${f.status} ${f.detail}`);if(f?.kind==="exception")throw new H(`Qiita GraphQL error: ${f.detail}`);if(f?.kind!=="ok"||!f.body)throw new H(`Unexpected Qiita response: ${JSON.stringify(f)}`);const J=f.body;if(Array.isArray(J.errors)&&J.errors.length){const L=J.errors.map((M)=>M.message).join("; ");if(/login required|unauthor|not authenticated/i.test(L))throw new K("qiita.com",`Qiita auth error: ${L}`);throw new H(`Qiita GraphQL error: ${L}`)}return J.data||{}}export async function qiitaDeleteByForm(F,z){const B=await F.evaluate(`(async () => {
18
18
  try {
19
19
  var meta = document.querySelector('meta[name="csrf-token"]');
20
20
  var token = meta && meta.content;
@@ -29,4 +29,4 @@ import{AuthRequiredError as K,CommandExecutionError as H}from"@jackwener/opencli
29
29
  var ok = r.type === 'opaqueredirect' || r.status === 0 || (r.status >= 200 && r.status < 400);
30
30
  return { kind: 'done', ok: ok, status: r.status };
31
31
  } catch (e) { return { kind: 'exception', detail: String(e && e.message || e) }; }
32
- })()`);if(B?.kind==="auth")throw new K("qiita.com","Not logged in. Run `opencli qiita login`.");if(B?.kind==="exception")throw new H(`Qiita delete error: ${B.detail}`);return{ok:!!B?.ok,status:B?.status??-1}}export async function qiitaViewer(F){const z=await qiitaGql(F,"{ viewer { urlName name originalId } }");if(!z.viewer||!z.viewer.urlName)throw new K("qiita.com","Qiita viewer empty — not logged in");return z.viewer}
32
+ })()`);if(B?.kind==="auth")throw new K("qiita.com","Not logged in. Run `ppcli qiita login`.");if(B?.kind==="exception")throw new H(`Qiita delete error: ${B.detail}`);return{ok:!!B?.ok,status:B?.status??-1}}export async function qiitaViewer(F){const z=await qiitaGql(F,"{ viewer { urlName name originalId } }");if(!z.viewer||!z.viewer.urlName)throw new K("qiita.com","Qiita viewer empty — not logged in");return z.viewer}
@@ -1 +1 @@
1
- import{ArgumentError as H,CommandExecutionError as K,EmptyResultError as M}from"@jackwener/opencli/errors";export const REST_COUNTRIES_BASE="https://restcountries.com/v3.1";const Q="opencli-rest-countries-adapter/1.0 (+https://github.com/jackwener/opencli; mailto:opencli@example.com)";export const REST_COUNTRIES_REGIONS=new Set(["africa","americas","asia","europe","oceania","antarctic"]),COUNTRY_FIELDS=["name","cca2","cca3","ccn3","capital","region","subregion","population","area","languages","currencies","flag","latlng","timezones","independent","unMember","landlocked"].join(",");export function requireString(q,z){const B=String(q??"").trim();if(!B)throw new H(`rest-countries ${z} cannot be empty`);return B}export function requireBoundedInt(q,z,B,D="limit"){const F=q??z,J=typeof F==="number"?F:Number(F);if(!Number.isInteger(J)||J<=0)throw new H(`rest-countries ${D} must be a positive integer`);if(J>B)throw new H(`rest-countries ${D} must be <= ${B}`);return J}export function requireRegion(q){const z=String(q??"").trim().toLowerCase();if(!z)throw new H('rest-countries region is required (e.g. "europe", "asia")');if(!REST_COUNTRIES_REGIONS.has(z))throw new H(`rest-countries region "${q}" is not recognised`,`Allowed regions: ${[...REST_COUNTRIES_REGIONS].join(", ")}.`);return z}export async function restCountriesFetch(q,z){let B;try{B=await fetch(q,{headers:{"user-agent":Q,accept:"application/json"}})}catch(F){throw new K(`${z} request failed: ${F?.message??F}`,"Check that restcountries.com is reachable from this network.")}if(B.status===404)throw new M(z,`REST Countries returned 404 for ${q}.`);if(B.status===429)throw new K(`${z} returned HTTP 429 (rate limited)`);if(!B.ok)throw new K(`${z} returned HTTP ${B.status}`);let D;try{D=await B.json()}catch(F){throw new K(`${z} returned malformed JSON: ${F?.message??F}`)}return D}export function joinCurrencies(q){if(!q||typeof q!=="object")return"";return Object.entries(q).map(([z,B])=>{const D=B&&typeof B.name==="string"?B.name:"";return D?`${z} (${D})`:z}).join(", ")}export function joinLanguages(q){if(!q||typeof q!=="object")return"";return Object.values(q).filter((z)=>typeof z==="string"&&z.trim()).join(", ")}export function projectCountry(q){const z=q?.name?.common??null,B=q?.name?.official??null,D=typeof q?.cca3==="string"?q.cca3:null;return{commonName:typeof z==="string"?z:null,officialName:typeof B==="string"?B:null,cca2:typeof q?.cca2==="string"?q.cca2:null,cca3:D,ccn3:typeof q?.ccn3==="string"?q.ccn3:null,capital:Array.isArray(q?.capital)?q.capital.join(", "):null,region:typeof q?.region==="string"?q.region:null,subregion:typeof q?.subregion==="string"?q.subregion:null,population:typeof q?.population==="number"?q.population:null,area:typeof q?.area==="number"?q.area:null,languages:joinLanguages(q?.languages),currencies:joinCurrencies(q?.currencies),latitude:Array.isArray(q?.latlng)&&typeof q.latlng[0]==="number"?q.latlng[0]:null,longitude:Array.isArray(q?.latlng)&&typeof q.latlng[1]==="number"?q.latlng[1]:null,timezones:Array.isArray(q?.timezones)?q.timezones.join(", "):null,independent:typeof q?.independent==="boolean"?q.independent:null,unMember:typeof q?.unMember==="boolean"?q.unMember:null,landlocked:typeof q?.landlocked==="boolean"?q.landlocked:null,flag:typeof q?.flag==="string"?q.flag:null,url:D?`https://restcountries.com/v3.1/alpha/${D.toLowerCase()}`:""}}
1
+ import{ArgumentError as H,CommandExecutionError as K,EmptyResultError as M}from"@jackwener/opencli/errors";export const REST_COUNTRIES_BASE="https://restcountries.com/v3.1";const Q="opencli-rest-countries-adapter/1.0 (+https://github.com/jackwener/OpenCLI; mailto:ppcli@example.com)";export const REST_COUNTRIES_REGIONS=new Set(["africa","americas","asia","europe","oceania","antarctic"]),COUNTRY_FIELDS=["name","cca2","cca3","ccn3","capital","region","subregion","population","area","languages","currencies","flag","latlng","timezones","independent","unMember","landlocked"].join(",");export function requireString(q,z){const B=String(q??"").trim();if(!B)throw new H(`rest-countries ${z} cannot be empty`);return B}export function requireBoundedInt(q,z,B,D="limit"){const F=q??z,J=typeof F==="number"?F:Number(F);if(!Number.isInteger(J)||J<=0)throw new H(`rest-countries ${D} must be a positive integer`);if(J>B)throw new H(`rest-countries ${D} must be <= ${B}`);return J}export function requireRegion(q){const z=String(q??"").trim().toLowerCase();if(!z)throw new H('rest-countries region is required (e.g. "europe", "asia")');if(!REST_COUNTRIES_REGIONS.has(z))throw new H(`rest-countries region "${q}" is not recognised`,`Allowed regions: ${[...REST_COUNTRIES_REGIONS].join(", ")}.`);return z}export async function restCountriesFetch(q,z){let B;try{B=await fetch(q,{headers:{"user-agent":Q,accept:"application/json"}})}catch(F){throw new K(`${z} request failed: ${F?.message??F}`,"Check that restcountries.com is reachable from this network.")}if(B.status===404)throw new M(z,`REST Countries returned 404 for ${q}.`);if(B.status===429)throw new K(`${z} returned HTTP 429 (rate limited)`);if(!B.ok)throw new K(`${z} returned HTTP ${B.status}`);let D;try{D=await B.json()}catch(F){throw new K(`${z} returned malformed JSON: ${F?.message??F}`)}return D}export function joinCurrencies(q){if(!q||typeof q!=="object")return"";return Object.entries(q).map(([z,B])=>{const D=B&&typeof B.name==="string"?B.name:"";return D?`${z} (${D})`:z}).join(", ")}export function joinLanguages(q){if(!q||typeof q!=="object")return"";return Object.values(q).filter((z)=>typeof z==="string"&&z.trim()).join(", ")}export function projectCountry(q){const z=q?.name?.common??null,B=q?.name?.official??null,D=typeof q?.cca3==="string"?q.cca3:null;return{commonName:typeof z==="string"?z:null,officialName:typeof B==="string"?B:null,cca2:typeof q?.cca2==="string"?q.cca2:null,cca3:D,ccn3:typeof q?.ccn3==="string"?q.ccn3:null,capital:Array.isArray(q?.capital)?q.capital.join(", "):null,region:typeof q?.region==="string"?q.region:null,subregion:typeof q?.subregion==="string"?q.subregion:null,population:typeof q?.population==="number"?q.population:null,area:typeof q?.area==="number"?q.area:null,languages:joinLanguages(q?.languages),currencies:joinCurrencies(q?.currencies),latitude:Array.isArray(q?.latlng)&&typeof q.latlng[0]==="number"?q.latlng[0]:null,longitude:Array.isArray(q?.latlng)&&typeof q.latlng[1]==="number"?q.latlng[1]:null,timezones:Array.isArray(q?.timezones)?q.timezones.join(", "):null,independent:typeof q?.independent==="boolean"?q.independent:null,unMember:typeof q?.unMember==="boolean"?q.unMember:null,landlocked:typeof q?.landlocked==="boolean"?q.landlocked:null,flag:typeof q?.flag==="string"?q.flag:null,url:D?`https://restcountries.com/v3.1/alpha/${D.toLowerCase()}`:""}}
package/clis/rfc/utils.js CHANGED
@@ -1 +1 @@
1
- import{ArgumentError as I,CommandExecutionError as H,EmptyResultError as J}from"@jackwener/opencli/errors";export const RFC_BASE="https://datatracker.ietf.org";const K="opencli-rfc-adapter (+https://github.com/jackwener/opencli)";export function requireRfcNumber(F){const q=F;if(q==null||String(q).trim()==="")throw new I("rfc number is required (e.g. 9000, 791, 2616)",'Pass the integer RFC number; do not include the "rfc" prefix.');const z=String(q).trim().toLowerCase().replace(/^rfc/,""),B=Number.parseInt(z,10);if(!Number.isInteger(B)||B<=0||String(B)!==z)throw new I(`rfc number "${F}" is not a valid RFC number`,"Pass a positive integer (e.g. 9000, 791, 2616).");if(B>999999)throw new I("rfc number must be <= 999999");return B}export async function rfcFetch(F,q){let z;try{z=await fetch(F,{headers:{"user-agent":K,accept:"application/json"}})}catch(G){throw new H(`${q} request failed: ${G?.message??G}`,"Check that datatracker.ietf.org is reachable from this network.")}if(z.status===404)throw new J(q,`IETF datatracker returned 404 for ${F}.`);if(z.status===429)throw new H(`${q} returned HTTP 429 (rate limited)`);if(!z.ok)throw new H(`${q} returned HTTP ${z.status}`);let B;try{B=await z.json()}catch(G){throw new H(`${q} returned malformed JSON: ${G?.message??G}`)}return B}export function trimDate(F){const q=String(F??"").trim();if(!q)return null;if(/^\d{4}-\d{2}-\d{2}/.test(q))return q.slice(0,10);return null}
1
+ import{ArgumentError as I,CommandExecutionError as H,EmptyResultError as J}from"@jackwener/opencli/errors";export const RFC_BASE="https://datatracker.ietf.org";const K="opencli-rfc-adapter (+https://github.com/jackwener/OpenCLI)";export function requireRfcNumber(F){const q=F;if(q==null||String(q).trim()==="")throw new I("rfc number is required (e.g. 9000, 791, 2616)",'Pass the integer RFC number; do not include the "rfc" prefix.');const z=String(q).trim().toLowerCase().replace(/^rfc/,""),B=Number.parseInt(z,10);if(!Number.isInteger(B)||B<=0||String(B)!==z)throw new I(`rfc number "${F}" is not a valid RFC number`,"Pass a positive integer (e.g. 9000, 791, 2616).");if(B>999999)throw new I("rfc number must be <= 999999");return B}export async function rfcFetch(F,q){let z;try{z=await fetch(F,{headers:{"user-agent":K,accept:"application/json"}})}catch(G){throw new H(`${q} request failed: ${G?.message??G}`,"Check that datatracker.ietf.org is reachable from this network.")}if(z.status===404)throw new J(q,`IETF datatracker returned 404 for ${F}.`);if(z.status===429)throw new H(`${q} returned HTTP 429 (rate limited)`);if(!z.ok)throw new H(`${q} returned HTTP ${z.status}`);let B;try{B=await z.json()}catch(G){throw new H(`${q} returned malformed JSON: ${G?.message??G}`)}return B}export function trimDate(F){const q=String(F??"").trim();if(!q)return null;if(/^\d{4}-\d{2}-\d{2}/.test(q))return q.slice(0,10);return null}
@@ -1 +1 @@
1
- import{ArgumentError as I,CommandExecutionError as K,EmptyResultError as L}from"@jackwener/opencli/errors";export const GEMS_BASE="https://rubygems.org/api/v1";const O="opencli-rubygems-adapter (+https://github.com/jackwener/opencli)",P=/^[A-Za-z0-9][A-Za-z0-9._-]*$/;export function requireString(B,q){const z=String(B??"").trim();if(!z)throw new I(`rubygems ${q} cannot be empty`);return z}export function requireBoundedInt(B,q,z,H="limit"){const D=B??q,J=typeof D==="number"?D:Number(D);if(!Number.isInteger(J)||J<=0)throw new I(`rubygems ${H} must be a positive integer`);if(J>z)throw new I(`rubygems ${H} must be <= ${z}`);return J}export function requireGemName(B){const q=String(B??"").trim();if(!q)throw new I('rubygems gem name is required (e.g. "rails", "sidekiq")');if(q.length>100||!P.test(q))throw new I(`rubygems gem "${B}" is not a valid gem name`,'Use letters / digits / "._-", starting with a letter or digit (max 100 chars).');return q}export async function gemsFetch(B,q){let z;try{z=await fetch(B,{headers:{"user-agent":O,accept:"application/json"}})}catch(D){throw new K(`${q} request failed: ${D?.message??D}`,"Check that rubygems.org is reachable from this network.")}if(z.status===404)throw new L(q,`RubyGems returned 404 for ${B}.`);if(z.status===429)throw new K(`${q} returned HTTP 429 (rate limited)`,"RubyGems throttles bursts; wait a few seconds and retry.");if(!z.ok)throw new K(`${q} returned HTTP ${z.status}`);let H;try{H=await z.json()}catch(D){throw new K(`${q} returned malformed JSON: ${D?.message??D}`)}return H}export function trimDate(B){const q=String(B??"").trim();if(!q)return null;const z=q.replace(/\.\d+/,"");return z.endsWith("Z")?z:`${z}Z`}
1
+ import{ArgumentError as I,CommandExecutionError as K,EmptyResultError as L}from"@jackwener/opencli/errors";export const GEMS_BASE="https://rubygems.org/api/v1";const O="opencli-rubygems-adapter (+https://github.com/jackwener/OpenCLI)",P=/^[A-Za-z0-9][A-Za-z0-9._-]*$/;export function requireString(B,q){const z=String(B??"").trim();if(!z)throw new I(`rubygems ${q} cannot be empty`);return z}export function requireBoundedInt(B,q,z,H="limit"){const D=B??q,J=typeof D==="number"?D:Number(D);if(!Number.isInteger(J)||J<=0)throw new I(`rubygems ${H} must be a positive integer`);if(J>z)throw new I(`rubygems ${H} must be <= ${z}`);return J}export function requireGemName(B){const q=String(B??"").trim();if(!q)throw new I('rubygems gem name is required (e.g. "rails", "sidekiq")');if(q.length>100||!P.test(q))throw new I(`rubygems gem "${B}" is not a valid gem name`,'Use letters / digits / "._-", starting with a letter or digit (max 100 chars).');return q}export async function gemsFetch(B,q){let z;try{z=await fetch(B,{headers:{"user-agent":O,accept:"application/json"}})}catch(D){throw new K(`${q} request failed: ${D?.message??D}`,"Check that rubygems.org is reachable from this network.")}if(z.status===404)throw new L(q,`RubyGems returned 404 for ${B}.`);if(z.status===429)throw new K(`${q} returned HTTP 429 (rate limited)`,"RubyGems throttles bursts; wait a few seconds and retry.");if(!z.ok)throw new K(`${q} returned HTTP ${z.status}`);let H;try{H=await z.json()}catch(D){throw new K(`${q} returned malformed JSON: ${D?.message??D}`)}return H}export function trimDate(B){const q=String(B??"").trim();if(!q)return null;const z=q.replace(/\.\d+/,"");return z.endsWith("Z")?z:`${z}Z`}
@@ -1 +1 @@
1
- import{ArgumentError as j,CommandExecutionError as T,EmptyResultError as G}from"@jackwener/opencli/errors";export const S2_GRAPH_BASE="https://api.semanticscholar.org/graph/v1",S2_REC_BASE="https://api.semanticscholar.org/recommendations/v1";const L="opencli-semanticscholar-adapter (+https://github.com/jackwener/opencli)",M=/^[0-9a-f]{40}$/i,F=/^10\.\S+$/,Y=/^\d{4}\.\d{4,5}(?:v\d+)?$/,S=/^[a-z-]+\/\d{7}(?:v\d+)?$/i,B=/^(ARXIV|MAG|ACL|PMID|PMCID|URL|CorpusId|DBLP):/i;export function requireString(q,z){const H=String(q??"").trim();if(!H)throw new j(`semanticscholar ${z} cannot be empty`);return H}export function requireBoundedInt(q,z,H,Q="limit"){const J=q??z,W=typeof J==="number"?J:Number(J);if(!Number.isInteger(W)||W<=0)throw new j(`semanticscholar ${Q} must be a positive integer`);if(W>H)throw new j(`semanticscholar ${Q} must be <= ${H}`);return W}export function requirePaperRef(q){const z=String(q??"").trim();if(!z)throw new j("semanticscholar paper id cannot be empty","Example: opencli semanticscholar paper 10.18653/v1/N19-1423");const H=z.match(/^https?:\/\/(?:www\.)?semanticscholar\.org\/paper\/(?:[^/]+\/)?([0-9a-f]{40})/i);if(H)return H[1].toLowerCase();if(M.test(z))return z.toLowerCase();if(B.test(z))return z;if(/^doi:/i.test(z)){const J=z.replace(/^doi:/i,"").trim();if(F.test(J))return`DOI:${J}`}const Q=z.match(/^https?:\/\/(?:dx\.)?doi\.org\/(.+)$/i);if(Q&&F.test(Q[1]))return`DOI:${Q[1]}`;if(F.test(z))return`DOI:${z}`;if(Y.test(z)||S.test(z))return`ARXIV:${z}`;throw new j(`semanticscholar paper id "${q}" is not recognised`,'Use a Semantic Scholar paperId, a DOI, an arXiv id, or a prefixed id (e.g. "ARXIV:1706.03762", "PMID:12345").')}export async function s2Fetch(q,z){const H={"user-agent":L,accept:"application/json"},Q=process.env.SEMANTIC_SCHOLAR_API_KEY;if(Q)H["x-api-key"]=Q;let J,W=0;while(!0){try{J=await fetch(q,{headers:H})}catch(Z){throw new T(`${z} request failed: ${Z?.message??Z}`,"Check that api.semanticscholar.org is reachable from this network.")}if(J.status===429&&W===0&&!Q){W+=1;await new Promise((Z)=>setTimeout(Z,1500));continue}break}if(J.status===404)throw new G(z,`Semantic Scholar returned 404 for ${q}.`);if(J.status===429)throw new T(`${z} returned HTTP 429 (rate limited)`,"Semantic Scholar throttles anonymous traffic; set SEMANTIC_SCHOLAR_API_KEY (free at https://www.semanticscholar.org/product/api) or wait a minute and retry.");if(!J.ok)throw new T(`${z} returned HTTP ${J.status}`);let $;try{$=await J.json()}catch(Z){throw new T(`${z} returned malformed JSON: ${Z?.message??Z}`)}if($&&typeof $==="object"&&$.error)throw new T(`${z} returned an error: ${$.error}`);return $}export function tldrText(q){if(q&&typeof q==="object"&&typeof q.text==="string")return q.text.trim();return""}export function firstAuthorName(q){if(!Array.isArray(q)||!q.length)return"";const z=q[0];if(z&&typeof z==="object"&&typeof z.name==="string")return z.name.trim();return""}export function optionalNumber(q,z){if(q==null)return null;if(typeof q!=="number"||!Number.isFinite(q))throw new T(`semanticscholar ${z} must be a number when present`);return q}export function normalizePaperRow(q,z,{rank:H}={}){if(!q||typeof q!=="object")throw new T(`semanticscholar ${z} row is not an object`);if(typeof q.paperId!=="string"||!q.paperId.trim())throw new T(`semanticscholar ${z} row is missing paperId`);if(typeof q.title!=="string"||!q.title.trim())throw new T(`semanticscholar ${z} row is missing title`);if(q.authors!=null&&!Array.isArray(q.authors))throw new T(`semanticscholar ${z} row has malformed authors`);const Q={paperId:q.paperId.trim(),doi:pickDoi(q.externalIds),title:q.title.trim(),year:optionalNumber(q.year,`${z} year`),firstAuthor:firstAuthorName(q.authors),citationCount:optionalNumber(q.citationCount,`${z} citationCount`),url:typeof q.url==="string"&&q.url.trim()?q.url.trim():`https://www.semanticscholar.org/paper/${q.paperId.trim()}`};if(H!=null)return{rank:H,...Q};return Q}export function pickDoi(q){if(q&&typeof q==="object"&&typeof q.DOI==="string")return q.DOI;return""}
1
+ import{ArgumentError as j,CommandExecutionError as T,EmptyResultError as G}from"@jackwener/opencli/errors";export const S2_GRAPH_BASE="https://api.semanticscholar.org/graph/v1",S2_REC_BASE="https://api.semanticscholar.org/recommendations/v1";const L="opencli-semanticscholar-adapter (+https://github.com/jackwener/OpenCLI)",M=/^[0-9a-f]{40}$/i,F=/^10\.\S+$/,Y=/^\d{4}\.\d{4,5}(?:v\d+)?$/,S=/^[a-z-]+\/\d{7}(?:v\d+)?$/i,B=/^(ARXIV|MAG|ACL|PMID|PMCID|URL|CorpusId|DBLP):/i;export function requireString(q,z){const H=String(q??"").trim();if(!H)throw new j(`semanticscholar ${z} cannot be empty`);return H}export function requireBoundedInt(q,z,H,Q="limit"){const J=q??z,W=typeof J==="number"?J:Number(J);if(!Number.isInteger(W)||W<=0)throw new j(`semanticscholar ${Q} must be a positive integer`);if(W>H)throw new j(`semanticscholar ${Q} must be <= ${H}`);return W}export function requirePaperRef(q){const z=String(q??"").trim();if(!z)throw new j("semanticscholar paper id cannot be empty","Example: ppcli semanticscholar paper 10.18653/v1/N19-1423");const H=z.match(/^https?:\/\/(?:www\.)?semanticscholar\.org\/paper\/(?:[^/]+\/)?([0-9a-f]{40})/i);if(H)return H[1].toLowerCase();if(M.test(z))return z.toLowerCase();if(B.test(z))return z;if(/^doi:/i.test(z)){const J=z.replace(/^doi:/i,"").trim();if(F.test(J))return`DOI:${J}`}const Q=z.match(/^https?:\/\/(?:dx\.)?doi\.org\/(.+)$/i);if(Q&&F.test(Q[1]))return`DOI:${Q[1]}`;if(F.test(z))return`DOI:${z}`;if(Y.test(z)||S.test(z))return`ARXIV:${z}`;throw new j(`semanticscholar paper id "${q}" is not recognised`,'Use a Semantic Scholar paperId, a DOI, an arXiv id, or a prefixed id (e.g. "ARXIV:1706.03762", "PMID:12345").')}export async function s2Fetch(q,z){const H={"user-agent":L,accept:"application/json"},Q=process.env.SEMANTIC_SCHOLAR_API_KEY;if(Q)H["x-api-key"]=Q;let J,W=0;while(!0){try{J=await fetch(q,{headers:H})}catch(Z){throw new T(`${z} request failed: ${Z?.message??Z}`,"Check that api.semanticscholar.org is reachable from this network.")}if(J.status===429&&W===0&&!Q){W+=1;await new Promise((Z)=>setTimeout(Z,1500));continue}break}if(J.status===404)throw new G(z,`Semantic Scholar returned 404 for ${q}.`);if(J.status===429)throw new T(`${z} returned HTTP 429 (rate limited)`,"Semantic Scholar throttles anonymous traffic; set SEMANTIC_SCHOLAR_API_KEY (free at https://www.semanticscholar.org/product/api) or wait a minute and retry.");if(!J.ok)throw new T(`${z} returned HTTP ${J.status}`);let $;try{$=await J.json()}catch(Z){throw new T(`${z} returned malformed JSON: ${Z?.message??Z}`)}if($&&typeof $==="object"&&$.error)throw new T(`${z} returned an error: ${$.error}`);return $}export function tldrText(q){if(q&&typeof q==="object"&&typeof q.text==="string")return q.text.trim();return""}export function firstAuthorName(q){if(!Array.isArray(q)||!q.length)return"";const z=q[0];if(z&&typeof z==="object"&&typeof z.name==="string")return z.name.trim();return""}export function optionalNumber(q,z){if(q==null)return null;if(typeof q!=="number"||!Number.isFinite(q))throw new T(`semanticscholar ${z} must be a number when present`);return q}export function normalizePaperRow(q,z,{rank:H}={}){if(!q||typeof q!=="object")throw new T(`semanticscholar ${z} row is not an object`);if(typeof q.paperId!=="string"||!q.paperId.trim())throw new T(`semanticscholar ${z} row is missing paperId`);if(typeof q.title!=="string"||!q.title.trim())throw new T(`semanticscholar ${z} row is missing title`);if(q.authors!=null&&!Array.isArray(q.authors))throw new T(`semanticscholar ${z} row has malformed authors`);const Q={paperId:q.paperId.trim(),doi:pickDoi(q.externalIds),title:q.title.trim(),year:optionalNumber(q.year,`${z} year`),firstAuthor:firstAuthorName(q.authors),citationCount:optionalNumber(q.citationCount,`${z} citationCount`),url:typeof q.url==="string"&&q.url.trim()?q.url.trim():`https://www.semanticscholar.org/paper/${q.paperId.trim()}`};if(H!=null)return{rank:H,...Q};return Q}export function pickDoi(q){if(q&&typeof q==="object"&&typeof q.DOI==="string")return q.DOI;return""}
@@ -1 +1 @@
1
- import{ArgumentError as z,AuthRequiredError as B,CommandExecutionError as y,ConfigError as F}from"@jackwener/opencli/errors";import{SLOCK_DOMAIN as G}from"./shared.js";export function dispatchEvaluateResult(b){switch(b&&b.kind){case"ok":return b.rows;case"auth":throw new B(G,b.detail);case"http":throw new y(`HTTP ${b.status} from ${b.where}`);case"no-server":throw new F(b.detail,"Run `opencli slock server-use <slug>` to set the active server.");case"unresolvable":throw new z(b.detail);case"no-thread":throw new y(`no-thread should be handled by caller, not dispatcher: ${b.parent}`);default:throw new y(`unexpected evaluate envelope: ${JSON.stringify(b)}`)}}
1
+ import{ArgumentError as z,AuthRequiredError as B,CommandExecutionError as y,ConfigError as F}from"@jackwener/opencli/errors";import{SLOCK_DOMAIN as G}from"./shared.js";export function dispatchEvaluateResult(b){switch(b&&b.kind){case"ok":return b.rows;case"auth":throw new B(G,b.detail);case"http":throw new y(`HTTP ${b.status} from ${b.where}`);case"no-server":throw new F(b.detail,"Run `ppcli slock server-use <slug>` to set the active server.");case"unresolvable":throw new z(b.detail);case"no-thread":throw new y(`no-thread should be handled by caller, not dispatcher: ${b.parent}`);default:throw new y(`unexpected evaluate envelope: ${JSON.stringify(b)}`)}}
@@ -1 +1 @@
1
- import{cli as X,Strategy as Y}from"@jackwener/opencli/registry";import{CliError as Z}from"@jackwener/opencli/errors";import{readFileSync as q,writeFileSync as j,mkdirSync as R,existsSync as f}from"fs";import{createServer as h}from"http";import{homedir as P}from"os";import{join as x}from"path";import{exec as m}from"child_process";import{assertSpotifyCredentialsConfigured as I,getFirstSpotifyTrack as v,mapSpotifyTrackResults as p,parseDotEnv as u,resolveSpotifyCredentials as C}from"./utils.js";const K=x(P(),".opencli","spotify.env");function w(){if(!f(K))return{};return u(q(K,"utf-8"))}const T=w(),O=C(T),V=O.clientId,D=O.clientSecret,U="http://127.0.0.1:8888/callback",S=["user-read-playback-state","user-modify-playback-state","user-read-currently-playing","playlist-read-private"].join(" "),F=x(P(),".opencli","spotify-tokens.json");function y(){try{return JSON.parse(q(F,"utf-8"))}catch{return null}}function L(z){R(x(P(),".opencli"),{recursive:!0});j(F,JSON.stringify(z,null,2))}async function E(z){const G=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${V}:${D}`).toString("base64")},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:z})});if(!G.ok){const J=await G.json().catch(()=>({}));throw new Z("REFRESH_FAILED",J?.error_description||`Token refresh failed (${G.status})`)}const H=await G.json(),M={access_token:H.access_token,refresh_token:H.refresh_token||z,expires_at:Date.now()+H.expires_in*1000};L(M);return M.access_token}async function g(){const z=y();if(!z)throw new Z("AUTH_REQUIRED","Not authenticated. Run: opencli spotify auth");if(!z.access_token||!z.refresh_token||!(z.expires_at>0))throw new Z("AUTH_CORRUPTED","Token file is corrupted. Run: opencli spotify auth");if(Date.now()>z.expires_at-60000)return E(z.refresh_token);return z.access_token}async function Q(z,G,H){const M=await g(),J=await fetch(`https://api.spotify.com/v1${G}`,{method:z,headers:{Authorization:`Bearer ${M}`,"Content-Type":"application/json"},body:H?JSON.stringify(H):void 0});if(J.status===204||J.status===202)return null;if(!J.ok){const W=await J.json().catch(()=>({}));throw new Z("API_ERROR",W?.error?.message||`Spotify API error ${J.status}`)}return J.json()}async function N(z){const G=await Q("GET",`/search?q=${encodeURIComponent(z)}&type=track&limit=1`),H=v(G);if(!H)throw new Z("EMPTY_RESULT",`No track found for: ${z}`);return H}function d(z){const G=process.platform==="win32"?`start "" "${z}"`:process.platform==="darwin"?`open "${z}"`:`xdg-open "${z}"`;m(G)}X({site:"spotify",name:"auth",access:"write",description:"Authenticate with Spotify (OAuth — run once)",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{I(O,K);return new Promise((z,G)=>{const H=h(async(J,W)=>{try{const $=new URL(J.url,"http://localhost:8888");if($.pathname!=="/callback"){W.end();return}const b=$.searchParams.get("code");if(!b){W.end("Missing code");return}const A=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${V}:${D}`).toString("base64")},body:new URLSearchParams({grant_type:"authorization_code",code:b,redirect_uri:U})});if(!A.ok){const _=await A.json().catch(()=>({}));H.close();G(new Z("AUTH_FAILED",_?.error_description||`Token exchange failed (${A.status})`));return}const B=await A.json();L({access_token:B.access_token,refresh_token:B.refresh_token,expires_at:Date.now()+B.expires_in*1000});W.writeHead(200,{"Content-Type":"text/html"});W.end("<h2>Spotify authenticated! You can close this tab.</h2>");H.close();z([{status:"Authenticated successfully"}])}catch($){H.close();G($)}});H.on("error",(J)=>{if(J.code==="EADDRINUSE")G(new Z("PORT_IN_USE","Port 8888 is already in use. Stop the other process and retry."));else G(J)});const M=setTimeout(()=>{H.close();G(new Z("AUTH_TIMEOUT","Authentication timed out after 5 minutes"))},300000);H.listen(8888,()=>{const J=`https://accounts.spotify.com/authorize?${new URLSearchParams({client_id:V,response_type:"code",redirect_uri:U,scope:S})}`;console.log("Opening browser for Spotify login...");console.log("If it does not open, visit:",J);d(J)});H.on("close",()=>clearTimeout(M))})}});X({site:"spotify",name:"status",access:"read",description:"Show current playback status",strategy:Y.PUBLIC,browser:!1,args:[],columns:["track","artist","album","status","progress"],func:async()=>{const z=await Q("GET","/me/player");if(!z||!z.item)return[{track:"Nothing playing",artist:"",album:"",status:"",progress:""}];const G=z.item;if(G.type!=="track")return[{track:G.name,artist:"",album:G.show?.name??"",status:z.is_playing?"playing":"paused",progress:""}];const H=(z.progress_ms??0)/1000|0,M=G.duration_ms/1000|0,J=(W)=>`${W/60|0}:${String(W%60).padStart(2,"0")}`;return[{track:G.name,artist:G.artists.map((W)=>W.name).join(", "),album:G.album.name,status:z.is_playing?"playing":"paused",progress:`${J(H)} / ${J(M)}`}]}});X({site:"spotify",name:"play",access:"write",description:"Resume playback or search and play a track/artist",strategy:Y.PUBLIC,browser:!1,args:[{name:"query",type:"str",default:"",positional:!0,help:"Track or artist to play (optional)"}],columns:["track","artist","status"],func:async(z)=>{if(z.query){const{uri:G,name:H,artist:M}=await N(z.query);await Q("PUT","/me/player/play",{uris:[G]});return[{track:H,artist:M,status:"playing"}]}await Q("PUT","/me/player/play");return[{track:"",artist:"",status:"resumed"}]}});X({site:"spotify",name:"pause",access:"write",description:"Pause playback",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await Q("PUT","/me/player/pause");return[{status:"paused"}]}});X({site:"spotify",name:"next",access:"write",description:"Skip to next track",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await Q("POST","/me/player/next");return[{status:"skipped to next"}]}});X({site:"spotify",name:"prev",access:"write",description:"Skip to previous track",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await Q("POST","/me/player/previous");return[{status:"skipped to previous"}]}});X({site:"spotify",name:"volume",access:"write",description:"Set playback volume (0-100)",strategy:Y.PUBLIC,browser:!1,args:[{name:"level",type:"int",default:50,positional:!0,required:!0,help:"Volume 0–100"}],columns:["volume"],func:async(z)=>{const G=Math.round(z.level);if(G<0||G>100)throw new Z("INVALID_ARGS","Volume must be between 0 and 100");await Q("PUT",`/me/player/volume?volume_percent=${G}`);return[{volume:`${G}%`}]}});X({site:"spotify",name:"search",access:"read",description:"Search for tracks",strategy:Y.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Search query"},{name:"limit",type:"int",default:10,help:"Number of results (default: 10)"}],columns:["track","artist","album","uri"],func:async(z)=>{const G=Math.min(50,Math.max(1,Math.round(z.limit))),H=await Q("GET",`/search?q=${encodeURIComponent(z.query)}&type=track&limit=${G}`),M=p(H);if(!M.length)throw new Z("EMPTY_RESULT",`No results found for: ${z.query}`);return M}});X({site:"spotify",name:"queue",access:"write",description:"Add a track to the playback queue",strategy:Y.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Track to add to queue"}],columns:["track","artist","status"],func:async(z)=>{const{uri:G,name:H,artist:M}=await N(z.query);await Q("POST",`/me/player/queue?uri=${encodeURIComponent(G)}`);return[{track:H,artist:M,status:"added to queue"}]}});X({site:"spotify",name:"shuffle",access:"write",description:"Toggle shuffle on/off",strategy:Y.PUBLIC,browser:!1,args:[{name:"state",type:"str",default:"on",positional:!0,choices:["on","off"],help:"on or off"}],columns:["shuffle"],func:async(z)=>{await Q("PUT",`/me/player/shuffle?state=${z.state==="on"}`);return[{shuffle:z.state}]}});X({site:"spotify",name:"repeat",access:"write",description:"Set repeat mode (off / track / context)",strategy:Y.PUBLIC,browser:!1,args:[{name:"mode",type:"str",default:"context",positional:!0,choices:["off","track","context"],help:"off / track / context"}],columns:["repeat"],func:async(z)=>{await Q("PUT",`/me/player/repeat?state=${z.mode}`);return[{repeat:z.mode}]}});
1
+ import{cli as X,Strategy as Y}from"@jackwener/opencli/registry";import{CliError as Z}from"@jackwener/opencli/errors";import{readFileSync as q,writeFileSync as j,mkdirSync as R,existsSync as f}from"fs";import{createServer as h}from"http";import{homedir as P}from"os";import{join as x}from"path";import{exec as m}from"child_process";import{assertSpotifyCredentialsConfigured as I,getFirstSpotifyTrack as v,mapSpotifyTrackResults as p,parseDotEnv as u,resolveSpotifyCredentials as C}from"./utils.js";const K=x(P(),".ppcli","spotify.env");function w(){if(!f(K))return{};return u(q(K,"utf-8"))}const T=w(),O=C(T),V=O.clientId,D=O.clientSecret,U="http://127.0.0.1:8888/callback",S=["user-read-playback-state","user-modify-playback-state","user-read-currently-playing","playlist-read-private"].join(" "),F=x(P(),".ppcli","spotify-tokens.json");function y(){try{return JSON.parse(q(F,"utf-8"))}catch{return null}}function L(z){R(x(P(),".ppcli"),{recursive:!0});j(F,JSON.stringify(z,null,2))}async function E(z){const G=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${V}:${D}`).toString("base64")},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:z})});if(!G.ok){const J=await G.json().catch(()=>({}));throw new Z("REFRESH_FAILED",J?.error_description||`Token refresh failed (${G.status})`)}const H=await G.json(),M={access_token:H.access_token,refresh_token:H.refresh_token||z,expires_at:Date.now()+H.expires_in*1000};L(M);return M.access_token}async function g(){const z=y();if(!z)throw new Z("AUTH_REQUIRED","Not authenticated. Run: ppcli spotify auth");if(!z.access_token||!z.refresh_token||!(z.expires_at>0))throw new Z("AUTH_CORRUPTED","Token file is corrupted. Run: ppcli spotify auth");if(Date.now()>z.expires_at-60000)return E(z.refresh_token);return z.access_token}async function Q(z,G,H){const M=await g(),J=await fetch(`https://api.spotify.com/v1${G}`,{method:z,headers:{Authorization:`Bearer ${M}`,"Content-Type":"application/json"},body:H?JSON.stringify(H):void 0});if(J.status===204||J.status===202)return null;if(!J.ok){const W=await J.json().catch(()=>({}));throw new Z("API_ERROR",W?.error?.message||`Spotify API error ${J.status}`)}return J.json()}async function N(z){const G=await Q("GET",`/search?q=${encodeURIComponent(z)}&type=track&limit=1`),H=v(G);if(!H)throw new Z("EMPTY_RESULT",`No track found for: ${z}`);return H}function d(z){const G=process.platform==="win32"?`start "" "${z}"`:process.platform==="darwin"?`open "${z}"`:`xdg-open "${z}"`;m(G)}X({site:"spotify",name:"auth",access:"write",description:"Authenticate with Spotify (OAuth — run once)",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{I(O,K);return new Promise((z,G)=>{const H=h(async(J,W)=>{try{const $=new URL(J.url,"http://localhost:8888");if($.pathname!=="/callback"){W.end();return}const b=$.searchParams.get("code");if(!b){W.end("Missing code");return}const A=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${V}:${D}`).toString("base64")},body:new URLSearchParams({grant_type:"authorization_code",code:b,redirect_uri:U})});if(!A.ok){const _=await A.json().catch(()=>({}));H.close();G(new Z("AUTH_FAILED",_?.error_description||`Token exchange failed (${A.status})`));return}const B=await A.json();L({access_token:B.access_token,refresh_token:B.refresh_token,expires_at:Date.now()+B.expires_in*1000});W.writeHead(200,{"Content-Type":"text/html"});W.end("<h2>Spotify authenticated! You can close this tab.</h2>");H.close();z([{status:"Authenticated successfully"}])}catch($){H.close();G($)}});H.on("error",(J)=>{if(J.code==="EADDRINUSE")G(new Z("PORT_IN_USE","Port 8888 is already in use. Stop the other process and retry."));else G(J)});const M=setTimeout(()=>{H.close();G(new Z("AUTH_TIMEOUT","Authentication timed out after 5 minutes"))},300000);H.listen(8888,()=>{const J=`https://accounts.spotify.com/authorize?${new URLSearchParams({client_id:V,response_type:"code",redirect_uri:U,scope:S})}`;console.log("Opening browser for Spotify login...");console.log("If it does not open, visit:",J);d(J)});H.on("close",()=>clearTimeout(M))})}});X({site:"spotify",name:"status",access:"read",description:"Show current playback status",strategy:Y.PUBLIC,browser:!1,args:[],columns:["track","artist","album","status","progress"],func:async()=>{const z=await Q("GET","/me/player");if(!z||!z.item)return[{track:"Nothing playing",artist:"",album:"",status:"",progress:""}];const G=z.item;if(G.type!=="track")return[{track:G.name,artist:"",album:G.show?.name??"",status:z.is_playing?"playing":"paused",progress:""}];const H=(z.progress_ms??0)/1000|0,M=G.duration_ms/1000|0,J=(W)=>`${W/60|0}:${String(W%60).padStart(2,"0")}`;return[{track:G.name,artist:G.artists.map((W)=>W.name).join(", "),album:G.album.name,status:z.is_playing?"playing":"paused",progress:`${J(H)} / ${J(M)}`}]}});X({site:"spotify",name:"play",access:"write",description:"Resume playback or search and play a track/artist",strategy:Y.PUBLIC,browser:!1,args:[{name:"query",type:"str",default:"",positional:!0,help:"Track or artist to play (optional)"}],columns:["track","artist","status"],func:async(z)=>{if(z.query){const{uri:G,name:H,artist:M}=await N(z.query);await Q("PUT","/me/player/play",{uris:[G]});return[{track:H,artist:M,status:"playing"}]}await Q("PUT","/me/player/play");return[{track:"",artist:"",status:"resumed"}]}});X({site:"spotify",name:"pause",access:"write",description:"Pause playback",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await Q("PUT","/me/player/pause");return[{status:"paused"}]}});X({site:"spotify",name:"next",access:"write",description:"Skip to next track",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await Q("POST","/me/player/next");return[{status:"skipped to next"}]}});X({site:"spotify",name:"prev",access:"write",description:"Skip to previous track",strategy:Y.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await Q("POST","/me/player/previous");return[{status:"skipped to previous"}]}});X({site:"spotify",name:"volume",access:"write",description:"Set playback volume (0-100)",strategy:Y.PUBLIC,browser:!1,args:[{name:"level",type:"int",default:50,positional:!0,required:!0,help:"Volume 0–100"}],columns:["volume"],func:async(z)=>{const G=Math.round(z.level);if(G<0||G>100)throw new Z("INVALID_ARGS","Volume must be between 0 and 100");await Q("PUT",`/me/player/volume?volume_percent=${G}`);return[{volume:`${G}%`}]}});X({site:"spotify",name:"search",access:"read",description:"Search for tracks",strategy:Y.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Search query"},{name:"limit",type:"int",default:10,help:"Number of results (default: 10)"}],columns:["track","artist","album","uri"],func:async(z)=>{const G=Math.min(50,Math.max(1,Math.round(z.limit))),H=await Q("GET",`/search?q=${encodeURIComponent(z.query)}&type=track&limit=${G}`),M=p(H);if(!M.length)throw new Z("EMPTY_RESULT",`No results found for: ${z.query}`);return M}});X({site:"spotify",name:"queue",access:"write",description:"Add a track to the playback queue",strategy:Y.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Track to add to queue"}],columns:["track","artist","status"],func:async(z)=>{const{uri:G,name:H,artist:M}=await N(z.query);await Q("POST",`/me/player/queue?uri=${encodeURIComponent(G)}`);return[{track:H,artist:M,status:"added to queue"}]}});X({site:"spotify",name:"shuffle",access:"write",description:"Toggle shuffle on/off",strategy:Y.PUBLIC,browser:!1,args:[{name:"state",type:"str",default:"on",positional:!0,choices:["on","off"],help:"on or off"}],columns:["shuffle"],func:async(z)=>{await Q("PUT",`/me/player/shuffle?state=${z.state==="on"}`);return[{shuffle:z.state}]}});X({site:"spotify",name:"repeat",access:"write",description:"Set repeat mode (off / track / context)",strategy:Y.PUBLIC,browser:!1,args:[{name:"mode",type:"str",default:"context",positional:!0,choices:["off","track","context"],help:"off / track / context"}],columns:["repeat"],func:async(z)=>{await Q("PUT",`/me/player/repeat?state=${z.mode}`);return[{repeat:z.mode}]}});
@@ -5,4 +5,4 @@ import{CliError as n}from"@jackwener/opencli/errors";const o=[/^your_spotify_cli
5
5
  3. Copy your Client ID and Client Secret
6
6
  4. Open the file: ${t}
7
7
  5. Fill in SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET, then save
8
- 6. Run: opencli spotify auth`)}export function mapSpotifyTrackResults(r){const t=r?.tracks?.items;if(!Array.isArray(t))return[];return t.map((e)=>({track:e?.name||"",artist:Array.isArray(e?.artists)?e.artists.map((i)=>i.name).join(", "):"",album:e?.album?.name||"",uri:e?.uri||""}))}export function getFirstSpotifyTrack(r){const t=mapSpotifyTrackResults(r)[0];if(!t)return null;return{uri:t.uri,name:t.track,artist:t.artist}}
8
+ 6. Run: ppcli spotify auth`)}export function mapSpotifyTrackResults(r){const t=r?.tracks?.items;if(!Array.isArray(t))return[];return t.map((e)=>({track:e?.name||"",artist:Array.isArray(e?.artists)?e.artists.map((i)=>i.name).join(", "):"",album:e?.album?.name||"",uri:e?.uri||""}))}export function getFirstSpotifyTrack(r){const t=mapSpotifyTrackResults(r)[0];if(!t)return null;return{uri:t.uri,name:t.track,artist:t.artist}}
@@ -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}});