publishport-opencli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.md +61 -61
  2. package/README.zh-CN.md +68 -68
  3. package/cli-manifest.json +954 -61
  4. package/clis/1688/shared.js +2 -2
  5. package/clis/36kr/news.js +1 -1
  6. package/clis/_atlassian/shared.js +1 -1
  7. package/clis/_shared/article/auth.js +1 -1
  8. package/clis/_shared/article/publish.js +1 -1
  9. package/clis/_shared/content-guard.js +1 -1
  10. package/clis/_shared/self-hosted-sites.js +2 -0
  11. package/clis/_shared/site-auth.js +1 -1
  12. package/clis/_shared/token-auth.js +2 -2
  13. package/clis/_shared/video-publish.js +1 -1
  14. package/clis/aibase/news.js +1 -1
  15. package/clis/amazon/shared.js +2 -2
  16. package/clis/antigravity/SKILL.md +12 -12
  17. package/clis/antigravity/storage.js +1 -1
  18. package/clis/apple-podcasts/episodes.js +1 -1
  19. package/clis/archive/item.js +1 -1
  20. package/clis/archive/search.js +1 -1
  21. package/clis/archive/snapshots.js +1 -1
  22. package/clis/archive/wayback.js +1 -1
  23. package/clis/arxiv/author.js +1 -1
  24. package/clis/baijiahao/publish.js +1 -1
  25. package/clis/bbc/utils.js +1 -1
  26. package/clis/bloomberg/businessweek.js +1 -1
  27. package/clis/bloomberg/utils.js +1 -1
  28. package/clis/booking/search.js +1 -1
  29. package/clis/boss/utils.js +1 -1
  30. package/clis/chatgpt/ask.js +1 -1
  31. package/clis/chatgpt/detail.js +1 -1
  32. package/clis/chatgpt/history.js +1 -1
  33. package/clis/chatgpt/project-file-add.js +1 -1
  34. package/clis/chatgpt/project-list.js +1 -1
  35. package/clis/chatgpt/utils.js +2 -2
  36. package/clis/chess/utils.js +1 -1
  37. package/clis/claude/ask.js +1 -1
  38. package/clis/claude/history.js +1 -1
  39. package/clis/claude/utils.js +1 -1
  40. package/clis/codex/extract-diff.js +1 -1
  41. package/clis/coingecko/coin.js +1 -1
  42. package/clis/crates/utils.js +1 -1
  43. package/clis/csdn/stats.js +1 -1
  44. package/clis/dblp/utils.js +1 -1
  45. package/clis/defillama/utils.js +1 -1
  46. package/clis/devto/publish.js +1 -1
  47. package/clis/discord-app/utils.js +1 -1
  48. package/clis/dockerhub/utils.js +1 -1
  49. package/clis/douban/utils.js +1 -1
  50. package/clis/douyin/_shared/tos-upload.js +1 -1
  51. package/clis/douyin/hashtag.js +1 -1
  52. package/clis/douyin/publish-image.js +1 -1
  53. package/clis/douyin/publish.js +1 -1
  54. package/clis/eastmoney/announcement.js +1 -1
  55. package/clis/eastmoney/rank.js +1 -1
  56. package/clis/eastmoney/sectors.js +1 -1
  57. package/clis/endoflife/utils.js +1 -1
  58. package/clis/flathub/utils.js +1 -1
  59. package/clis/gemini/detail.js +1 -1
  60. package/clis/gemini/read.js +1 -1
  61. package/clis/geogebra/add-circle.js +1 -1
  62. package/clis/geogebra/add-line.js +1 -1
  63. package/clis/geogebra/add-point.js +1 -1
  64. package/clis/geogebra/add-polygon.js +1 -1
  65. package/clis/geogebra/eval.js +1 -1
  66. package/clis/geogebra/hexagon.js +1 -1
  67. package/clis/geogebra/info.js +1 -1
  68. package/clis/geogebra/triangle.js +1 -1
  69. package/clis/ghost/login.js +1 -0
  70. package/clis/ghost/publish.js +1 -1
  71. package/clis/ghost/shared.js +24 -1
  72. package/clis/ghost/sites.js +1 -0
  73. package/clis/ghost/whoami.js +1 -1
  74. package/clis/gitee/user.js +2 -2
  75. package/clis/github-trending/repos.js +1 -1
  76. package/clis/goproxy/utils.js +1 -1
  77. package/clis/grok/export-all.js +1 -1
  78. package/clis/grok/export.js +1 -1
  79. package/clis/hashnode/publish.js +1 -1
  80. package/clis/hf/datasets.js +1 -1
  81. package/clis/hf/models.js +1 -1
  82. package/clis/hf/paper.js +1 -1
  83. package/clis/hf/spaces.js +1 -1
  84. package/clis/homebrew/utils.js +1 -1
  85. package/clis/instagram/note.js +1 -1
  86. package/clis/jimeng/generate.js +1 -1
  87. package/clis/juejin/utils.js +1 -1
  88. package/clis/kuaishou/publish.js +1 -1
  89. package/clis/lesswrong/tag.js +1 -1
  90. package/clis/lichess/utils.js +1 -1
  91. package/clis/linkedin/search.js +2 -2
  92. package/clis/linux-do/feed.js +2 -2
  93. package/clis/lobsters/domain.js +1 -1
  94. package/clis/mastodon/login.js +1 -0
  95. package/clis/mastodon/post.js +1 -1
  96. package/clis/mastodon/shared.js +10 -10
  97. package/clis/mastodon/sites.js +1 -0
  98. package/clis/mastodon/whoami.js +1 -1
  99. package/clis/maven/utils.js +1 -1
  100. package/clis/mdn/search.js +1 -1
  101. package/clis/medium/tag.js +1 -1
  102. package/clis/notebooklm/current.js +1 -1
  103. package/clis/notebooklm/get.js +1 -1
  104. package/clis/notebooklm/history.js +1 -1
  105. package/clis/notebooklm/note-list.js +1 -1
  106. package/clis/notebooklm/notes-get.js +1 -1
  107. package/clis/notebooklm/open.js +1 -1
  108. package/clis/notebooklm/source-fulltext.js +1 -1
  109. package/clis/notebooklm/source-get.js +1 -1
  110. package/clis/notebooklm/source-guide.js +1 -1
  111. package/clis/notebooklm/source-list.js +1 -1
  112. package/clis/notebooklm/summary.js +1 -1
  113. package/clis/notebooklm/utils.js +1 -1
  114. package/clis/npm/utils.js +1 -1
  115. package/clis/nuget/utils.js +1 -1
  116. package/clis/nvd/cve.js +1 -1
  117. package/clis/oeis/utils.js +1 -1
  118. package/clis/ones/common.js +2 -2
  119. package/clis/ones/me.js +1 -1
  120. package/clis/ones/tasks.js +1 -1
  121. package/clis/ones/token-info.js +1 -1
  122. package/clis/ones/worklog.js +1 -1
  123. package/clis/openalex/utils.js +1 -1
  124. package/clis/oschina/stats.js +1 -1
  125. package/clis/osv/utils.js +1 -1
  126. package/clis/packagist/utils.js +1 -1
  127. package/clis/pixiv/detail.js +1 -1
  128. package/clis/pixiv/user.js +1 -1
  129. package/clis/producthunt/utils.js +1 -1
  130. package/clis/pypi/utils.js +1 -1
  131. package/clis/qiita/gql.js +2 -2
  132. package/clis/quora/__fixtures__/dom.html +39 -0
  133. package/clis/quora/answer-comments.js +9 -0
  134. package/clis/quora/answer-detail.js +1 -0
  135. package/clis/quora/answer-queue.js +1 -0
  136. package/clis/quora/comment.js +23 -0
  137. package/clis/quora/create-answer.js +4 -4
  138. package/clis/quora/create-question.js +3 -3
  139. package/clis/quora/follow.js +15 -0
  140. package/clis/quora/followers.js +1 -0
  141. package/clis/quora/question.js +1 -0
  142. package/clis/quora/search.js +1 -0
  143. package/clis/quora/shared.js +26 -11
  144. package/clis/quora/stats.js +1 -0
  145. package/clis/quora/upvote.js +14 -0
  146. package/clis/quora/user-answers.js +1 -0
  147. package/clis/quora/user.js +1 -0
  148. package/clis/rest-countries/utils.js +1 -1
  149. package/clis/rfc/utils.js +1 -1
  150. package/clis/rubygems/utils.js +1 -1
  151. package/clis/semanticscholar/utils.js +1 -1
  152. package/clis/slock/errors.js +1 -1
  153. package/clis/spotify/spotify.js +1 -1
  154. package/clis/spotify/utils.js +1 -1
  155. package/clis/stackoverflow/utils.js +1 -1
  156. package/clis/steam/utils.js +1 -1
  157. package/clis/substack/auth.js +2 -2
  158. package/clis/suno/generate.js +2 -2
  159. package/clis/suno/list.js +3 -3
  160. package/clis/telegram/send.js +1 -1
  161. package/clis/tiktok/creator-videos.js +1 -1
  162. package/clis/tiktok/publish.js +1 -1
  163. package/clis/tiktok/utils.js +1 -1
  164. package/clis/trae-cn/activity.js +1 -1
  165. package/clis/trae-cn/approve.js +1 -1
  166. package/clis/trae-cn/ask.js +1 -1
  167. package/clis/trae-cn/dump.js +1 -1
  168. package/clis/trae-cn/export.js +1 -1
  169. package/clis/trae-cn/model.js +1 -1
  170. package/clis/trae-cn/new.js +1 -1
  171. package/clis/trae-cn/read.js +1 -1
  172. package/clis/trae-cn/screenshot.js +1 -1
  173. package/clis/trae-cn/select-model.js +1 -1
  174. package/clis/trae-cn/send.js +1 -1
  175. package/clis/trae-cn/setup.js +1 -1
  176. package/clis/trae-cn/status.js +1 -1
  177. package/clis/trae-cn/targets.js +1 -1
  178. package/clis/trae-cn/watch.js +1 -1
  179. package/clis/trae-solo/state-fs.js +1 -1
  180. package/clis/tumblr/publish.js +1 -1
  181. package/clis/tvmaze/utils.js +1 -1
  182. package/clis/twitter/bookmark-folder.js +1 -1
  183. package/clis/twitter/download.js +1 -1
  184. package/clis/twitter/followers.js +2 -2
  185. package/clis/twitter/following.js +2 -2
  186. package/clis/twitter/likes.js +1 -1
  187. package/clis/twitter/list-add-batch.js +1 -1
  188. package/clis/twitter/list-add-core.js +1 -1
  189. package/clis/twitter/list-add.js +1 -1
  190. package/clis/twitter/list-create.js +1 -1
  191. package/clis/twitter/list-delete.js +3 -3
  192. package/clis/twitter/list-remove-batch.js +1 -1
  193. package/clis/twitter/list-remove.js +1 -1
  194. package/clis/twitter/list-tweets.js +1 -1
  195. package/clis/twitter/profile.js +1 -1
  196. package/clis/twitter/search.js +1 -1
  197. package/clis/twitter/shared.js +1 -1
  198. package/clis/twitter/tweets.js +1 -1
  199. package/clis/uisdc/news.js +1 -1
  200. package/clis/v2ex/daily.js +2 -2
  201. package/clis/v2ex/me.js +2 -2
  202. package/clis/v2ex/notifications.js +1 -1
  203. package/clis/wikidata/utils.js +1 -1
  204. package/clis/wikipedia/page.js +2 -2
  205. package/clis/wikipedia/summary.js +1 -1
  206. package/clis/wikipedia/utils.js +1 -1
  207. package/clis/wordpress/login.js +1 -1
  208. package/clis/wordpress/publish.js +1 -1
  209. package/clis/wordpress/shared.js +3 -3
  210. package/clis/wordpress/sites.js +1 -0
  211. package/clis/wordpress/whoami.js +1 -1
  212. package/clis/woshipm/stats.js +1 -1
  213. package/clis/xiaoe/content.js +1 -1
  214. package/clis/xiaohongshu/draft-delete.js +1 -1
  215. package/clis/xiaohongshu/draft-open.js +1 -1
  216. package/clis/xiaohongshu/draft-utils.js +2 -2
  217. package/clis/xiaohongshu/publish-video.js +1 -1
  218. package/clis/xiaohongshu/publish.js +1 -1
  219. package/clis/xiaoyuzhou/auth.js +1 -1
  220. package/clis/xiaoyuzhou/transcript.js +1 -1
  221. package/clis/yollomi/background.js +1 -1
  222. package/clis/yollomi/edit.js +1 -1
  223. package/clis/yollomi/generate.js +1 -1
  224. package/clis/yollomi/try-on.js +1 -1
  225. package/clis/youtube/publish.js +1 -1
  226. package/clis/zhihu/answer-comments.js +1 -1
  227. package/clis/zhihu/answer-detail.js +1 -1
  228. package/clis/zhihu/collection.js +2 -2
  229. package/clis/zhihu/collections.js +1 -1
  230. package/clis/zhihu/question.js +1 -1
  231. package/clis/zhihu/search.js +1 -1
  232. package/clis/zhihu/target.js +1 -1
  233. package/clis/zhihu/user-arg.js +1 -1
  234. package/dist/src/adapter-shadow.js +1 -1
  235. package/dist/src/browser/analyze.js +1 -1
  236. package/dist/src/browser/article-extract.d.ts +1 -1
  237. package/dist/src/browser/base-page.js +3 -3
  238. package/dist/src/browser/cdp.js +1 -1
  239. package/dist/src/browser/daemon-client.d.ts +1 -1
  240. package/dist/src/browser/daemon-lifecycle.js +6 -6
  241. package/dist/src/browser/daemon-version.js +1 -1
  242. package/dist/src/browser/dom-snapshot.d.ts +1 -1
  243. package/dist/src/browser/dom-snapshot.js +2 -2
  244. package/dist/src/browser/errors.js +2 -2
  245. package/dist/src/browser/network-cache.js +1 -1
  246. package/dist/src/browser/profile.js +1 -1
  247. package/dist/src/browser/target-resolver.js +4 -4
  248. package/dist/src/browser/verify-fixture.js +1 -1
  249. package/dist/src/browser-session-lock.js +1 -1
  250. package/dist/src/build-manifest.d.ts +1 -1
  251. package/dist/src/build-manifest.js +1 -1
  252. package/dist/src/cli-argv-preprocess.d.ts +4 -4
  253. package/dist/src/cli-argv-preprocess.js +1 -1
  254. package/dist/src/cli.d.ts +1 -1
  255. package/dist/src/cli.js +19 -19
  256. package/dist/src/commanderAdapter.js +1 -1
  257. package/dist/src/commands/auth.js +2 -2
  258. package/dist/src/commands/daemon.d.ts +4 -4
  259. package/dist/src/commands/daemon.js +1 -1
  260. package/dist/src/completion-shared.js +12 -12
  261. package/dist/src/completion.d.ts +2 -2
  262. package/dist/src/constants.d.ts +1 -1
  263. package/dist/src/constants.js +1 -1
  264. package/dist/src/daemon-utils.d.ts +1 -1
  265. package/dist/src/daemon-utils.js +1 -1
  266. package/dist/src/daemon.d.ts +2 -2
  267. package/dist/src/daemon.js +1 -1
  268. package/dist/src/discovery.d.ts +7 -7
  269. package/dist/src/discovery.js +2 -2
  270. package/dist/src/doctor.d.ts +1 -1
  271. package/dist/src/doctor.js +6 -6
  272. package/dist/src/electron-apps.d.ts +1 -1
  273. package/dist/src/electron-apps.js +1 -1
  274. package/dist/src/errors.d.ts +2 -2
  275. package/dist/src/errors.js +1 -1
  276. package/dist/src/execution.js +1 -1
  277. package/dist/src/external.d.ts +1 -1
  278. package/dist/src/external.js +1 -1
  279. package/dist/src/help.d.ts +1 -1
  280. package/dist/src/help.js +3 -3
  281. package/dist/src/hooks.d.ts +1 -1
  282. package/dist/src/launcher.js +2 -2
  283. package/dist/src/logger.d.ts +1 -1
  284. package/dist/src/main.d.ts +1 -1
  285. package/dist/src/main.js +1 -1
  286. package/dist/src/observation/artifact.js +2 -2
  287. package/dist/src/package-paths.js +1 -1
  288. package/dist/src/package-paths.test.d.ts +1 -0
  289. package/dist/src/plugin-manifest.d.ts +12 -3
  290. package/dist/src/plugin-manifest.js +1 -1
  291. package/dist/src/plugin-scaffold.d.ts +2 -2
  292. package/dist/src/plugin-scaffold.js +9 -9
  293. package/dist/src/plugin.d.ts +4 -4
  294. package/dist/src/plugin.js +10 -10
  295. package/dist/src/rate-limit.js +1 -1
  296. package/dist/src/registry-api.d.ts +1 -1
  297. package/dist/src/runtime-detect.d.ts +1 -1
  298. package/dist/src/serialization.js +1 -1
  299. package/dist/src/skills.js +6 -6
  300. package/dist/src/user-runtime.d.ts +8 -0
  301. package/dist/src/user-runtime.js +1 -0
  302. package/dist/src/user-runtime.test.d.ts +1 -0
  303. package/dist/src/validate.js +1 -1
  304. package/dist/src/validate.test.d.ts +1 -1
  305. package/docs/guide/browser-bridge.md +23 -23
  306. package/docs/guide/electron-app-cli.md +3 -3
  307. package/docs/guide/exit-codes.md +3 -3
  308. package/docs/guide/extending-opencli.md +37 -37
  309. package/docs/guide/getting-started.md +19 -19
  310. package/docs/guide/installation.md +6 -6
  311. package/docs/guide/plugins.md +32 -32
  312. package/docs/guide/remote-orchestration.md +5 -5
  313. package/docs/guide/troubleshooting.md +9 -9
  314. package/package.json +3 -2
  315. package/scripts/fetch-adapters.js +6 -6
  316. package/scripts/postinstall.js +14 -14
  317. package/skills/opencli-adapter-author/SKILL.md +24 -24
  318. package/skills/opencli-adapter-author/references/adapter-template.md +13 -13
  319. package/skills/opencli-adapter-author/references/api-discovery.md +25 -25
  320. package/skills/opencli-adapter-author/references/coverage-matrix.md +3 -3
  321. package/skills/opencli-adapter-author/references/field-decode-playbook.md +10 -10
  322. package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +2 -2
  323. package/skills/opencli-adapter-author/references/site-memory.md +11 -11
  324. package/skills/opencli-adapter-author/references/site-recon.md +7 -7
  325. package/skills/opencli-adapter-author/references/strategy-selection.md +3 -3
  326. package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +4 -4
  327. package/skills/opencli-adapter-author/references/typed-errors.md +2 -2
  328. package/skills/opencli-autofix/SKILL.md +29 -29
  329. package/skills/opencli-browser/SKILL.md +55 -55
  330. package/skills/opencli-browser-sitemap/SKILL.md +7 -7
  331. package/skills/opencli-sitemap-author/SKILL.md +11 -11
  332. package/skills/opencli-sitemap-author/references/sitemap-schema.md +19 -19
  333. package/skills/opencli-usage/SKILL.md +43 -43
