@jackwener/opencli 1.7.12 → 1.7.13

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 (407) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12194 -6843
  4. package/clis/1point3acres/digest.js +35 -0
  5. package/clis/1point3acres/forum.js +51 -0
  6. package/clis/1point3acres/forums.js +44 -0
  7. package/clis/1point3acres/hot.js +35 -0
  8. package/clis/1point3acres/latest.js +35 -0
  9. package/clis/1point3acres/notifications.js +64 -0
  10. package/clis/1point3acres/search.js +71 -0
  11. package/clis/1point3acres/thread.js +117 -0
  12. package/clis/1point3acres/user.js +77 -0
  13. package/clis/1point3acres/utils.js +247 -0
  14. package/clis/_shared/desktop-commands.js +4 -0
  15. package/clis/aibase/news.js +110 -0
  16. package/clis/aibase/news.test.js +59 -0
  17. package/clis/amazon/discussion.test.js +1 -28
  18. package/clis/antigravity/watch.js +3 -2
  19. package/clis/arxiv/author.js +44 -0
  20. package/clis/baidu-scholar/search.js +0 -1
  21. package/clis/bbc/topic.js +57 -0
  22. package/clis/bbc/utils.js +79 -0
  23. package/clis/chaoxing/assignments.js +1 -1
  24. package/clis/chaoxing/exams.js +1 -1
  25. package/clis/chatgpt/ask.js +57 -0
  26. package/clis/chatgpt/commands.test.js +45 -0
  27. package/clis/chatgpt/detail.js +46 -0
  28. package/clis/chatgpt/history.js +39 -0
  29. package/clis/chatgpt/image.js +12 -11
  30. package/clis/chatgpt/image.test.js +23 -0
  31. package/clis/chatgpt/new.js +25 -0
  32. package/clis/chatgpt/read.js +43 -0
  33. package/clis/chatgpt/send.js +46 -0
  34. package/clis/chatgpt/status.js +29 -0
  35. package/clis/chatgpt/utils.js +294 -4
  36. package/clis/chatgpt/utils.test.js +13 -0
  37. package/clis/chatgpt-app/ask.js +6 -3
  38. package/clis/chatwise/ask.js +16 -43
  39. package/clis/chatwise/composer.test.js +186 -0
  40. package/clis/chatwise/send.js +2 -24
  41. package/clis/chatwise/utils.js +143 -0
  42. package/clis/claude/ask.js +1 -1
  43. package/clis/claude/detail.js +1 -0
  44. package/clis/claude/history.js +1 -0
  45. package/clis/claude/new.js +1 -0
  46. package/clis/claude/read.js +1 -0
  47. package/clis/claude/send.js +1 -0
  48. package/clis/claude/status.js +1 -0
  49. package/clis/codex/ask.js +15 -9
  50. package/clis/codex/history.js +16 -33
  51. package/clis/codex/projects.js +28 -0
  52. package/clis/codex/read.js +10 -4
  53. package/clis/codex/send.js +10 -3
  54. package/clis/codex/sidebar.js +356 -0
  55. package/clis/codex/sidebar.test.js +329 -0
  56. package/clis/coingecko/categories.js +75 -0
  57. package/clis/coingecko/coin.js +107 -0
  58. package/clis/coingecko/coingecko.test.js +109 -0
  59. package/clis/coingecko/derivatives.js +84 -0
  60. package/clis/coingecko/exchanges.js +74 -0
  61. package/clis/coingecko/global.js +71 -0
  62. package/clis/coingecko/top.js +64 -0
  63. package/clis/coingecko/trending.js +55 -0
  64. package/clis/coupang/add-to-cart.js +21 -13
  65. package/clis/coupang/coupang.test.js +159 -0
  66. package/clis/coupang/product.js +257 -0
  67. package/clis/coupang/search.js +38 -16
  68. package/clis/coupang/utils.js +55 -1
  69. package/clis/crates/crate.js +62 -0
  70. package/clis/crates/search.js +44 -0
  71. package/clis/crates/utils.js +72 -0
  72. package/clis/ctrip/ctrip.test.js +234 -0
  73. package/clis/ctrip/hotel-suggest.js +45 -0
  74. package/clis/ctrip/search.js +22 -68
  75. package/clis/ctrip/utils.js +175 -0
  76. package/clis/cursor/ask.js +6 -3
  77. package/clis/dblp/author.js +133 -0
  78. package/clis/dblp/venue.js +64 -0
  79. package/clis/deepseek/ask.js +12 -7
  80. package/clis/deepseek/ask.test.js +13 -13
  81. package/clis/deepseek/detail.js +38 -0
  82. package/clis/deepseek/detail.test.js +81 -0
  83. package/clis/deepseek/history.js +1 -0
  84. package/clis/deepseek/new.js +1 -0
  85. package/clis/deepseek/read.js +1 -0
  86. package/clis/deepseek/send.js +140 -0
  87. package/clis/deepseek/send.test.js +107 -0
  88. package/clis/deepseek/status.js +1 -0
  89. package/clis/deepseek/utils.js +66 -0
  90. package/clis/deepseek/utils.test.js +107 -1
  91. package/clis/defillama/defillama.test.js +99 -0
  92. package/clis/defillama/protocol.js +84 -0
  93. package/clis/defillama/protocols.js +55 -0
  94. package/clis/defillama/utils.js +99 -0
  95. package/clis/devto/latest.js +74 -0
  96. package/clis/dockerhub/image.js +52 -0
  97. package/clis/dockerhub/search.js +47 -0
  98. package/clis/dockerhub/utils.js +100 -0
  99. package/clis/doubao/ask.js +7 -3
  100. package/clis/doubao/detail.js +1 -0
  101. package/clis/doubao/history.js +1 -0
  102. package/clis/doubao/meeting-summary.js +1 -0
  103. package/clis/doubao/meeting-transcript.js +1 -0
  104. package/clis/doubao/new.js +1 -0
  105. package/clis/doubao/read.js +1 -0
  106. package/clis/doubao/send.js +1 -0
  107. package/clis/doubao/status.js +1 -0
  108. package/clis/douyin/draft.test.js +1 -30
  109. package/clis/endoflife/endoflife.test.js +51 -0
  110. package/clis/endoflife/product.js +55 -0
  111. package/clis/endoflife/utils.js +89 -0
  112. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  113. package/clis/facebook/notifications.js +326 -30
  114. package/clis/facebook/notifications.test.js +458 -0
  115. package/clis/flathub/app.js +71 -0
  116. package/clis/flathub/flathub.test.js +90 -0
  117. package/clis/flathub/search.js +80 -0
  118. package/clis/flathub/utils.js +114 -0
  119. package/clis/gemini/ask.js +7 -3
  120. package/clis/gemini/ask.test.js +2 -2
  121. package/clis/gemini/deep-research-result.js +6 -2
  122. package/clis/gemini/deep-research-result.test.js +15 -14
  123. package/clis/gemini/deep-research.js +8 -4
  124. package/clis/gemini/deep-research.test.js +15 -18
  125. package/clis/gemini/image.js +7 -2
  126. package/clis/gemini/new.js +1 -0
  127. package/clis/gemini/utils.js +0 -4
  128. package/clis/google-scholar/cite.js +0 -1
  129. package/clis/google-scholar/profile.js +0 -1
  130. package/clis/google-scholar/search.js +0 -1
  131. package/clis/goproxy/goproxy.test.js +103 -0
  132. package/clis/goproxy/module.js +47 -0
  133. package/clis/goproxy/utils.js +165 -0
  134. package/clis/goproxy/versions.js +59 -0
  135. package/clis/gov-law/recent.js +0 -1
  136. package/clis/gov-law/search.js +0 -1
  137. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  138. package/clis/gov-policy/__fixtures__/search.html +41 -0
  139. package/clis/gov-policy/gov-policy.test.js +224 -0
  140. package/clis/gov-policy/recent.js +66 -24
  141. package/clis/gov-policy/search.js +65 -23
  142. package/clis/gov-policy/utils.js +54 -0
  143. package/clis/grok/ask.js +49 -265
  144. package/clis/grok/ask.test.js +21 -46
  145. package/clis/grok/detail.js +60 -0
  146. package/clis/grok/history.js +48 -0
  147. package/clis/grok/{image.ts → image.js} +56 -70
  148. package/clis/grok/image.test.ts +20 -0
  149. package/clis/grok/new.js +20 -0
  150. package/clis/grok/read.js +39 -0
  151. package/clis/grok/send.js +50 -0
  152. package/clis/grok/status.js +41 -0
  153. package/clis/grok/utils.js +326 -0
  154. package/clis/grok/utils.test.js +103 -0
  155. package/clis/hf/datasets.js +88 -0
  156. package/clis/hf/hf.test.js +16 -0
  157. package/clis/hf/models.js +91 -0
  158. package/clis/hf/paper.js +79 -0
  159. package/clis/hf/spaces.js +101 -0
  160. package/clis/hf/top.js +1 -0
  161. package/clis/homebrew/cask.js +39 -0
  162. package/clis/homebrew/formula.js +41 -0
  163. package/clis/homebrew/popular.js +54 -0
  164. package/clis/homebrew/utils.js +100 -0
  165. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  166. package/clis/hupu/detail.js +0 -1
  167. package/clis/hupu/hot.js +156 -35
  168. package/clis/hupu/hot.test.js +224 -0
  169. package/clis/hupu/search.js +0 -1
  170. package/clis/instagram/note.js +1 -1
  171. package/clis/instagram/note.test.js +1 -29
  172. package/clis/instagram/post.js +1 -1
  173. package/clis/instagram/post.test.js +1 -1
  174. package/clis/instagram/reel.js +1 -1
  175. package/clis/instagram/story.js +1 -1
  176. package/clis/instagram/story.test.js +1 -34
  177. package/clis/jd/commands.test.js +1 -24
  178. package/clis/lichess/lichess.test.js +85 -0
  179. package/clis/lichess/top.js +46 -0
  180. package/clis/lichess/user.js +91 -0
  181. package/clis/lichess/utils.js +97 -0
  182. package/clis/linkedin/search.js +107 -10
  183. package/clis/linkedin/search.test.js +222 -0
  184. package/clis/linux-do/feed.js +2 -5
  185. package/clis/linux-do/feed.test.js +35 -0
  186. package/clis/lobsters/domain.js +92 -0
  187. package/clis/maven/artifact.js +49 -0
  188. package/clis/maven/search.js +51 -0
  189. package/clis/maven/utils.js +110 -0
  190. package/clis/mdn/search.js +97 -0
  191. package/clis/medium/tag.js +135 -0
  192. package/clis/npm/downloads.js +59 -0
  193. package/clis/npm/package.js +70 -0
  194. package/clis/npm/search.js +49 -0
  195. package/clis/npm/utils.js +76 -0
  196. package/clis/nuget/nuget.test.js +111 -0
  197. package/clis/nuget/package.js +101 -0
  198. package/clis/nuget/search.js +69 -0
  199. package/clis/nuget/utils.js +87 -0
  200. package/clis/nvd/cve.js +121 -0
  201. package/clis/oeis/oeis.test.js +88 -0
  202. package/clis/oeis/search.js +63 -0
  203. package/clis/oeis/sequence.js +71 -0
  204. package/clis/oeis/utils.js +88 -0
  205. package/clis/openalex/search.js +69 -0
  206. package/clis/openalex/utils.js +160 -0
  207. package/clis/openalex/work.js +65 -0
  208. package/clis/openfda/drug-label.js +74 -0
  209. package/clis/openfda/food-recall.js +65 -0
  210. package/clis/openfda/openfda.test.js +114 -0
  211. package/clis/openfda/utils.js +67 -0
  212. package/clis/osv/osv.test.js +97 -0
  213. package/clis/osv/query.js +72 -0
  214. package/clis/osv/utils.js +169 -0
  215. package/clis/osv/vulnerability.js +54 -0
  216. package/clis/packagist/package.js +49 -0
  217. package/clis/packagist/search.js +43 -0
  218. package/clis/packagist/utils.js +113 -0
  219. package/clis/paperreview/feedback.js +1 -1
  220. package/clis/paperreview/review.js +1 -1
  221. package/clis/paperreview/submit.js +1 -1
  222. package/clis/pixiv/download.test.js +1 -1
  223. package/clis/pixiv/illusts.test.js +1 -1
  224. package/clis/pixiv/search.test.js +1 -1
  225. package/clis/pubmed/article.js +50 -0
  226. package/clis/pubmed/author.js +64 -0
  227. package/clis/pubmed/citations.js +36 -0
  228. package/clis/pubmed/pubmed.test.js +276 -0
  229. package/clis/pubmed/related.js +45 -0
  230. package/clis/pubmed/search.js +75 -0
  231. package/clis/pubmed/utils.js +309 -0
  232. package/clis/pypi/downloads.js +66 -0
  233. package/clis/pypi/package.js +79 -0
  234. package/clis/pypi/utils.js +55 -0
  235. package/clis/quark/mv.js +1 -1
  236. package/clis/quark/save.js +1 -1
  237. package/clis/qwen/ask.js +85 -0
  238. package/clis/qwen/detail.js +62 -0
  239. package/clis/qwen/history.js +61 -0
  240. package/clis/qwen/image.js +179 -0
  241. package/clis/qwen/new.js +23 -0
  242. package/clis/qwen/read.js +41 -0
  243. package/clis/qwen/send.js +55 -0
  244. package/clis/qwen/status.js +37 -0
  245. package/clis/qwen/utils.js +409 -0
  246. package/clis/qwen/utils.test.js +45 -0
  247. package/clis/rest-countries/country.js +65 -0
  248. package/clis/rest-countries/region.js +64 -0
  249. package/clis/rest-countries/rest-countries.test.js +83 -0
  250. package/clis/rest-countries/utils.js +126 -0
  251. package/clis/reuters/article-detail.js +53 -0
  252. package/clis/reuters/reuters.test.js +299 -0
  253. package/clis/reuters/search.js +45 -34
  254. package/clis/reuters/utils.js +159 -0
  255. package/clis/rfc/rfc.js +52 -0
  256. package/clis/rfc/rfc.test.js +74 -0
  257. package/clis/rfc/utils.js +72 -0
  258. package/clis/rubygems/gem.js +42 -0
  259. package/clis/rubygems/search.js +47 -0
  260. package/clis/rubygems/utils.js +86 -0
  261. package/clis/stackoverflow/related.js +66 -0
  262. package/clis/stackoverflow/stackoverflow.test.js +58 -0
  263. package/clis/stackoverflow/tag.js +60 -0
  264. package/clis/stackoverflow/user.js +50 -0
  265. package/clis/stackoverflow/utils.js +118 -0
  266. package/clis/steam/app.js +67 -0
  267. package/clis/steam/search.js +58 -0
  268. package/clis/steam/steam.test.js +46 -0
  269. package/clis/steam/utils.js +107 -0
  270. package/clis/taobao/commands.test.js +1 -24
  271. package/clis/test-utils.js +61 -0
  272. package/clis/tieba/hot.js +0 -1
  273. package/clis/tiktok/comment.js +128 -41
  274. package/clis/tiktok/creator-videos.js +270 -0
  275. package/clis/tiktok/creator-videos.test.js +113 -0
  276. package/clis/tiktok/explore.js +137 -29
  277. package/clis/tiktok/follow.js +115 -33
  278. package/clis/tiktok/following.js +157 -36
  279. package/clis/tiktok/friends.js +139 -37
  280. package/clis/tiktok/live.js +137 -41
  281. package/clis/tiktok/notifications.js +141 -38
  282. package/clis/tiktok/refactor.test.js +389 -0
  283. package/clis/tiktok/unfollow.js +124 -38
  284. package/clis/tiktok/user.js +203 -29
  285. package/clis/tiktok/utils.js +505 -0
  286. package/clis/tiktok/write-refactor.test.js +370 -0
  287. package/clis/toutiao/articles.js +36 -62
  288. package/clis/toutiao/hot.js +63 -0
  289. package/clis/toutiao/toutiao.test.js +378 -0
  290. package/clis/toutiao/utils.js +161 -0
  291. package/clis/tvmaze/search.js +61 -0
  292. package/clis/tvmaze/show.js +60 -0
  293. package/clis/tvmaze/tvmaze.test.js +93 -0
  294. package/clis/tvmaze/utils.js +110 -0
  295. package/clis/twitter/accept.js +1 -1
  296. package/clis/twitter/followers.js +134 -69
  297. package/clis/twitter/reply-dm.js +1 -1
  298. package/clis/twitter/reply.test.js +1 -29
  299. package/clis/uisdc/news.js +105 -0
  300. package/clis/uisdc/news.test.js +66 -0
  301. package/clis/wanfang/search.js +0 -1
  302. package/clis/web/read.js +47 -17
  303. package/clis/web/read.test.js +101 -1
  304. package/clis/weixin/create-draft.js +1 -1
  305. package/clis/weixin/drafts.js +1 -1
  306. package/clis/weixin/drafts.test.js +5 -1
  307. package/clis/weixin/search.js +157 -0
  308. package/clis/weixin/search.test.js +227 -0
  309. package/clis/wikidata/entity.js +60 -0
  310. package/clis/wikidata/search.js +50 -0
  311. package/clis/wikidata/utils.js +117 -0
  312. package/clis/wikidata/wikidata.test.js +83 -0
  313. package/clis/wikipedia/page.js +95 -0
  314. package/clis/wttr/current.js +63 -0
  315. package/clis/wttr/forecast.js +71 -0
  316. package/clis/wttr/utils.js +50 -0
  317. package/clis/wttr/wttr.test.js +84 -0
  318. package/clis/xianyu/chat.js +16 -4
  319. package/clis/xianyu/chat.test.js +64 -0
  320. package/clis/xianyu/publish.js +485 -0
  321. package/clis/xianyu/publish.test.js +220 -0
  322. package/clis/xiaoe/catalog.js +105 -40
  323. package/clis/xiaoe/content.js +164 -29
  324. package/clis/xiaoe/courses.js +86 -29
  325. package/clis/xiaoe/xiaoe.test.js +486 -0
  326. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  327. package/clis/xiaohongshu/publish.js +16 -3
  328. package/clis/xiaohongshu/publish.test.js +46 -1
  329. package/clis/youtube/transcript.js +13 -19
  330. package/clis/youtube/transcript.test.js +17 -0
  331. package/clis/yuanbao/ask.js +17 -66
  332. package/clis/yuanbao/ask.test.js +5 -5
  333. package/clis/yuanbao/detail.js +65 -0
  334. package/clis/yuanbao/history.js +51 -0
  335. package/clis/yuanbao/new.js +1 -0
  336. package/clis/yuanbao/read.js +38 -0
  337. package/clis/yuanbao/send.js +57 -0
  338. package/clis/yuanbao/shared.js +297 -5
  339. package/clis/yuanbao/shared.test.js +80 -0
  340. package/clis/yuanbao/status.js +44 -0
  341. package/clis/zlibrary/commands.test.js +1 -11
  342. package/dist/src/browser/base-page.d.ts +9 -0
  343. package/dist/src/browser/base-page.js +44 -1
  344. package/dist/src/browser/base-page.test.js +66 -0
  345. package/dist/src/browser/cdp.d.ts +1 -0
  346. package/dist/src/browser/cdp.js +51 -9
  347. package/dist/src/browser/daemon-client.d.ts +4 -0
  348. package/dist/src/browser/errors.js +1 -1
  349. package/dist/src/browser/page.d.ts +1 -1
  350. package/dist/src/browser/page.js +3 -1
  351. package/dist/src/browser/page.test.js +29 -0
  352. package/dist/src/browser/target-errors.d.ts +2 -1
  353. package/dist/src/browser/target-errors.js +1 -0
  354. package/dist/src/browser/target-resolver.d.ts +25 -0
  355. package/dist/src/browser/target-resolver.js +43 -0
  356. package/dist/src/build-manifest.js +9 -4
  357. package/dist/src/build-manifest.test.js +2 -8
  358. package/dist/src/capabilityRouting.d.ts +16 -1
  359. package/dist/src/capabilityRouting.js +24 -1
  360. package/dist/src/capabilityRouting.test.js +19 -1
  361. package/dist/src/cli.js +76 -11
  362. package/dist/src/cli.test.js +150 -0
  363. package/dist/src/commanderAdapter.js +0 -5
  364. package/dist/src/commanderAdapter.test.js +0 -1
  365. package/dist/src/discovery.js +2 -5
  366. package/dist/src/errors.js +1 -1
  367. package/dist/src/execution.d.ts +1 -1
  368. package/dist/src/execution.js +111 -27
  369. package/dist/src/execution.test.js +326 -17
  370. package/dist/src/help.d.ts +23 -2
  371. package/dist/src/help.js +41 -19
  372. package/dist/src/help.test.d.ts +1 -0
  373. package/dist/src/help.test.js +54 -0
  374. package/dist/src/main.js +14 -1
  375. package/dist/src/manifest-types.d.ts +5 -3
  376. package/dist/src/pipeline/executor.js +1 -1
  377. package/dist/src/pipeline/executor.test.js +8 -0
  378. package/dist/src/pipeline/registry.d.ts +9 -0
  379. package/dist/src/pipeline/registry.js +13 -1
  380. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  381. package/dist/src/pipeline/steps/browser.js +10 -0
  382. package/dist/src/pipeline/steps/download.test.js +1 -0
  383. package/dist/src/registry-api.d.ts +1 -1
  384. package/dist/src/registry.d.ts +12 -11
  385. package/dist/src/registry.js +16 -6
  386. package/dist/src/registry.test.js +2 -2
  387. package/dist/src/runtime.d.ts +2 -1
  388. package/dist/src/runtime.js +1 -1
  389. package/dist/src/serialization.d.ts +2 -2
  390. package/dist/src/serialization.js +4 -6
  391. package/dist/src/serialization.test.js +17 -0
  392. package/dist/src/types.d.ts +17 -0
  393. package/dist/src/validate.js +15 -11
  394. package/dist/src/validate.test.d.ts +9 -0
  395. package/dist/src/validate.test.js +90 -0
  396. package/package.json +1 -1
  397. package/scripts/fetch-adapters.js +1 -1
  398. package/scripts/typed-error-lint-baseline.json +5 -77
  399. package/clis/ctrip/search.test.js +0 -64
  400. package/clis/gov-policy/commands.test.js +0 -27
  401. package/clis/linux-do/category.js +0 -37
  402. package/clis/linux-do/hot.js +0 -26
  403. package/clis/linux-do/latest.js +0 -19
  404. package/clis/pixiv/test-utils.js +0 -23
  405. package/clis/toutiao/articles.test.js +0 -30
  406. package/dist/src/analysis.d.ts +0 -40
  407. package/dist/src/analysis.js +0 -172
