@jackwener/opencli 1.7.12 → 1.7.14

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 (419) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12128 -6665
  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/quote.js +139 -0
  298. package/clis/twitter/quote.test.js +106 -0
  299. package/clis/twitter/reply-dm.js +1 -1
  300. package/clis/twitter/reply.test.js +1 -29
  301. package/clis/twitter/retweet.js +99 -0
  302. package/clis/twitter/retweet.test.js +69 -0
  303. package/clis/twitter/shared.js +38 -0
  304. package/clis/twitter/shared.test.js +28 -1
  305. package/clis/twitter/unlike.js +87 -0
  306. package/clis/twitter/unlike.test.js +72 -0
  307. package/clis/twitter/unretweet.js +99 -0
  308. package/clis/twitter/unretweet.test.js +69 -0
  309. package/clis/uisdc/news.js +105 -0
  310. package/clis/uisdc/news.test.js +66 -0
  311. package/clis/wanfang/search.js +0 -1
  312. package/clis/web/read.js +47 -17
  313. package/clis/web/read.test.js +101 -1
  314. package/clis/weixin/create-draft.js +1 -1
  315. package/clis/weixin/drafts.js +1 -1
  316. package/clis/weixin/drafts.test.js +5 -1
  317. package/clis/weixin/search.js +157 -0
  318. package/clis/weixin/search.test.js +227 -0
  319. package/clis/wikidata/entity.js +60 -0
  320. package/clis/wikidata/search.js +50 -0
  321. package/clis/wikidata/utils.js +117 -0
  322. package/clis/wikidata/wikidata.test.js +83 -0
  323. package/clis/wikipedia/page.js +95 -0
  324. package/clis/wttr/current.js +63 -0
  325. package/clis/wttr/forecast.js +71 -0
  326. package/clis/wttr/utils.js +50 -0
  327. package/clis/wttr/wttr.test.js +84 -0
  328. package/clis/xianyu/chat.js +16 -4
  329. package/clis/xianyu/chat.test.js +64 -0
  330. package/clis/xianyu/publish.js +485 -0
  331. package/clis/xianyu/publish.test.js +220 -0
  332. package/clis/xiaoe/catalog.js +105 -40
  333. package/clis/xiaoe/content.js +164 -29
  334. package/clis/xiaoe/courses.js +86 -29
  335. package/clis/xiaoe/xiaoe.test.js +486 -0
  336. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  337. package/clis/xiaohongshu/publish.js +16 -3
  338. package/clis/xiaohongshu/publish.test.js +46 -1
  339. package/clis/youtube/transcript.js +13 -19
  340. package/clis/youtube/transcript.test.js +17 -0
  341. package/clis/yuanbao/ask.js +17 -66
  342. package/clis/yuanbao/ask.test.js +5 -5
  343. package/clis/yuanbao/detail.js +65 -0
  344. package/clis/yuanbao/history.js +51 -0
  345. package/clis/yuanbao/new.js +1 -0
  346. package/clis/yuanbao/read.js +38 -0
  347. package/clis/yuanbao/send.js +57 -0
  348. package/clis/yuanbao/shared.js +297 -5
  349. package/clis/yuanbao/shared.test.js +80 -0
  350. package/clis/yuanbao/status.js +44 -0
  351. package/clis/zlibrary/commands.test.js +1 -11
  352. package/dist/src/browser/base-page.d.ts +9 -0
  353. package/dist/src/browser/base-page.js +44 -1
  354. package/dist/src/browser/base-page.test.js +66 -0
  355. package/dist/src/browser/bridge.js +47 -45
  356. package/dist/src/browser/cdp.d.ts +1 -0
  357. package/dist/src/browser/cdp.js +51 -9
  358. package/dist/src/browser/daemon-client.d.ts +4 -0
  359. package/dist/src/browser/errors.js +1 -1
  360. package/dist/src/browser/page.d.ts +1 -1
  361. package/dist/src/browser/page.js +3 -1
  362. package/dist/src/browser/page.test.js +29 -0
  363. package/dist/src/browser/target-errors.d.ts +2 -1
  364. package/dist/src/browser/target-errors.js +1 -0
  365. package/dist/src/browser/target-resolver.d.ts +25 -0
  366. package/dist/src/browser/target-resolver.js +43 -0
  367. package/dist/src/browser.test.js +18 -0
  368. package/dist/src/build-manifest.js +9 -4
  369. package/dist/src/build-manifest.test.js +2 -8
  370. package/dist/src/capabilityRouting.d.ts +16 -1
  371. package/dist/src/capabilityRouting.js +24 -1
  372. package/dist/src/capabilityRouting.test.js +19 -1
  373. package/dist/src/cli.js +76 -11
  374. package/dist/src/cli.test.js +241 -1
  375. package/dist/src/commanderAdapter.js +23 -9
  376. package/dist/src/commanderAdapter.test.js +0 -1
  377. package/dist/src/discovery.js +2 -5
  378. package/dist/src/errors.js +1 -1
  379. package/dist/src/execution.d.ts +1 -1
  380. package/dist/src/execution.js +111 -27
  381. package/dist/src/execution.test.js +326 -17
  382. package/dist/src/help.d.ts +27 -2
  383. package/dist/src/help.js +196 -23
  384. package/dist/src/help.test.d.ts +1 -0
  385. package/dist/src/help.test.js +54 -0
  386. package/dist/src/main.js +14 -1
  387. package/dist/src/manifest-types.d.ts +5 -3
  388. package/dist/src/pipeline/executor.js +1 -1
  389. package/dist/src/pipeline/executor.test.js +8 -0
  390. package/dist/src/pipeline/registry.d.ts +9 -0
  391. package/dist/src/pipeline/registry.js +13 -1
  392. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  393. package/dist/src/pipeline/steps/browser.js +10 -0
  394. package/dist/src/pipeline/steps/download.test.js +1 -0
  395. package/dist/src/registry-api.d.ts +1 -1
  396. package/dist/src/registry.d.ts +12 -11
  397. package/dist/src/registry.js +16 -6
  398. package/dist/src/registry.test.js +2 -2
  399. package/dist/src/runtime.d.ts +2 -1
  400. package/dist/src/runtime.js +1 -1
  401. package/dist/src/serialization.d.ts +2 -2
  402. package/dist/src/serialization.js +4 -6
  403. package/dist/src/serialization.test.js +17 -0
  404. package/dist/src/types.d.ts +17 -0
  405. package/dist/src/validate.js +15 -11
  406. package/dist/src/validate.test.d.ts +9 -0
  407. package/dist/src/validate.test.js +90 -0
  408. package/package.json +1 -1
  409. package/scripts/fetch-adapters.js +1 -1
  410. package/scripts/typed-error-lint-baseline.json +5 -77
  411. package/clis/ctrip/search.test.js +0 -64
  412. package/clis/gov-policy/commands.test.js +0 -27
  413. package/clis/linux-do/category.js +0 -37
  414. package/clis/linux-do/hot.js +0 -26
  415. package/clis/linux-do/latest.js +0 -19
  416. package/clis/pixiv/test-utils.js +0 -23
  417. package/clis/toutiao/articles.test.js +0 -30
  418. package/dist/src/analysis.d.ts +0 -40
  419. package/dist/src/analysis.js +0 -172
