publishport-opencli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/README.md +61 -61
  2. package/README.zh-CN.md +68 -68
  3. package/cli-manifest.json +502 -59
  4. package/clis/1688/shared.js +2 -2
  5. package/clis/36kr/news.js +1 -1
  6. package/clis/_atlassian/shared.js +1 -1
  7. package/clis/_shared/article/auth.js +1 -1
  8. package/clis/_shared/article/publish.js +1 -1
  9. package/clis/_shared/content-guard.js +1 -1
  10. package/clis/_shared/self-hosted-sites.js +2 -0
  11. package/clis/_shared/site-auth.js +1 -1
  12. package/clis/_shared/token-auth.js +2 -2
  13. package/clis/_shared/video-publish.js +1 -1
  14. package/clis/aibase/news.js +1 -1
  15. package/clis/amazon/shared.js +2 -2
  16. package/clis/antigravity/SKILL.md +12 -12
  17. package/clis/antigravity/storage.js +1 -1
  18. package/clis/apple-podcasts/episodes.js +1 -1
  19. package/clis/archive/item.js +1 -1
  20. package/clis/archive/search.js +1 -1
  21. package/clis/archive/snapshots.js +1 -1
  22. package/clis/archive/wayback.js +1 -1
  23. package/clis/arxiv/author.js +1 -1
  24. package/clis/baijiahao/publish.js +1 -1
  25. package/clis/bbc/utils.js +1 -1
  26. package/clis/bloomberg/businessweek.js +1 -1
  27. package/clis/bloomberg/utils.js +1 -1
  28. package/clis/booking/search.js +1 -1
  29. package/clis/boss/utils.js +1 -1
  30. package/clis/chatgpt/ask.js +1 -1
  31. package/clis/chatgpt/detail.js +1 -1
  32. package/clis/chatgpt/history.js +1 -1
  33. package/clis/chatgpt/project-file-add.js +1 -1
  34. package/clis/chatgpt/project-list.js +1 -1
  35. package/clis/chatgpt/utils.js +2 -2
  36. package/clis/chess/utils.js +1 -1
  37. package/clis/claude/ask.js +1 -1
  38. package/clis/claude/history.js +1 -1
  39. package/clis/claude/utils.js +1 -1
  40. package/clis/codex/extract-diff.js +1 -1
  41. package/clis/coingecko/coin.js +1 -1
  42. package/clis/crates/utils.js +1 -1
  43. package/clis/csdn/stats.js +1 -1
  44. package/clis/dblp/utils.js +1 -1
  45. package/clis/defillama/utils.js +1 -1
  46. package/clis/devto/publish.js +1 -1
  47. package/clis/discord-app/utils.js +1 -1
  48. package/clis/dockerhub/utils.js +1 -1
  49. package/clis/douban/utils.js +1 -1
  50. package/clis/douyin/_shared/tos-upload.js +1 -1
  51. package/clis/douyin/hashtag.js +1 -1
  52. package/clis/douyin/publish-image.js +1 -1
  53. package/clis/douyin/publish.js +1 -1
  54. package/clis/eastmoney/announcement.js +1 -1
  55. package/clis/eastmoney/rank.js +1 -1
  56. package/clis/eastmoney/sectors.js +1 -1
  57. package/clis/endoflife/utils.js +1 -1
  58. package/clis/flathub/utils.js +1 -1
  59. package/clis/gemini/detail.js +1 -1
  60. package/clis/gemini/read.js +1 -1
  61. package/clis/geogebra/add-circle.js +1 -1
  62. package/clis/geogebra/add-line.js +1 -1
  63. package/clis/geogebra/add-point.js +1 -1
  64. package/clis/geogebra/add-polygon.js +1 -1
  65. package/clis/geogebra/eval.js +1 -1
  66. package/clis/geogebra/hexagon.js +1 -1
  67. package/clis/geogebra/info.js +1 -1
  68. package/clis/geogebra/triangle.js +1 -1
  69. package/clis/ghost/login.js +1 -0
  70. package/clis/ghost/publish.js +1 -1
  71. package/clis/ghost/shared.js +24 -1
  72. package/clis/ghost/sites.js +1 -0
  73. package/clis/ghost/whoami.js +1 -1
  74. package/clis/gitee/user.js +2 -2
  75. package/clis/github-trending/repos.js +1 -1
  76. package/clis/goproxy/utils.js +1 -1
  77. package/clis/grok/export-all.js +1 -1
  78. package/clis/grok/export.js +1 -1
  79. package/clis/hashnode/publish.js +1 -1
  80. package/clis/hf/datasets.js +1 -1
  81. package/clis/hf/models.js +1 -1
  82. package/clis/hf/paper.js +1 -1
  83. package/clis/hf/spaces.js +1 -1
  84. package/clis/homebrew/utils.js +1 -1
  85. package/clis/instagram/note.js +1 -1
  86. package/clis/jimeng/generate.js +1 -1
  87. package/clis/juejin/utils.js +1 -1
  88. package/clis/kuaishou/publish.js +1 -1
  89. package/clis/lesswrong/tag.js +1 -1
  90. package/clis/lichess/utils.js +1 -1
  91. package/clis/linkedin/search.js +2 -2
  92. package/clis/linux-do/feed.js +2 -2
  93. package/clis/lobsters/domain.js +1 -1
  94. package/clis/mastodon/login.js +1 -0
  95. package/clis/mastodon/post.js +1 -1
  96. package/clis/mastodon/shared.js +10 -10
  97. package/clis/mastodon/sites.js +1 -0
  98. package/clis/mastodon/whoami.js +1 -1
  99. package/clis/maven/utils.js +1 -1
  100. package/clis/mdn/search.js +1 -1
  101. package/clis/medium/tag.js +1 -1
  102. package/clis/notebooklm/current.js +1 -1
  103. package/clis/notebooklm/get.js +1 -1
  104. package/clis/notebooklm/history.js +1 -1
  105. package/clis/notebooklm/note-list.js +1 -1
  106. package/clis/notebooklm/notes-get.js +1 -1
  107. package/clis/notebooklm/open.js +1 -1
  108. package/clis/notebooklm/source-fulltext.js +1 -1
  109. package/clis/notebooklm/source-get.js +1 -1
  110. package/clis/notebooklm/source-guide.js +1 -1
  111. package/clis/notebooklm/source-list.js +1 -1
  112. package/clis/notebooklm/summary.js +1 -1
  113. package/clis/notebooklm/utils.js +1 -1
  114. package/clis/npm/utils.js +1 -1
  115. package/clis/nuget/utils.js +1 -1
  116. package/clis/nvd/cve.js +1 -1
  117. package/clis/oeis/utils.js +1 -1
  118. package/clis/ones/common.js +2 -2
  119. package/clis/ones/me.js +1 -1
  120. package/clis/ones/tasks.js +1 -1
  121. package/clis/ones/token-info.js +1 -1
  122. package/clis/ones/worklog.js +1 -1
  123. package/clis/openalex/utils.js +1 -1
  124. package/clis/oschina/stats.js +1 -1
  125. package/clis/osv/utils.js +1 -1
  126. package/clis/packagist/utils.js +1 -1
  127. package/clis/pixiv/detail.js +1 -1
  128. package/clis/pixiv/user.js +1 -1
  129. package/clis/producthunt/utils.js +1 -1
  130. package/clis/pypi/utils.js +1 -1
  131. package/clis/qiita/gql.js +2 -2
  132. package/clis/rest-countries/utils.js +1 -1
  133. package/clis/rfc/utils.js +1 -1
  134. package/clis/rubygems/utils.js +1 -1
  135. package/clis/semanticscholar/utils.js +1 -1
  136. package/clis/slock/errors.js +1 -1
  137. package/clis/spotify/spotify.js +1 -1
  138. package/clis/spotify/utils.js +1 -1
  139. package/clis/stackoverflow/utils.js +1 -1
  140. package/clis/steam/utils.js +1 -1
  141. package/clis/substack/auth.js +2 -2
  142. package/clis/suno/generate.js +2 -2
  143. package/clis/suno/list.js +3 -3
  144. package/clis/telegram/send.js +1 -1
  145. package/clis/tiktok/creator-videos.js +1 -1
  146. package/clis/tiktok/publish.js +1 -1
  147. package/clis/tiktok/utils.js +1 -1
  148. package/clis/trae-cn/activity.js +1 -1
  149. package/clis/trae-cn/approve.js +1 -1
  150. package/clis/trae-cn/ask.js +1 -1
  151. package/clis/trae-cn/dump.js +1 -1
  152. package/clis/trae-cn/export.js +1 -1
  153. package/clis/trae-cn/model.js +1 -1
  154. package/clis/trae-cn/new.js +1 -1
  155. package/clis/trae-cn/read.js +1 -1
  156. package/clis/trae-cn/screenshot.js +1 -1
  157. package/clis/trae-cn/select-model.js +1 -1
  158. package/clis/trae-cn/send.js +1 -1
  159. package/clis/trae-cn/setup.js +1 -1
  160. package/clis/trae-cn/status.js +1 -1
  161. package/clis/trae-cn/targets.js +1 -1
  162. package/clis/trae-cn/watch.js +1 -1
  163. package/clis/trae-solo/state-fs.js +1 -1
  164. package/clis/tumblr/publish.js +1 -1
  165. package/clis/tvmaze/utils.js +1 -1
  166. package/clis/twitter/bookmark-folder.js +1 -1
  167. package/clis/twitter/download.js +1 -1
  168. package/clis/twitter/followers.js +2 -2
  169. package/clis/twitter/following.js +2 -2
  170. package/clis/twitter/likes.js +1 -1
  171. package/clis/twitter/list-add-batch.js +1 -1
  172. package/clis/twitter/list-add-core.js +1 -1
  173. package/clis/twitter/list-add.js +1 -1
  174. package/clis/twitter/list-create.js +1 -1
  175. package/clis/twitter/list-delete.js +3 -3
  176. package/clis/twitter/list-remove-batch.js +1 -1
  177. package/clis/twitter/list-remove.js +1 -1
  178. package/clis/twitter/list-tweets.js +1 -1
  179. package/clis/twitter/profile.js +1 -1
  180. package/clis/twitter/search.js +1 -1
  181. package/clis/twitter/shared.js +1 -1
  182. package/clis/twitter/tweets.js +1 -1
  183. package/clis/uisdc/news.js +1 -1
  184. package/clis/v2ex/daily.js +2 -2
  185. package/clis/v2ex/me.js +2 -2
  186. package/clis/v2ex/notifications.js +1 -1
  187. package/clis/wikidata/utils.js +1 -1
  188. package/clis/wikipedia/page.js +2 -2
  189. package/clis/wikipedia/summary.js +1 -1
  190. package/clis/wikipedia/utils.js +1 -1
  191. package/clis/wordpress/login.js +1 -1
  192. package/clis/wordpress/publish.js +1 -1
  193. package/clis/wordpress/shared.js +3 -3
  194. package/clis/wordpress/sites.js +1 -0
  195. package/clis/wordpress/whoami.js +1 -1
  196. package/clis/woshipm/stats.js +1 -1
  197. package/clis/xiaoe/content.js +1 -1
  198. package/clis/xiaohongshu/draft-delete.js +1 -1
  199. package/clis/xiaohongshu/draft-open.js +1 -1
  200. package/clis/xiaohongshu/draft-utils.js +2 -2
  201. package/clis/xiaohongshu/publish-video.js +1 -1
  202. package/clis/xiaohongshu/publish.js +1 -1
  203. package/clis/xiaoyuzhou/auth.js +1 -1
  204. package/clis/xiaoyuzhou/transcript.js +1 -1
  205. package/clis/yollomi/background.js +1 -1
  206. package/clis/yollomi/edit.js +1 -1
  207. package/clis/yollomi/generate.js +1 -1
  208. package/clis/yollomi/try-on.js +1 -1
  209. package/clis/youtube/publish.js +1 -1
  210. package/clis/zhihu/answer-comments.js +1 -1
  211. package/clis/zhihu/answer-detail.js +1 -1
  212. package/clis/zhihu/collection.js +2 -2
  213. package/clis/zhihu/collections.js +1 -1
  214. package/clis/zhihu/question.js +1 -1
  215. package/clis/zhihu/search.js +1 -1
  216. package/clis/zhihu/target.js +1 -1
  217. package/clis/zhihu/user-arg.js +1 -1
  218. package/dist/src/adapter-shadow.js +1 -1
  219. package/dist/src/browser/analyze.js +1 -1
  220. package/dist/src/browser/article-extract.d.ts +1 -1
  221. package/dist/src/browser/base-page.js +3 -3
  222. package/dist/src/browser/cdp.js +1 -1
  223. package/dist/src/browser/daemon-client.d.ts +1 -1
  224. package/dist/src/browser/daemon-lifecycle.js +6 -6
  225. package/dist/src/browser/daemon-version.js +1 -1
  226. package/dist/src/browser/dom-snapshot.d.ts +1 -1
  227. package/dist/src/browser/dom-snapshot.js +2 -2
  228. package/dist/src/browser/errors.js +2 -2
  229. package/dist/src/browser/network-cache.js +1 -1
  230. package/dist/src/browser/profile.js +1 -1
  231. package/dist/src/browser/target-resolver.js +4 -4
  232. package/dist/src/browser/verify-fixture.js +1 -1
  233. package/dist/src/browser-session-lock.js +1 -1
  234. package/dist/src/build-manifest.d.ts +1 -1
  235. package/dist/src/build-manifest.js +1 -1
  236. package/dist/src/cli-argv-preprocess.d.ts +4 -4
  237. package/dist/src/cli-argv-preprocess.js +1 -1
  238. package/dist/src/cli.d.ts +1 -1
  239. package/dist/src/cli.js +19 -19
  240. package/dist/src/commanderAdapter.js +1 -1
  241. package/dist/src/commands/auth.js +2 -2
  242. package/dist/src/commands/daemon.d.ts +4 -4
  243. package/dist/src/commands/daemon.js +1 -1
  244. package/dist/src/completion-shared.js +12 -12
  245. package/dist/src/completion.d.ts +2 -2
  246. package/dist/src/constants.d.ts +1 -1
  247. package/dist/src/constants.js +1 -1
  248. package/dist/src/daemon-utils.d.ts +1 -1
  249. package/dist/src/daemon-utils.js +1 -1
  250. package/dist/src/daemon.d.ts +2 -2
  251. package/dist/src/daemon.js +1 -1
  252. package/dist/src/discovery.d.ts +7 -7
  253. package/dist/src/discovery.js +2 -2
  254. package/dist/src/doctor.d.ts +1 -1
  255. package/dist/src/doctor.js +6 -6
  256. package/dist/src/electron-apps.d.ts +1 -1
  257. package/dist/src/electron-apps.js +1 -1
  258. package/dist/src/errors.d.ts +2 -2
  259. package/dist/src/errors.js +1 -1
  260. package/dist/src/execution.js +1 -1
  261. package/dist/src/external.d.ts +1 -1
  262. package/dist/src/external.js +1 -1
  263. package/dist/src/help.d.ts +1 -1
  264. package/dist/src/help.js +3 -3
  265. package/dist/src/hooks.d.ts +1 -1
  266. package/dist/src/launcher.js +2 -2
  267. package/dist/src/logger.d.ts +1 -1
  268. package/dist/src/main.d.ts +1 -1
  269. package/dist/src/main.js +1 -1
  270. package/dist/src/observation/artifact.js +2 -2
  271. package/dist/src/package-paths.js +1 -1
  272. package/dist/src/package-paths.test.d.ts +1 -0
  273. package/dist/src/plugin-manifest.d.ts +12 -3
  274. package/dist/src/plugin-manifest.js +1 -1
  275. package/dist/src/plugin-scaffold.d.ts +2 -2
  276. package/dist/src/plugin-scaffold.js +9 -9
  277. package/dist/src/plugin.d.ts +4 -4
  278. package/dist/src/plugin.js +10 -10
  279. package/dist/src/rate-limit.js +1 -1
  280. package/dist/src/registry-api.d.ts +1 -1
  281. package/dist/src/runtime-detect.d.ts +1 -1
  282. package/dist/src/serialization.js +1 -1
  283. package/dist/src/skills.js +6 -6
  284. package/dist/src/user-runtime.d.ts +8 -0
  285. package/dist/src/user-runtime.js +1 -0
  286. package/dist/src/user-runtime.test.d.ts +1 -0
  287. package/dist/src/validate.js +1 -1
  288. package/dist/src/validate.test.d.ts +1 -1
  289. package/docs/guide/browser-bridge.md +23 -23
  290. package/docs/guide/electron-app-cli.md +3 -3
  291. package/docs/guide/exit-codes.md +3 -3
  292. package/docs/guide/extending-opencli.md +37 -37
  293. package/docs/guide/getting-started.md +19 -19
  294. package/docs/guide/installation.md +6 -6
  295. package/docs/guide/plugins.md +32 -32
  296. package/docs/guide/remote-orchestration.md +5 -5
  297. package/docs/guide/troubleshooting.md +9 -9
  298. package/package.json +3 -2
  299. package/scripts/fetch-adapters.js +6 -6
  300. package/scripts/postinstall.js +14 -14
  301. package/skills/opencli-adapter-author/SKILL.md +24 -24
  302. package/skills/opencli-adapter-author/references/adapter-template.md +13 -13
  303. package/skills/opencli-adapter-author/references/api-discovery.md +25 -25
  304. package/skills/opencli-adapter-author/references/coverage-matrix.md +3 -3
  305. package/skills/opencli-adapter-author/references/field-decode-playbook.md +10 -10
  306. package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +2 -2
  307. package/skills/opencli-adapter-author/references/site-memory.md +11 -11
  308. package/skills/opencli-adapter-author/references/site-recon.md +7 -7
  309. package/skills/opencli-adapter-author/references/strategy-selection.md +3 -3
  310. package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +4 -4
  311. package/skills/opencli-adapter-author/references/typed-errors.md +2 -2
  312. package/skills/opencli-autofix/SKILL.md +29 -29
  313. package/skills/opencli-browser/SKILL.md +55 -55
  314. package/skills/opencli-browser-sitemap/SKILL.md +7 -7
  315. package/skills/opencli-sitemap-author/SKILL.md +11 -11
  316. package/skills/opencli-sitemap-author/references/sitemap-schema.md +19 -19
  317. package/skills/opencli-usage/SKILL.md +43 -43