@@ -0,0 +1,63 @@
1
+ // wttr current — current weather conditions for a city / lat,lon / airport code.
2
+ //
3
+ // Endpoint: GET /<location>?format=j1 → returns current_condition + nearest_area.
4
+ // One row (current snapshot).
5
+ import { cli, Strategy } from '@jackwener/opencli/registry';
6
+ import { EmptyResultError } from '@jackwener/opencli/errors';
7
+ import { requireString, wttrFetch, pickWeatherDesc } from './utils.js';
8
+
9
+ cli({
10
+ site: 'wttr',
11
+ name: 'current',
12
+ access: 'read',
13
+ description: 'Current weather conditions for a location (city, lat,lon, or airport code)',
14
+ domain: 'wttr.in',
15
+ strategy: Strategy.PUBLIC,
16
+ browser: false,
17
+ args: [
18
+ {
19
+ name: 'location',
20
+ positional: true,
21
+ required: true,
22
+ help: 'City name, "lat,lon", airport ICAO code, or "@domain"',
23
+ },
24
+ ],
25
+ columns: [
26
+ 'location', 'region', 'country', 'latitude', 'longitude',
27
+ 'observedAt', 'tempC', 'tempF', 'feelsLikeC', 'feelsLikeF',
28
+ 'description', 'humidity', 'cloudCover', 'pressure',
29
+ 'precipMm', 'visibilityKm', 'uvIndex',
30
+ 'windKmph', 'windDirection', 'windDirectionDegree',
31
+ ],
32
+ func: async (args) => {
33
+ const location = requireString(args.location, 'location');
34
+ const body = await wttrFetch(location, 'wttr current');
35
+ const cur = Array.isArray(body?.current_condition) ? body.current_condition[0] : null;
36
+ if (!cur) {
37
+ throw new EmptyResultError('wttr current', `wttr.in returned no current conditions for "${location}".`);
38
+ }
39
+ const area = Array.isArray(body?.nearest_area) ? body.nearest_area[0] : null;
40
+ return [{
41
+ location: pickWeatherDesc(area?.areaName) || location,
42
+ region: pickWeatherDesc(area?.region),
43
+ country: pickWeatherDesc(area?.country),
44
+ latitude: area?.latitude ?? null,
45
+ longitude: area?.longitude ?? null,
46
+ observedAt: cur.localObsDateTime ?? null,
47
+ tempC: cur.temp_C != null ? Number(cur.temp_C) : null,
48
+ tempF: cur.temp_F != null ? Number(cur.temp_F) : null,
49
+ feelsLikeC: cur.FeelsLikeC != null ? Number(cur.FeelsLikeC) : null,
50
+ feelsLikeF: cur.FeelsLikeF != null ? Number(cur.FeelsLikeF) : null,
51
+ description: pickWeatherDesc(cur.weatherDesc),
52
+ humidity: cur.humidity != null ? Number(cur.humidity) : null,
53
+ cloudCover: cur.cloudcover != null ? Number(cur.cloudcover) : null,
54
+ pressure: cur.pressure != null ? Number(cur.pressure) : null,
55
+ precipMm: cur.precipMM != null ? Number(cur.precipMM) : null,
56
+ visibilityKm: cur.visibility != null ? Number(cur.visibility) : null,
57
+ uvIndex: cur.uvIndex != null ? Number(cur.uvIndex) : null,
58
+ windKmph: cur.windspeedKmph != null ? Number(cur.windspeedKmph) : null,
59
+ windDirection: cur.winddir16Point ?? null,
60
+ windDirectionDegree: cur.winddirDegree != null ? Number(cur.winddirDegree) : null,
61
+ }];
62
+ },
63
+ });
@@ -0,0 +1,71 @@
1
+ // wttr forecast — multi-day forecast for a location.
2
+ //
3
+ // Endpoint: GET /<location>?format=j1 → returns weather[] (3 days max on free tier).
4
+ // Each day is collapsed into a single row with min/max/avg + summary description.
5
+ import { cli, Strategy } from '@jackwener/opencli/registry';
6
+ import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
7
+ import { requireString, wttrFetch, pickWeatherDesc } from './utils.js';
8
+
9
+ cli({
10
+ site: 'wttr',
11
+ name: 'forecast',
12
+ access: 'read',
13
+ description: 'Multi-day weather forecast (up to 3 days, wttr.in free tier max)',
14
+ domain: 'wttr.in',
15
+ strategy: Strategy.PUBLIC,
16
+ browser: false,
17
+ args: [
18
+ {
19
+ name: 'location',
20
+ positional: true,
21
+ required: true,
22
+ help: 'City name, "lat,lon", airport ICAO code, or "@domain"',
23
+ },
24
+ {
25
+ name: 'days',
26
+ type: 'int',
27
+ default: 3,
28
+ help: 'Max forecast days (1-3, wttr.in caps the response at 3 days)',
29
+ },
30
+ ],
31
+ columns: [
32
+ 'rank', 'date', 'minTempC', 'maxTempC', 'avgTempC',
33
+ 'minTempF', 'maxTempF', 'avgTempF',
34
+ 'sunHour', 'totalSnowCm', 'uvIndex',
35
+ 'description', 'sunrise', 'sunset',
36
+ ],
37
+ func: async (args) => {
38
+ const location = requireString(args.location, 'location');
39
+ const days = Number(args.days ?? 3);
40
+ if (!Number.isInteger(days) || days < 1 || days > 3) {
41
+ throw new ArgumentError('--days must be an integer between 1 and 3 (wttr.in caps the free-tier forecast at 3 days)');
42
+ }
43
+ const body = await wttrFetch(location, 'wttr forecast');
44
+ const list = Array.isArray(body?.weather) ? body.weather : [];
45
+ if (!list.length) {
46
+ throw new EmptyResultError('wttr forecast', `wttr.in returned no forecast for "${location}".`);
47
+ }
48
+ return list.slice(0, days).map((day, i) => {
49
+ // wttr.in's day-summary uses the noon hourly slot for "main" description.
50
+ // Index 4 = 12:00 in their 3-hour-step hourly array.
51
+ const noon = Array.isArray(day.hourly) && day.hourly[4] ? day.hourly[4] : day.hourly?.[0] ?? {};
52
+ const astro = Array.isArray(day.astronomy) ? day.astronomy[0] : null;
53
+ return {
54
+ rank: i + 1,
55
+ date: day.date ?? null,
56
+ minTempC: day.mintempC != null ? Number(day.mintempC) : null,
57
+ maxTempC: day.maxtempC != null ? Number(day.maxtempC) : null,
58
+ avgTempC: day.avgtempC != null ? Number(day.avgtempC) : null,
59
+ minTempF: day.mintempF != null ? Number(day.mintempF) : null,
60
+ maxTempF: day.maxtempF != null ? Number(day.maxtempF) : null,
61
+ avgTempF: day.avgtempF != null ? Number(day.avgtempF) : null,
62
+ sunHour: day.sunHour != null ? Number(day.sunHour) : null,
63
+ totalSnowCm: day.totalSnow_cm != null ? Number(day.totalSnow_cm) : null,
64
+ uvIndex: day.uvIndex != null ? Number(day.uvIndex) : null,
65
+ description: pickWeatherDesc(noon?.weatherDesc),
66
+ sunrise: astro?.sunrise ?? null,
67
+ sunset: astro?.sunset ?? null,
68
+ };
69
+ });
70
+ },
71
+ });
@@ -0,0 +1,50 @@
1
+ // wttr.in shared helpers — global weather (no auth, terminal-friendly JSON via ?format=j1).
2
+ //
3
+ // Coverage: worldwide. Unlike NWS (US-only), wttr.in geocodes any city/airport
4
+ // code/lat,lon string and serves a 3-day forecast + current conditions in one
5
+ // payload.
6
+ import { ArgumentError, EmptyResultError, CommandExecutionError } from '@jackwener/opencli/errors';
7
+
8
+ export const WTTR_BASE = 'https://wttr.in';
9
+ const UA = 'opencli-wttr/1.0';
10
+
11
+ export function requireString(value, name) {
12
+ if (typeof value !== 'string' || !value.trim()) {
13
+ throw new ArgumentError(`--${name} is required`);
14
+ }
15
+ return value.trim();
16
+ }
17
+
18
+ export async function wttrFetch(location, label) {
19
+ // wttr.in path-encodes the location. Spaces → %20 is fine; commas survive.
20
+ const url = `${WTTR_BASE}/${encodeURIComponent(location)}?format=j1`;
21
+ let resp;
22
+ try {
23
+ resp = await fetch(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });
24
+ } catch (err) {
25
+ throw new CommandExecutionError(`${label} request failed: ${err.message}`);
26
+ }
27
+ if (resp.status === 404) {
28
+ throw new EmptyResultError(label, `${label} could not find location "${location}".`);
29
+ }
30
+ if (!resp.ok) {
31
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}.`);
32
+ }
33
+ let body;
34
+ try {
35
+ body = await resp.json();
36
+ } catch (err) {
37
+ // wttr.in falls back to plain-text "Unknown location" for some bad inputs;
38
+ // promote that to EmptyResult instead of pretending we got JSON.
39
+ throw new EmptyResultError(label, `${label} returned non-JSON body (likely unknown location).`);
40
+ }
41
+ return body;
42
+ }
43
+
44
+ // wttr.in's "weatherDesc" / "lang_en" fields are arrays of `{ value: '...' }` objects.
45
+ // Single-element 99% of the time but the schema is a list.
46
+ export function pickWeatherDesc(arr) {
47
+ if (!Array.isArray(arr) || !arr.length) return '';
48
+ const first = arr[0];
49
+ return typeof first?.value === 'string' ? first.value.trim() : '';
50
+ }
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
4
+ import './current.js';
5
+ import './forecast.js';
6
+
7
+ const origFetch = global.fetch;
8
+ afterEach(() => { global.fetch = origFetch; });
9
+
10
+ const sampleBody = {
11
+ current_condition: [{
12
+ temp_C: '18', temp_F: '65', FeelsLikeC: '17', FeelsLikeF: '63',
13
+ humidity: '70', cloudcover: '50', pressure: '1015', precipMM: '0.2',
14
+ visibility: '10', uvIndex: '4', windspeedKmph: '12', winddir16Point: 'NE', winddirDegree: '45',
15
+ weatherDesc: [{ value: 'Partly cloudy' }],
16
+ localObsDateTime: '2026-05-06 10:00 AM',
17
+ }],
18
+ nearest_area: [{
19
+ areaName: [{ value: 'Tokyo' }],
20
+ country: [{ value: 'Japan' }],
21
+ region: [{ value: 'Tokyo' }],
22
+ latitude: '35.685', longitude: '139.752',
23
+ }],
24
+ weather: [
25
+ {
26
+ date: '2026-05-06', mintempC: '15', maxtempC: '22', avgtempC: '18',
27
+ mintempF: '59', maxtempF: '72', avgtempF: '65',
28
+ sunHour: '12.0', totalSnow_cm: '0.0', uvIndex: '4',
29
+ astronomy: [{ sunrise: '04:50 AM', sunset: '06:35 PM' }],
30
+ hourly: [
31
+ {}, {}, {}, {},
32
+ { weatherDesc: [{ value: 'Sunny' }] },
33
+ {},
34
+ ],
35
+ },
36
+ ],
37
+ };
38
+
39
+ describe('wttr current', () => {
40
+ const cmd = getRegistry().get('wttr/current');
41
+
42
+ it('rejects empty location', async () => {
43
+ await expect(cmd.func({ location: '' })).rejects.toBeInstanceOf(ArgumentError);
44
+ });
45
+
46
+ it('promotes 404 to EmptyResultError', async () => {
47
+ global.fetch = vi.fn(() => Promise.resolve(new Response('not found', { status: 404 })));
48
+ await expect(cmd.func({ location: 'Atlantis' })).rejects.toBeInstanceOf(EmptyResultError);
49
+ });
50
+
51
+ it('promotes non-JSON body to EmptyResultError', async () => {
52
+ global.fetch = vi.fn(() => Promise.resolve(new Response('Unknown location', { status: 200 })));
53
+ await expect(cmd.func({ location: 'asdf' })).rejects.toBeInstanceOf(EmptyResultError);
54
+ });
55
+
56
+ it('shapes current row + numeric coercion', async () => {
57
+ global.fetch = vi.fn(() => Promise.resolve(new Response(JSON.stringify(sampleBody), { status: 200 })));
58
+ const rows = await cmd.func({ location: 'Tokyo' });
59
+ expect(rows).toHaveLength(1);
60
+ expect(rows[0].location).toBe('Tokyo');
61
+ expect(rows[0].country).toBe('Japan');
62
+ expect(rows[0].tempC).toBe(18);
63
+ expect(rows[0].description).toBe('Partly cloudy');
64
+ expect(rows[0].windDirection).toBe('NE');
65
+ });
66
+ });
67
+
68
+ describe('wttr forecast', () => {
69
+ const cmd = getRegistry().get('wttr/forecast');
70
+
71
+ it('rejects --days out of range', async () => {
72
+ await expect(cmd.func({ location: 'Tokyo', days: 5 })).rejects.toBeInstanceOf(ArgumentError);
73
+ });
74
+
75
+ it('shapes forecast rows + picks noon description', async () => {
76
+ global.fetch = vi.fn(() => Promise.resolve(new Response(JSON.stringify(sampleBody), { status: 200 })));
77
+ const rows = await cmd.func({ location: 'Tokyo' });
78
+ expect(rows).toHaveLength(1);
79
+ expect(rows[0]).toMatchObject({
80
+ rank: 1, date: '2026-05-06', minTempC: 15, maxTempC: 22, avgTempC: 18,
81
+ description: 'Sunny', sunrise: '04:50 AM', sunset: '06:35 PM',
82
+ });
83
+ });
84
+ });
@@ -12,8 +12,9 @@ function buildExtractChatStateEvaluate() {
12
12
  const requiresAuth = /请先登录|登录后/.test(bodyText);
13
13
 
14
14
  const textarea = document.querySelector('textarea');
15
+ const normalizeBtn = (s) => (s || '').replace(/\\s+/g, '').trim();
15
16
  const sendButton = Array.from(document.querySelectorAll('button'))
16
- .find((btn) => clean(btn.textContent || '') === '发送');
17
+ .find((btn) => normalizeBtn(btn.textContent || '') === '发送');
17
18
  const topbar = document.querySelector('[class*="message-topbar"]');
18
19
  const itemCard = Array.from(document.querySelectorAll('a[href*="/item?id="]'))
19
20
  .find((el) => el.closest('main'));
@@ -51,7 +52,7 @@ function buildExtractChatStateEvaluate() {
51
52
  }
52
53
  function buildSendMessageEvaluate(text) {
53
54
  return `
