@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,175 @@
1
+ /**
2
+ * Shared helpers for ctrip public destination/hotel suggestion endpoints.
3
+ *
4
+ * The single backing endpoint `https://m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine`
5
+ * accepts a `searchType` discriminator:
6
+ * - `D` → destination suggest (cities, scenic spots, railway stations, landmarks)
7
+ * - `H` → hotel-context suggest (cities, business areas, individual hotels)
8
+ *
9
+ * Response shape is identical; we surface every field the endpoint emits as a
10
+ * stable column so callers do not silently lose geo / English / id metadata.
11
+ */
12
+ import { ArgumentError, CliError } from '@jackwener/opencli/errors';
13
+
14
+ const ENDPOINT = 'https://m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine';
15
+ const MIN_LIMIT = 1;
16
+ const MAX_LIMIT = 50;
17
+
18
+ export function parseLimit(raw, fallback = 15) {
19
+ if (raw === undefined || raw === null || raw === '') return fallback;
20
+ const parsed = Number(raw);
21
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
22
+ throw new ArgumentError(`--limit must be an integer between ${MIN_LIMIT} and ${MAX_LIMIT}, got ${JSON.stringify(raw)}`);
23
+ }
24
+ if (parsed < MIN_LIMIT || parsed > MAX_LIMIT) {
25
+ throw new ArgumentError(`--limit must be between ${MIN_LIMIT} and ${MAX_LIMIT}, got ${parsed}`);
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ export async function fetchSuggest(query, searchType) {
31
+ let response;
32
+ try {
33
+ response = await fetch(ENDPOINT, {
34
+ method: 'POST',
35
+ headers: { 'content-type': 'application/json' },
36
+ body: JSON.stringify({
37
+ keyword: query,
38
+ searchType,
39
+ platform: 'online',
40
+ pageID: '102001',
41
+ head: {
42
+ Locale: 'zh-CN',
43
+ LocaleController: 'zh_cn',
44
+ Currency: 'CNY',
45
+ PageId: '102001',
46
+ clientID: 'opencli-ctrip',
47
+ group: 'ctrip',
48
+ Frontend: { sessionID: 1, pvid: 1 },
49
+ HotelExtension: { group: 'CTRIP', WebpSupport: false },
50
+ },
51
+ }),
52
+ });
53
+ } catch (err) {
54
+ throw new CliError(
55
+ 'FETCH_ERROR',
56
+ `ctrip suggest fetch failed: ${err instanceof Error ? err.message : String(err)}`,
57
+ 'Check your network connection and retry',
58
+ );
59
+ }
60
+ if (!response.ok) {
61
+ throw new CliError(
62
+ 'FETCH_ERROR',
63
+ `ctrip suggest failed with status ${response.status}`,
64
+ 'Retry the command or verify ctrip.com is reachable',
65
+ );
66
+ }
67
+ let payload;
68
+ try {
69
+ payload = await response.json();
70
+ } catch (err) {
71
+ throw new CliError(
72
+ 'COMMAND_EXEC',
73
+ `ctrip suggest returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
74
+ 'Ctrip may have changed the endpoint response format; retry later',
75
+ );
76
+ }
77
+ if (payload && payload.Result === false) {
78
+ const code = payload.ErrorCode ?? 'unknown';
79
+ throw new CliError(
80
+ 'COMMAND_EXEC',
81
+ `ctrip suggest API returned Result=false (ErrorCode=${code})`,
82
+ 'Verify keyword and retry; this typically means upstream rejected the query envelope',
83
+ );
84
+ }
85
+ return Array.isArray(payload?.Response?.searchResults) ? payload.Response.searchResults : [];
86
+ }
87
+
88
+ /**
89
+ * Pick the best lat/lon pair available.
90
+ *
91
+ * Domestic Mainland China rows ship `gdLat`/`gdLon` (gaode); international rows
92
+ * ship `gLat`/`gLon` (google/wgs84). `lat`/`lon` is the legacy flat field — fall
93
+ * through to it last. Zero values are treated as "missing" since the endpoint
94
+ * uses 0.0 as a sentinel for unknown coords.
95
+ */
96
+ export function pickCoords(item) {
97
+ const candidates = [
98
+ [item.gdLat, item.gdLon],
99
+ [item.gLat, item.gLon],
100
+ [item.lat, item.lon],
101
+ ];
102
+ for (const [la, lo] of candidates) {
103
+ if (Number.isFinite(la) && Number.isFinite(lo) && (la !== 0 || lo !== 0)) {
104
+ return { lat: la, lon: lo };
105
+ }
106
+ }
107
+ return { lat: null, lon: null };
108
+ }
109
+
110
+ /**
111
+ * Build a canonical user-facing URL from the suggest item type + ids.
112
+ * Unknown types return null (do not silently fabricate URLs).
113
+ */
114
+ export function buildUrl(item) {
115
+ const id = item?.id ? String(item.id) : '';
116
+ const cityId = item?.cityId ?? '';
117
+ const cityName = item?.cityName ? String(item.cityName) : '';
118
+ switch (item?.type) {
119
+ case 'City':
120
+ return cityId ? `https://you.ctrip.com/place/${encodeURIComponent(cityName)}${cityId}.html` : null;
121
+ case 'Markland':
122
+ return id && cityId
123
+ ? `https://you.ctrip.com/sight/${encodeURIComponent(cityName)}${cityId}/${id}.html`
124
+ : null;
125
+ case 'Hotel':
126
+ return id ? `https://hotels.ctrip.com/hotels/detail/?hotelid=${id}` : null;
127
+ case 'BusinessArea':
128
+ case 'Zone':
129
+ return cityId && id
130
+ ? `https://hotels.ctrip.com/hotels/list?city=${cityId}&zone=${id}`
131
+ : null;
132
+ case 'RailwayStation':
133
+ return id ? `https://trains.ctrip.com/trainstation/${id}.html` : null;
134
+ default:
135
+ return null;
136
+ }
137
+ }
138
+
139
+ function nz(v) {
140
+ return Number.isFinite(v) && v !== 0 ? v : null;
141
+ }
142
+
143
+ function firstNonZero(...values) {
144
+ for (const v of values) {
145
+ const n = Number(v);
146
+ if (Number.isFinite(n) && n !== 0) return n;
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Project a raw suggest row into the stable adapter column shape.
153
+ * No silent fallbacks: every column has a deterministic value (string|number|null).
154
+ */
155
+ export function mapSuggestRow(item, index) {
156
+ const { lat, lon } = pickCoords(item);
157
+ return {
158
+ rank: index + 1,
159
+ id: item?.id ? String(item.id) : null,
160
+ type: item?.type ? String(item.type) : null,
161
+ displayType: item?.displayType ? String(item.displayType).trim() : null,
162
+ name: String(item?.displayName || item?.word || item?.cityName || '').replace(/\s+/g, ' ').trim() || null,
163
+ eName: item?.eName ? String(item.eName).trim() : null,
164
+ cityId: Number.isFinite(item?.cityId) && item.cityId !== 0 ? item.cityId : null,
165
+ cityName: item?.cityName ? String(item.cityName).trim() : null,
166
+ provinceName: item?.provinceName ? String(item.provinceName).trim() : null,
167
+ countryName: item?.countryName ? String(item.countryName).trim() : null,
168
+ lat,
169
+ lon,
170
+ score: firstNonZero(item?.commentScore, item?.cStar),
171
+ url: buildUrl(item),
172
+ };
173
+ }
174
+
175
+ export const __test__ = { ENDPOINT, MIN_LIMIT, MAX_LIMIT };
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { selectorError } from '@jackwener/opencli/errors';
2
+ import { ArgumentError, selectorError } from '@jackwener/opencli/errors';
3
3
  export const askCommand = cli({
4
4
  site: 'cursor',
5
5
  name: 'ask',
@@ -10,12 +10,15 @@ export const askCommand = cli({
10
10
  browser: true,
11
11
  args: [
12
12
  { name: 'text', required: true, positional: true, help: 'Prompt to send' },
13
- { name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default: '30' },
13
+ { name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait for response (default: 30)', default: 30 },
14
14
  ],
15
15
  columns: ['Role', 'Text'],
16
16
  func: async (page, kwargs) => {
17
17
  const text = kwargs.text;
18
- const timeout = parseInt(kwargs.timeout, 10) || 30;
18
+ const timeout = kwargs.timeout;
19
+ if (!Number.isInteger(timeout) || timeout < 1) {
20
+ throw new ArgumentError('--timeout must be a positive integer (seconds)');
21
+ }
19
22
  // Count existing messages before sending
20
23
  const beforeCount = await page.evaluate(`
21
24
  document.querySelectorAll('[data-message-role]').length
@@ -0,0 +1,133 @@
1
+ // dblp author — resolve an author name to a PID and list their publications
2
+ // newest first.
3
+ //
4
+ // Two-step lookup against dblp's public API:
5
+ // 1. `search/author/api?q=<name>` returns candidate authors (each with a
6
+ // stable PID URL like `https://dblp.org/pid/56/953`).
7
+ // 2. `pid/<pid>.xml` returns every publication under that PID, ordered
8
+ // newest first.
9
+ //
10
+ // We auto-resolve to the top hit. dblp's author search returns a single
11
+ // best-match for unique names; for ambiguous names (e.g. "Wei Wang") it
12
+ // returns multiple PIDs — we pick the highest-scored one and surface the
13
+ // resolved name + PID in the row metadata so the caller can refine.
14
+ //
15
+ // To bypass author search entirely, pass `--pid <prefix>/<id>` (e.g.
16
+ // `--pid 56/953`). This is the canonical disambiguator.
17
+ import { cli, Strategy } from '@jackwener/opencli/registry';
18
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
19
+ import {
20
+ DBLP_ORIGIN,
21
+ decodeXmlEntities,
22
+ dblpFetchJson,
23
+ dblpFetchXml,
24
+ extractRecordKey,
25
+ recordXmlToRow,
26
+ requireBoundedInt,
27
+ requireQuery,
28
+ } from './utils.js';
29
+
30
+ const PID_PATTERN = /^[0-9a-z]+(?:\/[0-9a-z-]+)+$/i;
31
+
32
+ function extractPidFromAuthorHit(hit) {
33
+ const url = String(hit?.info?.url ?? '').trim();
34
+ const m = url.match(/\/pid\/([^/]+(?:\/[^/]+)+)$/);
35
+ return m ? m[1] : '';
36
+ }
37
+
38
+ function pickTopAuthor(hits) {
39
+ // dblp returns hits sorted by score desc; we just take the head.
40
+ return hits[0];
41
+ }
42
+
43
+ function splitRecords(xml) {
44
+ // Each publication is wrapped in <r>…</r> directly under <dblpperson>.
45
+ const out = [];
46
+ const re = /<r>\s*([\s\S]*?)\s*<\/r>/g;
47
+ let m;
48
+ while ((m = re.exec(String(xml || ''))) !== null) {
49
+ const inner = m[1];
50
+ // Skip cross-references that have no concrete record (rare).
51
+ if (/^<crossref/.test(inner)) continue;
52
+ out.push(inner);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ cli({
58
+ site: 'dblp',
59
+ name: 'author',
60
+ access: 'read',
61
+ description: 'List dblp publications by a given author (newest first; resolves to top PID match)',
62
+ domain: 'dblp.org',
63
+ strategy: Strategy.PUBLIC,
64
+ browser: false,
65
+ args: [
66
+ { name: 'author', positional: true, required: false, help: 'Author name (e.g. "Yoshua Bengio"). Optional when --pid is given.' },
67
+ { name: 'pid', help: 'Canonical dblp PID (e.g. "56/953"). Bypasses author search.' },
68
+ { name: 'limit', type: 'int', default: 20, help: 'Max publications (1-200)' },
69
+ ],
70
+ columns: ['rank', 'key', 'title', 'authors', 'venue', 'year', 'type', 'doi', 'pid', 'url'],
71
+ func: async (args) => {
72
+ const limit = requireBoundedInt(args.limit, 20, 200);
73
+ const pidArg = args.pid != null ? String(args.pid).trim() : '';
74
+ let pid = '';
75
+ let resolvedName = '';
76
+ if (pidArg) {
77
+ if (!PID_PATTERN.test(pidArg)) {
78
+ throw new ArgumentError(
79
+ `dblp pid "${pidArg}" is not a valid PID`,
80
+ 'Expected something like "56/953" — visit the author page on dblp.org to find it.',
81
+ );
82
+ }
83
+ pid = pidArg;
84
+ }
85
+ else {
86
+ const name = requireQuery(args.author, 'author');
87
+ const json = await dblpFetchJson(
88
+ `/search/author/api?q=${encodeURIComponent(name)}&format=json&h=20`,
89
+ 'dblp author search',
90
+ );
91
+ const raw = json?.result?.hits?.hit;
92
+ const hits = Array.isArray(raw) ? raw : (raw ? [raw] : []);
93
+ if (!hits.length) {
94
+ throw new EmptyResultError(
95
+ 'dblp author',
96
+ `No dblp author matched "${name}". Try a different spelling, or pass --pid to bypass author search.`,
97
+ );
98
+ }
99
+ const top = pickTopAuthor(hits);
100
+ pid = extractPidFromAuthorHit(top);
101
+ if (!pid) {
102
+ throw new CommandExecutionError(
103
+ `dblp author search for "${name}" returned a hit without a PID URL`,
104
+ 'dblp may have changed its author-search response shape; retry or pass --pid manually.',
105
+ );
106
+ }
107
+ resolvedName = decodeXmlEntities(String(top?.info?.author ?? '')).trim();
108
+ }
109
+ const xml = await dblpFetchXml(`/pid/${pid}.xml`, `dblp pid ${pid}`);
110
+ const records = splitRecords(xml);
111
+ if (!records.length) {
112
+ throw new EmptyResultError(
113
+ 'dblp author',
114
+ `dblp PID ${pid}${resolvedName ? ` (${resolvedName})` : ''} has no publications.`,
115
+ );
116
+ }
117
+ return records.slice(0, limit).map((recordXml, i) => {
118
+ const row = recordXmlToRow(`<root>${recordXml}</root>`);
119
+ return {
120
+ rank: i + 1,
121
+ key: row.key || extractRecordKey(recordXml),
122
+ title: row.title,
123
+ authors: row.authors,
124
+ venue: row.venue,
125
+ year: row.year,
126
+ type: row.type,
127
+ doi: row.doi,
128
+ pid,
129
+ url: row.open_access_url || row.dblp_url,
130
+ };
131
+ });
132
+ },
133
+ });
@@ -0,0 +1,64 @@
1
+ // dblp venue — search dblp's venue (conference / journal) registry.
2
+ //
3
+ // Hits `https://dblp.org/search/venue/api?q=…&format=json&h=…`. Returns a
4
+ // row per matched venue. Useful for resolving an acronym (e.g. "ICLR" →
5
+ // dblp's canonical venue page) and for browsing venues that match a topic.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { EmptyResultError } from '@jackwener/opencli/errors';
8
+ import {
9
+ DBLP_ORIGIN,
10
+ decodeXmlEntities,
11
+ dblpFetchJson,
12
+ requireBoundedInt,
13
+ requireQuery,
14
+ } from './utils.js';
15
+
16
+ function simplifyVenueType(type) {
17
+ const t = String(type ?? '').trim();
18
+ if (!t) return '';
19
+ if (/Conference or Workshop/i.test(t)) return 'conf';
20
+ if (/Journal/i.test(t)) return 'journal';
21
+ if (/Series/i.test(t)) return 'series';
22
+ if (/Book/i.test(t)) return 'book';
23
+ if (/Reference/i.test(t)) return 'reference';
24
+ return t.toLowerCase().split(/\s+/)[0];
25
+ }
26
+
27
+ function venueHitToRow(hit, rank) {
28
+ const info = hit?.info ?? {};
29
+ const url = String(info.url ?? '').trim();
30
+ return {
31
+ rank,
32
+ acronym: String(info.acronym ?? '').trim(),
33
+ venue: decodeXmlEntities(info.venue ?? ''),
34
+ type: simplifyVenueType(info.type),
35
+ url: url.startsWith('http') ? url : url ? `${DBLP_ORIGIN}${url.startsWith('/') ? '' : '/'}${url}` : '',
36
+ };
37
+ }
38
+
39
+ cli({
40
+ site: 'dblp',
41
+ name: 'venue',
42
+ access: 'read',
43
+ description: 'Search dblp venue registry (conferences / journals) by name or acronym',
44
+ domain: 'dblp.org',
45
+ strategy: Strategy.PUBLIC,
46
+ browser: false,
47
+ args: [
48
+ { name: 'query', positional: true, required: true, help: 'Venue name or acronym (e.g. "ICLR", "neural networks")' },
49
+ { name: 'limit', type: 'int', default: 20, help: 'Max venues (1-100, single dblp page)' },
50
+ ],
51
+ columns: ['rank', 'acronym', 'venue', 'type', 'url'],
52
+ func: async (args) => {
53
+ const query = requireQuery(args.query);
54
+ const limit = requireBoundedInt(args.limit, 20, 100);
55
+ const path = `/search/venue/api?q=${encodeURIComponent(query)}&format=json&h=${limit}`;
56
+ const json = await dblpFetchJson(path, 'dblp venue');
57
+ const hits = json?.result?.hits?.hit;
58
+ const list = Array.isArray(hits) ? hits : [];
59
+ if (list.length === 0) {
60
+ throw new EmptyResultError('dblp venue', `No dblp venues matched "${query}".`);
61
+ }
62
+ return list.slice(0, limit).map((hit, i) => venueHitToRow(hit, i + 1));
63
+ },
64
+ });
@@ -3,6 +3,7 @@ import { CliError, CommandExecutionError, EXIT_CODES } from '@jackwener/opencli/
3
3
  import {
4
4
  DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature,
5
5
  sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
6
+ pickResumeUrl,
6
7
  } from './utils.js';
7
8
 
8
9
  export const askCommand = cli({
@@ -13,8 +14,8 @@ export const askCommand = cli({
13
14
  domain: DEEPSEEK_DOMAIN,
14
15
  strategy: Strategy.COOKIE,
15
16
  browser: true,
17
+ browserSession: { reuse: 'site' },
16
18
  navigateBefore: false,
17
- timeoutSeconds: 180,
18
19
  args: [
19
20
  { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
20
21
  { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
@@ -38,12 +39,16 @@ export const askCommand = cli({
38
39
  } else {
39
40
  const navigated = await ensureOnDeepSeek(page);
40
41
  if (navigated) {
41
- // Workspace was recycled; try to resume the most recent
42
- // conversation instead of starting a new one.
43
- await page.evaluate(`(() => {
44
- var link = document.querySelector('a[href*="/a/chat/s/"]');
45
- if (link) link.click();
46
- })()`);
42
+ // Pinned conversations sit in their own DOM section and are
43
+ // skipped so the resume never lands on a topped chat.
44
+ const resumeUrl = await pickResumeUrl(page);
45
+ if (!resumeUrl) {
46
+ throw new CommandExecutionError(
47
+ 'Workspace was recycled but no prior conversation could be loaded',
48
+ 'Pass --new to start a fresh chat, or wait for the sidebar to populate before retrying.',
49
+ );
50
+ }
51
+ await page.goto(resumeUrl);
47
52
  await page.wait(2);
48
53
  }
49
54
  }
@@ -11,6 +11,7 @@ const {
11
11
  mockWaitForResponse,
12
12
  mockParseBoolFlag,
13
13
  mockWithRetry,
14
+ mockPickResumeUrl,
14
15
  } = vi.hoisted(() => ({
15
16
  mockEnsureOnDeepSeek: vi.fn(),
16
17
  mockSelectModel: vi.fn(),
@@ -21,6 +22,7 @@ const {
21
22
  mockWaitForResponse: vi.fn(),
22
23
  mockParseBoolFlag: vi.fn((v) => v === true || v === 'true'),
23
24
  mockWithRetry: vi.fn(async (fn) => fn()),
25
+ mockPickResumeUrl: vi.fn(),
24
26
  }));
25
27
 
26
28
  vi.mock('./utils.js', () => ({
@@ -35,6 +37,7 @@ vi.mock('./utils.js', () => ({
35
37
  waitForResponse: mockWaitForResponse,
36
38
  parseBoolFlag: mockParseBoolFlag,
37
39
  withRetry: mockWithRetry,
40
+ pickResumeUrl: mockPickResumeUrl,
38
41
  }));
39
42
 
40
43
  import { askCommand } from './ask.js';
@@ -185,9 +188,8 @@ describe('deepseek ask conversation resume', () => {
185
188
 
186
189
  it('resumes the most recent conversation and skips model selection', async () => {
187
190
  mockEnsureOnDeepSeek.mockResolvedValue(true);
188
- // first evaluate: sidebar resume click (returns undefined)
189
- page.evaluate.mockResolvedValueOnce(undefined);
190
- // second evaluate: URL check (now inside a conversation)
191
+ mockPickResumeUrl.mockResolvedValue('https://chat.deepseek.com/a/chat/s/abc-123');
192
+ // URL check after resume navigation: now inside a conversation.
191
193
  page.evaluate.mockResolvedValueOnce('https://chat.deepseek.com/a/chat/s/abc-123');
192
194
 
193
195
  const rows = await askCommand.func(page, {
@@ -200,6 +202,7 @@ describe('deepseek ask conversation resume', () => {
200
202
  });
201
203
 
202
204
  expect(rows).toEqual([{ response: 'follow-up reply' }]);
205
+ expect(page.goto).toHaveBeenCalledWith('https://chat.deepseek.com/a/chat/s/abc-123');
203
206
  expect(mockSelectModel).not.toHaveBeenCalled();
204
207
  expect(mockSendMessage).toHaveBeenCalled();
205
208
  });
@@ -243,25 +246,22 @@ describe('deepseek ask conversation resume', () => {
243
246
  expect(mockSelectModel).not.toHaveBeenCalled();
244
247
  });
245
248
 
246
- it('still selects model when no conversation to resume', async () => {
249
+ it('fails fast when the workspace was recycled but no conversation surfaces in time', async () => {
247
250
  mockEnsureOnDeepSeek.mockResolvedValue(true);
248
- mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
249
- // first evaluate: sidebar resume click (no link found)
250
- page.evaluate.mockResolvedValueOnce(undefined);
251
- // second evaluate: URL check (still on root page)
252
- page.evaluate.mockResolvedValueOnce('https://chat.deepseek.com/');
251
+ mockPickResumeUrl.mockResolvedValue(null);
253
252
 
254
- const rows = await askCommand.func(page, {
253
+ await expect(askCommand.func(page, {
255
254
  prompt: 'hello',
256
255
  timeout: 120,
257
256
  new: false,
258
257
  model: 'instant',
259
258
  think: false,
260
259
  search: false,
261
- });
260
+ })).rejects.toBeInstanceOf(CommandExecutionError);
262
261
 
263
- expect(rows).toEqual([{ response: 'follow-up reply' }]);
264
- expect(mockSelectModel).toHaveBeenCalled();
262
+ expect(page.goto).not.toHaveBeenCalled();
263
+ expect(mockSelectModel).not.toHaveBeenCalled();
264
+ expect(mockSendMessage).not.toHaveBeenCalled();
265
265
  });
266
266
 
267
267
  it('skips search toggle in vision mode when search is not requested', async () => {
@@ -0,0 +1,38 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { EmptyResultError } from '@jackwener/opencli/errors';
3
+ import {
4
+ DEEPSEEK_DOMAIN,
5
+ ensureOnDeepSeek,
6
+ getVisibleMessages,
7
+ parseDeepSeekConversationId,
8
+ } from './utils.js';
9
+
10
+ export const detailCommand = cli({
11
+ site: 'deepseek',
12
+ name: 'detail',
13
+ access: 'read',
14
+ description: 'Read a specific DeepSeek conversation by ID',
15
+ domain: DEEPSEEK_DOMAIN,
16
+ strategy: Strategy.COOKIE,
17
+ browser: true,
18
+ browserSession: { reuse: 'site' },
19
+ navigateBefore: false,
20
+ args: [
21
+ { name: 'id', required: true, positional: true, help: 'Conversation ID (UUID) or full /a/chat/s/<id> URL' },
22
+ ],
23
+ columns: ['Role', 'Text'],
24
+ func: async (page, kwargs) => {
25
+ const id = parseDeepSeekConversationId(kwargs.id);
26
+ await ensureOnDeepSeek(page);
27
+ await page.goto(`https://chat.deepseek.com/a/chat/s/${id}`);
28
+ await page.wait(5);
29
+ const messages = await getVisibleMessages(page);
30
+ if (messages.length === 0) {
31
+ throw new EmptyResultError(
32
+ 'deepseek detail',
33
+ `No visible messages found for conversation ${id}. Verify the ID is correct and that you are logged in.`,
34
+ );
35
+ }
36
+ return messages;
37
+ },
38
+ });
@@ -0,0 +1,81 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { getRegistry } from '@jackwener/opencli/registry';
4
+
5
+ const {
6
+ mockEnsureOnDeepSeek,
7
+ mockGetVisibleMessages,
8
+ } = vi.hoisted(() => ({
9
+ mockEnsureOnDeepSeek: vi.fn(),
10
+ mockGetVisibleMessages: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('./utils.js', async () => {
14
+ const actual = await vi.importActual('./utils.js');
15
+ return {
16
+ ...actual,
17
+ ensureOnDeepSeek: mockEnsureOnDeepSeek,
18
+ getVisibleMessages: mockGetVisibleMessages,
19
+ };
20
+ });
21
+
22
+ import './detail.js';
23
+
24
+ describe('deepseek detail', () => {
25
+ const command = getRegistry().get('deepseek/detail');
26
+ const id = '749e6bbd-6a45-4440-beaa-ae5238bf06d8';
27
+
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ mockEnsureOnDeepSeek.mockResolvedValue(false);
31
+ });
32
+
33
+ it('registers as a cookie-browser read command', () => {
34
+ expect(command).toBeDefined();
35
+ expect(command.browser).toBe(true);
36
+ expect(command.strategy).toBe('cookie');
37
+ expect(command.access).toBe('read');
38
+ expect(command.columns).toEqual(['Role', 'Text']);
39
+ });
40
+
41
+ it('navigates to the conversation URL and returns visible messages', async () => {
42
+ mockGetVisibleMessages.mockResolvedValue([
43
+ { Role: 'user', Text: 'hello' },
44
+ { Role: 'assistant', Text: 'hi' },
45
+ ]);
46
+ const page = { wait: vi.fn().mockResolvedValue(undefined), goto: vi.fn().mockResolvedValue(undefined) };
47
+
48
+ const rows = await command.func(page, { id });
49
+
50
+ expect(rows).toEqual([
51
+ { Role: 'user', Text: 'hello' },
52
+ { Role: 'assistant', Text: 'hi' },
53
+ ]);
54
+ expect(page.goto).toHaveBeenCalledWith(`https://chat.deepseek.com/a/chat/s/${id}`);
55
+ expect(mockGetVisibleMessages).toHaveBeenCalledWith(page);
56
+ });
57
+
58
+ it('accepts a full chat URL and normalises it before navigation', async () => {
59
+ mockGetVisibleMessages.mockResolvedValue([{ Role: 'user', Text: 'hi' }]);
60
+ const page = { wait: vi.fn().mockResolvedValue(undefined), goto: vi.fn().mockResolvedValue(undefined) };
61
+
62
+ await command.func(page, { id: `https://chat.deepseek.com/a/chat/s/${id.toUpperCase()}?ref=foo` });
63
+
64
+ expect(page.goto).toHaveBeenCalledWith(`https://chat.deepseek.com/a/chat/s/${id}`);
65
+ });
66
+
67
+ it('rejects malformed IDs before browser navigation', async () => {
68
+ const page = { wait: vi.fn(), goto: vi.fn() };
69
+
70
+ await expect(command.func(page, { id: 'not-a-uuid' })).rejects.toThrow(ArgumentError);
71
+ expect(page.goto).not.toHaveBeenCalled();
72
+ expect(mockEnsureOnDeepSeek).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('throws EmptyResultError when the conversation has no visible messages', async () => {
76
+ mockGetVisibleMessages.mockResolvedValue([]);
77
+ const page = { wait: vi.fn().mockResolvedValue(undefined), goto: vi.fn().mockResolvedValue(undefined) };
78
+
79
+ await expect(command.func(page, { id })).rejects.toThrow(EmptyResultError);
80
+ });
81
+ });
@@ -9,6 +9,7 @@ export const historyCommand = cli({
9
9
  domain: DEEPSEEK_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
+ browserSession: { reuse: 'site' },
12
13
  navigateBefore: false,
13
14
  args: [
14
15
  { name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
@@ -9,6 +9,7 @@ export const newCommand = cli({
9
9
  domain: DEEPSEEK_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
+ browserSession: { reuse: 'site' },
12
13
  navigateBefore: false,
13
14
  args: [],
14
15
  columns: ['Status'],
@@ -9,6 +9,7 @@ export const readCommand = cli({
9
9
  domain: DEEPSEEK_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
+ browserSession: { reuse: 'site' },
12
13
  navigateBefore: false,
13
14
  args: [],
14
15
  columns: ['Role', 'Text'],