@@ -1,8 +1,8 @@
1
1
  import{ArgumentError as c,AuthRequiredError as x,CommandExecutionError as _}from"@jackwener/opencli/errors";export const SITE="1688",HOME_URL="https://www.1688.com/",SEARCH_URL_PREFIX="https://s.1688.com/selloffer/offer_search.htm?charset=utf8&keywords=",DETAIL_URL_PREFIX="https://detail.1688.com/offer/",STORE_MOBILE_URL_PREFIX="https://winport.m.1688.com/page/index.html?memberId=",STRATEGY="cookie",SEARCH_LIMIT_DEFAULT=20,SEARCH_LIMIT_MAX=100;const w=new Set(["www","detail","s","winport","work","air","dj"]),g=new Set(["spm","tracelog","clickid","source","scene","from","src","ns","cna","pvid"]),b="/_____tmd_____/punish",E=["请拖动下方滑块完成验证","请按住滑块,拖动到最右边","通过验证以确保正常访问","验证码拦截","访问验证","滑动验证"],R=["请登录","登录后","账号登录","手机登录","立即登录","扫码登录","请先完成登录","请先登录后查看"],T=["/member/login","passport","login.taobao.com","account.1688.com"];export const FACTORY_BADGE_PATTERNS=["源头工厂","深度验厂","实力工厂","工厂档案","加工专区","验厂报告","厂家直销","生产厂家","工厂直供"],SERVICE_BADGE_PATTERNS=["延期必赔","品质保障","破损包赔","退货包运费","晚发必赔","7*24小时响应","48小时发货","72小时发货","后天达","包邮","闪电拿样"];const u=["北京","天津","上海","重庆","河北","山西","辽宁","吉林","黑龙江","江苏","浙江","安徽","福建","江西","山东","河南","湖北","湖南","广东","海南","四川","贵州","云南","陕西","甘肃","青海","台湾","内蒙古","广西","西藏","宁夏","新疆","香港","澳门"];export function cleanText(e){return typeof e==="string"?e.replace(/\u00a0/g," ").replace(/\s+/g," ").trim():""}export function cleanMultilineText(e){return typeof e==="string"?e.replace(/\u00a0/g," ").split(`
2
2
  `).map((t)=>t.replace(/\s+/g," ").trim()).filter(Boolean).join(`
3
- `):""}export function uniqueNonEmpty(e){return[...new Set(e.map((t)=>cleanText(t)).filter(Boolean))]}export function parseSearchLimit(e){const t=Number.parseInt(String(e??SEARCH_LIMIT_DEFAULT),10);if(!Number.isFinite(t)||t<1)throw new c("1688 search --limit must be a positive integer",'Example: opencli 1688 search "桌面置物架" --limit 20');return Math.min(SEARCH_LIMIT_MAX,t)}export function buildSearchUrl(e){const t=cleanText(e);if(!t)throw new c("1688 search query cannot be empty",'Example: opencli 1688 search "桌面置物架" --limit 20');return`${SEARCH_URL_PREFIX}${encodeURIComponent(t)}`}export function buildDetailUrl(e){const t=extractOfferId(e);if(!t)throw new c("1688 item expects an offer URL or offer ID","Example: opencli 1688 item 887904326744");return`${DETAIL_URL_PREFIX}${t}.html`}export function resolveStoreUrl(e){const t=cleanText(e);if(!t)throw new c("1688 store expects a store URL or member ID","Example: opencli 1688 store https://yinuoweierfushi.1688.com/");const r=extractMemberId(t);if(r)return`${STORE_MOBILE_URL_PREFIX}${r}`;if(/^https?:\/\//i.test(t))return canonicalizeStoreUrl(t);if(t.endsWith(".1688.com"))return canonicalizeStoreUrl(`https://${t}`);if(/^[a-z0-9-]+$/i.test(t))return canonicalizeStoreUrl(`https://${t}.1688.com`);throw new c("1688 store expects a store URL or member ID","Example: opencli 1688 store b2b-22154705262941f196")}export function canonicalizeStoreUrl(e){const t=m(e),r=extractMemberId(t.toString());if(r)return`${STORE_MOBILE_URL_PREFIX}${r}`;const n=a(t.hostname);if(!n)throw new c("Invalid 1688 store URL","Example: opencli 1688 store https://yinuoweierfushi.1688.com/");return`https://${n}`}export function canonicalizeItemUrl(e){const t=extractOfferId(e);if(t)return`${DETAIL_URL_PREFIX}${t}.html`;const r=f(e);if(!r)return null;h(r);r.hash="";return r.toString()}export function canonicalizeSellerUrl(e){const t=extractMemberId(e);if(t)return`${STORE_MOBILE_URL_PREFIX}${t}`;const r=f(e);if(!r)return null;const n=a(r.hostname);if(!n)return null;return`https://${n}`}export function extractOfferId(e){const t=cleanText(e);if(!t)return null;const r=t.match(/^\d{6,}$/)?.[0];if(r)return r;const n=t.match(/\/offer\/(\d{6,})\.html/i);if(n)return n[1];const o=t.match(/[?&]offerId=(\d{6,})/i);if(o)return o[1];return null}export function extractMemberId(e){const t=cleanText(e);if(!t)return null;const r=t.match(/\bb2b-[a-z0-9]+\b/i)?.[0];if(r)return r;const n=t.match(/[?&]memberId=(b2b-[a-z0-9]+)/i);if(n)return n[1];const o=t.match(/\/winport\/(b2b-[a-z0-9]+)\.html/i);if(o)return o[1];return null}export function extractShopId(e){const t=cleanText(e);if(!t)return null;try{const r=new URL(/^https?:\/\//i.test(t)?t:`https://${t}`),n=a(r.hostname);if(!n)return null;return n.split(".")[0]??null}catch{return/^[a-z0-9-]+$/i.test(t)?t:null}}export function buildProvenance(e){return{source_url:e,fetched_at:new Date().toISOString(),strategy:STRATEGY}}export function parsePriceText(e){const t=l(cleanText(e)),n=(t.match(/\d+(?:,\d{3})*(?:\.\d+)?/g)??[]).map((o)=>Number.parseFloat(o.replace(/,/g,""))).filter((o)=>Number.isFinite(o));if(n.length===0)return{price_text:t,price_min:null,price_max:null,currency:null};return{price_text:t,price_min:n[0]??null,price_max:n[n.length-1]??n[0]??null,currency:t.includes("¥")||t.includes("元")?"CNY":null}}export function normalizePriceTiers(e,t){return e.map((r)=>{const n=toNumber(r.beginAmount),o=cleanText(r.price),i=toNumber(r.price);return{quantity_text:n!==null?`${n}${t??""}`:"",quantity_min:n,price_text:o,price:i,currency:o?"CNY":null}}).filter((r)=>r.price_text)}export function parseMoqText(e){const t=l(cleanText(e)),r=t.match(/(\d+(?:\.\d+)?)\s*(件|个|套|箱|包|双|台|把|只|pcs|piece|pieces)?\s*起批/i)??t.match(/≥\s*(\d+(?:\.\d+)?)/),n=t.match(/(\d+(?:\.\d+)?)\s*(?:~|-|至|到)\s*\d+(?:\.\d+)?\s*(件|个|套|箱|包|双|台|把|只|pcs|piece|pieces)/i);if(!r&&!n)return{moq_text:t,moq_value:null};return{moq_text:t,moq_value:Number.parseFloat((r??n)[1])}}export function extractLocation(e){const t=cleanMultilineText(e),r=t.split(/送至|发往/)[0]??t,n=r.split(`
3
+ `):""}export function uniqueNonEmpty(e){return[...new Set(e.map((t)=>cleanText(t)).filter(Boolean))]}export function parseSearchLimit(e){const t=Number.parseInt(String(e??SEARCH_LIMIT_DEFAULT),10);if(!Number.isFinite(t)||t<1)throw new c("1688 search --limit must be a positive integer",'Example: ppcli 1688 search "桌面置物架" --limit 20');return Math.min(SEARCH_LIMIT_MAX,t)}export function buildSearchUrl(e){const t=cleanText(e);if(!t)throw new c("1688 search query cannot be empty",'Example: ppcli 1688 search "桌面置物架" --limit 20');return`${SEARCH_URL_PREFIX}${encodeURIComponent(t)}`}export function buildDetailUrl(e){const t=extractOfferId(e);if(!t)throw new c("1688 item expects an offer URL or offer ID","Example: ppcli 1688 item 887904326744");return`${DETAIL_URL_PREFIX}${t}.html`}export function resolveStoreUrl(e){const t=cleanText(e);if(!t)throw new c("1688 store expects a store URL or member ID","Example: ppcli 1688 store https://yinuoweierfushi.1688.com/");const r=extractMemberId(t);if(r)return`${STORE_MOBILE_URL_PREFIX}${r}`;if(/^https?:\/\//i.test(t))return canonicalizeStoreUrl(t);if(t.endsWith(".1688.com"))return canonicalizeStoreUrl(`https://${t}`);if(/^[a-z0-9-]+$/i.test(t))return canonicalizeStoreUrl(`https://${t}.1688.com`);throw new c("1688 store expects a store URL or member ID","Example: ppcli 1688 store b2b-22154705262941f196")}export function canonicalizeStoreUrl(e){const t=m(e),r=extractMemberId(t.toString());if(r)return`${STORE_MOBILE_URL_PREFIX}${r}`;const n=a(t.hostname);if(!n)throw new c("Invalid 1688 store URL","Example: ppcli 1688 store https://yinuoweierfushi.1688.com/");return`https://${n}`}export function canonicalizeItemUrl(e){const t=extractOfferId(e);if(t)return`${DETAIL_URL_PREFIX}${t}.html`;const r=f(e);if(!r)return null;h(r);r.hash="";return r.toString()}export function canonicalizeSellerUrl(e){const t=extractMemberId(e);if(t)return`${STORE_MOBILE_URL_PREFIX}${t}`;const r=f(e);if(!r)return null;const n=a(r.hostname);if(!n)return null;return`https://${n}`}export function extractOfferId(e){const t=cleanText(e);if(!t)return null;const r=t.match(/^\d{6,}$/)?.[0];if(r)return r;const n=t.match(/\/offer\/(\d{6,})\.html/i);if(n)return n[1];const o=t.match(/[?&]offerId=(\d{6,})/i);if(o)return o[1];return null}export function extractMemberId(e){const t=cleanText(e);if(!t)return null;const r=t.match(/\bb2b-[a-z0-9]+\b/i)?.[0];if(r)return r;const n=t.match(/[?&]memberId=(b2b-[a-z0-9]+)/i);if(n)return n[1];const o=t.match(/\/winport\/(b2b-[a-z0-9]+)\.html/i);if(o)return o[1];return null}export function extractShopId(e){const t=cleanText(e);if(!t)return null;try{const r=new URL(/^https?:\/\//i.test(t)?t:`https://${t}`),n=a(r.hostname);if(!n)return null;return n.split(".")[0]??null}catch{return/^[a-z0-9-]+$/i.test(t)?t:null}}export function buildProvenance(e){return{source_url:e,fetched_at:new Date().toISOString(),strategy:STRATEGY}}export function parsePriceText(e){const t=l(cleanText(e)),n=(t.match(/\d+(?:,\d{3})*(?:\.\d+)?/g)??[]).map((o)=>Number.parseFloat(o.replace(/,/g,""))).filter((o)=>Number.isFinite(o));if(n.length===0)return{price_text:t,price_min:null,price_max:null,currency:null};return{price_text:t,price_min:n[0]??null,price_max:n[n.length-1]??n[0]??null,currency:t.includes("¥")||t.includes("元")?"CNY":null}}export function normalizePriceTiers(e,t){return e.map((r)=>{const n=toNumber(r.beginAmount),o=cleanText(r.price),i=toNumber(r.price);return{quantity_text:n!==null?`${n}${t??""}`:"",quantity_min:n,price_text:o,price:i,currency:o?"CNY":null}}).filter((r)=>r.price_text)}export function parseMoqText(e){const t=l(cleanText(e)),r=t.match(/(\d+(?:\.\d+)?)\s*(件|个|套|箱|包|双|台|把|只|pcs|piece|pieces)?\s*起批/i)??t.match(/≥\s*(\d+(?:\.\d+)?)/),n=t.match(/(\d+(?:\.\d+)?)\s*(?:~|-|至|到)\s*\d+(?:\.\d+)?\s*(件|个|套|箱|包|双|台|把|只|pcs|piece|pieces)/i);if(!r&&!n)return{moq_text:t,moq_value:null};return{moq_text:t,moq_value:Number.parseFloat((r??n)[1])}}export function extractLocation(e){const t=cleanMultilineText(e),r=t.split(/送至|发往/)[0]??t,n=r.split(`
4
4
  `);for(const i of n){const s=cleanText(i);if(!s||s.length>16)continue;if(u.some((d)=>s.startsWith(d)))return s}const o=new RegExp(`(${u.join("|")})[\\u4e00-\\u9fa5]{0,8}`);return r.match(o)?.[0]??null}export function extractAddress(e){const t=cleanMultilineText(e),r=t.match(/地址[::]\s*([^\n]+)/);if(r)return cleanText(r[1]);return t.split(`
5
- `).map((n)=>cleanText(n)).find((n)=>n.includes("省")||n.includes("市")||n.includes("区")||n.includes("县"))??null}export function extractMetric(e,t){const r=cleanMultilineText(e),n=r.match(new RegExp(`(?:^|\\n)\\s*${p(t)}[::]?\\s*([^\\n]+)`));if(n)return cleanText(n[1]);const o=r.match(new RegExp(`(?:^|\\n)\\s*${p(t)}\\n([^\\n]+)`));return o?cleanText(o[1]):null}export function extractYearsOnPlatform(e){return e.match(/入驻\d+年/)?.[0]??null}export function extractMainBusiness(e){const t=extractMetric(e,"主营");return t?t.replace(/^:/,"").trim():null}export function extractBadges(e,t){return uniqueNonEmpty(t.filter((r)=>cleanMultilineText(e).includes(r)))}export function guessTopCategories(e){const t=extractMainBusiness(e);if(!t)return[];return uniqueNonEmpty(t.split(/[、,/|]/).map((r)=>r.trim()))}export function isCaptchaState(e){const t=cleanText(e.href).toLowerCase(),r=cleanText(e.title),n=cleanMultilineText(e.body_text);if(t.includes(b))return!0;return E.some((o)=>r.includes(o)||n.includes(o))}export function isLoginState(e){const t=cleanText(e.href).toLowerCase(),r=cleanText(e.title),n=cleanMultilineText(e.body_text);if(T.some((o)=>t.includes(o)))return!0;return R.some((o)=>r.includes(o)||n.includes(o))}export function buildCaptchaHint(e){return[`Open a clean 1688 ${e} page in the shared Chrome profile and finish any slider challenge first.`,"If you run opencli via CDP, set OPENCLI_CDP_TARGET=1688.com or a more specific 1688 host before retrying."].join(" ")}export async function readPageState(e){const t=await e.evaluate(`
5
+ `).map((n)=>cleanText(n)).find((n)=>n.includes("省")||n.includes("市")||n.includes("区")||n.includes("县"))??null}export function extractMetric(e,t){const r=cleanMultilineText(e),n=r.match(new RegExp(`(?:^|\\n)\\s*${p(t)}[::]?\\s*([^\\n]+)`));if(n)return cleanText(n[1]);const o=r.match(new RegExp(`(?:^|\\n)\\s*${p(t)}\\n([^\\n]+)`));return o?cleanText(o[1]):null}export function extractYearsOnPlatform(e){return e.match(/入驻\d+年/)?.[0]??null}export function extractMainBusiness(e){const t=extractMetric(e,"主营");return t?t.replace(/^:/,"").trim():null}export function extractBadges(e,t){return uniqueNonEmpty(t.filter((r)=>cleanMultilineText(e).includes(r)))}export function guessTopCategories(e){const t=extractMainBusiness(e);if(!t)return[];return uniqueNonEmpty(t.split(/[、,/|]/).map((r)=>r.trim()))}export function isCaptchaState(e){const t=cleanText(e.href).toLowerCase(),r=cleanText(e.title),n=cleanMultilineText(e.body_text);if(t.includes(b))return!0;return E.some((o)=>r.includes(o)||n.includes(o))}export function isLoginState(e){const t=cleanText(e.href).toLowerCase(),r=cleanText(e.title),n=cleanMultilineText(e.body_text);if(T.some((o)=>t.includes(o)))return!0;return R.some((o)=>r.includes(o)||n.includes(o))}export function buildCaptchaHint(e){return[`Open a clean 1688 ${e} page in the shared Chrome profile and finish any slider challenge first.`,"If you run ppcli via CDP, set OPENCLI_CDP_TARGET=1688.com or a more specific 1688 host before retrying."].join(" ")}export async function readPageState(e){const t=await e.evaluate(`
6
6
  (() => ({
7
7
  href: window.location.href,
8
8
  title: document.title || '',
package/clis/36kr/news.js CHANGED
@@ -1 +1 @@
1
- import{cli as N,Strategy as O}from"@jackwener/opencli/registry";N({site:"36kr",name:"news",access:"read",description:"Latest tech/startup news from 36kr (36氪)",domain:"www.36kr.com",strategy:O.PUBLIC,args:[{name:"limit",type:"int",default:20,help:"Number of articles (max 50)"}],columns:["rank","title","summary","date","url"],func:async(C)=>{const D=Math.min(C.limit||20,50),z=await fetch("https://www.36kr.com/feed",{headers:{"User-Agent":"Mozilla/5.0 (compatible; opencli/1.0)"}});if(!z.ok)return[];const F=await z.text(),v=[],G=/<item>([\s\S]*?)<\/item>/g;let A;while((A=G.exec(F))&&v.length<D){const q=A[1],B=q.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim()??"",I=q.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1]??q.match(/<link>(.*?)<\/link>/)?.[1]??"",J=(q.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim()??"").slice(0,10),K=(q.match(/<description><!\[CDATA\[([\s\S]*?)\]\]>/)?.[1]??"").replace(/<[^>]+>/g," ").replace(/&nbsp;/g," ").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/\s+/g," ").trim().slice(0,120);if(B)v.push({rank:v.length+1,title:B,summary:K,date:J,url:I.trim()})}return v}});
1
+ import{cli as N,Strategy as O}from"@jackwener/opencli/registry";N({site:"36kr",name:"news",access:"read",description:"Latest tech/startup news from 36kr (36氪)",domain:"www.36kr.com",strategy:O.PUBLIC,args:[{name:"limit",type:"int",default:20,help:"Number of articles (max 50)"}],columns:["rank","title","summary","date","url"],func:async(C)=>{const D=Math.min(C.limit||20,50),z=await fetch("https://www.36kr.com/feed",{headers:{"User-Agent":"Mozilla/5.0 (compatible; ppcli/1.0)"}});if(!z.ok)return[];const F=await z.text(),v=[],G=/<item>([\s\S]*?)<\/item>/g;let A;while((A=G.exec(F))&&v.length<D){const q=A[1],B=q.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim()??"",I=q.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1]??q.match(/<link>(.*?)<\/link>/)?.[1]??"",J=(q.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim()??"").slice(0,10),K=(q.match(/<description><!\[CDATA\[([\s\S]*?)\]\]>/)?.[1]??"").replace(/<[^>]+>/g," ").replace(/&nbsp;/g," ").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/\s+/g," ").trim().slice(0,120);if(B)v.push({rank:v.length+1,title:B,summary:K,date:J,url:I.trim()})}return v}});
@@ -1,4 +1,4 @@
1
- import{readFile as O,stat as j}from"node:fs/promises";import{htmlToMarkdown as M}from"@jackwener/opencli/utils";import{ArgumentError as d,AuthRequiredError as b,CommandExecutionError as f,ConfigError as m,EmptyResultError as L}from"@jackwener/opencli/errors";const B="opencli-atlassian-adapter (+https://github.com/jackwener/opencli)",P=new Set(["cloud","datacenter","auto"]);function w(t){for(const r of t){const e=process.env[r]?.trim();if(e)return e}return""}function x(t,r){const e=String(t??"").trim();if(!e)throw new m(`Missing ${r}`,`Set ${r}, for example https://example.atlassian.net`);let n;try{n=new URL(e)}catch{throw new m(`Invalid ${r}: ${e}`,"Use an absolute http(s) URL.")}if(n.protocol!=="https:"&&n.protocol!=="http:")throw new m(`Invalid ${r}: ${e}`,"Use an http(s) URL.");n.hash="";n.search="";return n.toString().replace(/\/+$/,"")}function N(t,r){const e=String(t||"auto").trim().toLowerCase();if(!P.has(e))throw new m("Invalid ATLASSIAN_DEPLOYMENT","Expected one of: cloud, datacenter, auto.");if(e!=="auto")return e;const n=new URL(r).hostname;return n==="atlassian.net"||n.endsWith(".atlassian.net")?"cloud":"datacenter"}function q(t,r){const e=new URL(t),n=e.pathname.replace(/\/+$/,"");e.pathname=`${n}${r}`;return e.toString().replace(/\/+$/,"")}function H(t,r){if(r!=="cloud")return t;const n=new URL(t).pathname.replace(/\/+$/,"");if(n==="/wiki"||n.endsWith("/wiki"))return t;return q(t,"/wiki")}function D(t,r){return`Basic ${Buffer.from(`${t}:${r}`,"utf8").toString("base64")}`}function _(t,r){const e=w(["ATLASSIAN_BEARER_TOKEN","ATLASSIAN_OAUTH_TOKEN"]);if(e)return{Authorization:`Bearer ${e}`};const n=w(["ATLASSIAN_PAT",`${r.toUpperCase()}_PAT`]);if(t==="datacenter"&&n)return{Authorization:`Bearer ${n}`};const o=r.toUpperCase(),s=w(["ATLASSIAN_EMAIL","ATLASSIAN_USERNAME",`${o}_EMAIL`,`${o}_USERNAME`]),a=w(["ATLASSIAN_API_TOKEN","ATLASSIAN_PASSWORD",`${o}_API_TOKEN`,`${o}_PASSWORD`]);if(s&&a)return{Authorization:D(s,a)};if(t==="cloud")throw new m("Missing Atlassian Cloud credentials","Set ATLASSIAN_EMAIL and ATLASSIAN_API_TOKEN, or set ATLASSIAN_BEARER_TOKEN for OAuth.");throw new m("Missing Atlassian Data Center credentials","Set ATLASSIAN_PAT, ATLASSIAN_BEARER_TOKEN, or ATLASSIAN_USERNAME plus ATLASSIAN_PASSWORD.")}export function getJiraConfig(){const t=x(w(["ATLASSIAN_JIRA_BASE_URL","JIRA_BASE_URL"]),"ATLASSIAN_JIRA_BASE_URL"),r=N(process.env.ATLASSIAN_DEPLOYMENT,t);return{product:"jira",baseUrl:t,deployment:r,authHeaders:_(r,"jira")}}export function getConfluenceConfig(){const t=x(w(["ATLASSIAN_CONFLUENCE_BASE_URL","CONFLUENCE_BASE_URL"]),"ATLASSIAN_CONFLUENCE_BASE_URL"),r=N(process.env.ATLASSIAN_DEPLOYMENT,t);return{product:"confluence",baseUrl:H(t,r),deployment:r,authHeaders:_(r,"confluence")}}function F(t,r){if(/^https?:\/\//i.test(r))return r;const e=r.startsWith("/")?r:`/${r}`;return`${t}${e}`}function T(t,r){if(t&&typeof t==="object"){const e=[];if(Array.isArray(t.errorMessages))e.push(...t.errorMessages.filter(Boolean));if(typeof t.message==="string")e.push(t.message);if(typeof t.error==="string")e.push(t.error);if(typeof t.reason==="string")e.push(t.reason);if(t.errors&&typeof t.errors==="object")for(const[n,o]of Object.entries(t.errors))e.push(`${n}: ${String(o)}`);if(e.length)return e.join(" · ")}if(typeof t==="string"&&t.trim())return t.trim().slice(0,300);return r}async function J(t,r){let e;try{e=await t.text()}catch(n){throw new f(`${r} response body could not be read: ${n?.message??n}`,"Check whether the Atlassian instance, proxy, or network interrupted the response.")}if(!e)return null;try{return JSON.parse(e)}catch{return e}}export async function atlassianRequest(t,r,e={}){const n=(e.method??"GET").toUpperCase(),o=e.label??`${t.product} ${n} ${r}`,s={"user-agent":B,accept:"application/json",...t.authHeaders,...e.headers??{}};let a;if(e.body!==void 0){s["content-type"]=s["content-type"]??"application/json";a=typeof e.body==="string"?e.body:JSON.stringify(e.body)}let i;const p=F(t.baseUrl,r);try{i=await fetch(p,{method:n,headers:s,body:a})}catch(g){throw new f(`${o} request failed: ${g?.message??g}`,"Check the Atlassian base URL, VPN/network access, and proxy settings.")}const l=await J(i,o);if(i.status===401)throw new b(t.baseUrl,`${o} returned HTTP 401`,"Check Atlassian credentials and whether this instance accepts the configured auth method.");if(i.status===403)throw new b(t.baseUrl,`${o} returned HTTP 403: ${T(l,"forbidden")}`,"The authenticated user lacks permission for this Jira issue, Confluence page, or space.");if(i.status===404)throw new L(o,`Atlassian returned 404 for ${p}.`);if(i.status===409)throw new f(`${o} returned HTTP 409: ${T(l,"version conflict")}`,"Reload the current Confluence page version and retry the update.");if(i.status===429)throw new f(`${o} returned HTTP 429 (rate limited)`,"Wait and retry with a smaller limit.");if(!i.ok)throw new f(`${o} returned HTTP ${i.status}: ${T(l,i.statusText)}`);if(typeof l==="string")throw new f(`${o} returned a non-JSON response`,"Expected Atlassian REST API JSON. Check the base URL and whether an HTML login, SSO, or proxy page was returned.");return l}export function queryString(t){const r=new URLSearchParams;for(const[n,o]of Object.entries(t)){if(o===void 0||o===null||o==="")continue;if(Array.isArray(o))for(const s of o)r.append(n,String(s));else r.set(n,String(o))}const e=r.toString();return e?`?${e}`:""}export function requireString(t,r){const e=String(t??"").trim();if(!e)throw new d(`${r} is required`);return e}export function requirePayloadObject(t,r){if(!t||typeof t!=="object"||Array.isArray(t))throw new f(`${r} returned an unexpected payload shape; expected an object.`);return t}export function requirePayloadArray(t,r){if(!Array.isArray(t))throw new f(`${r} returned an unexpected payload shape; expected an array.`);return t}export function requirePayloadString(t,r,e){if(typeof t!=="string"&&typeof t!=="number")throw new f(`${e} did not include a stable ${r}.`);const n=String(t).trim();if(!n)throw new f(`${e} did not include a stable ${r}.`);return n}export function requireNonEmptyRows(t,r,e){if(!t.length)throw new L(r,e);return t}export function parseLimit(t,r=20,e=100,n="limit"){const o=t??r,s=typeof o==="number"?o:Number(o);if(!Number.isInteger(s)||s<=0)throw new d(`${n} must be a positive integer`);if(s>e)throw new d(`${n} must be <= ${e}`);return s}export function requireExecute(t,r){if(t.execute!==!0)throw new d(`${r} requires --execute to perform a remote write`)}export async function readUtf8File(t){const r=requireString(t,"--file");let e;try{e=await j(r)}catch{throw new d(`File not found: ${r}`)}if(!e.isFile())throw new d(`File must be a readable text file: ${r}`);let n;try{n=await O(r)}catch{throw new d(`File could not be read: ${r}`)}try{return new TextDecoder("utf-8",{fatal:!0}).decode(n)}catch{throw new d(`File could not be decoded as UTF-8 text: ${r}`)}}export function htmlEscape(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export function htmlToMarkdown(t){return M(String(t??""))}function v(t,r=[]){let e=t;for(const n of r){const o=n?.type;if(o==="link"&&n.attrs?.href)e=`[${e}](${n.attrs.href})`;else if(o==="strong")e=`**${e}**`;else if(o==="em")e=`_${e}_`;else if(o==="code")e=`\`${e}\``;else if(o==="strike")e=`~~${e}~~`}return e}function $(t,r=0){if(!t||typeof t!=="object")return"";const e=Array.isArray(t.content)?t.content:[],n=(o="")=>e.map((s)=>$(s,r)).filter(Boolean).join(o);switch(t.type){case"doc":return e.map((o)=>$(o,r)).filter(Boolean).join(`
1
+ import{readFile as O,stat as j}from"node:fs/promises";import{htmlToMarkdown as M}from"@jackwener/opencli/utils";import{ArgumentError as d,AuthRequiredError as b,CommandExecutionError as f,ConfigError as m,EmptyResultError as L}from"@jackwener/opencli/errors";const B="opencli-atlassian-adapter (+https://github.com/jackwener/OpenCLI)",P=new Set(["cloud","datacenter","auto"]);function w(t){for(const r of t){const e=process.env[r]?.trim();if(e)return e}return""}function x(t,r){const e=String(t??"").trim();if(!e)throw new m(`Missing ${r}`,`Set ${r}, for example https://example.atlassian.net`);let n;try{n=new URL(e)}catch{throw new m(`Invalid ${r}: ${e}`,"Use an absolute http(s) URL.")}if(n.protocol!=="https:"&&n.protocol!=="http:")throw new m(`Invalid ${r}: ${e}`,"Use an http(s) URL.");n.hash="";n.search="";return n.toString().replace(/\/+$/,"")}function N(t,r){const e=String(t||"auto").trim().toLowerCase();if(!P.has(e))throw new m("Invalid ATLASSIAN_DEPLOYMENT","Expected one of: cloud, datacenter, auto.");if(e!=="auto")return e;const n=new URL(r).hostname;return n==="atlassian.net"||n.endsWith(".atlassian.net")?"cloud":"datacenter"}function q(t,r){const e=new URL(t),n=e.pathname.replace(/\/+$/,"");e.pathname=`${n}${r}`;return e.toString().replace(/\/+$/,"")}function H(t,r){if(r!=="cloud")return t;const n=new URL(t).pathname.replace(/\/+$/,"");if(n==="/wiki"||n.endsWith("/wiki"))return t;return q(t,"/wiki")}function D(t,r){return`Basic ${Buffer.from(`${t}:${r}`,"utf8").toString("base64")}`}function _(t,r){const e=w(["ATLASSIAN_BEARER_TOKEN","ATLASSIAN_OAUTH_TOKEN"]);if(e)return{Authorization:`Bearer ${e}`};const n=w(["ATLASSIAN_PAT",`${r.toUpperCase()}_PAT`]);if(t==="datacenter"&&n)return{Authorization:`Bearer ${n}`};const o=r.toUpperCase(),s=w(["ATLASSIAN_EMAIL","ATLASSIAN_USERNAME",`${o}_EMAIL`,`${o}_USERNAME`]),a=w(["ATLASSIAN_API_TOKEN","ATLASSIAN_PASSWORD",`${o}_API_TOKEN`,`${o}_PASSWORD`]);if(s&&a)return{Authorization:D(s,a)};if(t==="cloud")throw new m("Missing Atlassian Cloud credentials","Set ATLASSIAN_EMAIL and ATLASSIAN_API_TOKEN, or set ATLASSIAN_BEARER_TOKEN for OAuth.");throw new m("Missing Atlassian Data Center credentials","Set ATLASSIAN_PAT, ATLASSIAN_BEARER_TOKEN, or ATLASSIAN_USERNAME plus ATLASSIAN_PASSWORD.")}export function getJiraConfig(){const t=x(w(["ATLASSIAN_JIRA_BASE_URL","JIRA_BASE_URL"]),"ATLASSIAN_JIRA_BASE_URL"),r=N(process.env.ATLASSIAN_DEPLOYMENT,t);return{product:"jira",baseUrl:t,deployment:r,authHeaders:_(r,"jira")}}export function getConfluenceConfig(){const t=x(w(["ATLASSIAN_CONFLUENCE_BASE_URL","CONFLUENCE_BASE_URL"]),"ATLASSIAN_CONFLUENCE_BASE_URL"),r=N(process.env.ATLASSIAN_DEPLOYMENT,t);return{product:"confluence",baseUrl:H(t,r),deployment:r,authHeaders:_(r,"confluence")}}function F(t,r){if(/^https?:\/\//i.test(r))return r;const e=r.startsWith("/")?r:`/${r}`;return`${t}${e}`}function T(t,r){if(t&&typeof t==="object"){const e=[];if(Array.isArray(t.errorMessages))e.push(...t.errorMessages.filter(Boolean));if(typeof t.message==="string")e.push(t.message);if(typeof t.error==="string")e.push(t.error);if(typeof t.reason==="string")e.push(t.reason);if(t.errors&&typeof t.errors==="object")for(const[n,o]of Object.entries(t.errors))e.push(`${n}: ${String(o)}`);if(e.length)return e.join(" · ")}if(typeof t==="string"&&t.trim())return t.trim().slice(0,300);return r}async function J(t,r){let e;try{e=await t.text()}catch(n){throw new f(`${r} response body could not be read: ${n?.message??n}`,"Check whether the Atlassian instance, proxy, or network interrupted the response.")}if(!e)return null;try{return JSON.parse(e)}catch{return e}}export async function atlassianRequest(t,r,e={}){const n=(e.method??"GET").toUpperCase(),o=e.label??`${t.product} ${n} ${r}`,s={"user-agent":B,accept:"application/json",...t.authHeaders,...e.headers??{}};let a;if(e.body!==void 0){s["content-type"]=s["content-type"]??"application/json";a=typeof e.body==="string"?e.body:JSON.stringify(e.body)}let i;const p=F(t.baseUrl,r);try{i=await fetch(p,{method:n,headers:s,body:a})}catch(g){throw new f(`${o} request failed: ${g?.message??g}`,"Check the Atlassian base URL, VPN/network access, and proxy settings.")}const l=await J(i,o);if(i.status===401)throw new b(t.baseUrl,`${o} returned HTTP 401`,"Check Atlassian credentials and whether this instance accepts the configured auth method.");if(i.status===403)throw new b(t.baseUrl,`${o} returned HTTP 403: ${T(l,"forbidden")}`,"The authenticated user lacks permission for this Jira issue, Confluence page, or space.");if(i.status===404)throw new L(o,`Atlassian returned 404 for ${p}.`);if(i.status===409)throw new f(`${o} returned HTTP 409: ${T(l,"version conflict")}`,"Reload the current Confluence page version and retry the update.");if(i.status===429)throw new f(`${o} returned HTTP 429 (rate limited)`,"Wait and retry with a smaller limit.");if(!i.ok)throw new f(`${o} returned HTTP ${i.status}: ${T(l,i.statusText)}`);if(typeof l==="string")throw new f(`${o} returned a non-JSON response`,"Expected Atlassian REST API JSON. Check the base URL and whether an HTML login, SSO, or proxy page was returned.");return l}export function queryString(t){const r=new URLSearchParams;for(const[n,o]of Object.entries(t)){if(o===void 0||o===null||o==="")continue;if(Array.isArray(o))for(const s of o)r.append(n,String(s));else r.set(n,String(o))}const e=r.toString();return e?`?${e}`:""}export function requireString(t,r){const e=String(t??"").trim();if(!e)throw new d(`${r} is required`);return e}export function requirePayloadObject(t,r){if(!t||typeof t!=="object"||Array.isArray(t))throw new f(`${r} returned an unexpected payload shape; expected an object.`);return t}export function requirePayloadArray(t,r){if(!Array.isArray(t))throw new f(`${r} returned an unexpected payload shape; expected an array.`);return t}export function requirePayloadString(t,r,e){if(typeof t!=="string"&&typeof t!=="number")throw new f(`${e} did not include a stable ${r}.`);const n=String(t).trim();if(!n)throw new f(`${e} did not include a stable ${r}.`);return n}export function requireNonEmptyRows(t,r,e){if(!t.length)throw new L(r,e);return t}export function parseLimit(t,r=20,e=100,n="limit"){const o=t??r,s=typeof o==="number"?o:Number(o);if(!Number.isInteger(s)||s<=0)throw new d(`${n} must be a positive integer`);if(s>e)throw new d(`${n} must be <= ${e}`);return s}export function requireExecute(t,r){if(t.execute!==!0)throw new d(`${r} requires --execute to perform a remote write`)}export async function readUtf8File(t){const r=requireString(t,"--file");let e;try{e=await j(r)}catch{throw new d(`File not found: ${r}`)}if(!e.isFile())throw new d(`File must be a readable text file: ${r}`);let n;try{n=await O(r)}catch{throw new d(`File could not be read: ${r}`)}try{return new TextDecoder("utf-8",{fatal:!0}).decode(n)}catch{throw new d(`File could not be decoded as UTF-8 text: ${r}`)}}export function htmlEscape(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export function htmlToMarkdown(t){return M(String(t??""))}function v(t,r=[]){let e=t;for(const n of r){const o=n?.type;if(o==="link"&&n.attrs?.href)e=`[${e}](${n.attrs.href})`;else if(o==="strong")e=`**${e}**`;else if(o==="em")e=`_${e}_`;else if(o==="code")e=`\`${e}\``;else if(o==="strike")e=`~~${e}~~`}return e}function $(t,r=0){if(!t||typeof t!=="object")return"";const e=Array.isArray(t.content)?t.content:[],n=(o="")=>e.map((s)=>$(s,r)).filter(Boolean).join(o);switch(t.type){case"doc":return e.map((o)=>$(o,r)).filter(Boolean).join(`
2
2
 
3
3
  `).trim();case"paragraph":return n("");case"text":return v(String(t.text??""),Array.isArray(t.marks)?t.marks:[]);case"hardBreak":return`
4
4
  `;case"heading":return`${"#".repeat(Math.max(1,Math.min(6,Number(t.attrs?.level??2))))} ${n("")}`;case"bulletList":return e.map((o)=>R(o,r,"-")).join(`
@@ -13,4 +13,4 @@ try {
13
13
  } catch (e) {
14
14
  return { isAuthenticated: false, userId: "", username: "", avatar: "", error: String((e && e.message) || e) };
15
15
  }
16
- })()`}export async function checkLogin(z,v){if(!v||!v.home)throw Error("checkLogin: profile.home is required");if(typeof v.checkAuth!=="function")throw Error("checkLogin: profile.checkAuth must be a function");try{await $(z,v.home,v.originRe)}catch(V){return{isAuthenticated:!1,userId:"",username:"",avatar:"",error:"未登录(无法进入需登录的页面,疑似被重定向到登录页)"}}const D=buildCheckAuthJs(v.checkAuth.toString());return await z.evaluate(D)||{isAuthenticated:!1,userId:"",username:"",avatar:"",error:"no result"}}export async function requireLogin(z,v,D){const B=await checkLogin(z,v);if(!B.isAuthenticated)throw new Y(`未登录${D||""}。请先在已连接 opencli 浏览器桥的 Chrome 里登录该平台。`+(B.error?`(${B.error})`:""));return B}export function cookieQuickCheck(z,v,D=[]){const B=new Set(v);return async(V)=>{return{logged_in:(await V.getCookies({url:z})).some((K)=>K.value&&(B.has(K.name)||D.some((X)=>K.name.startsWith(X))))}}}export const __test__={buildCheckAuthJs,checkLogin,requireLogin,cookieQuickCheck};
16
+ })()`}export async function checkLogin(z,v){if(!v||!v.home)throw Error("checkLogin: profile.home is required");if(typeof v.checkAuth!=="function")throw Error("checkLogin: profile.checkAuth must be a function");try{await $(z,v.home,v.originRe)}catch(V){return{isAuthenticated:!1,userId:"",username:"",avatar:"",error:"未登录(无法进入需登录的页面,疑似被重定向到登录页)"}}const D=buildCheckAuthJs(v.checkAuth.toString());return await z.evaluate(D)||{isAuthenticated:!1,userId:"",username:"",avatar:"",error:"no result"}}export async function requireLogin(z,v,D){const B=await checkLogin(z,v);if(!B.isAuthenticated)throw new Y(`未登录${D||""}。请先在已连接 ppcli 浏览器桥的 Chrome 里登录该平台。`+(B.error?`(${B.error})`:""));return B}export function cookieQuickCheck(z,v,D=[]){const B=new Set(v);return async(V)=>{return{logged_in:(await V.getCookies({url:z})).some((K)=>K.value&&(B.has(K.name)||D.some((X)=>K.name.startsWith(X))))}}}export const __test__={buildCheckAuthJs,checkLogin,requireLogin,cookieQuickCheck};
@@ -3,7 +3,7 @@ import{CommandExecutionError as O}from"@jackwener/opencli/errors";import{normali
3
3
  var onSite = new RegExp(${JSON.stringify(Z)}).test(href);
4
4
  var cookieOk = true; try { void document.cookie; } catch (e) { cookieOk = false; }
5
5
  return { href: href, onSite: onSite, cookieOk: cookieOk };
6
- })()`);if(D&&D.onSite&&D.cookieOk)return;await K.wait({time:1})}throw new O(`无法在浏览器中打开已登录的写作页(页面停留在空白页,未落到 ${Q})。`+"请确认登录该平台的 Chrome 已连接 opencli 浏览器桥。")}export function buildPublishJs(K,Q,X){return`(async () => {
6
+ })()`);if(D&&D.onSite&&D.cookieOk)return;await K.wait({time:1})}throw new O(`无法在浏览器中打开已登录的写作页(页面停留在空白页,未落到 ${Q})。`+"请确认登录该平台的 Chrome 已连接 ppcli 浏览器桥。")}export function buildPublishJs(K,Q,X){return`(async () => {
7
7
  `+S+`
8
8
  const I = `+JSON.stringify(K)+`;
9
9
  const __publish = (`+Q+`);
@@ -1 +1 @@
1
- import*as J from"node:fs";import*as K from"node:os";import{ArgumentError as G}from"@jackwener/opencli/errors";const M=/^@(?:\/|~\/|\.{1,2}\/)\S+$/,N=/^(?:\/|~\/)\S+$/;function O(y){return y.startsWith("~/")?`${K.homedir()}/${y.slice(2)}`:y}export function assertLiteralContent(y,{fileFlag:C=null,label:q="正文"}={}){const h=String(y??"").trim();if(!h)return h;const D=C?(z)=>`请改用 ${C} ${z},或把文件内容读出来作为${q}传入`:()=>`本命令没有文件参数,请把文件内容读出来作为${q}传入`;if(M.test(h))throw new G(`${q} "${h}" 看起来是文件引用——命令不会展开 @路径,照发会把这串字面量当${q}。${D(h.slice(1))}`);if(N.test(h)){let z=!1;try{z=J.statSync(O(h)).isFile()}catch{}if(z)throw new G(`${q} "${h}" 是一个本机已存在的文件路径——照发会把这串路径当${q}发出去。${D(h)}`)}return h}
1
+ import{ArgumentError as G}from"@jackwener/opencli/errors";import*as J from"node:fs";import*as K from"node:os";const M=/^@(?:\/|~\/|\.{1,2}\/)\S+$/,N=/^(?:\/|~\/)\S+$/;function O(y){return y.startsWith("~/")?`${K.homedir()}/${y.slice(2)}`:y}export function assertLiteralContent(y,{fileFlag:C=null,label:q="正文"}={}){const h=String(y??"").trim();if(!h)return h;const D=C?(z)=>`请改用 ${C} ${z},或把文件内容读出来作为${q}传入`:()=>`本命令没有文件参数,请把文件内容读出来作为${q}传入`;if(M.test(h))throw new G(`${q} "${h}" 看起来是文件引用——命令不会展开 @路径,照发会把这串字面量当${q}。${D(h.slice(1))}`);if(N.test(h)){let z=!1;try{z=J.statSync(O(h)).isFile()}catch{}if(z)throw new G(`${q} "${h}" 是一个本机已存在的文件路径——照发会把这串路径当${q}发出去。${D(h)}`)}return h}
@@ -0,0 +1,2 @@
1
+ import*as D from"node:fs";import*as B from"node:os";import*as T from"node:path";import{ArgumentError as K,CommandExecutionError as J}from"@jackwener/opencli/errors";import{Strategy as M}from"@jackwener/opencli/registry";const _=1,A=1e4;function k(q){return T.join(B.homedir(),".ppcli","sites",q,"connections.json")}function C(q){return T.join(B.homedir(),".opencli","sites",q,"connections.json")}function O(q){const Q=String(q??"").trim();if(!Q)throw new K("站点名称不能为空");if(Q.length>64)throw new K("站点名称不能超过 64 个字符");if(/[\u0000-\u001f\u007f]/.test(Q))throw new K("站点名称不能包含控制字符");return Q}export function normalizeSiteUrl(q,{originOnly:Q=!1,label:H="站点网址"}={}){const X=String(q??"").trim();if(!X)throw new K(`${H}不能为空`);let Z;try{Z=new URL(X.includes("://")?X:`https://${X}`)}catch{throw new K(`${H}不是合法网址: ${X}`)}if(Z.protocol!=="http:"&&Z.protocol!=="https:")throw new K(`${H}必须是 http(s) 网址: ${X}`);if(Z.username||Z.password||Z.search||Z.hash)throw new K(`${H}只应包含站点根地址,不要带账号、查询参数或锚点`);if(Q)return Z.origin;const $=Z.pathname.replace(/\/+$/,"");return`${Z.origin}${$==="/"?"":$}`}function b(){return{version:_,default:"",sites:[]}}function I(q,Q){if(!Q||Q.version!==_||!Array.isArray(Q.sites))throw new J(`${q} 站点连接文件格式不受支持`,"请删除损坏的 connections.json 后重新添加站点。");const H=Q.sites.filter(($)=>$&&typeof $.name==="string"&&typeof $.url==="string").map(($)=>({name:$.name,url:$.url})),X=H[0]?.name??"",Z=typeof Q.default==="string"?Q.default:"";return{version:_,default:H.some(($)=>$.name===Z)?Z:X,sites:H}}export function loadSiteConnections(q){const Q=k(q);try{return I(q,JSON.parse(D.readFileSync(Q,"utf8")))}catch(H){if(H?.code==="ENOENT"){const X=C(q);try{const Z=I(q,JSON.parse(D.readFileSync(X,"utf8")));V(q,Z);return Z}catch(Z){if(Z?.code==="ENOENT")return b();if(Z instanceof J)throw Z;throw new J(`${q} 旧版站点连接读取失败: ${Z?.message??Z}`)}}if(H instanceof J)throw H;throw new J(`${q} 站点连接读取失败: ${H?.message??H}`)}}function V(q,Q){const H=k(q),X=T.dirname(H),Z=`${H}.${process.pid}.tmp`;try{D.mkdirSync(X,{recursive:!0,mode:448});D.writeFileSync(Z,`${JSON.stringify(Q,null,2)}
2
+ `,{mode:384});D.renameSync(Z,H);try{D.chmodSync(H,384)}catch{}}catch($){try{D.rmSync(Z,{force:!0})}catch{}throw new J(`${q} 站点连接保存失败: ${$?.message??$}`)}}function L(q,Q){const H=String(Q??"").trim();return q.sites.find((X)=>X.name===H)??q.sites.find((X)=>X.name.toLocaleLowerCase()===H.toLocaleLowerCase())}export function addSiteConnection(q,Q,H,X={}){const Z=O(Q),$=normalizeSiteUrl(H,X),G=loadSiteConnections(q),W=L(G,Z);if(W){if(W.url===$)return{status:"unchanged",name:W.name,url:$,default:G.default===W.name};throw new K(`站点名称“${W.name}”已指向 ${W.url}`,"请换一个名称,或先删除原连接。")}const F=G.sites.find((j)=>j.url===$);if(F)throw new K(`${$} 已保存为“${F.name}”`);G.sites.push({name:Z,url:$});if(!G.default)G.default=Z;V(q,G);return{status:"added",name:Z,url:$,default:G.default===Z}}export function removeSiteConnection(q,Q){const H=loadSiteConnections(q),X=L(H,Q);if(!X)throw new K(`未找到站点连接: ${String(Q??"").trim()}`);H.sites=H.sites.filter((Z)=>Z.name!==X.name);if(H.default===X.name)H.default=H.sites[0]?.name??"";V(q,H);return{status:"removed",name:X.name,url:X.url}}export function useSiteConnection(q,Q){const H=loadSiteConnections(q),X=L(H,Q);if(!X)throw new K(`未找到站点连接: ${String(Q??"").trim()}`);H.default=X.name;V(q,H);return{status:"default",name:X.name,url:X.url,default:!0}}export function listSiteConnectionRows(q){const Q=loadSiteConnections(q);return Q.sites.map((H)=>({...H,default:H.name===Q.default}))}function N(q,Q){const H=Q.replace(/\/+$/,"");if(q==="mastodon")return`${H}/api/v2/instance`;if(q==="wordpress")return`${H}/wp-json/`;if(q==="ghost")return`${H}/ghost/api/admin/site/`;throw new K(`不支持校验站点类型: ${q}`)}async function P(q){const Q=new AbortController,H=setTimeout(()=>Q.abort(),A);try{return await fetch(q,{method:"GET",headers:{accept:"application/json"},redirect:"follow",signal:Q.signal})}finally{clearTimeout(H)}}async function x(q){const Q=q.headers?.get?.("content-type")??"";if(!/json/i.test(Q))return null;try{return await q.json()}catch{return null}}function R(q){if(!q||typeof q!=="object"||Array.isArray(q))return!1;const Q=q.domain??q.uri;return typeof Q==="string"&&Q.length>0&&(typeof q.version==="string"||q.title&&typeof q.title==="string")}function U(q){if(!q||typeof q!=="object"||Array.isArray(q))return!1;if((Array.isArray(q.namespaces)?q.namespaces:[]).some((X)=>X==="wp/v2"||String(X).startsWith("wp/v2/")))return!0;const H=q.routes;return!!H&&typeof H==="object"&&Object.keys(H).some((X)=>X.startsWith("/wp/v2/"))}function E(q,Q){if(!q||typeof q!=="object"||Array.isArray(q))return!1;if(q.site&&typeof q.site==="object")return!0;if(Q!==401&&Q!==403)return!1;return(Array.isArray(q.errors)?q.errors:[]).some((X)=>{if(!X||typeof X!=="object")return!1;const Z=String(X.type??""),$=String(X.context??"");return/GhostError|NoPermissionError|UnauthorizedError|NotFoundError/i.test(Z)||/ghost admin api/i.test($)})}export async function verifySiteKind(q,Q,{originOnly:H=!1}={}){const X=normalizeSiteUrl(Q,{originOnly:H}),Z=N(q,X);let $;try{$=await P(Z)}catch(F){const j=F?.name==="AbortError"?"连接超时":F?.message??String(F);throw new J(`连接不到 ${q} 站点 ${X}: ${j}`,"请检查站点地址、网络和 HTTPS 证书后重试。")}const G=await x($),W=q==="mastodon"?$.ok&&R(G):q==="wordpress"?$.ok&&U(G):E(G,$.status);if(!W&&q==="mastodon"&&$.status===404)try{const F=await P(`${X.replace(/\/+$/,"")}/api/v1/instance`),j=await x(F);if(F.ok&&R(j))return{platform:q,url:X,verified:!0}}catch{}if(!W){const F=q==="mastodon"?"Mastodon":q==="wordpress"?"WordPress":"Ghost";throw new K(`${X} 不是可识别的 ${F} 站点`,`请填写 ${F} 站点根地址,当前检测地址: ${Z}`)}return{platform:q,url:X,verified:!0}}export function resolveSiteConnection(q,Q={},{selector:H="site",legacySelectors:X=[],envUrl:Z,originOnly:$=!1}={}){const G=X.map((Y)=>Q[Y]).find((Y)=>Y!=null&&String(Y).trim());if(G!=null)return{name:"",url:normalizeSiteUrl(G,{originOnly:$}),saved:!1};const W=Q[H];if(W!=null&&String(W).trim()){const Y=String(W).trim(),h=loadSiteConnections(q),z=L(h,Y);if(z)return{...z,saved:!0};if(Y.includes("://")||Y.includes(".")||/^localhost(?::\d+)?(?:\/|$)/i.test(Y)||/^\[[0-9a-f:]+\](?::\d+)?(?:\/|$)/i.test(Y))return{name:"",url:normalizeSiteUrl(Y,{originOnly:$}),saved:!1};throw new K(`未找到 ${q} 站点连接“${Y}”`,`运行 ppcli ${q} sites 查看已保存站点。`)}const F=loadSiteConnections(q),j=L(F,F.default)??F.sites[0];if(j)return{...j,saved:!0};if(Z&&String(Z).trim())return{name:"",url:normalizeSiteUrl(Z,{originOnly:$}),saved:!1};throw new K(`${q} 尚未添加站点`,`先运行 ppcli ${q} add-site --name <名称> --url <网址>,或在本次命令传 --${H} <网址>。`)}export function connectionCommandDefinitions(q,{originOnly:Q=!1}={}){return[{site:q,name:"sites",access:"read",description:`列出已保存的 ${q} 自部署站点连接。`,strategy:M.LOCAL,browser:!1,args:[],columns:["name","url","default"],func:async()=>listSiteConnectionRows(q)},{site:q,name:"add-site",access:"write",description:`校验并保存一个命名的 ${q} 自部署站点连接(只保存名称和网址)。`,strategy:M.LOCAL,browser:!1,args:[{name:"name",required:!0,help:"连接名称,例如“公司博客”"},{name:"url",required:!0,help:"站点根网址,例如 https://blog.example.com"}],columns:["status","name","url","default"],func:async(H)=>{await verifySiteKind(q,H.url,{originOnly:Q});return[addSiteConnection(q,H.name,H.url,{originOnly:Q})]}},{site:q,name:"remove-site",access:"write",description:`删除一个 ${q} 站点连接;不会清除浏览器里的站点 cookie。`,strategy:M.LOCAL,browser:!1,args:[{name:"site",required:!0,help:"要删除的连接名称"}],columns:["status","name","url"],func:async(H)=>[removeSiteConnection(q,H.site)]},{site:q,name:"use-site",access:"write",description:`设置 ${q} 默认站点;之后省略 --site 时使用它。`,strategy:M.LOCAL,browser:!1,args:[{name:"site",required:!0,help:"要设为默认的连接名称"}],columns:["status","name","url","default"],func:async(H)=>[useSiteConnection(q,H.site)]}]}
@@ -1 +1 @@
1
- import{AuthRequiredError as q,TimeoutError as z,getErrorMessage as N}from"@jackwener/opencli/errors";import{cli as $,Strategy as Q}from"@jackwener/opencli/registry";const V=300,b=2000;function D(v,x){const G=x&&typeof x==="object"&&!Array.isArray(x)?x:{};return{logged_in:!0,site:v,...G}}function j(v){return v instanceof q}async function Y(v,x,G){const K=G==="poll"&&v.poll?v.poll:v.verify;return D(v.site,await K(x,{phase:G}))}async function J(v){if(typeof v?.closeWindow==="function")try{await v.closeWindow()}catch{}}function F(v){return`Run \`opencli ${v.site} login\` to open the login page, then retry.`}function H(v){return["logged_in","site",...v.columns??["id","username","name"]]}function I(v){if(typeof v==="boolean")return{logged_in:v};if(v&&typeof v==="object"&&!Array.isArray(v))return{logged_in:!!v.logged_in,...v};return{logged_in:!1}}function P(v){if(v&&typeof v==="object"&&!Array.isArray(v))return v;return{touched:!0}}export function registerSiteAuthCommands(v){if(!v?.site||!v?.domain||!v?.loginUrl||typeof v.verify!=="function")throw Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");$({site:v.site,name:"whoami",access:"read",description:v.whoamiDescription??`Show the current logged-in ${v.site} account`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:H(v),authStatus:{...typeof v.quickCheck==="function"?{quickCheck:async(x)=>I(await v.quickCheck(x))}:{},...v.quickCheckRequiresNavigation===!0?{quickCheckRequiresNavigation:!0}:{},...typeof v.refresh==="function"?{refresh:async(x,G)=>P(await v.refresh(x,G))}:{}},func:async(x)=>Y(v,x,"identity")});$({site:v.site,name:"login",access:"write",description:v.loginDescription??`Open ${v.site} login and wait until the browser session is authenticated`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:V,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...H(v)],func:async(x,G)=>{try{const B={status:"already_logged_in",...await Y(v,x,"identity")};await J(x);return B}catch(B){if(!j(B)){await J(x);throw B}}await x.goto(v.loginUrl);const K=Number(G.timeout??V),Z=Date.now()+K*1000;let X="";while(Date.now()<Z){await x.wait(Math.min(b/1000,Math.max(0.2,(Z-Date.now())/1000)));try{const B=await Y(v,x,"poll");await J(x);return{status:"login_complete",...B}}catch(B){if(!j(B)){await J(x);throw B}X=N(B)}}await J(x);throw new z(`${v.site} login`,K,X?`${F(v)} Last auth check: ${X}`:F(v))}})}
1
+ import{AuthRequiredError as q,TimeoutError as z,getErrorMessage as N}from"@jackwener/opencli/errors";import{cli as $,Strategy as Q}from"@jackwener/opencli/registry";const V=300,b=2000;function D(v,x){const G=x&&typeof x==="object"&&!Array.isArray(x)?x:{};return{logged_in:!0,site:v,...G}}function j(v){return v instanceof q}async function Y(v,x,G){const K=G==="poll"&&v.poll?v.poll:v.verify;return D(v.site,await K(x,{phase:G}))}async function J(v){if(typeof v?.closeWindow==="function")try{await v.closeWindow()}catch{}}function F(v){return`Run \`ppcli ${v.site} login\` to open the login page, then retry.`}function H(v){return["logged_in","site",...v.columns??["id","username","name"]]}function I(v){if(typeof v==="boolean")return{logged_in:v};if(v&&typeof v==="object"&&!Array.isArray(v))return{logged_in:!!v.logged_in,...v};return{logged_in:!1}}function P(v){if(v&&typeof v==="object"&&!Array.isArray(v))return v;return{touched:!0}}export function registerSiteAuthCommands(v){if(!v?.site||!v?.domain||!v?.loginUrl||typeof v.verify!=="function")throw Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");$({site:v.site,name:"whoami",access:"read",description:v.whoamiDescription??`Show the current logged-in ${v.site} account`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:H(v),authStatus:{...typeof v.quickCheck==="function"?{quickCheck:async(x)=>I(await v.quickCheck(x))}:{},...v.quickCheckRequiresNavigation===!0?{quickCheckRequiresNavigation:!0}:{},...typeof v.refresh==="function"?{refresh:async(x,G)=>P(await v.refresh(x,G))}:{}},func:async(x)=>Y(v,x,"identity")});$({site:v.site,name:"login",access:"write",description:v.loginDescription??`Open ${v.site} login and wait until the browser session is authenticated`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:V,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...H(v)],func:async(x,G)=>{try{const B={status:"already_logged_in",...await Y(v,x,"identity")};await J(x);return B}catch(B){if(!j(B)){await J(x);throw B}}await x.goto(v.loginUrl);const K=Number(G.timeout??V),Z=Date.now()+K*1000;let X="";while(Date.now()<Z){await x.wait(Math.min(b/1000,Math.max(0.2,(Z-Date.now())/1000)));try{const B=await Y(v,x,"poll");await J(x);return{status:"login_complete",...B}}catch(B){if(!j(B)){await J(x);throw B}X=N(B)}}await J(x);throw new z(`${v.site} login`,K,X?`${F(v)} Last auth check: ${X}`:F(v))}})}
@@ -1,2 +1,2 @@
1
- import*as D from"node:fs";import*as O from"node:os";import*as K from"node:path";import{cli as L,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as T}from"@jackwener/opencli/errors";function I(b){return K.join(O.homedir(),".opencli","sites",b,"credentials.json")}export function loadCredentials(b){try{return JSON.parse(D.readFileSync(I(b),"utf8"))}catch(x){if(x&&x.code==="ENOENT")return null;throw x}}export function saveCredentials(b,x){const z=I(b);D.mkdirSync(K.dirname(z),{recursive:!0});D.writeFileSync(z,`${JSON.stringify(x,null,2)}
2
- `,{mode:384});try{D.chmodSync(z,384)}catch{}}export function clearCredentials(b){try{D.rmSync(I(b));return!0}catch(x){if(x&&x.code==="ENOENT")return!1;throw x}}export function requireCredentials(b){const x=loadCredentials(b);if(!x)throw new T(b,`${b} not configured. Run \`opencli ${b} login\` with your credentials first.`);return x}export function registerTokenAuth(b){if(!b?.site||!b?.domain||!Array.isArray(b.fields)||typeof b.validate!=="function")throw Error("registerTokenAuth requires site, domain, fields[], and validate(creds)");const x=b.identityColumns??["id","username","name"];L({site:b.site,name:"whoami",access:"read",description:b.whoamiDescription??`Show the configured ${b.site} account`,domain:b.domain,strategy:M.LOCAL,browser:!1,args:[],columns:["logged_in","site",...x],func:async()=>{const z=requireCredentials(b.site),F=await b.validate(z);return{logged_in:!0,site:b.site,...F}}});L({site:b.site,name:"login",access:"write",description:b.loginDescription??`Configure ${b.site} credentials (token/key based, no browser). Pass each field as a flag.`,domain:b.domain,strategy:M.LOCAL,browser:!1,args:b.fields.map((z)=>({name:z.name,type:"string",required:!1,help:z.help??`${b.site} ${z.name}`,...z.default!==void 0?{default:z.default}:{}})),columns:["status","logged_in","site",...x],func:async(z)=>{const F=loadCredentials(b.site)??{},G={};for(const B of b.fields){const H=z[B.name]??F[B.name]??B.default;if(H!==void 0&&H!==null&&H!=="")G[B.name]=H}const J=b.fields.filter((B)=>B.required!==!1&&G[B.name]===void 0);if(J.length)throw new Q(`Missing required credential(s): ${J.map((B)=>`--${B.name}`).join(", ")}`,`Run \`opencli ${b.site} login -h\` to see all fields.`);const N=await b.validate(G);saveCredentials(b.site,G);return{status:Object.keys(F).length?"updated":"login_complete",logged_in:!0,site:b.site,...N}}})}
1
+ import*as D from"node:fs";import*as O from"node:os";import*as K from"node:path";import{cli as L,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as T}from"@jackwener/opencli/errors";function I(b){return K.join(O.homedir(),".ppcli","sites",b,"credentials.json")}export function loadCredentials(b){try{return JSON.parse(D.readFileSync(I(b),"utf8"))}catch(x){if(x&&x.code==="ENOENT")return null;throw x}}export function saveCredentials(b,x){const z=I(b);D.mkdirSync(K.dirname(z),{recursive:!0});D.writeFileSync(z,`${JSON.stringify(x,null,2)}
2
+ `,{mode:384});try{D.chmodSync(z,384)}catch{}}export function clearCredentials(b){try{D.rmSync(I(b));return!0}catch(x){if(x&&x.code==="ENOENT")return!1;throw x}}export function requireCredentials(b){const x=loadCredentials(b);if(!x)throw new T(b,`${b} not configured. Run \`ppcli ${b} login\` with your credentials first.`);return x}export function registerTokenAuth(b){if(!b?.site||!b?.domain||!Array.isArray(b.fields)||typeof b.validate!=="function")throw Error("registerTokenAuth requires site, domain, fields[], and validate(creds)");const x=b.identityColumns??["id","username","name"];L({site:b.site,name:"whoami",access:"read",description:b.whoamiDescription??`Show the configured ${b.site} account`,domain:b.domain,strategy:M.LOCAL,browser:!1,args:[],columns:["logged_in","site",...x],func:async()=>{const z=requireCredentials(b.site),F=await b.validate(z);return{logged_in:!0,site:b.site,...F}}});L({site:b.site,name:"login",access:"write",description:b.loginDescription??`Configure ${b.site} credentials (token/key based, no browser). Pass each field as a flag.`,domain:b.domain,strategy:M.LOCAL,browser:!1,args:b.fields.map((z)=>({name:z.name,type:"string",required:!1,help:z.help??`${b.site} ${z.name}`,...z.default!==void 0?{default:z.default}:{}})),columns:["status","logged_in","site",...x],func:async(z)=>{const F=loadCredentials(b.site)??{},G={};for(const B of b.fields){const H=z[B.name]??F[B.name]??B.default;if(H!==void 0&&H!==null&&H!=="")G[B.name]=H}const J=b.fields.filter((B)=>B.required!==!1&&G[B.name]===void 0);if(J.length)throw new Q(`Missing required credential(s): ${J.map((B)=>`--${B.name}`).join(", ")}`,`Run \`ppcli ${b.site} login -h\` to see all fields.`);const N=await b.validate(G);saveCredentials(b.site,G);return{status:Object.keys(F).length?"updated":"login_complete",logged_in:!0,site:b.site,...N}}})}
@@ -1,4 +1,4 @@
1
- import*as Z from"node:fs";import*as L from"node:path";import{ArgumentError as J,CommandExecutionError as Y}from"@jackwener/opencli/errors";export const SUPPORTED_VIDEO_EXTENSIONS=new Set([".mp4",".mov",".avi",".mkv",".m4v",".webm",".flv",".wmv"]),SUPPORTED_IMAGE_EXTENSIONS=new Set([".jpg",".jpeg",".png",".webp",".bmp"]);export function parseScheduleDate(q){if(q==null||q==="")return null;const y=String(q),z=/^\d+$/.test(y)?new Date(Number(y)<1000000000000?Number(y)*1000:Number(y)):new Date(y);if(Number.isNaN(z.getTime()))throw new J(`无法解析定时时间: ${q}`);if(z.getTime()<=Date.now())throw new J("定时发布时间必须晚于当前时间");return z}export function unwrap(q){if(q&&typeof q==="object"&&"data"in q&&"session"in q)return q.data;return q}export async function evalPage(q,y){return unwrap(await q.evaluate(y))}export function sleep(q){return new Promise((y)=>setTimeout(y,q*1000))}export function resolveVideoFile(q){const y=L.resolve(String(q));let z;try{z=Z.statSync(y)}catch{throw new J(`视频文件不存在: ${y}`)}if(!z.isFile())throw new J(`视频路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_VIDEO_EXTENSIONS.has(B))throw new J(`不支持的视频格式: ${B}(支持 ${[...SUPPORTED_VIDEO_EXTENSIONS].join("/")})`);return y}export function resolveImageFile(q){const y=L.resolve(String(q));let z;try{z=Z.statSync(y)}catch{throw new J(`图片文件不存在: ${y}`)}if(!z.isFile())throw new J(`图片路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_IMAGE_EXTENSIONS.has(B))throw new J(`不支持的图片格式: ${B}(支持 ${[...SUPPORTED_IMAGE_EXTENSIONS].join("/")})`);return y}export function throwIfFileAccessDenied(q){if(!(q instanceof Error?q.message:String(q)).includes("Not allowed"))return;throw new Y("Chrome 拦截了扩展读取本地文件(DOM.setFileInputFiles → Not allowed):商店安装的扩展默认没有本地文件访问权限","请让用户在 Chrome 打开 chrome://extensions,找到「OpenCLI Browser Bridge」→「详情」→ 打开「允许访问文件网址」(Allow access to file URLs),开关即时生效,然后重试本命令")}export async function setVideoInput(q,y,z){if(!q.setFileInput)throw new Y("浏览器扩展不支持 CDP 文件上传(set-file-input),无法上传视频;请升级 PublishPort 客户端/扩展");let B;for(const H of y)try{await q.setFileInput(z,H);return H}catch(K){throwIfFileAccessDenied(K);B=K}throw new Y(`未找到可用的文件上传输入框(尝试过: ${y.join(" , ")}):${B instanceof Error?B.message:String(B)}`)}export async function currentUrl(q){try{if(q.getCurrentUrl){const y=await q.getCurrentUrl();if(y)return String(y)}}catch{}try{return String(await evalPage(q,"() => location.href")||"")}catch{return""}}export async function waitForUrlIncludes(q,y,z=60000,B=500){const H=Array.isArray(y)?y:[y],K=Date.now()+z;while(Date.now()<K){const Q=await currentUrl(q);if(Q&&H.some(($)=>Q.includes($)))return!0;await q.wait({time:B/1000})}return!1}export async function countByText(q,y){return Number(unwrap(await q.evaluate(`
1
+ import*as Z from"node:fs";import*as L from"node:path";import{ArgumentError as J,CommandExecutionError as Y}from"@jackwener/opencli/errors";export const SUPPORTED_VIDEO_EXTENSIONS=new Set([".mp4",".mov",".avi",".mkv",".m4v",".webm",".flv",".wmv"]),SUPPORTED_IMAGE_EXTENSIONS=new Set([".jpg",".jpeg",".png",".webp",".bmp"]);export function parseScheduleDate(q){if(q==null||q==="")return null;const y=String(q),z=/^\d+$/.test(y)?new Date(Number(y)<1000000000000?Number(y)*1000:Number(y)):new Date(y);if(Number.isNaN(z.getTime()))throw new J(`无法解析定时时间: ${q}`);if(z.getTime()<=Date.now())throw new J("定时发布时间必须晚于当前时间");return z}export function unwrap(q){if(q&&typeof q==="object"&&"data"in q&&"session"in q)return q.data;return q}export async function evalPage(q,y){return unwrap(await q.evaluate(y))}export function sleep(q){return new Promise((y)=>setTimeout(y,q*1000))}export function resolveVideoFile(q){const y=L.resolve(String(q));let z;try{z=Z.statSync(y)}catch{throw new J(`视频文件不存在: ${y}`)}if(!z.isFile())throw new J(`视频路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_VIDEO_EXTENSIONS.has(B))throw new J(`不支持的视频格式: ${B}(支持 ${[...SUPPORTED_VIDEO_EXTENSIONS].join("/")})`);return y}export function resolveImageFile(q){const y=L.resolve(String(q));let z;try{z=Z.statSync(y)}catch{throw new J(`图片文件不存在: ${y}`)}if(!z.isFile())throw new J(`图片路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_IMAGE_EXTENSIONS.has(B))throw new J(`不支持的图片格式: ${B}(支持 ${[...SUPPORTED_IMAGE_EXTENSIONS].join("/")})`);return y}export function throwIfFileAccessDenied(q){if(!(q instanceof Error?q.message:String(q)).includes("Not allowed"))return;throw new Y("Chrome 拦截了扩展读取本地文件(DOM.setFileInputFiles → Not allowed):商店安装的扩展默认没有本地文件访问权限","请让用户在 Chrome 打开 chrome://extensions,找到「ppcli Browser Bridge」→「详情」→ 打开「允许访问文件网址」(Allow access to file URLs),开关即时生效,然后重试本命令")}export async function setVideoInput(q,y,z){if(!q.setFileInput)throw new Y("浏览器扩展不支持 CDP 文件上传(set-file-input),无法上传视频;请升级 PublishPort 客户端/扩展");let B;for(const H of y)try{await q.setFileInput(z,H);return H}catch(K){throwIfFileAccessDenied(K);B=K}throw new Y(`未找到可用的文件上传输入框(尝试过: ${y.join(" , ")}):${B instanceof Error?B.message:String(B)}`)}export async function currentUrl(q){try{if(q.getCurrentUrl){const y=await q.getCurrentUrl();if(y)return String(y)}}catch{}try{return String(await evalPage(q,"() => location.href")||"")}catch{return""}}export async function waitForUrlIncludes(q,y,z=60000,B=500){const H=Array.isArray(y)?y:[y],K=Date.now()+z;while(Date.now()<K){const Q=await currentUrl(q);if(Q&&H.some(($)=>Q.includes($)))return!0;await q.wait({time:B/1000})}return!1}export async function countByText(q,y){return Number(unwrap(await q.evaluate(`
2
2
  ((want) => {
3
3
  let n = 0;
4
4
  const nodes = document.querySelectorAll('body *');
@@ -1,4 +1,4 @@
1
- import{cli as h,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as s,EmptyResultError as d,getErrorMessage as b}from"@jackwener/opencli/errors";const g="https://www.aibase.com/zh/daily",a=20,o=50;function l(e){const r=Number(e??a);if(!Number.isInteger(r)||r<=0)throw new c("limit must be a positive integer",`Example: opencli aibase news --limit ${a}`);if(r>o)throw new c(`limit must be <= ${o}`,`Example: opencli aibase news --limit ${o}`);return r}function u(e){return String(e??"").replace(/\s+/g," ").trim()}function m(){return`
1
+ import{cli as h,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as s,EmptyResultError as d,getErrorMessage as b}from"@jackwener/opencli/errors";const g="https://www.aibase.com/zh/daily",a=20,o=50;function l(e){const r=Number(e??a);if(!Number.isInteger(r)||r<=0)throw new c("limit must be a positive integer",`Example: ppcli aibase news --limit ${a}`);if(r>o)throw new c(`limit must be <= ${o}`,`Example: ppcli aibase news --limit ${o}`);return r}function u(e){return String(e??"").replace(/\s+/g," ").trim()}function m(){return`
2
2
  (() => {
3
3
  const anchors = Array.from(document.querySelectorAll('.bg-white .grid a[href], a[href*="/zh/daily/"]'))
4
4
  .filter((anchor) => {
@@ -1,6 +1,6 @@
1
- import{ArgumentError as q,CommandExecutionError as K}from"@jackwener/opencli/errors";export const SITE="amazon",DOMAIN="amazon.com",HOME_URL="https://www.amazon.com/",BESTSELLERS_URL="https://www.amazon.com/Best-Sellers/zgbs",NEW_RELEASES_URL="https://www.amazon.com/gp/new-releases",MOVERS_SHAKERS_URL="https://www.amazon.com/gp/movers-and-shakers",SEARCH_URL_PREFIX="https://www.amazon.com/s?k=",PRODUCT_URL_PREFIX="https://www.amazon.com/dp/",DISCUSSION_URL_PREFIX="https://www.amazon.com/product-reviews/",STRATEGY="cookie",PRIMARY_PRICE_SELECTORS=["#corePrice_feature_div .a-offscreen","#corePriceDisplay_desktop_feature_div .a-offscreen","#corePrice_desktop .a-offscreen","#apex_desktop .a-offscreen","#newAccordionRow_0 .a-offscreen","#price_inside_buybox","#priceblock_ourprice","#priceblock_dealprice","#tp_price_block_total_price_ww"];const V=["Sorry, we just need to make sure you're not a robot","Enter the characters you see below","Type the characters you see in this image","To discuss automated access to Amazon data please contact"],f={bestsellers:{commandName:"bestsellers",rootUrl:BESTSELLERS_URL,pathPattern:/(?:^|\/)zgbs(?:\/|$)/i,invalidInputMessage:"amazon bestsellers expects a best sellers URL or /zgbs path",invalidInputHint:"Example: opencli amazon bestsellers https://www.amazon.com/Best-Sellers/zgbs"},new_releases:{commandName:"new-releases",rootUrl:NEW_RELEASES_URL,pathPattern:/\/gp\/new-releases(?:\/|$)/i,invalidInputMessage:"amazon new-releases expects a new releases URL or /gp/new-releases path",invalidInputHint:"Example: opencli amazon new-releases https://www.amazon.com/gp/new-releases"},movers_shakers:{commandName:"movers-shakers",rootUrl:MOVERS_SHAKERS_URL,pathPattern:/\/gp\/movers-and-shakers(?:\/|$)/i,invalidInputMessage:"amazon movers-shakers expects a movers-and-shakers URL or /gp/movers-and-shakers path",invalidInputHint:"Example: opencli amazon movers-shakers https://www.amazon.com/gp/movers-and-shakers"}};export function cleanText(Q){return typeof Q==="string"?Q.replace(/\u00a0/g," ").replace(/\s+/g," ").trim():""}export function cleanMultilineText(Q){return typeof Q==="string"?Q.replace(/\u00a0/g," ").split(`
1
+ import{ArgumentError as q,CommandExecutionError as K}from"@jackwener/opencli/errors";export const SITE="amazon",DOMAIN="amazon.com",HOME_URL="https://www.amazon.com/",BESTSELLERS_URL="https://www.amazon.com/Best-Sellers/zgbs",NEW_RELEASES_URL="https://www.amazon.com/gp/new-releases",MOVERS_SHAKERS_URL="https://www.amazon.com/gp/movers-and-shakers",SEARCH_URL_PREFIX="https://www.amazon.com/s?k=",PRODUCT_URL_PREFIX="https://www.amazon.com/dp/",DISCUSSION_URL_PREFIX="https://www.amazon.com/product-reviews/",STRATEGY="cookie",PRIMARY_PRICE_SELECTORS=["#corePrice_feature_div .a-offscreen","#corePriceDisplay_desktop_feature_div .a-offscreen","#corePrice_desktop .a-offscreen","#apex_desktop .a-offscreen","#newAccordionRow_0 .a-offscreen","#price_inside_buybox","#priceblock_ourprice","#priceblock_dealprice","#tp_price_block_total_price_ww"];const V=["Sorry, we just need to make sure you're not a robot","Enter the characters you see below","Type the characters you see in this image","To discuss automated access to Amazon data please contact"],f={bestsellers:{commandName:"bestsellers",rootUrl:BESTSELLERS_URL,pathPattern:/(?:^|\/)zgbs(?:\/|$)/i,invalidInputMessage:"amazon bestsellers expects a best sellers URL or /zgbs path",invalidInputHint:"Example: ppcli amazon bestsellers https://www.amazon.com/Best-Sellers/zgbs"},new_releases:{commandName:"new-releases",rootUrl:NEW_RELEASES_URL,pathPattern:/\/gp\/new-releases(?:\/|$)/i,invalidInputMessage:"amazon new-releases expects a new releases URL or /gp/new-releases path",invalidInputHint:"Example: ppcli amazon new-releases https://www.amazon.com/gp/new-releases"},movers_shakers:{commandName:"movers-shakers",rootUrl:MOVERS_SHAKERS_URL,pathPattern:/\/gp\/movers-and-shakers(?:\/|$)/i,invalidInputMessage:"amazon movers-shakers expects a movers-and-shakers URL or /gp/movers-and-shakers path",invalidInputHint:"Example: ppcli amazon movers-shakers https://www.amazon.com/gp/movers-and-shakers"}};export function cleanText(Q){return typeof Q==="string"?Q.replace(/\u00a0/g," ").replace(/\s+/g," ").trim():""}export function cleanMultilineText(Q){return typeof Q==="string"?Q.replace(/\u00a0/g," ").split(`
2
2
  `).map((J)=>J.replace(/\s+/g," ").trim()).filter(Boolean).join(`
3
- `):""}export function uniqueNonEmpty(Q){return[...new Set(Q.map((J)=>cleanText(J)).filter(Boolean))]}export function buildProvenance(Q){return{source_url:Q,fetched_at:new Date().toISOString(),strategy:STRATEGY}}export function buildSearchUrl(Q){const J=cleanText(Q);if(!J)throw new q("amazon search query cannot be empty");return`${SEARCH_URL_PREFIX}${encodeURIComponent(J)}`}export function extractAsin(Q){const J=cleanText(Q);if(!J)return null;if(/^[A-Z0-9]{10}$/i.test(J))return J.toUpperCase();const $=J.match(/\/(?:dp|gp\/product|product-reviews)\/([A-Z0-9]{10})/i);return $?$[1].toUpperCase():null}export function buildProductUrl(Q){const J=extractAsin(Q);if(!J)throw new q("amazon product expects an ASIN or product URL","Example: opencli amazon product B0FJS72893");return`${PRODUCT_URL_PREFIX}${J}`}export function buildDiscussionUrl(Q){const J=extractAsin(Q);if(!J)throw new q("amazon discussion expects an ASIN or product URL","Example: opencli amazon discussion B0FJS72893");return`${DISCUSSION_URL_PREFIX}${J}`}function Y(Q){return f[Q]}export function isSupportedRankingPath(Q,J){try{const $=new URL(J);return Y(Q).pathPattern.test($.pathname)}catch{return!1}}export function resolveRankingUrl(Q,J){const $=Y(Q),j=cleanText(J);if(!j||j==="root")return $.rootUrl;let W;if(j.startsWith("/"))W=new URL(j,HOME_URL).toString();else if(/^https?:\/\//i.test(j))W=canonicalizeAmazonUrl(j);else if(j.includes("amazon.")&&j.includes("/"))W=canonicalizeAmazonUrl(`https://${j.replace(/^\/+/,"")}`);else throw new q($.invalidInputMessage,$.invalidInputHint);if(!isSupportedRankingPath(Q,W))throw new q($.invalidInputMessage,$.invalidInputHint);return w(W)}function w(Q){try{const J=new URL(Q),$=J.pathname.split("/").filter(Boolean).filter((j)=>!/^ref=/i.test(j));J.pathname=`/${$.join("/")}`;J.hash="";J.searchParams.delete("ref");return J.toString()}catch{return Q}}export function isRankingPaginationUrl(Q,J){const $=toAbsoluteAmazonUrl(J);if(!$||!isSupportedRankingPath(Q,$))return!1;try{const j=new URL($),W=cleanText(j.searchParams.get("ref")).toLowerCase();return j.searchParams.has("pg")||/(?:^|_)pg(?:_|$)/.test(W)||/zg_bs(?:nr|ms)?_pg_/.test(W)}catch{return!1}}export function extractCategoryNodeId(Q){const J=toAbsoluteAmazonUrl(Q);if(!J)return null;try{const $=new URL(J);for(const B of["node","nodeid","nodeId","browseNode"]){const G=cleanText($.searchParams.get(B));if(/^\d{4,}$/.test(G))return G}const j=cleanText($.searchParams.get("rh")),W=decodeURIComponent(j).match(/(?:^|,)\s*n:(\d{4,})(?:,|$)/i);if(W)return W[1];const Z=[...$.pathname.matchAll(/\/(\d{4,})(?=\/|$)/g)];if(Z.length>0)return Z[Z.length-1][1]}catch{return null}return null}export function resolveBestsellersUrl(Q){return resolveRankingUrl("bestsellers",Q)}export function canonicalizeAmazonUrl(Q){try{const J=new URL(Q);if(!J.hostname.endsWith(DOMAIN))throw Error("not-amazon");return J.toString()}catch{throw new q("Invalid Amazon URL")}}export function toAbsoluteAmazonUrl(Q){const J=cleanText(Q);if(!J)return null;try{return new URL(J,HOME_URL).toString()}catch{return null}}export function normalizeProductUrl(Q){const J=cleanText(Q),$=extractAsin(J);if($)return buildProductUrl($);return toAbsoluteAmazonUrl(J)}export function parsePriceText(Q){const J=cleanText(Q),$=J.match(/([$€£])\s*(\d+(?:,\d{3})*(?:\.\d+)?)/);if(!$)return{price_text:J||null,price_value:null,currency:null};const j={$:"USD","€":"EUR","£":"GBP"};return{price_text:`${$[1]}${$[2]}`,price_value:Number.parseFloat($[2].replace(/,/g,"")),currency:j[$[1]]??null}}export function parseRatingValue(Q){const $=cleanText(Q).match(/(\d+(?:\.\d+)?)\s*out of 5/i);return $?Number.parseFloat($[1]):null}export function parseReviewCount(Q){const J=cleanText(Q),$=J.match(/(\d+(?:\.\d+)?)\s*([kKmM])/);if($){const W=Number.parseFloat($[1]),Z=/m/i.test($[2])?1e6:1000;return Number.isFinite(W)?Math.round(W*Z):null}const j=J.match(/([\d,]+)/);return j?Number.parseInt(j[1].replace(/,/g,""),10):null}export function extractReviewCountFromCardText(Q){const J=cleanMultilineText(Q),$=J.match(/out of 5 stars(?:, rating details)?\s*([\d,]+)/i);if($)return $[1];return J.split(`
3
+ `):""}export function uniqueNonEmpty(Q){return[...new Set(Q.map((J)=>cleanText(J)).filter(Boolean))]}export function buildProvenance(Q){return{source_url:Q,fetched_at:new Date().toISOString(),strategy:STRATEGY}}export function buildSearchUrl(Q){const J=cleanText(Q);if(!J)throw new q("amazon search query cannot be empty");return`${SEARCH_URL_PREFIX}${encodeURIComponent(J)}`}export function extractAsin(Q){const J=cleanText(Q);if(!J)return null;if(/^[A-Z0-9]{10}$/i.test(J))return J.toUpperCase();const $=J.match(/\/(?:dp|gp\/product|product-reviews)\/([A-Z0-9]{10})/i);return $?$[1].toUpperCase():null}export function buildProductUrl(Q){const J=extractAsin(Q);if(!J)throw new q("amazon product expects an ASIN or product URL","Example: ppcli amazon product B0FJS72893");return`${PRODUCT_URL_PREFIX}${J}`}export function buildDiscussionUrl(Q){const J=extractAsin(Q);if(!J)throw new q("amazon discussion expects an ASIN or product URL","Example: ppcli amazon discussion B0FJS72893");return`${DISCUSSION_URL_PREFIX}${J}`}function Y(Q){return f[Q]}export function isSupportedRankingPath(Q,J){try{const $=new URL(J);return Y(Q).pathPattern.test($.pathname)}catch{return!1}}export function resolveRankingUrl(Q,J){const $=Y(Q),j=cleanText(J);if(!j||j==="root")return $.rootUrl;let W;if(j.startsWith("/"))W=new URL(j,HOME_URL).toString();else if(/^https?:\/\//i.test(j))W=canonicalizeAmazonUrl(j);else if(j.includes("amazon.")&&j.includes("/"))W=canonicalizeAmazonUrl(`https://${j.replace(/^\/+/,"")}`);else throw new q($.invalidInputMessage,$.invalidInputHint);if(!isSupportedRankingPath(Q,W))throw new q($.invalidInputMessage,$.invalidInputHint);return w(W)}function w(Q){try{const J=new URL(Q),$=J.pathname.split("/").filter(Boolean).filter((j)=>!/^ref=/i.test(j));J.pathname=`/${$.join("/")}`;J.hash="";J.searchParams.delete("ref");return J.toString()}catch{return Q}}export function isRankingPaginationUrl(Q,J){const $=toAbsoluteAmazonUrl(J);if(!$||!isSupportedRankingPath(Q,$))return!1;try{const j=new URL($),W=cleanText(j.searchParams.get("ref")).toLowerCase();return j.searchParams.has("pg")||/(?:^|_)pg(?:_|$)/.test(W)||/zg_bs(?:nr|ms)?_pg_/.test(W)}catch{return!1}}export function extractCategoryNodeId(Q){const J=toAbsoluteAmazonUrl(Q);if(!J)return null;try{const $=new URL(J);for(const B of["node","nodeid","nodeId","browseNode"]){const G=cleanText($.searchParams.get(B));if(/^\d{4,}$/.test(G))return G}const j=cleanText($.searchParams.get("rh")),W=decodeURIComponent(j).match(/(?:^|,)\s*n:(\d{4,})(?:,|$)/i);if(W)return W[1];const Z=[...$.pathname.matchAll(/\/(\d{4,})(?=\/|$)/g)];if(Z.length>0)return Z[Z.length-1][1]}catch{return null}return null}export function resolveBestsellersUrl(Q){return resolveRankingUrl("bestsellers",Q)}export function canonicalizeAmazonUrl(Q){try{const J=new URL(Q);if(!J.hostname.endsWith(DOMAIN))throw Error("not-amazon");return J.toString()}catch{throw new q("Invalid Amazon URL")}}export function toAbsoluteAmazonUrl(Q){const J=cleanText(Q);if(!J)return null;try{return new URL(J,HOME_URL).toString()}catch{return null}}export function normalizeProductUrl(Q){const J=cleanText(Q),$=extractAsin(J);if($)return buildProductUrl($);return toAbsoluteAmazonUrl(J)}export function parsePriceText(Q){const J=cleanText(Q),$=J.match(/([$€£])\s*(\d+(?:,\d{3})*(?:\.\d+)?)/);if(!$)return{price_text:J||null,price_value:null,currency:null};const j={$:"USD","€":"EUR","£":"GBP"};return{price_text:`${$[1]}${$[2]}`,price_value:Number.parseFloat($[2].replace(/,/g,"")),currency:j[$[1]]??null}}export function parseRatingValue(Q){const $=cleanText(Q).match(/(\d+(?:\.\d+)?)\s*out of 5/i);return $?Number.parseFloat($[1]):null}export function parseReviewCount(Q){const J=cleanText(Q),$=J.match(/(\d+(?:\.\d+)?)\s*([kKmM])/);if($){const W=Number.parseFloat($[1]),Z=/m/i.test($[2])?1e6:1000;return Number.isFinite(W)?Math.round(W*Z):null}const j=J.match(/([\d,]+)/);return j?Number.parseInt(j[1].replace(/,/g,""),10):null}export function extractReviewCountFromCardText(Q){const J=cleanMultilineText(Q),$=J.match(/out of 5 stars(?:, rating details)?\s*([\d,]+)/i);if($)return $[1];return J.split(`
4
4
  `).map((W)=>cleanText(W)).find((W)=>/^[\d,]+$/.test(W))??null}export function isAmazonEntity(Q){return cleanText(Q).toLowerCase().includes("amazon")}export function firstMeaningfulLine(Q){return cleanMultilineText(Q).split(`
5
5
  `).map((J)=>cleanText(J)).find(Boolean)??""}export function trimRatingPrefix(Q){const J=cleanText(Q);if(!J)return null;return J.replace(/^\d+(?:\.\d+)?\s*out of 5 stars\s*/i,"").trim()||J}export function isRobotState(Q){const J=cleanText(Q.title),$=cleanMultilineText(Q.body_text);return V.some((j)=>J.includes(j)||$.includes(j))}export function buildChallengeHint(Q){return[`Open a clean Amazon ${Q} page in the shared Chrome profile and clear any robot check first.`,"If you are using CDP, set OPENCLI_CDP_TARGET=amazon.com and avoid parallel Amazon commands against the same browser target."].join(" ")}export async function readPageState(Q){const J=await Q.evaluate(`
6
6
  (() => ({
@@ -1,14 +1,14 @@
1
1
  ---
2
- description: How to automate Antigravity using OpenCLI
2
+ description: How to automate Antigravity using ppcli
3
3
  ---
4
4
 
5
5
  # Antigravity Automation Skill
6
6
 
7
- This skill allows AI agents to control the [Antigravity](https://github.com/chengazhen/Antigravity) desktop app (and any Electron app with CDP enabled) programmatically via OpenCLI.
7
+ This skill allows AI agents to control the [Antigravity](https://github.com/chengazhen/Antigravity) desktop app (and any Electron app with CDP enabled) programmatically via ppcli.
8
8
 
9
9
  ## Requirements
10
- opencli automatically detects, launches (with `--remote-debugging-port=9234`), and connects to Antigravity.
11
- If Antigravity is already running without CDP, opencli will prompt to restart it.
10
+ ppcli automatically detects, launches (with `--remote-debugging-port=9234`), and connects to Antigravity.
11
+ If Antigravity is already running without CDP, ppcli will prompt to restart it.
12
12
 
13
13
  If the endpoint exposes multiple inspectable targets, set:
14
14
  \`\`\`bash
@@ -16,23 +16,23 @@ export OPENCLI_CDP_TARGET="antigravity"
16
16
  \`\`\`
17
17
 
18
18
  ## High-Level Capabilities
19
- 1. **Send Messages (`opencli antigravity send <message>`)**: Type and send a message directly into the chat UI.
20
- 2. **Read History (`opencli antigravity read`)**: Scrape the raw chat transcript from the main UI container.
21
- 3. **Extract Code (`opencli antigravity extract-code`)**: Automatically isolate and extract source code text blocks from the AI's recent answers.
22
- 4. **Switch Models (`opencli antigravity model <name>`)**: Instantly toggle the active LLM (e.g., \`gemini\`, \`claude\`).
23
- 5. **Clear Context (`opencli antigravity new`)**: Start a fresh conversation.
19
+ 1. **Send Messages (`ppcli antigravity send <message>`)**: Type and send a message directly into the chat UI.
20
+ 2. **Read History (`ppcli antigravity read`)**: Scrape the raw chat transcript from the main UI container.
21
+ 3. **Extract Code (`ppcli antigravity extract-code`)**: Automatically isolate and extract source code text blocks from the AI's recent answers.
22
+ 4. **Switch Models (`ppcli antigravity model <name>`)**: Instantly toggle the active LLM (e.g., \`gemini\`, \`claude\`).
23
+ 5. **Clear Context (`ppcli antigravity new`)**: Start a fresh conversation.
24
24
 
25
25
  ## Examples for Automated Workflows
26
26
 
27
27
  ### Generating and Saving Code
28
28
  \`\`\`bash
29
- opencli antigravity send "Write a python script to fetch HN top stories"
29
+ ppcli antigravity send "Write a python script to fetch HN top stories"
30
30
  # wait ~10-15 seconds for output to render
31
- opencli antigravity extract-code > hn_fetcher.py
31
+ ppcli antigravity extract-code > hn_fetcher.py
32
32
  \`\`\`
33
33
 
34
34
  ### Reading Real-time Logs
35
35
  Agents can run long-running streaming watch instances:
36
36
  \`\`\`bash
37
- opencli antigravity watch
37
+ ppcli antigravity watch
38
38
  \`\`\`
@@ -1,5 +1,5 @@
1
1
  import*as J from"node:fs";import*as q from"node:path";import*as b from"node:os";import{execFileSync as f}from"node:child_process";import{cli as B,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as h,CommandExecutionError as D,EmptyResultError as K}from"@jackwener/opencli/errors";import{unwrapEvaluateResult as P}from"./_actions.js";const W=["Index","Key","Bytes","Name","Preview","Database","Version","Kind","Path","Workspace Id","Folder","Modified","Field","Value"],y=q.join(b.homedir(),"Library/Application Support/Antigravity"),L=q.join(y,"User"),x=q.join(L,"globalStorage/state.vscdb"),M=q.join(L,"workspaceStorage"),U=q.join(L,"settings.json");function v(H,z){if(!J.existsSync(H))throw new D(`state.vscdb not found: ${H}`,"Has Antigravity been run at least once?");try{return f("/usr/bin/sqlite3",[H,z],{encoding:"utf-8",maxBuffer:67108864})}catch(Z){throw new D(`sqlite3 failed on ${q.basename(H)}: ${Z.message}`,"The DB may be locked by a running Antigravity instance. Try closing it or wait a few seconds.")}}function G(H){return v(H,"SELECT key FROM ItemTable ORDER BY key;").split(`
2
- `).map((Z)=>Z.trim()).filter(Boolean)}function C(H,z){const Z=z.replace(/'/g,"''"),Y=v(H,`SELECT value FROM ItemTable WHERE key = '${Z}';`).trim();if(!Y)return null;try{return JSON.parse(Y)}catch{return Y}}function T(H){const z=H?.workspace?String(H.workspace).trim():"";if(!z)return x;const Z=q.join(M,z,"state.vscdb");if(!J.existsSync(Z))throw new D(`Workspace state.vscdb not found: ${Z}`,"List workspace ids with `opencli antigravity workspaces-list`.");return Z}B({site:"antigravity",name:"storage-keys",access:"read",description:"List localStorage / sessionStorage keys on the Antigravity renderer (CDP).",domain:"127.0.0.1",strategy:I.UI,browser:!0,args:[{name:"storage",required:!1,default:"local",help:'"local" or "session"'},{name:"filter",required:!1,help:"Case-insensitive substring filter"},{name:"limit",type:"int",required:!1,default:100,help:"Max rows to return"}],columns:W,func:async(H,z)=>{const Z=String(z?.storage||"local").trim().toLowerCase();if(Z!=="local"&&Z!=="session")throw new h("storage",'must be "local" or "session"');const Y=Z==="session"?"sessionStorage":"localStorage",X=P(await H.evaluate(`(() => {
2
+ `).map((Z)=>Z.trim()).filter(Boolean)}function C(H,z){const Z=z.replace(/'/g,"''"),Y=v(H,`SELECT value FROM ItemTable WHERE key = '${Z}';`).trim();if(!Y)return null;try{return JSON.parse(Y)}catch{return Y}}function T(H){const z=H?.workspace?String(H.workspace).trim():"";if(!z)return x;const Z=q.join(M,z,"state.vscdb");if(!J.existsSync(Z))throw new D(`Workspace state.vscdb not found: ${Z}`,"List workspace ids with `ppcli antigravity workspaces-list`.");return Z}B({site:"antigravity",name:"storage-keys",access:"read",description:"List localStorage / sessionStorage keys on the Antigravity renderer (CDP).",domain:"127.0.0.1",strategy:I.UI,browser:!0,args:[{name:"storage",required:!1,default:"local",help:'"local" or "session"'},{name:"filter",required:!1,help:"Case-insensitive substring filter"},{name:"limit",type:"int",required:!1,default:100,help:"Max rows to return"}],columns:W,func:async(H,z)=>{const Z=String(z?.storage||"local").trim().toLowerCase();if(Z!=="local"&&Z!=="session")throw new h("storage",'must be "local" or "session"');const Y=Z==="session"?"sessionStorage":"localStorage",X=P(await H.evaluate(`(() => {
3
3
  const s = ${Y};
4
4
  const out = [];
5
5
  for (let i = 0; i < s.length; i++) {
@@ -1 +1 @@
1
- import{cli as z,Strategy as A}from"@jackwener/opencli/registry";import{CliError as B}from"@jackwener/opencli/errors";import{itunesFetch as G,formatDuration as H,formatDate as I}from"./utils.js";z({site:"apple-podcasts",name:"episodes",access:"read",description:"List recent episodes of an Apple Podcast (use ID from search)",strategy:A.PUBLIC,browser:!1,args:[{name:"id",positional:!0,required:!0,help:"Podcast ID (collectionId from search output)"},{name:"limit",type:"int",default:15,help:"Max episodes to show"}],columns:["title","duration","date"],func:async(q)=>{const v=Math.max(1,Math.min(Number(q.limit),200)),x=((await G(`/lookup?id=${q.id}&entity=podcastEpisode&limit=${v+1}`)).results??[]).filter((b)=>b.kind==="podcast-episode");if(!x.length)throw new B("NOT_FOUND","No episodes found","Check the podcast ID from: opencli apple-podcasts search <keyword>");return x.slice(0,v).map((b)=>({title:b.trackName,duration:H(b.trackTimeMillis),date:I(b.releaseDate)}))}});
1
+ import{cli as z,Strategy as A}from"@jackwener/opencli/registry";import{CliError as B}from"@jackwener/opencli/errors";import{itunesFetch as G,formatDuration as H,formatDate as I}from"./utils.js";z({site:"apple-podcasts",name:"episodes",access:"read",description:"List recent episodes of an Apple Podcast (use ID from search)",strategy:A.PUBLIC,browser:!1,args:[{name:"id",positional:!0,required:!0,help:"Podcast ID (collectionId from search output)"},{name:"limit",type:"int",default:15,help:"Max episodes to show"}],columns:["title","duration","date"],func:async(q)=>{const v=Math.max(1,Math.min(Number(q.limit),200)),x=((await G(`/lookup?id=${q.id}&entity=podcastEpisode&limit=${v+1}`)).results??[]).filter((b)=>b.kind==="podcast-episode");if(!x.length)throw new B("NOT_FOUND","No episodes found","Check the podcast ID from: ppcli apple-podcasts search <keyword>");return x.slice(0,v).map((b)=>({title:b.trackName,duration:H(b.trackTimeMillis),date:I(b.releaseDate)}))}});
@@ -1 +1 @@
1
- import{cli as U,Strategy as V}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as v,EmptyResultError as W}from"@jackwener/opencli/errors";const L=/^[A-Za-z0-9._-]+$/;U({site:"archive",name:"item",access:"read",description:"Fetch metadata for a single Internet Archive item by identifier.",domain:"archive.org",strategy:V.PUBLIC,browser:!1,args:[{name:"identifier",positional:!0,required:!0,help:'Archive item identifier (e.g. "open-syllabus", "FinalFantasy2_356").'}],columns:["identifier","title","creator","date","mediatype","collection","description","file_count","url"],func:async(J)=>{const q=String(J.identifier??"").trim();if(!q)throw new K("archive item identifier cannot be empty","Example: opencli archive item open-syllabus");if(!L.test(q))throw new K(`archive item identifier "${J.identifier}" is not valid`,'Archive item identifiers may only contain letters, digits, ".", "_", "-".');const M=`https://archive.org/metadata/${encodeURIComponent(q)}`;let G;try{G=await fetch(M,{headers:{Accept:"application/json","User-Agent":"opencli/1.0 (+https://github.com/jackwener/opencli)"}})}catch(B){throw new v(`archive item request failed: ${B?.message||B}`)}if(!G.ok)throw new v(`archive item failed: HTTP ${G.status}`);let H;try{H=await G.json()}catch(B){throw new v(`archive item returned malformed JSON: ${B?.message||B}`)}const b=H?.metadata;if(!b||typeof b!=="object"||!b.identifier)throw new W("archive item",`No public metadata for "${q}" on archive.org.`);const z=String(b.identifier);if(!L.test(z))throw new v("archive item returned malformed payload: metadata.identifier is not stable");if(z!==q)throw new v(`archive item returned metadata for "${z}" instead of "${q}"`);const O=Array.isArray(b.creator)?b.creator.join(", "):String(b.creator??""),P=Array.isArray(b.collection)?b.collection.join(", "):String(b.collection??""),Q=Array.isArray(b.description)?b.description.join(" "):String(b.description??"");if(!Array.isArray(H.files))throw new v("archive item returned malformed payload: files must be an array");return[{identifier:z,title:String(b.title??""),creator:O,date:b.date?String(b.date).slice(0,10):"",mediatype:String(b.mediatype??""),collection:P,description:Q,file_count:H.files.length,url:`https://archive.org/details/${z}`}]}});
1
+ import{cli as U,Strategy as V}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as v,EmptyResultError as W}from"@jackwener/opencli/errors";const L=/^[A-Za-z0-9._-]+$/;U({site:"archive",name:"item",access:"read",description:"Fetch metadata for a single Internet Archive item by identifier.",domain:"archive.org",strategy:V.PUBLIC,browser:!1,args:[{name:"identifier",positional:!0,required:!0,help:'Archive item identifier (e.g. "open-syllabus", "FinalFantasy2_356").'}],columns:["identifier","title","creator","date","mediatype","collection","description","file_count","url"],func:async(J)=>{const q=String(J.identifier??"").trim();if(!q)throw new K("archive item identifier cannot be empty","Example: ppcli archive item open-syllabus");if(!L.test(q))throw new K(`archive item identifier "${J.identifier}" is not valid`,'Archive item identifiers may only contain letters, digits, ".", "_", "-".');const M=`https://archive.org/metadata/${encodeURIComponent(q)}`;let G;try{G=await fetch(M,{headers:{Accept:"application/json","User-Agent":"ppcli/1.0 (+https://github.com/jackwener/OpenCLI)"}})}catch(B){throw new v(`archive item request failed: ${B?.message||B}`)}if(!G.ok)throw new v(`archive item failed: HTTP ${G.status}`);let H;try{H=await G.json()}catch(B){throw new v(`archive item returned malformed JSON: ${B?.message||B}`)}const b=H?.metadata;if(!b||typeof b!=="object"||!b.identifier)throw new W("archive item",`No public metadata for "${q}" on archive.org.`);const z=String(b.identifier);if(!L.test(z))throw new v("archive item returned malformed payload: metadata.identifier is not stable");if(z!==q)throw new v(`archive item returned metadata for "${z}" instead of "${q}"`);const O=Array.isArray(b.creator)?b.creator.join(", "):String(b.creator??""),P=Array.isArray(b.collection)?b.collection.join(", "):String(b.collection??""),Q=Array.isArray(b.description)?b.description.join(" "):String(b.description??"");if(!Array.isArray(H.files))throw new v("archive item returned malformed payload: files must be an array");return[{identifier:z,title:String(b.title??""),creator:O,date:b.date?String(b.date).slice(0,10):"",mediatype:String(b.mediatype??""),collection:P,description:Q,file_count:H.files.length,url:`https://archive.org/details/${z}`}]}});
@@ -1 +1 @@
1
- import{cli as g,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as o,CommandExecutionError as a,EmptyResultError as S}from"@jackwener/opencli/errors";const m=["downloads","date","addeddate","week","title"],A={added:"addeddate",published:"date"},h=["texts","movies","audio","software","image","web","data","collection"],$=/^[A-Za-z0-9._-]+$/;g({site:"archive",name:"search",access:"read",description:"Search Internet Archive items across books, movies, audio, software, and web.",domain:"archive.org",strategy:b.PUBLIC,browser:!1,args:[{name:"query",positional:!0,required:!0,help:"Full-text query (matches title, description, creator, subject)."},{name:"mediatype",type:"string",required:!1,help:`Restrict to mediatype: ${h.join(", ")}`},{name:"sort",type:"string",default:"downloads",help:`Sort key: ${m.join(", ")}`},{name:"limit",type:"int",default:20,help:"Max items (max 100; one API page)."}],columns:["rank","identifier","title","creator","date","mediatype","downloads","url"],func:async(r)=>{const l=String(r.sort??"downloads").toLowerCase(),u=A[l]??l;if(!m.includes(u))throw new o(`archive search sort must be one of ${m.join(", ")}`);if(r.mediatype&&!h.includes(String(r.mediatype)))throw new o(`archive search mediatype must be one of ${h.join(", ")}`);const i=Number(r.limit??20);if(!Number.isInteger(i)||i<=0)throw new o("archive search limit must be a positive integer");if(i>100)throw new o("archive search limit must be <= 100");const n=String(r.query??"").trim();if(!n)throw new o("archive search query must not be empty");const f=r.mediatype?`(${n}) AND mediatype:${r.mediatype}`:n,t=new URL("https://archive.org/advancedsearch.php");t.searchParams.set("q",f);t.searchParams.set("output","json");t.searchParams.set("rows",String(i));t.searchParams.set("sort[]",`${u} desc`);for(const e of["identifier","title","creator","date","mediatype","downloads"])t.searchParams.append("fl[]",e);let c;try{c=await fetch(t,{headers:{Accept:"application/json","User-Agent":"opencli/1.0 (+https://github.com/jackwener/opencli)"}})}catch(e){throw new a(`archive search request failed: ${e?.message||e}`)}if(!c.ok)throw new a(`archive search failed: HTTP ${c.status}`);let p;try{p=await c.json()}catch(e){throw new a(`archive search returned malformed JSON: ${e?.message||e}`)}const d=p?.response?.docs;if(!Array.isArray(d))throw new a("archive search returned malformed payload: response.docs must be an array");if(d.length===0)throw new S("archive search",`No items match "${n}" on archive.org.`);return d.slice(0,i).map((e,y)=>{const s=String(e.identifier??"");if(!$.test(s))throw new a("archive search returned malformed payload: result row is missing a stable identifier");const w=Number(e.downloads??0);if(!Number.isFinite(w))throw new a(`archive search returned malformed payload for "${s}": downloads must be numeric`);const v=Array.isArray(e.creator)?e.creator.join(", "):String(e.creator??"");return{rank:y+1,identifier:s,title:String(e.title??""),creator:v,date:e.date?String(e.date).slice(0,10):"",mediatype:String(e.mediatype??""),downloads:w,url:s?`https://archive.org/details/${s}`:""}})}});
1
+ import{cli as g,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as o,CommandExecutionError as a,EmptyResultError as S}from"@jackwener/opencli/errors";const m=["downloads","date","addeddate","week","title"],A={added:"addeddate",published:"date"},h=["texts","movies","audio","software","image","web","data","collection"],$=/^[A-Za-z0-9._-]+$/;g({site:"archive",name:"search",access:"read",description:"Search Internet Archive items across books, movies, audio, software, and web.",domain:"archive.org",strategy:b.PUBLIC,browser:!1,args:[{name:"query",positional:!0,required:!0,help:"Full-text query (matches title, description, creator, subject)."},{name:"mediatype",type:"string",required:!1,help:`Restrict to mediatype: ${h.join(", ")}`},{name:"sort",type:"string",default:"downloads",help:`Sort key: ${m.join(", ")}`},{name:"limit",type:"int",default:20,help:"Max items (max 100; one API page)."}],columns:["rank","identifier","title","creator","date","mediatype","downloads","url"],func:async(r)=>{const l=String(r.sort??"downloads").toLowerCase(),u=A[l]??l;if(!m.includes(u))throw new o(`archive search sort must be one of ${m.join(", ")}`);if(r.mediatype&&!h.includes(String(r.mediatype)))throw new o(`archive search mediatype must be one of ${h.join(", ")}`);const i=Number(r.limit??20);if(!Number.isInteger(i)||i<=0)throw new o("archive search limit must be a positive integer");if(i>100)throw new o("archive search limit must be <= 100");const n=String(r.query??"").trim();if(!n)throw new o("archive search query must not be empty");const f=r.mediatype?`(${n}) AND mediatype:${r.mediatype}`:n,t=new URL("https://archive.org/advancedsearch.php");t.searchParams.set("q",f);t.searchParams.set("output","json");t.searchParams.set("rows",String(i));t.searchParams.set("sort[]",`${u} desc`);for(const e of["identifier","title","creator","date","mediatype","downloads"])t.searchParams.append("fl[]",e);let c;try{c=await fetch(t,{headers:{Accept:"application/json","User-Agent":"ppcli/1.0 (+https://github.com/jackwener/OpenCLI)"}})}catch(e){throw new a(`archive search request failed: ${e?.message||e}`)}if(!c.ok)throw new a(`archive search failed: HTTP ${c.status}`);let p;try{p=await c.json()}catch(e){throw new a(`archive search returned malformed JSON: ${e?.message||e}`)}const d=p?.response?.docs;if(!Array.isArray(d))throw new a("archive search returned malformed payload: response.docs must be an array");if(d.length===0)throw new S("archive search",`No items match "${n}" on archive.org.`);return d.slice(0,i).map((e,y)=>{const s=String(e.identifier??"");if(!$.test(s))throw new a("archive search returned malformed payload: result row is missing a stable identifier");const w=Number(e.downloads??0);if(!Number.isFinite(w))throw new a(`archive search returned malformed payload for "${s}": downloads must be numeric`);const v=Array.isArray(e.creator)?e.creator.join(", "):String(e.creator??"");return{rank:y+1,identifier:s,title:String(e.title??""),creator:v,date:e.date?String(e.date).slice(0,10):"",mediatype:String(e.mediatype??""),downloads:w,url:s?`https://archive.org/details/${s}`:""}})}});
@@ -1 +1 @@
1
- import{cli as R,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as N,CommandExecutionError as G,EmptyResultError as W}from"@jackwener/opencli/errors";function X(B,F){if(!B||!F)return"";return`https://web.archive.org/web/${B}/${F}`}function O(B,F){const H=B[F];if(!Number.isInteger(H))throw new G(`archive snapshots returned malformed CDX payload: missing "${F}" column`);return H}R({site:"archive",name:"snapshots",access:"read",description:"List Wayback Machine snapshots over time for a URL via the CDX API.",domain:"archive.org",strategy:S.PUBLIC,browser:!1,args:[{name:"url",positional:!0,required:!0,help:"URL to look up (with or without scheme)."},{name:"from",type:"string",required:!1,help:"Earliest year/timestamp (YYYY[MM[DD[hh[mm[ss]]]]])"},{name:"to",type:"string",required:!1,help:"Latest year/timestamp (YYYY[MM[DD[hh[mm[ss]]]]])"},{name:"limit",type:"int",default:20,help:"Max snapshots to return (max 1000)."}],columns:["timestamp","snapshot_url","status","mimetype","original_url"],func:async(B)=>{const F=String(B.url??"").trim();if(!F)throw new N("archive snapshots url cannot be empty","Example: opencli archive snapshots wikipedia.org");const H=Number(B.limit??20);if(!Number.isInteger(H)||H<=0)throw new N("archive snapshots limit must be a positive integer");if(H>1000)throw new N("archive snapshots limit must be <= 1000");for(const z of["from","to"]){const I=B[z];if(I!=null&&!/^\d{4,14}$/.test(String(I)))throw new N(`archive snapshots ${z} must be a digit-only timestamp (YYYY[MM[DD[hh[mm[ss]]]]])`)}const J=new URL("http://web.archive.org/cdx/search/cdx");J.searchParams.set("url",F);J.searchParams.set("output","json");J.searchParams.set("limit",String(H));if(B.from)J.searchParams.set("from",String(B.from));if(B.to)J.searchParams.set("to",String(B.to));let L;try{L=await fetch(J,{headers:{Accept:"application/json","User-Agent":"opencli/1.0 (+https://github.com/jackwener/opencli)"}})}catch(z){throw new G(`archive snapshots request failed: ${z?.message||z}`)}if(!L.ok)throw new G(`archive snapshots failed: HTTP ${L.status}`);let M;try{M=await L.json()}catch(z){throw new G(`archive snapshots returned malformed JSON: ${z?.message||z}`)}if(!Array.isArray(M))throw new G("archive snapshots returned malformed CDX payload: top-level payload must be an array");if(M.length<2)throw new W("archive snapshots",`No Wayback snapshots for "${F}".`);const[V,...Y]=M;if(!Array.isArray(V))throw new G("archive snapshots returned malformed CDX payload: header row must be an array");const K={};V.forEach((z,I)=>{K[z]=I});const Z=O(K,"timestamp"),_=O(K,"original"),$=O(K,"statuscode"),D=O(K,"mimetype");return Y.slice(0,H).map((z)=>{if(!Array.isArray(z))throw new G("archive snapshots returned malformed CDX payload: snapshot row must be an array");const I=String(z[Z]??""),P=String(z[_]??""),Q=z[$],T=z[D];if(!/^\d{14}$/.test(I)||!P)throw new G("archive snapshots returned malformed CDX payload: snapshot row is missing timestamp/original URL");if(Q==null||T==null||String(Q)===""||String(T)==="")throw new G("archive snapshots returned malformed CDX payload: snapshot row is missing statuscode/mimetype");return{timestamp:I,snapshot_url:X(I,P),status:String(Q),mimetype:String(T),original_url:P}})}});
1
+ import{cli as R,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as N,CommandExecutionError as G,EmptyResultError as W}from"@jackwener/opencli/errors";function X(B,F){if(!B||!F)return"";return`https://web.archive.org/web/${B}/${F}`}function O(B,F){const H=B[F];if(!Number.isInteger(H))throw new G(`archive snapshots returned malformed CDX payload: missing "${F}" column`);return H}R({site:"archive",name:"snapshots",access:"read",description:"List Wayback Machine snapshots over time for a URL via the CDX API.",domain:"archive.org",strategy:S.PUBLIC,browser:!1,args:[{name:"url",positional:!0,required:!0,help:"URL to look up (with or without scheme)."},{name:"from",type:"string",required:!1,help:"Earliest year/timestamp (YYYY[MM[DD[hh[mm[ss]]]]])"},{name:"to",type:"string",required:!1,help:"Latest year/timestamp (YYYY[MM[DD[hh[mm[ss]]]]])"},{name:"limit",type:"int",default:20,help:"Max snapshots to return (max 1000)."}],columns:["timestamp","snapshot_url","status","mimetype","original_url"],func:async(B)=>{const F=String(B.url??"").trim();if(!F)throw new N("archive snapshots url cannot be empty","Example: ppcli archive snapshots wikipedia.org");const H=Number(B.limit??20);if(!Number.isInteger(H)||H<=0)throw new N("archive snapshots limit must be a positive integer");if(H>1000)throw new N("archive snapshots limit must be <= 1000");for(const z of["from","to"]){const I=B[z];if(I!=null&&!/^\d{4,14}$/.test(String(I)))throw new N(`archive snapshots ${z} must be a digit-only timestamp (YYYY[MM[DD[hh[mm[ss]]]]])`)}const J=new URL("http://web.archive.org/cdx/search/cdx");J.searchParams.set("url",F);J.searchParams.set("output","json");J.searchParams.set("limit",String(H));if(B.from)J.searchParams.set("from",String(B.from));if(B.to)J.searchParams.set("to",String(B.to));let L;try{L=await fetch(J,{headers:{Accept:"application/json","User-Agent":"ppcli/1.0 (+https://github.com/jackwener/OpenCLI)"}})}catch(z){throw new G(`archive snapshots request failed: ${z?.message||z}`)}if(!L.ok)throw new G(`archive snapshots failed: HTTP ${L.status}`);let M;try{M=await L.json()}catch(z){throw new G(`archive snapshots returned malformed JSON: ${z?.message||z}`)}if(!Array.isArray(M))throw new G("archive snapshots returned malformed CDX payload: top-level payload must be an array");if(M.length<2)throw new W("archive snapshots",`No Wayback snapshots for "${F}".`);const[V,...Y]=M;if(!Array.isArray(V))throw new G("archive snapshots returned malformed CDX payload: header row must be an array");const K={};V.forEach((z,I)=>{K[z]=I});const Z=O(K,"timestamp"),_=O(K,"original"),$=O(K,"statuscode"),D=O(K,"mimetype");return Y.slice(0,H).map((z)=>{if(!Array.isArray(z))throw new G("archive snapshots returned malformed CDX payload: snapshot row must be an array");const I=String(z[Z]??""),P=String(z[_]??""),Q=z[$],T=z[D];if(!/^\d{14}$/.test(I)||!P)throw new G("archive snapshots returned malformed CDX payload: snapshot row is missing timestamp/original URL");if(Q==null||T==null||String(Q)===""||String(T)==="")throw new G("archive snapshots returned malformed CDX payload: snapshot row is missing statuscode/mimetype");return{timestamp:I,snapshot_url:X(I,P),status:String(Q),mimetype:String(T),original_url:P}})}});
@@ -1 +1 @@
1
- import{cli as L,Strategy as N}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as G,EmptyResultError as O}from"@jackwener/opencli/errors";function P(q){const b=String(q).replace(/[^0-9]/g,"");if(!/^\d{4,14}$/.test(b)||b.length%2!==0&&b.length!==4)throw new K("archive wayback timestamp must be YYYY[MM[DD[hh[mm[ss]]]]] or an ISO date");return b}L({site:"archive",name:"wayback",access:"read",description:"Look up the closest Wayback Machine snapshot for a URL.",domain:"archive.org",strategy:N.PUBLIC,browser:!1,args:[{name:"url",positional:!0,required:!0,help:"URL to look up (with or without scheme)."},{name:"timestamp",type:"string",required:!1,help:"Target timestamp (YYYY[MM[DD[hh[mm[ss]]]]] or ISO date). Defaults to most recent snapshot."}],columns:["original_url","requested_timestamp","snapshot_timestamp","snapshot_url","status"],func:async(q)=>{const b=String(q.url??"").trim();if(!b)throw new K("archive wayback url cannot be empty","Example: opencli archive wayback wikipedia.org");const H=q.timestamp?P(q.timestamp):"",I=new URL("https://archive.org/wayback/available");I.searchParams.set("url",b);if(H)I.searchParams.set("timestamp",H);let F;try{F=await fetch(I,{headers:{Accept:"application/json","User-Agent":"opencli/1.0 (+https://github.com/jackwener/opencli)"}})}catch(B){throw new G(`archive wayback request failed: ${B?.message||B}`)}if(!F.ok)throw new G(`archive wayback failed: HTTP ${F.status}`);let J;try{J=await F.json()}catch(B){throw new G(`archive wayback returned malformed JSON: ${B?.message||B}`)}const f=J?.archived_snapshots?.closest;if(!f||!f.available)throw new O("archive wayback",`No Wayback snapshot for "${b}".`);if(typeof f.url!=="string"||!f.url||!/^\d{14}$/.test(String(f.timestamp??"")))throw new G("archive wayback returned malformed payload: closest snapshot is missing url/timestamp");return[{original_url:String(J.url??b),requested_timestamp:H,snapshot_timestamp:String(f.timestamp??""),snapshot_url:String(f.url),status:String(f.status??"")}]}});
1
+ import{cli as L,Strategy as N}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as G,EmptyResultError as O}from"@jackwener/opencli/errors";function P(q){const b=String(q).replace(/[^0-9]/g,"");if(!/^\d{4,14}$/.test(b)||b.length%2!==0&&b.length!==4)throw new K("archive wayback timestamp must be YYYY[MM[DD[hh[mm[ss]]]]] or an ISO date");return b}L({site:"archive",name:"wayback",access:"read",description:"Look up the closest Wayback Machine snapshot for a URL.",domain:"archive.org",strategy:N.PUBLIC,browser:!1,args:[{name:"url",positional:!0,required:!0,help:"URL to look up (with or without scheme)."},{name:"timestamp",type:"string",required:!1,help:"Target timestamp (YYYY[MM[DD[hh[mm[ss]]]]] or ISO date). Defaults to most recent snapshot."}],columns:["original_url","requested_timestamp","snapshot_timestamp","snapshot_url","status"],func:async(q)=>{const b=String(q.url??"").trim();if(!b)throw new K("archive wayback url cannot be empty","Example: ppcli archive wayback wikipedia.org");const H=q.timestamp?P(q.timestamp):"",I=new URL("https://archive.org/wayback/available");I.searchParams.set("url",b);if(H)I.searchParams.set("timestamp",H);let F;try{F=await fetch(I,{headers:{Accept:"application/json","User-Agent":"ppcli/1.0 (+https://github.com/jackwener/OpenCLI)"}})}catch(B){throw new G(`archive wayback request failed: ${B?.message||B}`)}if(!F.ok)throw new G(`archive wayback failed: HTTP ${F.status}`);let J;try{J=await F.json()}catch(B){throw new G(`archive wayback returned malformed JSON: ${B?.message||B}`)}const f=J?.archived_snapshots?.closest;if(!f||!f.available)throw new O("archive wayback",`No Wayback snapshot for "${b}".`);if(typeof f.url!=="string"||!f.url||!/^\d{14}$/.test(String(f.timestamp??"")))throw new G("archive wayback returned malformed payload: closest snapshot is missing url/timestamp");return[{original_url:String(J.url??b),requested_timestamp:H,snapshot_timestamp:String(f.timestamp??""),snapshot_url:String(f.url),status:String(f.status??"")}]}});
@@ -1 +1 @@
1
- import{cli as I,Strategy as J}from"@jackwener/opencli/registry";import{ArgumentError as K,EmptyResultError as M}from"@jackwener/opencli/errors";import{arxivFetch as N,normalizeArxivLimit as O,parseEntries as P}from"./utils.js";I({site:"arxiv",name:"author",access:"read",description:"List arXiv papers by a given author (newest first)",strategy:J.PUBLIC,browser:!1,args:[{name:"author",positional:!0,required:!0,help:'Author name (e.g. "Yoshua Bengio" or "Y Bengio")'},{name:"limit",type:"int",default:20,help:"Max papers to return (max 50)"}],columns:["id","title","authors","published","primary_category","url"],func:async(B)=>{const f=String(B.author||"").trim();if(!f)throw new K("arxiv author cannot be empty",'Example: opencli arxiv author "Yoshua Bengio"');const D=O(B.limit,20,50),G=encodeURIComponent(`au:"${f}"`),H=await N(`search_query=${G}&max_results=${D}&sortBy=submittedDate&sortOrder=descending`),C=P(H);if(!C.length)throw new M("arxiv author",`No papers found for author "${f}". Try alternate spellings (e.g. initials).`);return C.map((b)=>({id:b.id,title:b.title,authors:b.authors,published:b.published,primary_category:b.primary_category,url:b.url}))}});
1
+ import{cli as I,Strategy as J}from"@jackwener/opencli/registry";import{ArgumentError as K,EmptyResultError as M}from"@jackwener/opencli/errors";import{arxivFetch as N,normalizeArxivLimit as O,parseEntries as P}from"./utils.js";I({site:"arxiv",name:"author",access:"read",description:"List arXiv papers by a given author (newest first)",strategy:J.PUBLIC,browser:!1,args:[{name:"author",positional:!0,required:!0,help:'Author name (e.g. "Yoshua Bengio" or "Y Bengio")'},{name:"limit",type:"int",default:20,help:"Max papers to return (max 50)"}],columns:["id","title","authors","published","primary_category","url"],func:async(B)=>{const f=String(B.author||"").trim();if(!f)throw new K("arxiv author cannot be empty",'Example: ppcli arxiv author "Yoshua Bengio"');const D=O(B.limit,20,50),G=encodeURIComponent(`au:"${f}"`),H=await N(`search_query=${G}&max_results=${D}&sortBy=submittedDate&sortOrder=descending`),C=P(H);if(!C.length)throw new M("arxiv author",`No papers found for author "${f}". Try alternate spellings (e.g. initials).`);return C.map((b)=>({id:b.id,title:b.title,authors:b.authors,published:b.published,primary_category:b.primary_category,url:b.url}))}});
@@ -47,7 +47,7 @@ import{cli as $,Strategy as A}from"@jackwener/opencli/registry";import{ArgumentE
47
47
  }
48
48
  return false;
49
49
  })()
50
- `))))throw new H("找不到百家号「定时发布」入口");if(!await M(q,"div.select-wrap",3000))throw new H("百家号定时面板未展开");await q.wait({time:2});await _(q,G)}$({site:"baijiahao",name:"publish",access:"write",description:"发布视频到百家号(一条命令完成上传+填写+发布;可选定时)",domain:"baijiahao.baidu.com",strategy:A.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(不足 8 字会自动补「你不知道的」,截断到 30 字)"},{name:"schedule",default:"",help:"定时发布时间(ISO8601 或 Unix 秒;不填即立即发布)"}],columns:["status","title","url"],func:async(q,G)=>{const z=String(G.title??"").trim();if(!z)throw new N("--title is required");const K=f(G.video),Q=O(G.schedule);await q.goto(R);if(!await Y(q,"builder/rc/edit",60000))throw new H("未能进入百家号视频编辑页(登录态可能失效,请先 opencli baijiahao login)");if(!await M(q,"div[class^='video-main-container'] input, input[type='file']",30000))throw new H("百家号视频上传输入框未出现");await B(q,j,[K]);if(!await M(q,"div#formMain",60000))throw new H("百家号视频发布表单未出现");await q.wait({time:1});const J=await P(q,z);if(!await V(q))throw new H(`百家号视频上传失败: ${K}`);const Z=Date.now()+120000;while(Date.now()<Z){if(Boolean(W(await q.evaluate("(() => !!document.querySelector('div.cheetah-spin-container img'))()"))))break;await q.wait({time:3})}if(Q)await b(q,Q);else await X(q,"发布",{exact:!0,timeoutMs:1e4});await q.wait({time:2});if(Boolean(W(await q.evaluate(`
50
+ `))))throw new H("找不到百家号「定时发布」入口");if(!await M(q,"div.select-wrap",3000))throw new H("百家号定时面板未展开");await q.wait({time:2});await _(q,G)}$({site:"baijiahao",name:"publish",access:"write",description:"发布视频到百家号(一条命令完成上传+填写+发布;可选定时)",domain:"baijiahao.baidu.com",strategy:A.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(不足 8 字会自动补「你不知道的」,截断到 30 字)"},{name:"schedule",default:"",help:"定时发布时间(ISO8601 或 Unix 秒;不填即立即发布)"}],columns:["status","title","url"],func:async(q,G)=>{const z=String(G.title??"").trim();if(!z)throw new N("--title is required");const K=f(G.video),Q=O(G.schedule);await q.goto(R);if(!await Y(q,"builder/rc/edit",60000))throw new H("未能进入百家号视频编辑页(登录态可能失效,请先 ppcli baijiahao login)");if(!await M(q,"div[class^='video-main-container'] input, input[type='file']",30000))throw new H("百家号视频上传输入框未出现");await B(q,j,[K]);if(!await M(q,"div#formMain",60000))throw new H("百家号视频发布表单未出现");await q.wait({time:1});const J=await P(q,z);if(!await V(q))throw new H(`百家号视频上传失败: ${K}`);const Z=Date.now()+120000;while(Date.now()<Z){if(Boolean(W(await q.evaluate("(() => !!document.querySelector('div.cheetah-spin-container img'))()"))))break;await q.wait({time:3})}if(Q)await b(q,Q);else await X(q,"发布",{exact:!0,timeoutMs:1e4});await q.wait({time:2});if(Boolean(W(await q.evaluate(`
51
51
  (() => {
52
52
  for (const el of document.querySelectorAll('div.passMod_dialog-container')) {
53
53
  if (el.offsetParent !== null && (el.innerText || '').includes('百度安全验证')) return true;
package/clis/bbc/utils.js CHANGED
@@ -1 +1 @@
1
- import{ArgumentError as Q,CommandExecutionError as W}from"@jackwener/opencli/errors";export const BBC_FEED_BASE="https://feeds.bbci.co.uk/news";const X="opencli-bbc-adapter (+https://github.com/jackwener/opencli)",Z={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&apos;":"'","&#39;":"'","&nbsp;":" "};export function decodeHtmlEntities(G){return String(G??"").replace(/&#x([0-9a-fA-F]+);/g,(q,z)=>String.fromCodePoint(parseInt(z,16))).replace(/&#(\d+);/g,(q,z)=>String.fromCodePoint(parseInt(z,10))).replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g,(q)=>Z[q]||q)}export function extractRssTag(G,q){const z=G.match(new RegExp(`<${q}[^>]*>\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*<\\/${q}>`));if(z)return z[1];const J=G.match(new RegExp(`<${q}[^>]*>([\\s\\S]*?)<\\/${q}>`));return J?J[1]:""}export function parseRssItems(G){const q=[],z=/<item[^>]*>([\s\S]*?)<\/item>/g;let J;while((J=z.exec(String(G||"")))!==null){const K=J[1];q.push({title:decodeHtmlEntities(extractRssTag(K,"title")).trim(),description:decodeHtmlEntities(extractRssTag(K,"description")).trim(),link:decodeHtmlEntities(extractRssTag(K,"link")).trim(),pubDate:decodeHtmlEntities(extractRssTag(K,"pubDate")).trim(),guid:decodeHtmlEntities(extractRssTag(K,"guid")).trim()})}return q}export function requireBoundedInt(G,q,z,J="limit"){const K=G??q,P=typeof K==="number"?K:Number(K);if(!Number.isInteger(P)||P<=0)throw new Q(`bbc ${J} must be a positive integer`);if(P>z)throw new Q(`bbc ${J} must be <= ${z}`);return P}export async function bbcFetchRss(G,q){const z=`${BBC_FEED_BASE}/${G}`;let J;try{J=await fetch(z,{headers:{"user-agent":X,accept:"application/rss+xml, application/xml"}})}catch(K){throw new W(`${q} request failed: ${K?.message??K}`,"Check that feeds.bbci.co.uk is reachable from this network.")}if(!J.ok)throw new W(`${q} returned HTTP ${J.status} (${z})`);return J.text()}export function pubDateToIso(G){if(!G)return"";const q=new Date(G);if(Number.isNaN(q.getTime()))return"";return q.toISOString().slice(0,10)}
1
+ import{ArgumentError as Q,CommandExecutionError as W}from"@jackwener/opencli/errors";export const BBC_FEED_BASE="https://feeds.bbci.co.uk/news";const X="opencli-bbc-adapter (+https://github.com/jackwener/OpenCLI)",Z={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&apos;":"'","&#39;":"'","&nbsp;":" "};export function decodeHtmlEntities(G){return String(G??"").replace(/&#x([0-9a-fA-F]+);/g,(q,z)=>String.fromCodePoint(parseInt(z,16))).replace(/&#(\d+);/g,(q,z)=>String.fromCodePoint(parseInt(z,10))).replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g,(q)=>Z[q]||q)}export function extractRssTag(G,q){const z=G.match(new RegExp(`<${q}[^>]*>\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*<\\/${q}>`));if(z)return z[1];const J=G.match(new RegExp(`<${q}[^>]*>([\\s\\S]*?)<\\/${q}>`));return J?J[1]:""}export function parseRssItems(G){const q=[],z=/<item[^>]*>([\s\S]*?)<\/item>/g;let J;while((J=z.exec(String(G||"")))!==null){const K=J[1];q.push({title:decodeHtmlEntities(extractRssTag(K,"title")).trim(),description:decodeHtmlEntities(extractRssTag(K,"description")).trim(),link:decodeHtmlEntities(extractRssTag(K,"link")).trim(),pubDate:decodeHtmlEntities(extractRssTag(K,"pubDate")).trim(),guid:decodeHtmlEntities(extractRssTag(K,"guid")).trim()})}return q}export function requireBoundedInt(G,q,z,J="limit"){const K=G??q,P=typeof K==="number"?K:Number(K);if(!Number.isInteger(P)||P<=0)throw new Q(`bbc ${J} must be a positive integer`);if(P>z)throw new Q(`bbc ${J} must be <= ${z}`);return P}export async function bbcFetchRss(G,q){const z=`${BBC_FEED_BASE}/${G}`;let J;try{J=await fetch(z,{headers:{"user-agent":X,accept:"application/rss+xml, application/xml"}})}catch(K){throw new W(`${q} request failed: ${K?.message??K}`,"Check that feeds.bbci.co.uk is reachable from this network.")}if(!J.ok)throw new W(`${q} returned HTTP ${J.status} (${z})`);return J.text()}export function pubDateToIso(G){if(!G)return"";const q=new Date(G);if(Number.isNaN(q.getTime()))return"";return q.toISOString().slice(0,10)}
@@ -1,4 +1,4 @@
1
- import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{ArgumentError as $,CliError as Q}from"@jackwener/opencli/errors";const A="https://www.bloomberg.com/businessweek";export function parseBusinessweekLimit(f){const j=f==null||f===""?1:Number(f);if(!Number.isInteger(j)||j<1||j>20)throw new $("bloomberg businessweek --limit must be an integer between 1 and 20","Example: opencli bloomberg businessweek --limit 5");return j}export function normalizeBusinessweekStoryPath(f){const j=typeof f==="string"?f.trim():"";if(!j)return"";let q;try{q=new URL(j,"https://www.bloomberg.com")}catch{return""}if(q.protocol!=="https:"||q.hostname!=="www.bloomberg.com")return"";if(!/^\/(?:news|features)\//.test(q.pathname))return"";return`${q.pathname}${q.search}`}export function extractBusinessweekStoriesFromNextData(f){const j=f&&f.props&&f.props.pageProps&&f.props.pageProps.initialState&&f.props.pageProps.initialState.modulesById;if(!j||typeof j!=="object")return null;const q=new Set,J=[];for(const G of Object.values(j)){const K=G&&Array.isArray(G.items)?G.items:[];for(const b of K){const H=b&&typeof b.headline==="string"?b.headline.trim():"",M=normalizeBusinessweekStoryPath(b&&typeof b.url==="string"?b.url:"");if(!H||!M)continue;const V=M.split("?")[0];if(q.has(V))continue;q.add(V);const X=b.summary&&String(b.summary).trim()||(b.eyebrow&&b.eyebrow.text?String(b.eyebrow.text).trim():""),W=b.image&&(b.image.baseUrl||b.image.url)||b.lede&&(b.lede.baseUrl||b.lede.url)||"";J.push({title:H,summary:X,link:`https://www.bloomberg.com${M}`,mediaLinks:W?[W]:[]})}}return J}export const command=Y({site:"bloomberg",name:"businessweek",access:"read",description:"Bloomberg Businessweek top stories",domain:"www.bloomberg.com",strategy:Z.PUBLIC,browser:!0,args:[{name:"limit",type:"int",default:1,help:"Number of stories to return (max 20)"}],columns:["title","summary","link","mediaLinks"],func:async(f,j)=>{const q=parseBusinessweekLimit(j.limit);await f.goto(A);await f.wait({selector:"#__NEXT_DATA__",timeout:8});const J=normalizeBusinessweekStoryPath.toString(),G=extractBusinessweekStoriesFromNextData.toString(),K=async()=>f.evaluate(`(() => {
1
+ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{ArgumentError as $,CliError as Q}from"@jackwener/opencli/errors";const A="https://www.bloomberg.com/businessweek";export function parseBusinessweekLimit(f){const j=f==null||f===""?1:Number(f);if(!Number.isInteger(j)||j<1||j>20)throw new $("bloomberg businessweek --limit must be an integer between 1 and 20","Example: ppcli bloomberg businessweek --limit 5");return j}export function normalizeBusinessweekStoryPath(f){const j=typeof f==="string"?f.trim():"";if(!j)return"";let q;try{q=new URL(j,"https://www.bloomberg.com")}catch{return""}if(q.protocol!=="https:"||q.hostname!=="www.bloomberg.com")return"";if(!/^\/(?:news|features)\//.test(q.pathname))return"";return`${q.pathname}${q.search}`}export function extractBusinessweekStoriesFromNextData(f){const j=f&&f.props&&f.props.pageProps&&f.props.pageProps.initialState&&f.props.pageProps.initialState.modulesById;if(!j||typeof j!=="object")return null;const q=new Set,J=[];for(const G of Object.values(j)){const K=G&&Array.isArray(G.items)?G.items:[];for(const b of K){const H=b&&typeof b.headline==="string"?b.headline.trim():"",M=normalizeBusinessweekStoryPath(b&&typeof b.url==="string"?b.url:"");if(!H||!M)continue;const V=M.split("?")[0];if(q.has(V))continue;q.add(V);const X=b.summary&&String(b.summary).trim()||(b.eyebrow&&b.eyebrow.text?String(b.eyebrow.text).trim():""),W=b.image&&(b.image.baseUrl||b.image.url)||b.lede&&(b.lede.baseUrl||b.lede.url)||"";J.push({title:H,summary:X,link:`https://www.bloomberg.com${M}`,mediaLinks:W?[W]:[]})}}return J}export const command=Y({site:"bloomberg",name:"businessweek",access:"read",description:"Bloomberg Businessweek top stories",domain:"www.bloomberg.com",strategy:Z.PUBLIC,browser:!0,args:[{name:"limit",type:"int",default:1,help:"Number of stories to return (max 20)"}],columns:["title","summary","link","mediaLinks"],func:async(f,j)=>{const q=parseBusinessweekLimit(j.limit);await f.goto(A);await f.wait({selector:"#__NEXT_DATA__",timeout:8});const J=normalizeBusinessweekStoryPath.toString(),G=extractBusinessweekStoriesFromNextData.toString(),K=async()=>f.evaluate(`(() => {
2
2
  ${J}
3
3
  ${G}
4
4
  const el = document.getElementById('__NEXT_DATA__');
@@ -1,4 +1,4 @@
1
- import{CliError as H}from"@jackwener/opencli/errors";export const BLOOMBERG_FEEDS={main:"https://feeds.bloomberg.com/news.rss",markets:"https://feeds.bloomberg.com/markets/news.rss",economics:"https://feeds.bloomberg.com/economics/news.rss",industries:"https://feeds.bloomberg.com/industries/news.rss",tech:"https://feeds.bloomberg.com/technology/news.rss",politics:"https://feeds.bloomberg.com/politics/news.rss",opinions:"https://feeds.bloomberg.com/bview/news.rss",green:"https://feeds.bloomberg.com/green/news.rss",crypto:"https://feeds.bloomberg.com/crypto/news.rss",pursuits:"https://feeds.bloomberg.com/pursuits/news.rss"};const L="Mozilla/5.0 (compatible; opencli)";export async function fetchBloombergFeed(q,J=1){const K=BLOOMBERG_FEEDS[q];if(!K)throw new H("ARGUMENT",`Unknown Bloomberg feed: ${q}`);let P;for(let Q=0;Q<3;Q+=1){if(Q>0)await new Promise((A)=>setTimeout(A,400*Q));const V=await fetch(K,{headers:{"User-Agent":L}});if(!V.ok){P=new H("FETCH_ERROR",`Bloomberg RSS HTTP ${V.status}`,"Bloomberg may be temporarily unavailable; try again later.");continue}const Z=await V.text(),W=parseBloombergRss(Z);if(W.length){const A=Math.max(1,Math.min(Number(J)||1,20));return W.slice(0,A)}P=new H("NOT_FOUND","Bloomberg RSS feed returned no items","Bloomberg may have changed the feed format.")}throw P}export function parseBloombergRss(q){const J=[],K=/<item\b[^>]*>([\s\S]*?)<\/item>/gi;let P;while(P=K.exec(q)){const Q=P[1],V=G(Q,"title"),Z=G(Q,"description"),W=G(Q,"link")||G(Q,"guid"),A=M(Q);if(!V||!W)continue;J.push({title:V,summary:Z,link:W,mediaLinks:A})}return J}export function normalizeBloombergLink(q){const J=String(q||"").trim();if(!J)throw new H("ARGUMENT","A Bloomberg link is required");if(J.startsWith("/"))return`https://www.bloomberg.com${J}`;return J}export function validateBloombergLink(q){const J=normalizeBloombergLink(q);let K;try{K=new URL(J)}catch{throw new H("ARGUMENT",`Invalid Bloomberg link: ${q}`,"Pass a full https://www.bloomberg.com/... URL or a relative Bloomberg path.")}if(!/(?:\.|^)bloomberg\.com$/i.test(K.hostname))throw new H("ARGUMENT",`Expected a bloomberg.com link, got: ${K.hostname}`,"Pass a Bloomberg article URL from bloomberg.com.");return K.toString()}export function renderStoryBody(q){return(Array.isArray(q?.content)?q.content:[]).map((P)=>D(P,0)).map((P)=>j(P)).filter(Boolean).join(`
1
+ import{CliError as H}from"@jackwener/opencli/errors";export const BLOOMBERG_FEEDS={main:"https://feeds.bloomberg.com/news.rss",markets:"https://feeds.bloomberg.com/markets/news.rss",economics:"https://feeds.bloomberg.com/economics/news.rss",industries:"https://feeds.bloomberg.com/industries/news.rss",tech:"https://feeds.bloomberg.com/technology/news.rss",politics:"https://feeds.bloomberg.com/politics/news.rss",opinions:"https://feeds.bloomberg.com/bview/news.rss",green:"https://feeds.bloomberg.com/green/news.rss",crypto:"https://feeds.bloomberg.com/crypto/news.rss",pursuits:"https://feeds.bloomberg.com/pursuits/news.rss"};const L="Mozilla/5.0 (compatible; ppcli)";export async function fetchBloombergFeed(q,J=1){const K=BLOOMBERG_FEEDS[q];if(!K)throw new H("ARGUMENT",`Unknown Bloomberg feed: ${q}`);let P;for(let Q=0;Q<3;Q+=1){if(Q>0)await new Promise((A)=>setTimeout(A,400*Q));const V=await fetch(K,{headers:{"User-Agent":L}});if(!V.ok){P=new H("FETCH_ERROR",`Bloomberg RSS HTTP ${V.status}`,"Bloomberg may be temporarily unavailable; try again later.");continue}const Z=await V.text(),W=parseBloombergRss(Z);if(W.length){const A=Math.max(1,Math.min(Number(J)||1,20));return W.slice(0,A)}P=new H("NOT_FOUND","Bloomberg RSS feed returned no items","Bloomberg may have changed the feed format.")}throw P}export function parseBloombergRss(q){const J=[],K=/<item\b[^>]*>([\s\S]*?)<\/item>/gi;let P;while(P=K.exec(q)){const Q=P[1],V=G(Q,"title"),Z=G(Q,"description"),W=G(Q,"link")||G(Q,"guid"),A=M(Q);if(!V||!W)continue;J.push({title:V,summary:Z,link:W,mediaLinks:A})}return J}export function normalizeBloombergLink(q){const J=String(q||"").trim();if(!J)throw new H("ARGUMENT","A Bloomberg link is required");if(J.startsWith("/"))return`https://www.bloomberg.com${J}`;return J}export function validateBloombergLink(q){const J=normalizeBloombergLink(q);let K;try{K=new URL(J)}catch{throw new H("ARGUMENT",`Invalid Bloomberg link: ${q}`,"Pass a full https://www.bloomberg.com/... URL or a relative Bloomberg path.")}if(!/(?:\.|^)bloomberg\.com$/i.test(K.hostname))throw new H("ARGUMENT",`Expected a bloomberg.com link, got: ${K.hostname}`,"Pass a Bloomberg article URL from bloomberg.com.");return K.toString()}export function renderStoryBody(q){return(Array.isArray(q?.content)?q.content:[]).map((P)=>D(P,0)).map((P)=>j(P)).filter(Boolean).join(`
2
2
 
3
3
  `).replace(/\n{3,}/g,`
4
4
 
@@ -90,4 +90,4 @@ import{ArgumentError as Y,CommandExecutionError as j,EmptyResultError as p}from"
90
90
  const totalText = trim(totalEl && totalEl.textContent);
91
91
  return { ok: true, items, blocked: false, totalText };
92
92
  })()
93
- `;y({site:"booking",name:"search",description:"Search Booking.com hotels by destination and dates (server-rendered card scrape).",access:"read",example:"opencli booking search Tokyo --checkin 2026-06-15 --checkout 2026-06-17 -f yaml",domain:"www.booking.com",strategy:x.PUBLIC,browser:!0,args:[{name:"destination",required:!0,positional:!0,help:"Destination keyword (city, district, or hotel name)"},{name:"checkin",required:!0,help:"Check-in date YYYY-MM-DD"},{name:"checkout",required:!0,help:"Check-out date YYYY-MM-DD"},{name:"adults",type:"int",default:2,help:"Number of adults (1-30)"},{name:"rooms",type:"int",default:1,help:"Number of rooms (1-30)"},{name:"children",type:"int",default:0,help:"Number of children (0-10)"},{name:"currency",required:!1,help:"Force result currency (e.g. USD, JPY, CNY)"},{name:"lang",required:!1,help:"Force result language (e.g. en-us, zh-cn, ja)"},{name:"limit",type:"int",default:25,help:"Max rows to return (1-100; Booking pages 25 per request)"},{name:"offset",type:"int",default:0,help:"Result offset for pagination (multiple of 25)"}],columns:["rank","name","country","slug","star_rating","review_score","review_count","price_amount","price_currency","distance","recommended_room","url"],func:async(F,q)=>{const H=String(q.destination||"").trim();if(!H)throw new Y("destination is required");const J=D(q.checkin,"checkin"),Z=D(q.checkout,"checkout");if(J>=Z)throw new Y(`checkout (${Z}) must be after checkin (${J})`);const K=_(q.adults,2,"adults",30),$=_(q.rooms,1,"rooms",30),W=S(q.children,0,"children",10),G=N(q.currency),O=T(q.lang),Q=_(q.limit,25,"limit",100),P=S(q.offset,0,"offset",1000),A=b({destination:H,checkin:J,checkout:Z,adults:K,rooms:$,children:W,offset:P,currency:G,lang:O});try{await F.goto(A)}catch(B){throw new j(`Failed to load Booking.com search page: ${B?.message||B}`)}try{await F.wait("selector","[data-testid=property-card]",{timeoutMs:20000})}catch(B){}let M;try{M=await F.evaluate(C)}catch(B){throw new j(`Failed to extract Booking.com cards: ${B?.message||B}`)}if(M&&typeof M==="object"&&M.data&&M.session)M=M.data;if(!M||typeof M!=="object")throw new j("Booking.com page returned no extractable data");if(M.blocked)throw new j("Booking.com served a verification / captcha page; retry later or change profile");if(M.ok!==!0)throw new j("Booking.com extractor returned an invalid status");if(!Array.isArray(M.items))throw new j("Booking.com extractor returned malformed items");const V=M.items;if(V.length===0){const B=String(M.totalText||"").trim();if(z(B))throw new j(`Booking.com page declared results but no property cards were parsed: ${B}`);throw new p(`booking search ${JSON.stringify(H)}`,B?`No hotels rendered (${B}). Try a broader destination, different dates, or check the URL in a browser.`:"No hotels rendered. Try a broader destination, different dates, or check the URL in a browser.")}return V.slice(0,Q).map((B,f)=>{if(!B||typeof B!=="object")throw new j("Booking.com extractor returned malformed hotel row");const I=String(B.name||"").trim(),X=String(B.country||"").trim(),U=String(B.slug||"").trim(),L=String(B.url||"").trim(),h=X&&U?`https://www.booking.com/hotel/${X}/${U}.html`:"";if(!I||!/^[a-z]{2}$/.test(X)||!U||L!==h)throw new j("Booking.com hotel row is missing stable name/url identity");return{rank:P+f+1,name:I,country:X,slug:U,star_rating:B.star_rating,review_score:B.review_score,review_count:B.review_count,price_amount:B.price_amount,price_currency:B.price_amount==null?"":G||B.price_currency||"",distance:B.distance,recommended_room:B.recommended_room,url:L}})}});export const __test__={normalizePositiveInt:_,normalizeNonNegativeInt:S,normalizeDate:D,normalizeCurrency:N,normalizeLang:T,hasPositiveResultCount:z,buildSearchUrl:b,EXTRACTOR:C};
93
+ `;y({site:"booking",name:"search",description:"Search Booking.com hotels by destination and dates (server-rendered card scrape).",access:"read",example:"ppcli booking search Tokyo --checkin 2026-06-15 --checkout 2026-06-17 -f yaml",domain:"www.booking.com",strategy:x.PUBLIC,browser:!0,args:[{name:"destination",required:!0,positional:!0,help:"Destination keyword (city, district, or hotel name)"},{name:"checkin",required:!0,help:"Check-in date YYYY-MM-DD"},{name:"checkout",required:!0,help:"Check-out date YYYY-MM-DD"},{name:"adults",type:"int",default:2,help:"Number of adults (1-30)"},{name:"rooms",type:"int",default:1,help:"Number of rooms (1-30)"},{name:"children",type:"int",default:0,help:"Number of children (0-10)"},{name:"currency",required:!1,help:"Force result currency (e.g. USD, JPY, CNY)"},{name:"lang",required:!1,help:"Force result language (e.g. en-us, zh-cn, ja)"},{name:"limit",type:"int",default:25,help:"Max rows to return (1-100; Booking pages 25 per request)"},{name:"offset",type:"int",default:0,help:"Result offset for pagination (multiple of 25)"}],columns:["rank","name","country","slug","star_rating","review_score","review_count","price_amount","price_currency","distance","recommended_room","url"],func:async(F,q)=>{const H=String(q.destination||"").trim();if(!H)throw new Y("destination is required");const J=D(q.checkin,"checkin"),Z=D(q.checkout,"checkout");if(J>=Z)throw new Y(`checkout (${Z}) must be after checkin (${J})`);const K=_(q.adults,2,"adults",30),$=_(q.rooms,1,"rooms",30),W=S(q.children,0,"children",10),G=N(q.currency),O=T(q.lang),Q=_(q.limit,25,"limit",100),P=S(q.offset,0,"offset",1000),A=b({destination:H,checkin:J,checkout:Z,adults:K,rooms:$,children:W,offset:P,currency:G,lang:O});try{await F.goto(A)}catch(B){throw new j(`Failed to load Booking.com search page: ${B?.message||B}`)}try{await F.wait("selector","[data-testid=property-card]",{timeoutMs:20000})}catch(B){}let M;try{M=await F.evaluate(C)}catch(B){throw new j(`Failed to extract Booking.com cards: ${B?.message||B}`)}if(M&&typeof M==="object"&&M.data&&M.session)M=M.data;if(!M||typeof M!=="object")throw new j("Booking.com page returned no extractable data");if(M.blocked)throw new j("Booking.com served a verification / captcha page; retry later or change profile");if(M.ok!==!0)throw new j("Booking.com extractor returned an invalid status");if(!Array.isArray(M.items))throw new j("Booking.com extractor returned malformed items");const V=M.items;if(V.length===0){const B=String(M.totalText||"").trim();if(z(B))throw new j(`Booking.com page declared results but no property cards were parsed: ${B}`);throw new p(`booking search ${JSON.stringify(H)}`,B?`No hotels rendered (${B}). Try a broader destination, different dates, or check the URL in a browser.`:"No hotels rendered. Try a broader destination, different dates, or check the URL in a browser.")}return V.slice(0,Q).map((B,f)=>{if(!B||typeof B!=="object")throw new j("Booking.com extractor returned malformed hotel row");const I=String(B.name||"").trim(),X=String(B.country||"").trim(),U=String(B.slug||"").trim(),L=String(B.url||"").trim(),h=X&&U?`https://www.booking.com/hotel/${X}/${U}.html`:"";if(!I||!/^[a-z]{2}$/.test(X)||!U||L!==h)throw new j("Booking.com hotel row is missing stable name/url identity");return{rank:P+f+1,name:I,country:X,slug:U,star_rating:B.star_rating,review_score:B.review_score,review_count:B.review_count,price_amount:B.price_amount,price_currency:B.price_amount==null?"":G||B.price_currency||"",distance:B.distance,recommended_room:B.recommended_room,url:L}})}});export const __test__={normalizePositiveInt:_,normalizeNonNegativeInt:S,normalizeDate:D,normalizeCurrency:N,normalizeLang:T,hasPositiveResultCount:z,buildSearchUrl:b,EXTRACTOR:C};
@@ -72,7 +72,7 @@ import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,Empt
72
72
  }
73
73
  return { clicked: false };
74
74
  }
75
- `)).clicked)await z.pressKey("Enter");return!0}export function verbose(z){if(process.env.OPENCLI_VERBOSE)console.error(`[opencli:boss] ${z}`)}export const IDENTITY_MISMATCH_CODE=24;const N=`https://${q}/web/geek/chat`;export async function navigateToGeekChat(z,J=2){await z.goto(N);await z.wait({time:J})}export async function readEncryptSystemId(z){return await z.evaluate(`
75
+ `)).clicked)await z.pressKey("Enter");return!0}export function verbose(z){if(process.env.OPENCLI_VERBOSE)console.error(`[ppcli:boss] ${z}`)}export const IDENTITY_MISMATCH_CODE=24;const N=`https://${q}/web/geek/chat`;export async function navigateToGeekChat(z,J=2){await z.goto(N);await z.wait({time:J})}export async function readEncryptSystemId(z){return await z.evaluate(`
76
76
  (() => {
77
77
  // 1. Vue app state / Pinia / $route.query
78
78
  // The chat component reads encryptSystemId from the app runtime to build
@@ -1 +1 @@
1
- import{cli as j,Strategy as z}from"@jackwener/opencli/registry";import{ArgumentError as f,CommandExecutionError as L}from"@jackwener/opencli/errors";import{CHATGPT_DOMAIN as B,CHATGPT_URL as I,currentChatGPTUrl as M,ensureChatGPTComposer as R,ensureOnChatGPT as A,getChatGPTResponsePairCounts as S,getVisibleMessages as b,normalizeBooleanFlag as Y,openChatGPTConversation as F,requireNonEmptyPrompt as U,requirePositiveInt as E,parseChatGPTConversationId as G,sendChatGPTMessage as v,selectChatGPTTool as d,isGenerating as h,startNewChat as P,navigateToProject as T,waitForChatGPTResponse as C}from"./utils.js";async function m(K,J=30){const Q=Date.now();while(Date.now()-Q<J*1000){const X=await M(K);try{return{conversationId:G(X),conversationUrl:X}}catch{await K.wait(1)}}throw new L("ChatGPT did not create a conversation URL after sending the message.")}export const askCommand=j({site:"chatgpt",name:"ask",access:"write",description:"Send a prompt to ChatGPT web and wait for the response",domain:B,strategy:z.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"prompt",positional:!0,required:!0,help:"Prompt to send"},{name:"timeout",type:"int",default:120,help:"Max seconds to wait for response"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"},{name:"conversation",valueRequired:!0,help:"Continue an existing ChatGPT conversation ID or /c/<id> URL"},{name:"project",valueRequired:!0,help:"Start a new chat inside a ChatGPT project ID or /g/g-p-<id> URL"},{name:"wait",type:"boolean",default:!0,help:"Wait for the assistant response after sending"},{name:"deep-research",type:"boolean",default:!1,help:"Enable ChatGPT 深度研究 (Deep Research)"},{name:"web-search",type:"boolean",default:!1,help:"Enable ChatGPT 网页搜索 (Web Search)"}],columns:["conversationId","conversationUrl","tool","response"],func:async(K,J)=>{const Q=U(J.prompt,"chatgpt ask"),X=E(Number(J.timeout??120),"chatgpt ask --timeout",'Example: opencli chatgpt ask "hello" --timeout 120'),Z=Y(J["deep-research"],!1),V=Y(J["web-search"],!1),x=Y(J.wait,!0);if(Z&&V)throw new f("chatgpt ask cannot enable both --deep-research and --web-search","Choose one ChatGPT composer tool for this message.");if(Y(J.new)&&J.conversation)throw new f("chatgpt ask cannot use --new and --conversation together","Choose either a new chat or an existing conversation.");if(J.project&&J.conversation)throw new f("chatgpt ask cannot use --project and --conversation together","Choose either a project new chat or an existing conversation.");const q=Z?"deep-research":V?"web-search":null;if(J.conversation)await F(K,J.conversation);else if(J.project)await T(K,J.project);else if(Y(J.new))await P(K);else await A(K);await R(K,"ChatGPT ask requires a logged-in ChatGPT session with a visible composer.");const H=q?await d(K,q):null,y=Date.now();while(await h(K)){if(Date.now()-y>X*1000)throw new L("ChatGPT conversation is still generating; wait for it to finish before sending another message.");await K.wait(3)}const O=await b(K),D=O.length,N=S(O,Q);if(!await v(K,Q))throw new L("Failed to send message to ChatGPT",`Open ${I} and verify the composer is ready.`);const{conversationId:_,conversationUrl:$}=await m(K);if(!x)return[{conversationId:_,conversationUrl:$,tool:H?.Tool??"",response:""}];const W=await C(K,D,Q,X,{baselinePairCounts:N,conversationUrl:$});return[{conversationId:_,conversationUrl:$,tool:H?.Tool??"",response:W}]}});
1
+ import{cli as j,Strategy as z}from"@jackwener/opencli/registry";import{ArgumentError as f,CommandExecutionError as L}from"@jackwener/opencli/errors";import{CHATGPT_DOMAIN as B,CHATGPT_URL as I,currentChatGPTUrl as M,ensureChatGPTComposer as R,ensureOnChatGPT as A,getChatGPTResponsePairCounts as S,getVisibleMessages as b,normalizeBooleanFlag as Y,openChatGPTConversation as F,requireNonEmptyPrompt as U,requirePositiveInt as E,parseChatGPTConversationId as G,sendChatGPTMessage as v,selectChatGPTTool as d,isGenerating as h,startNewChat as P,navigateToProject as T,waitForChatGPTResponse as C}from"./utils.js";async function m(K,J=30){const Q=Date.now();while(Date.now()-Q<J*1000){const X=await M(K);try{return{conversationId:G(X),conversationUrl:X}}catch{await K.wait(1)}}throw new L("ChatGPT did not create a conversation URL after sending the message.")}export const askCommand=j({site:"chatgpt",name:"ask",access:"write",description:"Send a prompt to ChatGPT web and wait for the response",domain:B,strategy:z.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"prompt",positional:!0,required:!0,help:"Prompt to send"},{name:"timeout",type:"int",default:120,help:"Max seconds to wait for response"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"},{name:"conversation",valueRequired:!0,help:"Continue an existing ChatGPT conversation ID or /c/<id> URL"},{name:"project",valueRequired:!0,help:"Start a new chat inside a ChatGPT project ID or /g/g-p-<id> URL"},{name:"wait",type:"boolean",default:!0,help:"Wait for the assistant response after sending"},{name:"deep-research",type:"boolean",default:!1,help:"Enable ChatGPT 深度研究 (Deep Research)"},{name:"web-search",type:"boolean",default:!1,help:"Enable ChatGPT 网页搜索 (Web Search)"}],columns:["conversationId","conversationUrl","tool","response"],func:async(K,J)=>{const Q=U(J.prompt,"chatgpt ask"),X=E(Number(J.timeout??120),"chatgpt ask --timeout",'Example: ppcli chatgpt ask "hello" --timeout 120'),Z=Y(J["deep-research"],!1),V=Y(J["web-search"],!1),x=Y(J.wait,!0);if(Z&&V)throw new f("chatgpt ask cannot enable both --deep-research and --web-search","Choose one ChatGPT composer tool for this message.");if(Y(J.new)&&J.conversation)throw new f("chatgpt ask cannot use --new and --conversation together","Choose either a new chat or an existing conversation.");if(J.project&&J.conversation)throw new f("chatgpt ask cannot use --project and --conversation together","Choose either a project new chat or an existing conversation.");const q=Z?"deep-research":V?"web-search":null;if(J.conversation)await F(K,J.conversation);else if(J.project)await T(K,J.project);else if(Y(J.new))await P(K);else await A(K);await R(K,"ChatGPT ask requires a logged-in ChatGPT session with a visible composer.");const H=q?await d(K,q):null,y=Date.now();while(await h(K)){if(Date.now()-y>X*1000)throw new L("ChatGPT conversation is still generating; wait for it to finish before sending another message.");await K.wait(3)}const O=await b(K),D=O.length,N=S(O,Q);if(!await v(K,Q))throw new L("Failed to send message to ChatGPT",`Open ${I} and verify the composer is ready.`);const{conversationId:_,conversationUrl:$}=await m(K);if(!x)return[{conversationId:_,conversationUrl:$,tool:H?.Tool??"",response:""}];const W=await C(K,D,Q,X,{baselinePairCounts:N,conversationUrl:$});return[{conversationId:_,conversationUrl:$,tool:H?.Tool??"",response:W}]}});