@@ -0,0 +1,85 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
4
+ import './user.js';
5
+ import './top.js';
6
+
7
+ afterEach(() => {
8
+ vi.unstubAllGlobals();
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ describe('lichess user adapter', () => {
13
+ const cmd = getRegistry().get('lichess/user');
14
+
15
+ it('rejects bad usernames before fetching', async () => {
16
+ const fetchMock = vi.fn();
17
+ vi.stubGlobal('fetch', fetchMock);
18
+ await expect(cmd.func({ username: '' })).rejects.toThrow(ArgumentError);
19
+ await expect(cmd.func({ username: 'a' })).rejects.toThrow(ArgumentError); // too short
20
+ await expect(cmd.func({ username: 'has space' })).rejects.toThrow(ArgumentError);
21
+ expect(fetchMock).not.toHaveBeenCalled();
22
+ });
23
+
24
+ it('maps HTTP 429 to CommandExecutionError', async () => {
25
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('rate limited', { status: 429 })));
26
+ await expect(cmd.func({ username: 'somebody' })).rejects.toThrow(CommandExecutionError);
27
+ });
28
+
29
+ it('treats disabled accounts as EmptyResultError (not row of nulls)', async () => {
30
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
31
+ id: 'closed-user', username: 'ClosedUser', disabled: true,
32
+ }), { status: 200 })));
33
+ await expect(cmd.func({ username: 'ClosedUser' })).rejects.toThrow(EmptyResultError);
34
+ });
35
+
36
+ it('picks the most-played perf as topPerf', async () => {
37
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
38
+ id: 'someplayer',
39
+ username: 'SomePlayer',
40
+ createdAt: 1543000000000,
41
+ seenAt: 1700000000000,
42
+ count: { all: 100, win: 50, loss: 30, draw: 20 },
43
+ perfs: {
44
+ bullet: { games: 9000, rating: 2700 },
45
+ blitz: { games: 100, rating: 2200 },
46
+ puzzle: { games: 99999, rating: 2900 }, // ignored — not playable
47
+ },
48
+ }), { status: 200 })));
49
+ const rows = await cmd.func({ username: 'SomePlayer' });
50
+ expect(rows[0]).toMatchObject({
51
+ username: 'SomePlayer', id: 'someplayer',
52
+ gamesAll: 100, topPerfName: 'bullet', topPerfRating: 2700, topPerfGames: 9000,
53
+ url: 'https://lichess.org/@/SomePlayer',
54
+ });
55
+ });
56
+ });
57
+
58
+ describe('lichess top adapter', () => {
59
+ const cmd = getRegistry().get('lichess/top');
60
+
61
+ it('rejects unknown perf types before fetching', async () => {
62
+ const fetchMock = vi.fn();
63
+ vi.stubGlobal('fetch', fetchMock);
64
+ await expect(cmd.func({ perf: '' })).rejects.toThrow(ArgumentError);
65
+ await expect(cmd.func({ perf: 'turbo' })).rejects.toThrow(ArgumentError);
66
+ await expect(cmd.func({ perf: 'blitz', limit: 9999 })).rejects.toThrow(ArgumentError);
67
+ expect(fetchMock).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it('throws EmptyResultError on empty leaderboard', async () => {
71
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({ users: [] }), { status: 200 })));
72
+ await expect(cmd.func({ perf: 'blitz', limit: 5 })).rejects.toThrow(EmptyResultError);
73
+ });
74
+
75
+ it('round-trips username from leaderboard into perf-specific URL', async () => {
76
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
77
+ users: [{ id: 'magnus', username: 'Magnus', title: 'GM', perfs: { blitz: { rating: 3001, progress: 7 } } }],
78
+ }), { status: 200 })));
79
+ const rows = await cmd.func({ perf: 'blitz', limit: 3 });
80
+ expect(rows[0]).toMatchObject({
81
+ rank: 1, username: 'Magnus', title: 'GM', rating: 3001, progress: 7,
82
+ url: 'https://lichess.org/@/Magnus/perf/blitz',
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,46 @@
1
+ // lichess top — top-N leaderboard for a given perf type.
2
+ //
3
+ // Hits `/api/player/top/<n>/<perf>`. Returns the leaderboard rows; usernames
4
+ // round-trip into `lichess user` for full profile detail.
5
+ import { cli, Strategy } from '@jackwener/opencli/registry';
6
+ import { EmptyResultError } from '@jackwener/opencli/errors';
7
+ import { LICHESS_BASE, lichessFetch, requireBoundedInt, requirePerf } from './utils.js';
8
+
9
+ cli({
10
+ site: 'lichess',
11
+ name: 'top',
12
+ access: 'read',
13
+ description: 'Top-N Lichess leaderboard for a perf type (bullet/blitz/rapid/classical/...)',
14
+ domain: 'lichess.org',
15
+ strategy: Strategy.PUBLIC,
16
+ browser: false,
17
+ args: [
18
+ { name: 'perf', positional: true, required: true, help: 'Perf type (bullet, blitz, rapid, classical, ultraBullet, chess960, ...)' },
19
+ { name: 'limit', type: 'int', default: 10, help: 'Top-N rows (1-200)' },
20
+ ],
21
+ columns: ['rank', 'username', 'id', 'title', 'rating', 'progress', 'patron', 'url'],
22
+ func: async (args) => {
23
+ const perf = requirePerf(args.perf);
24
+ const limit = requireBoundedInt(args.limit, 10, 200);
25
+ const url = `${LICHESS_BASE}/api/player/top/${limit}/${encodeURIComponent(perf)}`;
26
+ const body = await lichessFetch(url, 'lichess top');
27
+ const list = Array.isArray(body?.users) ? body.users : [];
28
+ if (!list.length) {
29
+ throw new EmptyResultError('lichess top', `Lichess returned no leaderboard rows for perf "${perf}".`);
30
+ }
31
+ return list.slice(0, limit).map((u, i) => {
32
+ const username = typeof u?.username === 'string' ? u.username : '';
33
+ const perfBlock = u?.perfs && typeof u.perfs === 'object' ? u.perfs[perf] ?? {} : {};
34
+ return {
35
+ rank: i + 1,
36
+ username,
37
+ id: typeof u?.id === 'string' ? u.id : null,
38
+ title: typeof u?.title === 'string' ? u.title : null,
39
+ rating: typeof perfBlock.rating === 'number' ? perfBlock.rating : null,
40
+ progress: typeof perfBlock.progress === 'number' ? perfBlock.progress : null,
41
+ patron: u?.patron === true,
42
+ url: username ? `${LICHESS_BASE}/@/${encodeURIComponent(username)}/perf/${encodeURIComponent(perf)}` : '',
43
+ };
44
+ });
45
+ },
46
+ });
@@ -0,0 +1,91 @@
1
+ // lichess user — fetch a public Lichess player profile.
2
+ //
3
+ // Hits `/api/user/<username>`. Returns the agent-useful slice: handle, title,
4
+ // flags (online / patron), counts, top-rated perf, profile bio.
5
+ import { cli, Strategy } from '@jackwener/opencli/registry';
6
+ import { EmptyResultError } from '@jackwener/opencli/errors';
7
+ import { LICHESS_BASE, formatTimestamp, lichessFetch, requireUsername } from './utils.js';
8
+
9
+ cli({
10
+ site: 'lichess',
11
+ name: 'user',
12
+ access: 'read',
13
+ description: 'Fetch a Lichess player profile by username (rating, perfs, counts)',
14
+ domain: 'lichess.org',
15
+ strategy: Strategy.PUBLIC,
16
+ browser: false,
17
+ args: [
18
+ { name: 'username', positional: true, required: true, help: 'Lichess username (case-insensitive)' },
19
+ ],
20
+ columns: [
21
+ 'username',
22
+ 'id',
23
+ 'title',
24
+ 'patron',
25
+ 'online',
26
+ 'tosViolation',
27
+ 'createdAt',
28
+ 'seenAt',
29
+ 'gamesAll',
30
+ 'gamesWin',
31
+ 'gamesLoss',
32
+ 'gamesDraw',
33
+ 'topPerfName',
34
+ 'topPerfRating',
35
+ 'topPerfGames',
36
+ 'fideRating',
37
+ 'country',
38
+ 'bio',
39
+ 'url',
40
+ ],
41
+ func: async (args) => {
42
+ const username = requireUsername(args.username);
43
+ const url = `${LICHESS_BASE}/api/user/${encodeURIComponent(username)}`;
44
+ const body = await lichessFetch(url, 'lichess user');
45
+ if (!body || typeof body !== 'object') {
46
+ throw new EmptyResultError('lichess user', `Lichess user "${username}" returned empty payload.`);
47
+ }
48
+ // Lichess marks closed accounts with `disabled: true` and strips data.
49
+ // Surface as EmptyResultError instead of a row of nulls (silent-fallback).
50
+ if (body.disabled === true) {
51
+ throw new EmptyResultError('lichess user', `Lichess user "${username}" is closed/disabled.`);
52
+ }
53
+ const perfs = body.perfs && typeof body.perfs === 'object' ? body.perfs : {};
54
+ // Pick the perf with the most games (excluding puzzle/storm/racer ephemera).
55
+ const playablePerfs = Object.entries(perfs).filter(([k, v]) => v && typeof v === 'object' && !['puzzle', 'storm', 'racer', 'streak'].includes(k));
56
+ let topPerfName = null;
57
+ let topPerfRating = null;
58
+ let topPerfGames = null;
59
+ for (const [name, p] of playablePerfs) {
60
+ const games = typeof p.games === 'number' ? p.games : 0;
61
+ if (topPerfGames == null || games > topPerfGames) {
62
+ topPerfName = name;
63
+ topPerfGames = games;
64
+ topPerfRating = typeof p.rating === 'number' ? p.rating : null;
65
+ }
66
+ }
67
+ const counts = body.count && typeof body.count === 'object' ? body.count : {};
68
+ const profile = body.profile && typeof body.profile === 'object' ? body.profile : {};
69
+ return [{
70
+ username: typeof body.username === 'string' ? body.username : username,
71
+ id: typeof body.id === 'string' ? body.id : null,
72
+ title: typeof body.title === 'string' ? body.title : null,
73
+ patron: body.patron === true,
74
+ online: body.online === true,
75
+ tosViolation: body.tosViolation === true,
76
+ createdAt: formatTimestamp(body.createdAt),
77
+ seenAt: formatTimestamp(body.seenAt),
78
+ gamesAll: typeof counts.all === 'number' ? counts.all : null,
79
+ gamesWin: typeof counts.win === 'number' ? counts.win : null,
80
+ gamesLoss: typeof counts.loss === 'number' ? counts.loss : null,
81
+ gamesDraw: typeof counts.draw === 'number' ? counts.draw : null,
82
+ topPerfName,
83
+ topPerfRating,
84
+ topPerfGames,
85
+ fideRating: typeof profile.fideRating === 'number' ? profile.fideRating : null,
86
+ country: typeof profile.country === 'string' ? profile.country : null,
87
+ bio: typeof profile.bio === 'string' ? profile.bio.trim() : null,
88
+ url: `${LICHESS_BASE}/@/${encodeURIComponent(typeof body.username === 'string' ? body.username : username)}`,
89
+ }];
90
+ },
91
+ });
@@ -0,0 +1,97 @@
1
+ // Shared helpers for the lichess.org public REST adapters.
2
+ //
3
+ // Lichess exposes a generous unauthenticated API at `lichess.org/api`. We keep
4
+ // the surface narrow: `user` (profile) + `top` (per-perf top-N leaderboard).
5
+ // No API key required; rate limit is 60 req/min per IP.
6
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
7
+
8
+ export const LICHESS_BASE = 'https://lichess.org';
9
+ const UA = 'opencli-lichess-adapter/1.0 (+https://github.com/jackwener/opencli; mailto:opencli@example.com)';
10
+
11
+ // Lichess usernames are 2-30 chars: letters, digits, underscore, dash. Case-insensitive.
12
+ const USERNAME_PATTERN = /^[A-Za-z0-9_-]{2,30}$/;
13
+
14
+ // `perfType` values lichess accepts for the `/api/player/top/<n>/<perf>` endpoint.
15
+ // Source: lichess-org/api docs.
16
+ export const LICHESS_PERFS = new Set([
17
+ 'ultraBullet', 'bullet', 'blitz', 'rapid', 'classical',
18
+ 'chess960', 'crazyhouse', 'antichess', 'atomic', 'horde',
19
+ 'kingOfTheHill', 'racingKings', 'threeCheck',
20
+ ]);
21
+
22
+ export function requireUsername(value) {
23
+ const raw = String(value ?? '').trim();
24
+ if (!raw) throw new ArgumentError('lichess username is required');
25
+ if (!USERNAME_PATTERN.test(raw)) {
26
+ throw new ArgumentError(
27
+ `lichess username "${value}" is not a valid handle`,
28
+ 'Allowed: letters, digits, underscore, dash; length 2-30.',
29
+ );
30
+ }
31
+ return raw;
32
+ }
33
+
34
+ export function requirePerf(value) {
35
+ const raw = String(value ?? '').trim();
36
+ if (!raw) throw new ArgumentError('lichess perf is required (e.g. "blitz", "bullet", "rapid")');
37
+ if (!LICHESS_PERFS.has(raw)) {
38
+ throw new ArgumentError(
39
+ `lichess perf "${value}" is not recognised`,
40
+ `Allowed values: ${[...LICHESS_PERFS].join(', ')}.`,
41
+ );
42
+ }
43
+ return raw;
44
+ }
45
+
46
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
47
+ const raw = value ?? defaultValue;
48
+ const n = typeof raw === 'number' ? raw : Number(raw);
49
+ if (!Number.isInteger(n) || n <= 0) {
50
+ throw new ArgumentError(`lichess ${label} must be a positive integer`);
51
+ }
52
+ if (n > maxValue) {
53
+ throw new ArgumentError(`lichess ${label} must be <= ${maxValue}`);
54
+ }
55
+ return n;
56
+ }
57
+
58
+ export async function lichessFetch(url, label) {
59
+ let resp;
60
+ try {
61
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
62
+ }
63
+ catch (err) {
64
+ throw new CommandExecutionError(
65
+ `${label} request failed: ${err?.message ?? err}`,
66
+ 'Check that lichess.org is reachable from this network.',
67
+ );
68
+ }
69
+ if (resp.status === 404) {
70
+ throw new EmptyResultError(label, `Lichess returned 404 for ${url}.`);
71
+ }
72
+ if (resp.status === 429) {
73
+ throw new CommandExecutionError(
74
+ `${label} returned HTTP 429 (rate limited)`,
75
+ 'Lichess throttles anonymous traffic at ~60 req/min; back off and retry.',
76
+ );
77
+ }
78
+ if (!resp.ok) {
79
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
80
+ }
81
+ let body;
82
+ try {
83
+ body = await resp.json();
84
+ }
85
+ catch (err) {
86
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
87
+ }
88
+ return body;
89
+ }
90
+
91
+ /** Format a lichess unix-ms timestamp as ISO date (YYYY-MM-DD). `null` when missing. */
92
+ export function formatTimestamp(ms) {
93
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) return null;
94
+ const d = new Date(ms);
95
+ if (Number.isNaN(d.getTime())) return null;
96
+ return d.toISOString();
97
+ }
@@ -1,5 +1,9 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
3
+ const LINKEDIN_DOMAIN = 'linkedin.com';
4
+ const MIN_LIMIT = 1;
5
+ const MAX_LIMIT = 100;
6
+ const MIN_START = 0;
3
7
  // ── Filter value mappings ──────────────────────────────────────────────