@@ -1 +1 @@
1
- import{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}
@@ -0,0 +1,39 @@
1
+ <!doctype html>
2
+ <html><head><title>Fixture User - Quora</title></head><body>
3
+ <section id="fixture-question">
4
+ <div class="puppeteer_test_question_title">How does a frozen Quora fixture work?</div>
5
+ <div class="dom_annotate_question_answer_item_0"><a href="/profile/Ada-Lovelace">Ada Lovelace</a><a class="answer_timestamp" href="/How-does-a-frozen-Quora-fixture-work/answer/Ada-Lovelace">2h</a><div class="puppeteer_test_answer_content"><p>It preserves the visible semantic hooks.</p></div><button class="puppeteer_test_votable_upvote_button" aria-label="Upvote">Upvote 1.2K</button><button aria-label="12 comments">12</button><button aria-label="3 shares">3</button></div>
6
+ <div class="dom_annotate_question_answer_item_1"><a href="/profile/Grace-Hopper">Grace Hopper</a><a class="answer_timestamp" href="/How-does-a-frozen-Quora-fixture-work/answer/Grace-Hopper">1d</a><div class="puppeteer_test_answer_content">Selectors are tested against real page shapes. (more)</div><button class="puppeteer_test_votable_upvote_button" aria-label="Upvote">Upvote 42</button><button aria-label="Comment"></button><button aria-label="Share"></button></div>
7
+ </section>
8
+ <section id="fixture-search-question">
9
+ <div class="search-card"><a href="/What-is-artificial-intelligence"><div class="puppeteer_test_question_title">What is artificial intelligence?</div></a><a href="/What-is-artificial-intelligence">993 answers</a></div>
10
+ <div class="search-card"><a href="/Is-AI-safe"><div class="puppeteer_test_question_title">Is AI safe?</div></a><a href="/Is-AI-safe">1.5K answers</a></div>
11
+ </section>
12
+ <section id="fixture-answer-detail">
13
+ <a href="/What-is-artificial-intelligence"><div class="puppeteer_test_question_title">What is artificial intelligence?</div></a>
14
+ <a href="/profile/Joel-4940">Joel</a><a class="answer_timestamp" href="/What-is-artificial-intelligence/answer/Joel-4940">1y</a>
15
+ <p>Artificial intelligence creates systems that perform cognitive tasks.</p><p>It includes learning, reasoning, and perception.</p>
16
+ <button class="puppeteer_test_votable_upvote_button" aria-label="Upvote">Upvote 16</button><button aria-label="1 comment">1</button><button aria-label="12 shares">12</button>
17
+ <div><p>This paragraph is after the action bar and must not pollute the answer.</p></div>
18
+ </section>
19
+ <section id="fixture-answer-queue">
20
+ <div class="puppeteer_test_question_component_base"><a href="/unanswered/How-can-I-build-trust"><div class="puppeteer_test_question_title">How can I build trust?</div></a><a href="/unanswered/How-can-I-build-trust">No answer yet</a><button>Answer</button></div>
21
+ <div class="puppeteer_test_question_component_base"><a href="/How-does-Rust-work"><div class="puppeteer_test_question_title">How does Rust work?</div></a><a href="/How-does-Rust-work">5 answers</a><button>Answer</button></div>
22
+ </section>
23
+ <section id="fixture-profile">Fixture User
24
+ <div>Builds reliable browser automation.</div><div>72,173 followers</div><div>42 following</div>
25
+ <div role="tab">15.9K Answers</div><div role="tab">2 Questions</div><div role="tab">3 Posts</div>
26
+ <div>102.2M content views</div><div>206.7K this month</div>
27
+ </section>
28
+ <section id="fixture-followers">
29
+ <div class="follower-card"><a href="/profile/Ada-Lovelace">Ada Lovelace</a><div>Computing pioneer</div><div>3.5K followers</div><button>Follow</button></div>
30
+ <div class="follower-card"><a href="/profile/Grace-Hopper">Grace Hopper</a><div>Computer scientist</div><div>42 followers</div><button>Follow</button></div>
31
+ </section>
32
+ <section id="fixture-stats">Your content &amp; stats
33
+ <div>Overview</div><div>Last 30 days</div><div>Views<br>–<br>120</div><div>Upvotes<br>–<br>8</div><div>Comments<br>–<br>3</div><div>Shares (external)<br>–<br>2</div>
34
+ </section>
35
+ <section id="fixture-comments">
36
+ <div class="comment-card"><a href="/profile/Commenter-One">Commenter One</a><a href="/comment/1">10h</a><p>A useful comment.</p><button aria-label="Upvote">Upvote 7</button><button>Reply</button></div>
37
+ <div class="comment-card"><a href="/profile/Commenter-Two">Commenter Two</a><a href="/comment/2">Mon</a><p>Another useful comment.</p><button aria-label="Upvote">Upvote</button><button>Reply</button></div>
38
+ </section>
39
+ </body></html>
@@ -0,0 +1,9 @@
1
+ import{CommandExecutionError as J,EmptyResultError as K}from"@jackwener/opencli/errors";import{cli as M,Strategy as N}from"@jackwener/opencli/registry";import{evaluateQuoraDom as O,normalizeLimit as T,normalizeQuoraUrl as V,openQuoraPage as W}from"./shared.js";M({site:"quora",name:"answer-comments",access:"read",description:"Read visible comments on one Quora answer.",domain:"www.quora.com",strategy:N.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora answer URL"},{name:"limit",type:"int",default:20,help:"Number of comments (max 50)"}],columns:["rank","author","content","upvotes","published","authorUrl"],func:async(b,B)=>{const H=V(B.url,{allowProfile:!1,allowQuestion:!1,allowAnswer:!0}),I=T(B.limit,20,50);await W(b,H.url,"answer comments");if(!(await b.evaluate(`(() => {
2
+ const button = document.querySelector('#mainContent .dom_annotate_answer_action_bar_comment button')
3
+ || Array.from(document.querySelectorAll('#mainContent button[aria-label]'))
4
+ .find((item) => /comment|评论|comentario|commentaire|kommentar/i.test(item.getAttribute('aria-label') || ''));
5
+ if (!button) return { ok: false };
6
+ button.click();
7
+ return { ok: true, label: button.getAttribute('aria-label') || '' };
8
+ })()`))?.ok)throw new J("Quora answer comment control was not found.");for(let A=0;A<15;A++){if(await b.evaluate(`(() => Array.from(document.querySelectorAll('#mainContent button'))
9
+ .some((button) => /^(reply|回复|responder|répondre|antworten)$/i.test((button.innerText || button.textContent || '').trim())))()`))break;await b.wait(1)}const F=((await O(b,"comments"))?.rows||[]).slice(0,I).map((A,G)=>({...A,rank:G+1}));if(!F.length)throw new K("quora answer-comments","This answer has no visible comments.");return F}});
@@ -0,0 +1 @@
1
+ import{CommandExecutionError as o,EmptyResultError as n}from"@jackwener/opencli/errors";import{cli as s,Strategy as i}from"@jackwener/opencli/registry";import{evaluateQuoraDom as u,normalizeQuoraUrl as l,openQuoraPage as w,waitForSelector as m}from"./shared.js";s({site:"quora",name:"answer-detail",access:"read",description:"Read one complete Quora answer with question, author and engagement metrics.",domain:"www.quora.com",strategy:i.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora answer URL"}],columns:["question","author","content","upvotes","comments","shares","published","url"],func:async(e,t)=>{const a=l(t.url,{allowProfile:!1,allowQuestion:!1,allowAnswer:!0});await w(e,a.url,"answer detail");await m(e,".puppeteer_test_question_title","answer question title");const r=(await u(e,"answer-detail"))?.rows?.[0];if(!r)throw new n("quora answer-detail","The answer was not found.");if(!r.content)throw new o("Quora answer body was empty; the visible answer DOM may have changed.");return[r]}});
@@ -0,0 +1 @@
1
+ import{EmptyResultError as u}from"@jackwener/opencli/errors";import{cli as n,Strategy as i}from"@jackwener/opencli/registry";import{evaluateQuoraDom as m,loadVisibleItems as w,normalizeLimit as l,openQuoraPage as c}from"./shared.js";n({site:"quora",name:"answer-queue",access:"read",description:"List personalized Questions for You from Quora's Answer queue.",domain:"www.quora.com",strategy:i.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:10,help:"Number of questions (max 50)"}],columns:["rank","title","answers","url"],func:async(e,o)=>{const r=l(o.limit,10,50);await c(e,"https://www.quora.com/answer","answer queue");await w(e,".puppeteer_test_question_component_base",r);const t=((await m(e,"answer-queue"))?.rows||[]).slice(0,r).map((a,s)=>({...a,rank:s+1}));if(!t.length)throw new u("quora answer-queue","Quora returned no Questions for You.");return t}});
@@ -0,0 +1,23 @@
1
+ import{ArgumentError as m,CommandExecutionError as e}from"@jackwener/opencli/errors";import{cli as c,Strategy as l}from"@jackwener/opencli/registry";import{assertLiteralContent as d}from"../_shared/content-guard.js";import{insertEditorText as w,normalizeQuoraUrl as b,openQuoraPage as f,readEditorTextJs as p,requireExecute as y}from"./shared.js";c({site:"quora",name:"comment",access:"write",description:"Post a top-level comment on a Quora answer through the visible editor.",domain:"www.quora.com",strategy:l.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora answer URL"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text"},{name:"execute",type:"boolean",help:"Actually post the comment"}],columns:["status","outcome","targetType","target","message"],func:async(t,r)=>{y(r);const a=b(r.url,{allowProfile:!1,allowQuestion:!1,allowAnswer:!0}),o=String(r.text??"");if(!o.trim())throw new m("Comment text cannot be empty");d(o,{label:"comment text"});await f(t,a.url,"comment");if(!await t.evaluate(`(() => {
2
+ const button = document.querySelector('#mainContent .dom_annotate_answer_action_bar_comment button')
3
+ || Array.from(document.querySelectorAll('#mainContent button[aria-label]'))
4
+ .find((item) => /comment|评论|comentario|commentaire|kommentar/i.test(item.getAttribute('aria-label') || ''));
5
+ if (!button) return false;
6
+ button.click();
7
+ return true;
8
+ })()`))throw new e("Quora answer comment control was not found.");let n=!1;for(let u=0;u<15;u++){n=await t.evaluate(`(() => {
9
+ const editor = document.querySelector('[contenteditable="true"][data-kind="doc"]');
10
+ if (!editor) return false;
11
+ editor.setAttribute('data-pp-quora-editor', '1');
12
+ editor.focus();
13
+ return true;
14
+ })()`);if(n)break;await t.wait(1)}if(!n)throw new e("Quora comment editor did not appear.");const i=await w(t,o);if(!i?.ok)throw new e(i?.message||"Failed to write the Quora comment.");if(!await t.evaluate(`(() => {
15
+ const editor = document.querySelector('[data-pp-quora-editor="1"]');
16
+ let scope = editor;
17
+ const isPost = (button) => /^(post|发布|publicar|publier|veröffentlichen)$/i.test((button.innerText || '').trim());
18
+ while (scope && !Array.from(scope.querySelectorAll('button')).some(isPost)) scope = scope.parentElement;
19
+ const button = scope && Array.from(scope.querySelectorAll('button')).find(isPost);
20
+ if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false;
21
+ button.click();
22
+ return true;
23
+ })()`))throw new e("Quora comment Post button was missing or disabled.");await t.wait(1);const s=await t.evaluate(p());if(String(s??"").trim()===o.trim())throw new e("Quora did not confirm the comment; check the answer before retrying.");return[{status:"success",outcome:"created",targetType:"answer",target:a.url,message:"Comment posted."}]}});
@@ -1,4 +1,4 @@
1
- import{ArgumentError as Q,AuthRequiredError as M,CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as f,Strategy as h}from"@jackwener/opencli/registry";import{assertLiteralContent as B}from"../_shared/content-guard.js";import{tagEditorJs as P,readEditorTextJs as j,insertEditorText as N,clickSubmitJs as I}from"./shared.js";const T=20,S=15,b=["\\banswer\\b","add answer","回答","写回答","répondre","responder","antworten","回答する"],J=["\\bpost\\b","submit","发布","提交","publier","publicar","veröffentlichen","投稿"],O=["\\bpost\\b","done","save","continue","confirm","确定","完成","保存","继续"];function U(){return`(() => {
1
+ import{ArgumentError as $,AuthRequiredError as f,CommandExecutionError as G}from"@jackwener/opencli/errors";import{cli as h,Strategy as B}from"@jackwener/opencli/registry";import{assertLiteralContent as I}from"../_shared/content-guard.js";import{tagEditorJs as P,readEditorTextJs as j,insertEditorText as N,clickSubmitJs as M,requireExecute as b,resolveTextInput as J}from"./shared.js";const O=20,A=15,T=["\\banswer\\b","add answer","回答","写回答","répondre","responder","antworten","回答する"],U=["\\bpost\\b","submit","发布","提交","publier","publicar","veröffentlichen","投稿"],y=["\\bpost\\b","done","save","continue","confirm","确定","完成","保存","继续"];function R(){return`(() => {
2
2
  const path = location.pathname || '';
3
3
  if (/\\blogin\\b/i.test(path + (location.search || ''))) return 'login';
4
4
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
@@ -9,14 +9,14 @@ import{ArgumentError as Q,AuthRequiredError as M,CommandExecutionError as z}from
9
9
  .find((el) => (el.getBoundingClientRect().height || 0) >= 24);
10
10
  if (editor) return 'editor';
11
11
  // 否则找「Answer」入口按钮点开
12
- const re = new RegExp(${JSON.stringify(b.join("|"))}, 'i');
12
+ const re = new RegExp(${JSON.stringify(T.join("|"))}, 'i');
13
13
  const btn = Array.from(document.querySelectorAll('button, [role="button"], a'))
14
14
  .filter(visible)
15
15
  .find((el) => re.test((el.getAttribute('aria-label') || '') + ' ' + (el.textContent || '')));
16
16
  if (btn) { btn.click(); return 'clicked'; }
17
17
  return 'wait';
18
- })()`}f({site:"quora",name:"create-answer",access:"write",description:"Publish an answer to a Quora question from your logged-in account. Pass the target question URL and the answer body (multi-line allowed). Write a genuine, substantive answer — Quora aggressively flags low-effort/automated content.",domain:"quora.com",strategy:h.UI,browser:!0,args:[{name:"questionUrl",type:"string",required:!0,positional:!0,help:"Target Quora question URL (https://www.quora.com/<question-slug>)"},{name:"content",type:"string",required:!0,positional:!0,help:"Answer body text (multi-line allowed)"}],columns:["status","questionUrl","message"],func:async(v,Z)=>{if(!v)throw new z("Browser session required for quora create-answer");const G=String(Z.questionUrl??"").trim();if(!G)throw new Q("Question URL is required.");let $;try{$=new URL(G)}catch{throw new Q(`Invalid question URL: ${G}`)}if(!/(^|\.)quora\.com$/.test($.hostname))throw new Q("URL must be a quora.com question URL.");const H=String(Z.content??"");B(H,{label:"回答正文"});if(!H.trim())throw new Q("Answer content cannot be empty.");await v.goto(G,{waitUntil:"none"});let D=!1;for(let K=0;K<T;K++){const X=await v.evaluate(U());if(X==="login")throw new M("www.quora.com","Quora 重定向到登录页——未登录");if(X==="editor"){D=!0;break}await v.wait(1)}if(!D)throw new z("未能打开回答编辑器——问题可能不可回答,或页面结构已变(需真机 recon 收紧锚点)。");if(await v.evaluate(P())!=="ready")throw new z("回答编辑器打标失败——未定位到可编辑正文区。");const F=await N(v,H);if(!F?.ok)throw new z(F?.message??"无法把正文写入 Quora 回答编辑器。");const W=await v.evaluate(I(J));if(!W?.ok)throw new z(W?.message??"未找到 Quora 回答提交按钮。");await v.wait(2);if((await v.evaluate(`(() => {
18
+ })()`}h({site:"quora",name:"create-answer",access:"write",description:"Publish an answer to a Quora question from your logged-in account. Pass the target question URL and the answer body (multi-line allowed). Write a genuine, substantive answer — Quora aggressively flags low-effort/automated content.",domain:"quora.com",strategy:B.UI,browser:!0,args:[{name:"questionUrl",type:"string",required:!0,positional:!0,help:"Target Quora question URL (https://www.quora.com/<question-slug>)"},{name:"content",type:"string",positional:!0,help:"Answer body text, or use --file for long content"},{name:"file",type:"string",help:"Read answer body from a local UTF-8 text file"},{name:"execute",type:"boolean",help:"Actually post the answer"}],columns:["status","questionUrl","message"],func:async(z,K)=>{if(!z)throw new G("Browser session required for quora create-answer");b(K);const H=String(K.questionUrl??"").trim();if(!H)throw new $("Question URL is required.");let v;try{v=new URL(H)}catch{throw new $(`Invalid question URL: ${H}`)}if(!/(^|\.)quora\.com$/.test(v.hostname))throw new $("URL must be a quora.com question URL.");const V=await J(K,{field:"content",label:"Answer content"});if(!K.file)I(V,{label:"回答正文",fileFlag:"--file"});await z.goto(H,{waitUntil:"none"});let D=!1;for(let Q=0;Q<O;Q++){const X=await z.evaluate(R());if(X==="login")throw new f("www.quora.com","Quora 重定向到登录页——未登录");if(X==="editor"){D=!0;break}await z.wait(1)}if(!D)throw new G("未能打开回答编辑器——问题可能不可回答,或页面结构已变(需真机 recon 收紧锚点)。");if(await z.evaluate(P())!=="ready")throw new G("回答编辑器打标失败——未定位到可编辑正文区。");const F=await N(z,V);if(!F?.ok)throw new G(F?.message??"无法把正文写入 Quora 回答编辑器。");const W=await z.evaluate(M(U));if(!W?.ok)throw new G(W?.message??"未找到 Quora 回答提交按钮。");await z.wait(2);if((await z.evaluate(`(() => {
19
19
  const dlg = document.querySelector('[role="dialog"]');
20
20
  if (!dlg || (dlg.offsetParent === null && dlg.getClientRects().length === 0)) return { present: false };
21
21
  return { present: true };
22
- })()`))?.present){await v.evaluate(I(O));await v.wait(2)}const V=(K)=>String(K||"").replace(/\s+/g," ").trim(),Y=await v.evaluate(j());if(Y!=null&&V(Y).length>0&&V(Y)===V(H))throw new z("Quora 回答提交后正文仍在编辑器中——提交被拦(限流/风控/无权限,或 credential 弹窗未确认)。回查后再决定是否重试。");return[{status:"success",questionUrl:G,message:"Answer posted."}]}});
22
+ })()`))?.present){await z.evaluate(M(y));await z.wait(2)}const Y=(Q)=>String(Q||"").replace(/\s+/g," ").trim(),Z=await z.evaluate(j());if(Z!=null&&Y(Z).length>0&&Y(Z)===Y(V))throw new G("Quora 回答提交后正文仍在编辑器中——提交被拦(限流/风控/无权限,或 credential 弹窗未确认)。回查后再决定是否重试。");return[{status:"success",questionUrl:H,message:"Answer posted."}]}});
@@ -1,4 +1,4 @@
1
- import{ArgumentError as G,AuthRequiredError as X,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as D,Strategy as M}from"@jackwener/opencli/registry";import{assertLiteralContent as P}from"../_shared/content-guard.js";import{QUORA_ORIGIN as Q,insertEditorText as f,readEditorTextJs as h,clickSubmitJs as B}from"./shared.js";const J=20,N=["add question","ask question","\\bask\\b","提问","添加问题","poser une question","preguntar","frage stellen"],U=["add question","\\bnext\\b","submit","done","\\bpost\\b","提交","下一步","发布","完成","添加问题"];function j(){return`(() => {
1
+ import{ArgumentError as X,AuthRequiredError as q,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as D,Strategy as M}from"@jackwener/opencli/registry";import{assertLiteralContent as P}from"../_shared/content-guard.js";import{QUORA_ORIGIN as Q,insertEditorText as f,readEditorTextJs as h,clickSubmitJs as B,requireExecute as J}from"./shared.js";const N=20,U=["add question","ask question","\\bask\\b","提问","添加问题","poser une question","preguntar","frage stellen"],j=["add question","\\bnext\\b","submit","done","\\bpost\\b","提交","下一步","发布","完成","添加问题"];function I(){return`(() => {
2
2
  if (/\\blogin\\b/i.test((location.pathname || '') + (location.search || ''))) return 'login';
3
3
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
4
4
  const dlg = document.querySelector('[role="dialog"]');
@@ -15,10 +15,10 @@ import{ArgumentError as G,AuthRequiredError as X,CommandExecutionError as F}from
15
15
  }
16
16
  return 'wait';
17
17
  }
18
- const re = new RegExp(${JSON.stringify(N.join("|"))}, 'i');
18
+ const re = new RegExp(${JSON.stringify(U.join("|"))}, 'i');
19
19
  const btn = Array.from(document.querySelectorAll('button, [role="button"], a'))
20
20
  .filter(visible)
21
21
  .find((el) => re.test((el.getAttribute('aria-label') || '') + ' ' + (el.textContent || '')));
22
22
  if (btn) { btn.click(); return 'clicked'; }
23
23
  return 'wait';
24
- })()`}D({site:"quora",name:"create-question",access:"write",description:"Post a new question to Quora from your logged-in account. Pass the question title. Ask a clear, genuine question — Quora aggressively flags low-effort/automated content.",domain:"quora.com",strategy:M.UI,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"The question title (a clear, complete question)"}],columns:["status","title","message"],func:async(v,q)=>{if(!v)throw new F("Browser session required for quora create-question");const z=String(q.title??"").trim();if(!z)throw new G("Question title is required.");P(z,{label:"问题标题"});await v.goto(`${Q}/`,{waitUntil:"none"});let W=!1;for(let H=0;H<J;H++){const $=await v.evaluate(j());if($==="login")throw new X("www.quora.com","Quora 重定向到登录页——未登录");if($==="editor"){W=!0;break}await v.wait(1)}if(!W)throw new F("未能打开 Quora 提问弹窗——顶栏入口或弹窗结构已变(需真机 recon 收紧锚点)。");const Y=await f(v,z);if(!Y?.ok)throw new F(Y?.message??"无法把标题写入 Quora 提问框。");const Z=await v.evaluate(B(U));if(!Z?.ok)throw new F(Z?.message??"未找到 Quora 提问提交按钮。");await v.wait(2);const K=(H)=>String(H||"").replace(/\s+/g," ").trim(),V=await v.evaluate(h());if(V!=null&&K(V).length>0&&K(V)===K(z))throw new F("Quora 提问提交后标题仍在框中——提交被拦(限流/风控/需选主题或身份未确认)。回查后再决定是否重试。");return[{status:"success",title:z,message:"Question posted."}]}});
24
+ })()`}D({site:"quora",name:"create-question",access:"write",description:"Post a new question to Quora from your logged-in account. Pass the question title. Ask a clear, genuine question — Quora aggressively flags low-effort/automated content.",domain:"quora.com",strategy:M.UI,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"The question title (a clear, complete question)"},{name:"execute",type:"boolean",help:"Actually post the question"}],columns:["status","title","message"],func:async(v,W)=>{if(!v)throw new F("Browser session required for quora create-question");J(W);const z=String(W.title??"").trim();if(!z)throw new X("Question title is required.");P(z,{label:"问题标题"});await v.goto(`${Q}/`,{waitUntil:"none"});let Y=!1;for(let H=0;H<N;H++){const G=await v.evaluate(I());if(G==="login")throw new q("www.quora.com","Quora 重定向到登录页——未登录");if(G==="editor"){Y=!0;break}await v.wait(1)}if(!Y)throw new F("未能打开 Quora 提问弹窗——顶栏入口或弹窗结构已变(需真机 recon 收紧锚点)。");const Z=await f(v,z);if(!Z?.ok)throw new F(Z?.message??"无法把标题写入 Quora 提问框。");const $=await v.evaluate(B(j));if(!$?.ok)throw new F($?.message??"未找到 Quora 提问提交按钮。");await v.wait(2);const K=(H)=>String(H||"").replace(/\s+/g," ").trim(),V=await v.evaluate(h());if(V!=null&&K(V).length>0&&K(V)===K(z))throw new F("Quora 提问提交后标题仍在框中——提交被拦(限流/风控/需选主题或身份未确认)。回查后再决定是否重试。");return[{status:"success",title:z,message:"Question posted."}]}});
@@ -0,0 +1,15 @@
1
+ import{CommandExecutionError as n}from"@jackwener/opencli/errors";import{cli as l,Strategy as i}from"@jackwener/opencli/registry";import{normalizeQuoraUrl as s,openQuoraPage as u,requireExecute as c}from"./shared.js";const f=["follow","关注","seguir","suivre","folgen"],w=["following","unfollow","已关注","取消关注","siguiendo","abonné","gefolgt"];function a(e){return`(() => {
2
+ const clean = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
3
+ const following = new RegExp(${JSON.stringify(w.join("|"))}, 'i');
4
+ const follow = new RegExp('^(' + ${JSON.stringify(f.join("|"))} + ')(?:\\s*·.*)?$', 'i');
5
+ const candidates = Array.from(document.querySelectorAll('#mainContent button')).filter((button) => {
6
+ const text = clean((button.getAttribute('aria-label') || '') + ' ' + (button.innerText || ''));
7
+ return following.test(text) || follow.test(text);
8
+ });
9
+ const button = candidates[0];
10
+ if (!button) return { ok: false, message: 'Follow control missing' };
11
+ const state = clean((button.getAttribute('aria-label') || '') + ' ' + (button.innerText || ''));
12
+ if (following.test(state)) return { ok: true, already: true };
13
+ if (${JSON.stringify(e)}) button.click();
14
+ return { ok: true, already: false };
15
+ })()`}l({site:"quora",name:"follow",access:"write",description:"Follow a Quora profile or question through the visible UI.",domain:"www.quora.com",strategy:i.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora profile or question URL"},{name:"execute",type:"boolean",help:"Actually follow the target"}],columns:["status","outcome","targetType","target","message"],func:async(e,o)=>{c(o);const t=s(o.url,{allowProfile:!0,allowQuestion:!0,allowAnswer:!1});await u(e,t.url,"follow");const r=await e.evaluate(a(!0));if(!r?.ok)throw new n(`Quora ${t.kind} follow control was not found.`);if(r.already)return[{status:"success",outcome:"already_applied",targetType:t.kind,target:t.url,message:`${t.kind} was already followed.`}];await e.wait(1);if(!(await e.evaluate(a(!1)))?.already)throw new n(`Quora did not confirm the ${t.kind} follow; check the target before retrying.`);return[{status:"success",outcome:"applied",targetType:t.kind,target:t.url,message:`${t.kind} followed.`}]}});
@@ -0,0 +1 @@
1
+ import{EmptyResultError as s}from"@jackwener/opencli/errors";import{cli as n,Strategy as m}from"@jackwener/opencli/registry";import{evaluateQuoraDom as u,loadVisibleItems as f,normalizeLimit as w,normalizeProfileTarget as c,openQuoraPage as p}from"./shared.js";n({site:"quora",name:"followers",access:"read",description:"List followers of a Quora profile.",domain:"www.quora.com",strategy:m.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Profile slug or Quora profile URL"},{name:"limit",type:"int",default:20,help:"Number of followers (max 50)"}],columns:["rank","name","headline","followers","url"],func:async(r,o)=>{const e=c(o.user),l=w(o.limit,20,50);await p(r,`${e.url}/followers`,"followers");await f(r,'#mainContent a[href*="/profile/"]',l);const t=((await u(r,"followers"))?.rows||[]).slice(0,l).map((a,i)=>({...a,rank:i+1}));if(!t.length)throw new s("quora followers",`${e.slug} has no visible followers.`);return t}});
@@ -0,0 +1 @@
1
+ import{EmptyResultError as u}from"@jackwener/opencli/errors";import{cli as l,Strategy as m}from"@jackwener/opencli/registry";import{evaluateQuoraDom as c,loadVisibleItems as w,normalizeLimit as q,normalizeQuoraUrl as d,openQuoraPage as p}from"./shared.js";l({site:"quora",name:"question",access:"read",description:"Read a Quora question and its visible answers, including engagement metrics.",domain:"www.quora.com",strategy:m.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora question URL"},{name:"limit",type:"int",default:10,help:"Number of answers (max 50)"}],columns:["rank","question","author","excerpt","upvotes","comments","shares","published","url"],func:async(e,t)=>{const a=d(t.url,{allowProfile:!1,allowQuestion:!0,allowAnswer:!1}),o=q(t.limit,10,50);await p(e,a.url,"question");await w(e,'[class*="dom_annotate_question_answer_item_"]',o);const r=await c(e,"question"),i=(r?.rows||[]).slice(0,o).map((s,n)=>({...s,rank:n+1,question:r.title||s.question}));if(!i.length)throw new u("quora question","No visible answers were found for this question.");return i}});
@@ -0,0 +1 @@
1
+ import{ArgumentError as i,EmptyResultError as l}from"@jackwener/opencli/errors";import{cli as c,Strategy as m}from"@jackwener/opencli/registry";import{evaluateQuoraDom as p,normalizeLimit as h,openQuoraPage as q,loadVisibleItems as w}from"./shared.js";c({site:"quora",name:"search",access:"read",description:"Search Quora questions or answers from the visible logged-in search results.",domain:"www.quora.com",strategy:m.COOKIE,browser:!0,args:[{name:"query",type:"string",required:!0,positional:!0,help:"Search query"},{name:"type",type:"string",default:"question",choices:["question","answer"],help:"Result type: question or answer"},{name:"limit",type:"int",default:10,help:"Number of results (max 50)"}],columns:["rank","type","title","author","excerpt","answers","upvotes","comments","shares","published","url"],func:async(r,s)=>{const n=String(s.query??"").trim();if(!n)throw new i("Search query is required");const e=String(s.type??"question");if(!["question","answer"].includes(e))throw new i("--type must be question or answer");const a=h(s.limit,10,50),o=`https://www.quora.com/search?q=${encodeURIComponent(n)}&type=${e}`;await q(r,o,"search");await w(r,".puppeteer_test_question_title",a);const u=((await p(r,e==="answer"?"search-answer":"search-question"))?.rows||[]).slice(0,a).map((t)=>({rank:t.rank,type:e,title:e==="answer"?t.question:t.title,author:e==="answer"?t.author:null,excerpt:e==="answer"?t.excerpt:null,answers:e==="question"?t.answers:null,upvotes:e==="answer"?t.upvotes:null,comments:e==="answer"?t.comments:null,shares:e==="answer"?t.shares:null,published:e==="answer"?t.published:null,url:t.url}));if(!u.length)throw new l("quora search",`No ${e} results for "${n}"; the page structure may have changed.`);return u}});
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as P}from"@jackwener/opencli/errors";export const QUORA_ORIGIN="https://www.quora.com";export async function hasQuoraAuthCookie(f){return(await f.getCookies({url:QUORA_ORIGIN})).some((F)=>F.name==="m-b"&&F.value)}export async function verifyQuoraAuth(f){if(!await hasQuoraAuthCookie(f))throw new P("www.quora.com","Quora m-b cookie 缺失——当前是游客/未登录会话");await f.goto(QUORA_ORIGIN);await f.wait(2);const y=await f.evaluate("location.pathname + location.search");if(/\blogin\b/i.test(String(y||"")))throw new P("www.quora.com",`Quora 停在登录页 (${y})——已登出`);if(!await hasQuoraAuthCookie(f))throw new P("www.quora.com","Quora m-b cookie 在导航后消失——会话失效");return{logged_in:!0}}export const EDITOR_MARK='[data-pp-quora-editor="1"]';export function tagEditorJs(){return`(() => {
1
+ import{ArgumentError as M,AuthRequiredError as T,CommandExecutionError as _}from"@jackwener/opencli/errors";import{readFile as C}from"node:fs/promises";export const QUORA_ORIGIN="https://www.quora.com";export async function hasQuoraAuthCookie(H){return(await H.getCookies({url:QUORA_ORIGIN})).some((Y)=>Y.name==="m-b"&&Y.value)}export async function verifyQuoraAuth(H){if(!await hasQuoraAuthCookie(H))throw new T("www.quora.com","Quora m-b cookie 缺失——当前是游客/未登录会话");await H.goto(QUORA_ORIGIN);await H.wait(2);const J=await H.evaluate("location.pathname + location.search");if(/\blogin\b/i.test(String(J||"")))throw new T("www.quora.com",`Quora 停在登录页 (${J})——已登出`);if(!await hasQuoraAuthCookie(H))throw new T("www.quora.com","Quora m-b cookie 在导航后消失——会话失效");return{logged_in:!0}}export const EDITOR_MARK='[data-pp-quora-editor="1"]';export function tagEditorJs(){return`(() => {
2
2
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
3
3
  const scope = document.querySelector('[role="dialog"]') || document;
4
4
  const cands = Array.from(scope.querySelectorAll('[contenteditable="true"]'))
@@ -12,32 +12,47 @@ import{AuthRequiredError as P}from"@jackwener/opencli/errors";export const QUORA
12
12
  return 'ready';
13
13
  })()`}export function readEditorTextJs(){return`(() => {
14
14
  const ed = document.querySelector('${EDITOR_MARK}');
15
- return ed ? (ed.innerText || ed.textContent || '') : null;
16
- })()`}export function execInsertJs(f){return`(() => {
15
+ return ed ? ('value' in ed ? ed.value : (ed.innerText || ed.textContent || '')) : null;
16
+ })()`}export function execInsertJs(H){return`(() => {
17
17
  try {
18
18
  const ed = document.querySelector('${EDITOR_MARK}');
19
19
  if (!ed) return { ok: false, message: '编辑器已消失,无法写入。' };
20
20
  ed.focus();
21
21
  const normalize = (s) => String(s || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim();
22
- if (!document.execCommand('insertText', false, ${JSON.stringify(f)})) {
22
+ if (!document.execCommand('insertText', false, ${JSON.stringify(H)})) {
23
23
  const dt = new DataTransfer();
24
- dt.setData('text/plain', ${JSON.stringify(f)});
24
+ dt.setData('text/plain', ${JSON.stringify(H)});
25
25
  ed.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
26
26
  }
27
- const actual = ed.innerText || ed.textContent || '';
28
- if (normalize(actual).includes(normalize(${JSON.stringify(f)}))) return { ok: true };
27
+ const actual = 'value' in ed ? ed.value : (ed.innerText || ed.textContent || '');
28
+ if (normalize(actual).includes(normalize(${JSON.stringify(H)}))) return { ok: true };
29
29
  return { ok: false, message: '写入后未在编辑器中校验到正文。', actualText: actual };
30
30
  } catch (e) { return { ok: false, message: String(e) }; }
31
- })()`}export async function insertEditorText(f,y){const F=(H)=>String(H||"").replace(/ /g," ").replace(/\s+/g," ").trim(),S=[f.nativeType?.bind(f),f.insertText?.bind(f)].filter(Boolean);for(const H of S)try{await H(y);const B=await f.evaluate(readEditorTextJs());if(B!=null&&F(B).includes(F(y)))return{ok:!0}}catch(B){const L=(B instanceof Error?B.message:String(B)).toLowerCase();if(!(L.includes("unknown action")||L.includes("not supported")||L.includes("no inserted flag")))throw B}return f.evaluate(execInsertJs(y))}export function clickSubmitJs(f){return`(() => {
31
+ })()`}export async function insertEditorText(H,J){const Y=(L)=>String(L||"").replace(/ /g," ").replace(/\s+/g," ").trim(),X=[H.nativeType?.bind(H),H.insertText?.bind(H)].filter(Boolean);for(const L of X)try{await L(J);const $=await H.evaluate(readEditorTextJs());if($!=null&&Y($).includes(Y(J)))return{ok:!0}}catch($){const U=($ instanceof Error?$.message:String($)).toLowerCase();if(!(U.includes("unknown action")||U.includes("not supported")||U.includes("no inserted flag")))throw $}return H.evaluate(execInsertJs(J))}export function clickSubmitJs(H){return`(() => {
32
32
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
33
- const re = new RegExp(${JSON.stringify(f.join("|"))}, 'i');
33
+ const re = new RegExp(${JSON.stringify(H.join("|"))}, 'i');
34
34
  const scope = document.querySelector('[role="dialog"]') || document;
35
35
  const btns = Array.from(scope.querySelectorAll('button, [role="button"]')).filter(visible);
36
36
  // 文案匹配至多兜底:优先取未禁用且文案命中的按钮
37
37
  const enabled = (el) => !el.disabled && el.getAttribute('aria-disabled') !== 'true';
38
- const hit = btns.find((el) => enabled(el)
38
+ const hit = scope.querySelector('button.puppeteer_test_modal_submit:not([disabled])')
39
+ || btns.find((el) => enabled(el)
39
40
  && re.test((el.getAttribute('aria-label') || '') + ' ' + (el.textContent || '')));
40
41
  if (!hit) return { ok: false, message: '未找到可用的提交按钮(可能未渲染/被禁用/文案不在兜底名单)。' };
41
42
  hit.click();
42
43
  return { ok: true };
43
- })()`}
44
+ })()`}export function requireExecute(H){if(H.execute!==!0)throw new M("Refusing to write without --execute. Review the target and content, then rerun with --execute.")}export async function resolveTextInput(H,{field:J="content",label:Y="content"}={}){const X=H[J]==null?"":String(H[J]),L=H.file==null?"":String(H.file).trim();if(X&&L)throw new M(`Use either <${J}> or --file, not both`);let $=X;if(L)try{$=await C(L,"utf8")}catch(D){throw new M(`Cannot read --file ${L}: ${D instanceof Error?D.message:String(D)}`)}if(!$.trim())throw new M(`${Y} cannot be empty`);return $}export function normalizeLimit(H,J=10,Y=50){const X=H==null?J:Number(H);if(!Number.isInteger(X)||X<1)throw new M("--limit must be a positive integer");if(X>Y)throw new M(`--limit must be <= ${Y}`);return X}export function parseCompactCount(H){const Y=String(H??"").trim().replace(/,/g,"").match(/(-?\d+(?:\.\d+)?)\s*([KMB])?/i);if(!Y)return null;const X={K:1000,M:1e6,B:1e9}[String(Y[2]||"").toUpperCase()]||1;return Math.round(Number(Y[1])*X)}export function normalizeQuoraUrl(H,{allowProfile:J=!0,allowQuestion:Y=!0,allowAnswer:X=!0}={}){const L=String(H??"").trim();if(!L)throw new M("A Quora URL is required");let $;try{$=new URL(L.startsWith("/")?`${QUORA_ORIGIN}${L}`:L)}catch{throw new M(`Invalid Quora URL: ${L}`)}if($.protocol!=="https:"||$.username||$.password||$.port||!/(^|\.)quora\.com$/i.test($.hostname))throw new M("URL must be an https://*.quora.com URL");const D=$.pathname.replace(/\/+$/,"")||"/",U=/^\/profile\/[^/]+$/i.test(D),y=/^\/(?!profile\/|topic\/|search$|answer$|stats$)[^/]+\/answers?\/[^/]+$/i.test(D),j=/^\/(?:unanswered\/)?(?!profile\/|topic\/|search$|answer$|stats$)[^/]+$/i.test(D);if((!J||!U)&&(!X||!y)&&(!Y||!j))throw new M("Expected a Quora profile, question, or answer URL supported by this command");$.hash="";return{url:$.toString(),path:D,kind:U?"profile":y?"answer":"question"}}export function normalizeProfileTarget(H){const J=String(H??"").trim();if(!J)throw new M("Profile slug or URL is required");if(/^[^/?#\s]+$/.test(J))return{slug:J,url:`${QUORA_ORIGIN}/profile/${encodeURIComponent(J)}`};const Y=normalizeQuoraUrl(J,{allowProfile:!0,allowQuestion:!1,allowAnswer:!1});return{slug:decodeURIComponent(Y.path.split("/").pop()),url:Y.url}}export async function waitForSelector(H,J,Y,X=20){for(let L=0;L<X;L++){if(await H.evaluate(`(() => !!document.querySelector(${JSON.stringify(J)}))()`))return;await H.wait(1)}throw new _(`Timed out waiting for ${Y}; Quora's visible page structure may have changed.`)}export async function openQuoraPage(H,J,Y){if(!H)throw new _(`Browser session required for quora ${Y}`);try{await H.goto(J,{waitUntil:"none"})}catch(L){throw new _(`Failed to open Quora ${Y}: ${L instanceof Error?L.message:String(L)}`)}const X=new URL(J).pathname.replace(/\/+$/,"")||"/";for(let L=0;L<20;L++){const $=await H.evaluate(`(() => ({
45
+ login: /\\blogin\\b/i.test((location.pathname || '') + (location.search || '')),
46
+ ready: !!document.querySelector('#mainContent'),
47
+ path: (location.pathname || '/').replace(/\\/+$/, '') || '/',
48
+ }))()`);if($?.login)throw new T("www.quora.com",`Quora login is required for ${Y}`);if($?.ready&&$?.path===X)return;await H.wait(1)}throw new _(`Timed out opening Quora ${Y}`)}export async function loadVisibleItems(H,J,Y){let X=-1;for(let L=0;L<12;L++){const $=await H.evaluate(`(() => document.querySelectorAll(${JSON.stringify(J)}).length)()`);if($>=Y)return $;if($===X&&L>=2)return $;X=$;await H.evaluate("(() => { window.scrollTo(0, document.body.scrollHeight); return true; })()");await H.wait(1)}return H.evaluate(`(() => document.querySelectorAll(${JSON.stringify(J)}).length)()`)}export function extractQuoraDom(H,J="#mainContent"){const Y=document.querySelector(J);if(!Y)return{ok:!1,reason:"main-content-missing",rows:[]};const X=(j)=>String(j??"").replace(/\u00a0/g," ").replace(/[ \t]+/g," ").replace(/\n{3,}/g,`
49
+
50
+ `).trim(),L=(j)=>{const W=X(j).replace(/,/g,"").match(/(-?\d+(?:\.\d+)?)\s*([KMB])?/i);if(!W)return null;const Z={K:1000,M:1e6,B:1e9};return Math.round(Number(W[1])*(Z[String(W[2]||"").toUpperCase()]||1))},$=(j)=>{if(!j)return"";try{return new URL(j,location.origin).toString()}catch{return""}},D=(j,W)=>{const Z=W==="upvotes"?"button.puppeteer_test_votable_upvote_button":W==="comments"?".dom_annotate_answer_action_bar_comment button":".dom_annotate_answer_action_bar_share button",B=j.querySelector(Z)||Array.from(j.querySelectorAll("button[aria-label]")).find((G)=>{const q=G.getAttribute("aria-label")||"";return W==="upvotes"?/upvot|赞同|votar|vote/i.test(q):W==="comments"?/comment|评论|comentario|commentaire|kommentar/i.test(q):/share|分享|compartir|partager|teilen/i.test(q)});if(!B)return 0;const K=B.getAttribute("aria-label")||"";return L(`${K} ${B.innerText||B.textContent||""}`)||0},U=(j,W)=>{let Z=j;while(Z.parentElement&&Z.parentElement!==Y&&Z.parentElement.querySelectorAll(W).length===1)Z=Z.parentElement;return Z},y=()=>Array.from(Y.querySelectorAll(".puppeteer_test_answer_content")).map((j,W)=>{const Z=U(j,".puppeteer_test_answer_content"),z=Array.from(Z.querySelectorAll('a[href*="/profile/"]')).find((q)=>X(q.innerText||q.textContent)),B=Z.querySelector("a.answer_timestamp[href]"),K=Z.querySelector(".puppeteer_test_question_title"),G=X(Z.innerText||Z.textContent).split(`
51
+ `).filter(Boolean);return{rank:W+1,question:X(K?.innerText||K?.textContent),author:X(z?.innerText||z?.textContent||G[0]||"Anonymous"),excerpt:X(j.innerText||j.textContent).replace(/\s*\(more\)\s*$/i,""),upvotes:D(Z,"upvotes"),comments:D(Z,"comments"),shares:D(Z,"shares"),published:X(B?.innerText||B?.textContent),url:$(B?.getAttribute("href")||"")}});if(H==="question"||H==="answers"||H==="search-answer")return{ok:!0,title:X(Y.querySelector(".puppeteer_test_question_title")?.innerText||Y.querySelector(".puppeteer_test_question_title")?.textContent),rows:y()};if(H==="search-question"){const j=new Set,W=[];for(const Z of Y.querySelectorAll(".puppeteer_test_question_title")){const z=Z.closest("a[href]")||Z.parentElement?.closest("a[href]"),B=$(z?.getAttribute("href")||"");if(!B||j.has(B))continue;j.add(B);const K=U(Z,".puppeteer_test_question_title"),G=Array.from(K.querySelectorAll("a[href]")).map((q)=>X(q.innerText||q.textContent)).find((q)=>/answers?/i.test(q))||"";W.push({rank:W.length+1,title:X(Z.innerText||Z.textContent),answers:L(G)||0,url:B})}return{ok:!0,rows:W}}if(H==="answer-detail"){const j=Y.querySelector("a.answer_timestamp[href]"),W=Y.querySelector("button.puppeteer_test_votable_upvote_button[aria-label]"),Z=Array.from(Y.querySelectorAll('a[href*="/profile/"]')).find((G)=>X(G.innerText||G.textContent)),z=Y.querySelector(".puppeteer_test_question_title"),B=Array.from(Y.querySelectorAll("p, li")).filter((G)=>{if(!j||!W)return!1;return Boolean(j.compareDocumentPosition(G)&4)&&Boolean(G.compareDocumentPosition(W)&4)}).map((G)=>X(G.innerText||G.textContent)).filter(Boolean),K=B.length?B.join(`
52
+
53
+ `):X(Y.querySelector(".puppeteer_test_answer_content")?.innerText||Y.querySelector(".puppeteer_test_answer_content")?.textContent);return{ok:!0,rows:[{question:X(z?.innerText||z?.textContent),author:X(Z?.innerText||Z?.textContent||"Anonymous"),content:K,upvotes:D(Y,"upvotes"),comments:D(Y,"comments"),shares:D(Y,"shares"),published:X(j?.innerText||j?.textContent),url:$(j?.getAttribute("href")||location.href)}]}}if(H==="answer-queue")return{ok:!0,rows:Array.from(Y.querySelectorAll(".puppeteer_test_question_component_base")).map((W,Z)=>{const z=W.querySelector(".puppeteer_test_question_title"),B=z?.closest("a[href]")||z?.parentElement?.closest("a[href]"),K=Array.from(W.querySelectorAll("a[href]")).map((G)=>X(G.innerText||G.textContent)).find((G)=>/answers?|no answer/i.test(G))||"";return{rank:Z+1,title:X(z?.innerText||z?.textContent),answers:/no answer/i.test(K)?0:L(K)||0,url:$(B?.getAttribute("href")||"")}})};if(H==="profile"){const j=X(Y.innerText||Y.textContent),W=J==="#mainContent"?X(document.body.innerText||document.body.textContent):j,Z=j.split(`
54
+ `).map(X).filter(Boolean),z=X(document.title.replace(/\s*-\s*Quora\s*$/i,""))||Z[0]||"",B=Array.from(Y.querySelectorAll("div, span")).map((q)=>X(q.innerText||q.textContent)).filter((q)=>q&&q.length<=300),K=(q)=>L(j.match(q)?.[1]||""),G=L(W.match(/([\d.,]+\s*[KMB]?)\s+content views/i)?.[1]||"");return{ok:!0,rows:[{name:z,bio:B.find((q)=>q!==z&&!/followers?|following|answers?|questions?|posts?|profile|content views|this month/i.test(q))||Z.find((q)=>q!==z&&!/followers?|following|answers?|questions?|posts?|profile|content views|this month/i.test(q))||"",followers:K(/([\d.,]+\s*[KMB]?)\s+followers/i),following:K(/([\d.,]+\s*[KMB]?)\s+following/i),answers:K(/([\d.,]+\s*[KMB]?)\s+answers/i),questions:K(/([\d.,]+\s*[KMB]?)\s+questions/i),posts:K(/([\d.,]+\s*[KMB]?)\s+posts/i),contentViews:G,url:location.href.split(/[?#]/)[0].replace(/\/(?:answers|questions|posts|followers|following)\/?$/,"")}]}}if(H==="followers"){const j=new Set,W=[];for(const Z of Y.querySelectorAll('a[href*="/profile/"]')){const z=X(Z.innerText||Z.textContent),B=$(Z.getAttribute("href")||"");if(!z||!B||j.has(B))continue;let K=Z;while(K.parentElement&&K.parentElement!==Y){const F=K.parentElement;if(new Set(Array.from(F.querySelectorAll('a[href*="/profile/"]'),(I)=>$(I.getAttribute("href")||""))).size>1)break;K=F;if(/\d[\d.,]*\s*[KMB]?\s+followers?/i.test(X(K.innerText||K.textContent)))break}const G=X(K.innerText||K.textContent),q=G.match(/([\d.,]+\s*[KMB]?)\s+followers?/i),V=G.split(`
55
+ `).map(X).filter(Boolean),S=Array.from(K.querySelectorAll("div, span")).map((F)=>X(F.innerText||F.textContent)).filter((F)=>F&&F.length<=300);W.push({rank:W.length+1,name:z,headline:S.find((F)=>F!==z&&!/followers?|^follow$/i.test(F))||V.find((F)=>F!==z&&!/followers?|^follow$/i.test(F))||"",followers:L(q?.[1]||"")||0,url:B});j.add(B)}return{ok:!0,rows:W}}if(H==="stats"){const j=X(Y.innerText||Y.textContent),W=(z)=>{const B=j.match(new RegExp(`${z}\\s*[–—-]?\\s*([\\d.,]+\\s*[KMB]?)`,"i"));return L(B?.[1]||"")||0};return{ok:!0,rows:[{period:j.match(/Last\s+\d+\s+days|All time/i)?.[0]||"Current",views:W("Views"),upvotes:W("Upvotes"),comments:W("Comments"),shares:W("Shares \\(external\\)")}]}}if(H==="comments"){const j=new Set,W=[],Z=Array.from(Y.querySelectorAll("button")).filter((z)=>/^reply$/i.test(X(z.innerText||z.textContent)));for(const z of Z){let B=z;while(B.parentElement&&B.parentElement!==Y){const P=B.parentElement;if(Array.from(P.querySelectorAll("button")).filter((Q)=>/^reply$/i.test(X(Q.innerText||Q.textContent))).length!==1)break;B=P;if(B.querySelector('a[href*="/profile/"]'))break}const K=Array.from(B.querySelectorAll('a[href*="/profile/"]')).find((P)=>X(P.innerText||P.textContent)),G=X(K?.innerText||K?.textContent),q=$(K?.getAttribute("href")||""),V=X(B.innerText||B.textContent),S=`${q}:${V}`;if(!G||j.has(S))continue;const F=Array.from(B.querySelectorAll("a")).map((P)=>X(P.innerText||P.textContent)).find((P)=>/^(?:updated\s+)?(?:\d+[smhdwy]|mon|tue|wed|thu|fri|sat|sun|[A-Z][a-z]{2}\s+\d{1,2})$/i.test(P))||V.match(/(?:^|\s)(\d+[smhdwy]|mon|tue|wed|thu|fri|sat|sun)(?:\s|$)/i)?.[1]||"",O=Array.from(B.querySelectorAll("p")).map((P)=>X(P.innerText||P.textContent)).filter(Boolean),I=O.length?O.join(`
56
+ `):V.split(`
57
+ `).map(X).filter((P)=>P&&P!==G&&P!==F&&!/^reply$/i.test(P)).join(`
58
+ `);W.push({rank:W.length+1,author:G,content:I,upvotes:D(B,"upvotes"),published:F,authorUrl:q});j.add(S)}return{ok:!0,rows:W}}return{ok:!1,reason:`unsupported-mode:${H}`,rows:[]}}export async function evaluateQuoraDom(H,J,Y="#mainContent"){return H.evaluate(`(${extractQuoraDom.toString()})(${JSON.stringify(J)}, ${JSON.stringify(Y)})`)}
@@ -0,0 +1 @@
1
+ import{CommandExecutionError as r}from"@jackwener/opencli/errors";import{cli as e,Strategy as a}from"@jackwener/opencli/registry";import{evaluateQuoraDom as s,openQuoraPage as n,waitForSelector as c}from"./shared.js";e({site:"quora",name:"stats",access:"read",description:"Read the logged-in account's Quora content overview for the current stats period.",domain:"www.quora.com",strategy:a.COOKIE,browser:!0,args:[],columns:["period","views","upvotes","comments","shares"],func:async(t)=>{await n(t,"https://www.quora.com/stats","content stats");await c(t,"#mainContent","stats content");const o=(await s(t,"stats"))?.rows?.[0];if(!o)throw new r("Quora stats overview was not found.");return[o]}});
@@ -0,0 +1,14 @@
1
+ import{CommandExecutionError as a}from"@jackwener/opencli/errors";import{cli as u,Strategy as n}from"@jackwener/opencli/registry";import{normalizeQuoraUrl as s,openQuoraPage as i,requireExecute as l,waitForSelector as p}from"./shared.js";u({site:"quora",name:"upvote",access:"write",description:"Upvote one Quora answer through the visible logged-in UI. Idempotent when already upvoted.",domain:"www.quora.com",strategy:n.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora answer URL"},{name:"execute",type:"boolean",help:"Actually apply the upvote"}],columns:["status","outcome","targetType","target","message"],func:async(t,o)=>{l(o);const e=s(o.url,{allowProfile:!1,allowQuestion:!1,allowAnswer:!0});await i(t,e.url,"upvote");await p(t,"button.puppeteer_test_votable_upvote_button","answer upvote button");const r=await t.evaluate(`(() => {
2
+ const button = document.querySelector('button.puppeteer_test_votable_upvote_button');
3
+ if (!button) return { ok: false, message: 'Upvote button missing' };
4
+ const state = ((button.getAttribute('aria-label') || '') + ' ' + (button.innerText || '')).trim();
5
+ const selected = button.getAttribute('aria-pressed') === 'true' || /upvoted|remove upvote/i.test(state);
6
+ if (selected) return { ok: true, already: true };
7
+ button.click();
8
+ return { ok: true, already: false };
9
+ })()`);if(!r?.ok)throw new a(r?.message||"Failed to find the Quora upvote control.");if(r.already)return[{status:"success",outcome:"already_applied",targetType:"answer",target:e.url,message:"Answer was already upvoted."}];await t.wait(1);if(!await t.evaluate(`(() => {
10
+ const button = document.querySelector('button.puppeteer_test_votable_upvote_button');
11
+ if (!button) return false;
12
+ const state = ((button.getAttribute('aria-label') || '') + ' ' + (button.innerText || '')).trim();
13
+ return button.getAttribute('aria-pressed') === 'true' || /upvoted|remove upvote/i.test(state);
14
+ })()`))throw new a("Quora did not confirm the upvote; check the answer before retrying.");return[{status:"success",outcome:"applied",targetType:"answer",target:e.url,message:"Answer upvoted."}]}});
@@ -0,0 +1 @@
1
+ import{EmptyResultError as n}from"@jackwener/opencli/errors";import{cli as u,Strategy as l}from"@jackwener/opencli/registry";import{evaluateQuoraDom as m,loadVisibleItems as w,normalizeLimit as c,normalizeProfileTarget as p,openQuoraPage as f}from"./shared.js";u({site:"quora",name:"user-answers",access:"read",description:"List answers published by a Quora profile with engagement metrics.",domain:"www.quora.com",strategy:l.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Profile slug or Quora profile URL"},{name:"limit",type:"int",default:10,help:"Number of answers (max 50)"}],columns:["rank","question","author","excerpt","upvotes","comments","shares","published","url"],func:async(e,r)=>{const t=p(r.user),s=c(r.limit,10,50);await f(e,`${t.url}/answers`,"user answers");await w(e,".puppeteer_test_answer_content",s);const a=((await m(e,"answers"))?.rows||[]).slice(0,s).map((o,i)=>({...o,rank:i+1}));if(!a.length)throw new n("quora user-answers",`${t.slug} has no visible answers.`);return a}});
@@ -0,0 +1 @@
1
+ import{CommandExecutionError as t}from"@jackwener/opencli/errors";import{cli as i,Strategy as n}from"@jackwener/opencli/registry";import{evaluateQuoraDom as s,normalizeProfileTarget as l,openQuoraPage as u}from"./shared.js";i({site:"quora",name:"user",access:"read",description:"Read a Quora profile with follower, content and view totals.",domain:"www.quora.com",strategy:n.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Profile slug or Quora profile URL"}],columns:["name","bio","followers","following","answers","questions","posts","contentViews","url"],func:async(r,o)=>{const a=l(o.user);await u(r,a.url,"profile");await r.wait(2);const e=(await s(r,"profile"))?.rows?.[0];if(!e?.name)throw new t("Quora profile header was not found; the page may be unavailable or changed.");return[e]}});
@@ -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}}