54
- (() => {
55
+ (async () => {
55
56
  const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
56
57
  const textarea = document.querySelector('textarea');
57
58
  if (!textarea || textarea.disabled) {
@@ -63,13 +64,22 @@ function buildSendMessageEvaluate(text) {
63
64
  return { ok: false, reason: 'textarea-setter-not-found' };
64
65
  }
65
66
 
67
+ // Click textarea first to activate chat and trigger send button to appear
68
+ textarea.click();
66
69
  textarea.focus();
67
70
  setter.call(textarea, ${JSON.stringify(text)});
68
71
  textarea.dispatchEvent(new Event('input', { bubbles: true }));
69
72
  textarea.dispatchEvent(new Event('change', { bubbles: true }));
70
73
 
71
- const sendButton = Array.from(document.querySelectorAll('button'))
72
- .find((btn) => clean(btn.textContent || '') === '发送');
74
+ // Poll up to 3s for send button (may appear after textarea interaction)
75
+ const normalizeBtn = (s) => (s || '').replace(/\\s+/g, '').trim();
76
+ let sendButton = null;
77
+ for (let i = 0; i < 30; i++) {
78
+ sendButton = Array.from(document.querySelectorAll('button'))
79
+ .find((btn) => normalizeBtn(btn.textContent || '') === '发送');
80
+ if (sendButton) break;
81
+ await new Promise(r => setTimeout(r, 100));
82
+ }
73
83
  if (!sendButton) {
74
84
  return { ok: false, reason: 'send-button-not-found' };
75
85
  }
@@ -144,4 +154,6 @@ cli({
144
154
  export const __test__ = {
145
155
  normalizeNumericId,
146
156
  buildChatUrl,
157
+ buildExtractChatStateEvaluate,
158
+ buildSendMessageEvaluate,
147
159
  };
@@ -1,5 +1,13 @@
1
+ import { JSDOM } from 'jsdom';
1
2
  import { describe, expect, it } from 'vitest';
2
3
  import { __test__ } from './chat.js';
4
+
5
+ async function runBrowserScript(html, script, { url = 'https://www.goofish.com/im', beforeEval } = {}) {
6
+ const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
7
+ beforeEval?.(dom.window);
8
+ return dom.window.eval(script);
9
+ }
10
+
3
11
  describe('xianyu chat helpers', () => {
4
12
  it('builds goofish im urls from ids', () => {
5
13
  expect(__test__.buildChatUrl('1038951278192', '3650092411')).toBe('https://www.goofish.com/im?itemId=1038951278192&peerUserId=3650092411');
@@ -12,4 +20,60 @@ describe('xianyu chat helpers', () => {
12
20
  expect(() => __test__.normalizeNumericId('abc', 'item_id', '1038951278192')).toThrow();
13
21
  expect(() => __test__.normalizeNumericId('3650092411x', 'user_id', '3650092411')).toThrow();
14
22
  });
23
+
24
+ it('detects send buttons with whitespace-split text in the in-browser state extractor', async () => {
25
+ const state = await runBrowserScript(`
26
+ <main>
27
+ <textarea></textarea>
28
+ <button>发 送</button>
29
+ <div id="message-list-scrollable"><div class="bubble">你好</div></div>
30
+ </main>
31
+ `, __test__.buildExtractChatStateEvaluate());
32
+
33
+ expect(state.can_input).toBe(true);
34
+ expect(state.can_send).toBe(true);
35
+ expect(state.visible_messages).toEqual(['你好']);
36
+ });
37
+
38
+ it('activates the textarea and waits for a whitespace-split send button before clicking it', async () => {
39
+ let inputValue = '';
40
+ let sendClicked = false;
41
+ const result = await runBrowserScript(`
42
+ <main>
43
+ <textarea></textarea>
44
+ </main>
45
+ `, __test__.buildSendMessageEvaluate('还在吗?'), {
46
+ beforeEval(window) {
47
+ const textarea = window.document.querySelector('textarea');
48
+ textarea.addEventListener('input', () => {
49
+ inputValue = textarea.value;
50
+ });
51
+ textarea.addEventListener('click', () => {
52
+ const button = window.document.createElement('button');
53
+ button.textContent = '发 送';
54
+ button.addEventListener('click', () => {
55
+ sendClicked = true;
56
+ });
57
+ window.document.body.append(button);
58
+ });
59
+ },
60
+ });
61
+
62
+ expect(result).toEqual({ ok: true });
63
+ expect(inputValue).toBe('还在吗?');
64
+ expect(sendClicked).toBe(true);
65
+ });
66
+
67
+ it('returns a typed failure reason when activation still does not reveal the send button', async () => {
68
+ const result = await runBrowserScript('<textarea></textarea>', __test__.buildSendMessageEvaluate('ping'), {
69
+ beforeEval(window) {
70
+ window.setTimeout = (fn) => {
71
+ fn();
72
+ return 0;
73
+ };
74
+ },
75
+ });
76
+
77
+ expect(result).toEqual({ ok: false, reason: 'send-button-not-found' });
78
+ });
15
79
  });