4
8
  const EXPERIENCE_LEVELS = {
5
9
  internship: '1',
@@ -66,6 +70,19 @@ function mapFilterValues(input, mapping, label) {
66
70
  function normalizeWhitespace(value) {
67
71
  return String(value ?? '').replace(/\s+/g, ' ').trim();
68
72
  }
73
+ function parseIntegerArg(value, label, fallback, min, max = Infinity) {
74
+ if (value === undefined || value === null || value === '')
75
+ return fallback;
76
+ const parsed = Number(value);
77
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
78
+ throw new ArgumentError(`${label} must be an integer, got ${JSON.stringify(value)}`);
79
+ }
80
+ if (parsed < min || parsed > max) {
81
+ const range = Number.isFinite(max) ? `between ${min} and ${max}` : `at least ${min}`;
82
+ throw new ArgumentError(`${label} must be ${range}, got ${parsed}`);
83
+ }
84
+ return parsed;
85
+ }
69
86
  function decodeLinkedinRedirect(url) {
70
87
  if (!url)
71
88
  return '';
@@ -120,6 +137,31 @@ function buildVoyagerUrl(input, offset, count) {
120
137
  .replace(/%29/gi, ')');
121
138
  return '/voyager/api/voyagerJobsDashJobCards?' + params.toString() + '&query=' + query + '&start=' + offset;
122
139
  }
140
+ function looksLinkedInAuthWallText(value) {
141
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
142
+ if (!text)
143
+ return false;
144
+ return /\b(sign in|log in|join linkedin)\b/.test(text) ||
145
+ /linkedin\.com\/(login|checkpoint|authwall)/i.test(text) ||
146
+ /\b(captcha|verification required)\b/.test(text) ||
147
+ /(请登录|登录领英|安全验证)/.test(text);
148
+ }
149
+ function buildLinkedInAuthProbeScript() {
150
+ return `(() => {
151
+ const text = [
152
+ window.location.href || '',
153
+ document.title || '',
154
+ document.body ? (document.body.innerText || '').slice(0, 4000) : '',
155
+ ].join('\\n');
156
+ return ${looksLinkedInAuthWallText.toString()}(text);
157
+ })()`;
158
+ }
159
+ async function assertLinkedInAuthenticated(page, context) {
160
+ const authRequired = await page.evaluate(buildLinkedInAuthProbeScript());
161
+ if (authRequired) {
162
+ throw new AuthRequiredError(LINKEDIN_DOMAIN, `${context} requires an active signed-in LinkedIn browser session`);
163
+ }
164
+ }
123
165
  // ── Company ID resolution (requires DOM interaction) ──────────────────
124
166
  async function resolveCompanyIds(page, input) {
125
167
  const rawValues = parseCsvArg(input);
@@ -209,13 +251,25 @@ async function fetchJobCards(page, input) {
209
251
  const batch = await page.evaluate(`(async () => {
210
252
  const jsession = document.cookie.split(';').map(p => p.trim())
211
253
  .find(p => p.startsWith('JSESSIONID='))?.slice('JSESSIONID='.length);
212
- if (!jsession) return { error: 'LinkedIn JSESSIONID cookie not found. Please sign in to LinkedIn in the browser.' };
254
+ if (!jsession) {
255
+ return {
256
+ authRequired: true,
257
+ error: 'LinkedIn JSESSIONID cookie not found. Please sign in to LinkedIn in the browser.'
258
+ };
259
+ }
213
260
 
214
261
  const csrf = jsession.replace(/^"|"$/g, '');
215
262
  const res = await fetch(${JSON.stringify(apiPath)}, {
216
263
  credentials: 'include',
217
264
  headers: { 'csrf-token': csrf, 'x-restli-protocol-version': '2.0.0' },
218
265
  });
266
+ if (res.status === 401 || res.status === 403) {
267
+ const text = await res.text();
268
+ return {
269
+ authRequired: true,
270
+ error: 'LinkedIn API authentication failed: HTTP ' + res.status + ' ' + text.slice(0, 200)
271
+ };
272
+ }
219
273
  if (!res.ok) {
220
274
  const text = await res.text();
221
275
  return { error: 'LinkedIn API error: HTTP ' + res.status + ' ' + text.slice(0, 200) };
@@ -223,6 +277,9 @@ async function fetchJobCards(page, input) {
223
277
  return res.json();
224
278
  })()`);
225
279
  if (!batch || batch.error) {
280
+ if (batch?.authRequired) {
281
+ throw new AuthRequiredError(LINKEDIN_DOMAIN, batch.error);
282
+ }
226
283
  throw new CommandExecutionError(batch?.error || 'LinkedIn search returned an unexpected response');
227
284
  }
228
285
  const elements = Array.isArray(batch?.elements) ? batch.elements : [];
@@ -259,17 +316,31 @@ async function fetchJobCards(page, input) {
259
316
  }));
260
317
  }
261
318
  // ── Job detail enrichment (--details flag) ────────────────────────────
319
+ //
320
+ // Per-row failures should NOT abort the whole list (--details enriches N rows;
321
+ // partial failure is expected). But silent empty-string fields hide the failure
322
+ // from callers — previously `catch {}` and the `if (!job.url)` early-return
323
+ // both produced indistinguishable `description: '', apply_url: ''` payloads,
324
+ // so users could not tell "fetch failed" from "upstream had no description".
325
+ //
326
+ // The fix: surface `null` instead of `''` for missing/failed rows, set
327
+ // `detail_error` to a short reason ("no url" / "fetch failed: <message>" /
328
+ // "missing description"), and log every failure to stderr with the offending
329
+ // URL so debugging is possible. Successful rows have `detail_error: null`.
262
330
  async function enrichJobDetails(page, jobs) {
263
331
  const enriched = [];
264
332
  for (let i = 0; i < jobs.length; i++) {
265
333
  const job = jobs[i];
266
334
  console.error(`[opencli:linkedin] Fetching details ${i + 1}/${jobs.length}: ${job.title}`);
267
335
  if (!job.url) {
268
- enriched.push({ ...job, description: '', apply_url: '' });
336
+ const reason = 'no url';
337
+ console.error(`[opencli:linkedin] Skipping detail for "${job.title}": ${reason}`);
338
+ enriched.push({ ...job, description: null, apply_url: null, detail_error: reason });
269
339
  continue;
270
340
  }
271
341
  try {
272
342
  await page.goto(job.url);
343
+ await assertLinkedInAuthenticated(page, 'LinkedIn job detail');
273
344
  await page.wait({ text: 'About the job', timeout: 8 });
274
345
  // Expand "Show more" button if present
275
346
  await page.evaluate(`(() => {
@@ -301,14 +372,25 @@ async function enrichJobDetails(page, jobs) {
301
372
 
302
373
  return { description, applyUrl: applyLink?.href || '' };
303
374
  })()`);
375
+ const description = normalizeWhitespace(detail?.description);
376
+ const apply_url = decodeLinkedinRedirect(String(detail?.applyUrl ?? ''));
377
+ // Empty description after a successful fetch is itself a
378
+ // recognizable signal — surface it via detail_error instead of
379
+ // silently emitting an empty string.
380
+ const detail_error = description ? null : 'missing description';
304
381
  enriched.push({
305
382
  ...job,
306
- description: normalizeWhitespace(detail?.description),
307
- apply_url: decodeLinkedinRedirect(String(detail?.applyUrl ?? '')),
383
+ description: description || null,
384
+ apply_url: apply_url || null,
385
+ detail_error,
308
386
  });
309
387
  }
310
- catch {
311
- enriched.push({ ...job, description: '', apply_url: '' });
388
+ catch (err) {
389
+ if (err instanceof AuthRequiredError)
390
+ throw err;
391
+ const reason = `fetch failed: ${err?.message || err}`;
392
+ console.error(`[opencli:linkedin] Detail fetch failed for ${job.url}: ${reason}`);
393
+ enriched.push({ ...job, description: null, apply_url: null, detail_error: reason });
312
394
  }
313
395
  }
314
396
  return enriched;
@@ -320,7 +402,7 @@ cli({
320
402
  access: 'read',
321
403
  description: 'Search LinkedIn jobs',
322
404
  domain: 'www.linkedin.com',
323
- strategy: Strategy.HEADER,
405
+ strategy: Strategy.COOKIE,
324
406
  browser: true,
325
407
  args: [
326
408
  { name: 'query', type: 'string', required: true, positional: true, help: 'Job search keywords' },
@@ -336,8 +418,8 @@ cli({
336
418
  ],
337
419
  columns: ['rank', 'title', 'company', 'location', 'listed', 'salary', 'url'],
338
420
  func: async (page, kwargs) => {
339
- const limit = Math.max(1, Math.min(kwargs.limit ?? 10, 100));
340
- const start = Math.max(0, kwargs.start ?? 0);
421
+ const limit = parseIntegerArg(kwargs.limit, '--limit', 10, MIN_LIMIT, MAX_LIMIT);
422
+ const start = parseIntegerArg(kwargs.start, '--start', 0, MIN_START);
341
423
  const includeDetails = Boolean(kwargs.details);
342
424
  const location = (kwargs.location ?? '').trim();
343
425
  const keywords = String(kwargs.query ?? '').trim();
@@ -347,6 +429,7 @@ cli({
347
429
  if (location)
348
430
  searchParams.set('location', location);
349
431
  await page.goto(`https://www.linkedin.com/jobs/search/?${searchParams.toString()}`);
432
+ await assertLinkedInAuthenticated(page, 'LinkedIn search');
350
433
  await page.wait({ text: 'Jobs', timeout: 10 });
351
434
  const companyIds = await resolveCompanyIds(page, kwargs.company);
352
435
  const input = {
@@ -366,3 +449,17 @@ cli({
366
449
  return enrichJobDetails(page, data);
367
450
  },
368
451
  });
452
+
453
+ export const __test__ = {
454
+ parseCsvArg,
455
+ parseIntegerArg,
456
+ mapFilterValues,
457
+ decodeLinkedinRedirect,
458
+ looksLinkedInAuthWallText,
459
+ assertLinkedInAuthenticated,
460
+ enrichJobDetails,
461
+ EXPERIENCE_LEVELS,
462
+ JOB_TYPES,
463
+ DATE_POSTED,
464
+ REMOTE_TYPES,
465
+ };