@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,55 @@
1
+ // endoflife product — release cycles + EOL / support dates for one product.
2
+ //
3
+ // Hits `https://endoflife.date/api/<product>.json`. Returns one row per cycle
4
+ // (newest first), with the latest version, release / EOL / LTS / support dates,
5
+ // and an `eolStatus` projection (`active` / `eol` / `ongoing`) so agents can
6
+ // answer "is this version still supported" without parsing dates themselves.
7
+ import { cli, Strategy } from '@jackwener/opencli/registry';
8
+ import { EmptyResultError } from '@jackwener/opencli/errors';
9
+ import { EOL_BASE, eolFetch, normaliseDateOrFlag, requireProduct } from './utils.js';
10
+
11
+ cli({
12
+ site: 'endoflife',
13
+ name: 'product',
14
+ access: 'read',
15
+ description: 'Release cycles + EOL / LTS / support dates for one product on endoflife.date',
16
+ domain: 'endoflife.date',
17
+ strategy: Strategy.PUBLIC,
18
+ browser: false,
19
+ args: [
20
+ { name: 'product', positional: true, type: 'string', required: true, help: 'endoflife.date product slug (e.g. "nodejs", "python", "ubuntu")' },
21
+ ],
22
+ columns: [
23
+ 'product', 'cycle', 'releaseDate', 'latest', 'latestReleaseDate',
24
+ 'lts', 'support', 'eol', 'extendedSupport', 'eolStatus', 'url',
25
+ ],
26
+ func: async (args) => {
27
+ const product = requireProduct(args.product);
28
+ const cycles = await eolFetch(`${EOL_BASE}/${encodeURIComponent(product)}.json`, `endoflife product ${product}`);
29
+ if (!Array.isArray(cycles) || cycles.length === 0) {
30
+ throw new EmptyResultError('endoflife product', `endoflife.date returned no cycles for "${product}".`);
31
+ }
32
+ const today = new Date().toISOString().slice(0, 10);
33
+ return cycles.map((c) => {
34
+ const eol = normaliseDateOrFlag(c?.eol);
35
+ // eolStatus projection — best-effort, derived from eol vs today (not from a remote field).
36
+ let eolStatus = null;
37
+ if (eol === 'ongoing') eolStatus = 'ongoing';
38
+ else if (typeof eol === 'string' && eol >= today) eolStatus = 'active';
39
+ else if (typeof eol === 'string') eolStatus = 'eol';
40
+ return {
41
+ product,
42
+ cycle: String(c?.cycle ?? '').trim(),
43
+ releaseDate: typeof c?.releaseDate === 'string' ? c.releaseDate : null,
44
+ latest: String(c?.latest ?? '').trim(),
45
+ latestReleaseDate: typeof c?.latestReleaseDate === 'string' ? c.latestReleaseDate : null,
46
+ lts: normaliseDateOrFlag(c?.lts),
47
+ support: normaliseDateOrFlag(c?.support),
48
+ eol,
49
+ extendedSupport: normaliseDateOrFlag(c?.extendedSupport),
50
+ eolStatus,
51
+ url: `https://endoflife.date/${product}`,
52
+ };
53
+ });
54
+ },
55
+ });
@@ -0,0 +1,89 @@
1
+ // Shared helpers for the endoflife.date adapters.
2
+ //
3
+ // endoflife.date publishes a free, unauthenticated REST API with cycle / EOL /
4
+ // LTS data for hundreds of products. Docs: https://endoflife.date/docs/api/
5
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
6
+
7
+ export const EOL_BASE = 'https://endoflife.date/api';
8
+ const UA = 'opencli-endoflife-adapter (+https://github.com/jackwener/opencli)';
9
+
10
+ // endoflife.date product slugs are lowercase ascii + digits + dashes / dots, up to 80 chars.
11
+ const PRODUCT = /^[a-z0-9][a-z0-9._-]{0,79}$/;
12
+
13
+ export function requireProduct(value) {
14
+ const s = String(value ?? '').trim().toLowerCase();
15
+ if (!s) {
16
+ throw new ArgumentError(
17
+ 'endoflife product is required (e.g. "nodejs", "python", "ubuntu")',
18
+ 'Use the slug visible at https://endoflife.date/<product>.',
19
+ );
20
+ }
21
+ if (!PRODUCT.test(s)) {
22
+ throw new ArgumentError(
23
+ `endoflife product "${value}" is not a valid endoflife.date slug`,
24
+ 'Slugs are lowercase ASCII letters/digits/"._-", e.g. "nodejs", "python", "ubuntu".',
25
+ );
26
+ }
27
+ return s;
28
+ }
29
+
30
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
31
+ const raw = value ?? defaultValue;
32
+ const n = typeof raw === 'number' ? raw : Number(raw);
33
+ if (!Number.isInteger(n) || n <= 0) {
34
+ throw new ArgumentError(`endoflife ${label} must be a positive integer`);
35
+ }
36
+ if (n > maxValue) {
37
+ throw new ArgumentError(`endoflife ${label} must be <= ${maxValue}`);
38
+ }
39
+ return n;
40
+ }
41
+
42
+ export async function eolFetch(url, label) {
43
+ let resp;
44
+ try {
45
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
46
+ }
47
+ catch (err) {
48
+ throw new CommandExecutionError(
49
+ `${label} request failed: ${err?.message ?? err}`,
50
+ 'Check that endoflife.date is reachable from this network.',
51
+ );
52
+ }
53
+ if (resp.status === 404) {
54
+ throw new EmptyResultError(label, `endoflife.date returned 404 for ${url}.`);
55
+ }
56
+ if (resp.status === 429) {
57
+ throw new CommandExecutionError(
58
+ `${label} returned HTTP 429 (rate limited)`,
59
+ 'endoflife.date throttles unauthenticated traffic; wait a few seconds and retry.',
60
+ );
61
+ }
62
+ if (!resp.ok) {
63
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
64
+ }
65
+ let body;
66
+ try {
67
+ body = await resp.json();
68
+ }
69
+ catch (err) {
70
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
71
+ }
72
+ return body;
73
+ }
74
+
75
+ // endoflife.date returns scalar fields that are either an ISO date "YYYY-MM-DD",
76
+ // a boolean (true = supported / ongoing, false = not LTS), or null. Normalise:
77
+ // - boolean true -> the literal string "ongoing"
78
+ // - boolean false -> null (matches "no LTS phase" / "not in extended support")
79
+ // - date string -> as-is
80
+ // - anything else -> null
81
+ export function normaliseDateOrFlag(value) {
82
+ if (value === true) return 'ongoing';
83
+ if (value === false || value == null) return null;
84
+ if (typeof value === 'string') {
85
+ const s = value.trim();
86
+ return s || null;
87
+ }
88
+ return null;
89
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head><meta charset="utf-8"><title>通知 | Facebook</title></head>
4
+ <body>
5
+ <div role="main">
6
+ <div class="x1rdy4ex" role="listitem"><div><div><div><div><div><div><span dir="auto"><div><div><h2><span><span>新通知</span></span></h2></div><div><div><div>&nbsp;</div><div><div></div></div></div></div></div></span></div></div></div></div></div></div></div>
7
+ <div class="x1n2onr6" role="listitem"><div data-visualcompletion="ignore-dynamic" role="none"><a href="https://www.facebook.com/photo/?fbid=104250644186433&amp;set=a.104250310853133&amp;notif_id=1777926497886652&amp;notif_t=onthisday&amp;ref=notif" role="link" tabindex="0"><div><div><div><div data-visualcompletion="ignore"><div><div aria-hidden="true"></div><div role="none" data-visualcompletion="ignore"></div></div></div></div></div><div><div><div><div><div><span dir="auto"><span><div>未读</div>回顾你在 2019年6月的那段过往.</span></span></div><div><span dir="auto"><span><span><span><abbr aria-label="2天前"><span>2天</span></abbr></span></span></span></span></div></div></div><div><div></div></div></div><div><div><div><div aria-label="标记为已读,回顾你在 2019年6月的那段过往." role="button" tabindex="0"><span data-visualcompletion="ignore"></span></div></div></div></div></div></div><div role="none" data-visualcompletion="ignore"></div></a></div><div><div><div><div aria-controls="_r_1o_" aria-expanded="false" aria-haspopup="dialog" aria-label="管理Jake Win通知设置" role="button" tabindex="0"><div role="none" data-visualcompletion="ignore"></div></div></div></div></div></div>
8
+ <div class="x1n2onr6" role="listitem"><div data-visualcompletion="ignore-dynamic" role="none"><a href="https://www.facebook.com/jake.win.39/posts/pfbid0JYwqY2r7MnMtowrMXbB2xuho8CMjPbS9fJgQYppcrk6JgNcNhDVUMQQX3SMeUwnDl?story_gqlid=UzpfSTEwMDAzODA0NDkzMjEzNToxMDQyNDkzNjQxODY1NjE6MTA0MjQ5MzY0MTg2NTYx&amp;notif_id=1777926497887116&amp;notif_t=onthisday&amp;ref=notif" role="link" tabindex="0"><div><div><div><div data-visualcompletion="ignore"><div><div aria-hidden="true"></div><div role="none" data-visualcompletion="ignore"></div></div></div></div></div><div><div><div><div><div><span dir="auto"><span><div>未读</div>看看你在 2019年6月发布的帖子,瞬间回到过去的美好时光.</span></span></div><div><span dir="auto"><span><span><span><abbr aria-label="2天前"><span>2天</span></abbr></span></span></span></span></div></div></div><div><div></div></div></div><div><div><div><div aria-label="标记为已读,看看你在 2019年6月发布的帖子,瞬间回到过去的美好时光." role="button" tabindex="0"><span data-visualcompletion="ignore"></span></div></div></div></div></div></div><div role="none" data-visualcompletion="ignore"></div></a></div><div><div><div><div aria-controls="_r_1s_" aria-expanded="false" aria-haspopup="dialog" aria-label="管理Jake Win通知设置" role="button" tabindex="0"><div role="none" data-visualcompletion="ignore"></div></div></div></div></div></div>
9
+ <div class="x1n2onr6" role="listitem"><div data-visualcompletion="ignore-dynamic" role="none"><a href="https://www.facebook.com/afad/?approval_id=1202094468666719&amp;notif_id=1773680546395691&amp;notif_t=approve_from_another_device&amp;ref=notif" role="link" tabindex="0"><div><div><div><div data-visualcompletion="ignore"><div><div aria-hidden="true"></div><div role="none" data-visualcompletion="ignore"></div></div></div></div></div><div><div><div><div><div><span dir="auto"><span><div>未读</div>有人尝试登录,但我们已阻止。</span></span></div><div><span dir="auto"><span><span><span><abbr aria-label="7周前"><span>7周</span></abbr></span></span></span></span></div></div></div><div><div></div></div></div><div><div><div><div aria-label="标记为已读,有人尝试登录,但我们已阻止。" role="button" tabindex="0"><span data-visualcompletion="ignore"></span></div></div></div></div></div></div><div role="none" data-visualcompletion="ignore"></div></a></div><div><div><div><div aria-controls="_r_20_" aria-expanded="false" aria-haspopup="dialog" aria-label="管理Jake Win通知设置" role="button" tabindex="0"><div role="none" data-visualcompletion="ignore"></div></div></div></div></div></div>
10
+ <div class="x1n2onr6" role="listitem"><div data-visualcompletion="ignore-dynamic" role="none"><a href="https://www.facebook.com/groups/discover/?source=recommendation_notif&amp;hoisted_group_id=720850587716560&amp;notif_id=1777892286665396&amp;notif_t=group_recommendation&amp;ref=notif" role="link" tabindex="0"><div><div><div><div data-visualcompletion="ignore"><div><div aria-hidden="true"></div><div role="none" data-visualcompletion="ignore"></div></div></div></div></div><div><div><div><div><div><span dir="auto"><span><div>未读</div>你可能会喜欢 <b>ETYCAL VIBEZ fan's page</b> 。</span></span></div><div><span dir="auto"><span><span><span><abbr aria-label="2天前"><span>2天</span></abbr></span></span></span></span></div></div></div><div><div></div></div></div><div><div><div><div aria-label="标记为已读,你可能会喜欢 ETYCAL VIBEZ fan's page 。" role="button" tabindex="0"><span data-visualcompletion="ignore"></span></div></div></div></div></div></div><div role="none" data-visualcompletion="ignore"></div></a></div><div><div><div><div aria-controls="_r_24_" aria-expanded="false" aria-haspopup="dialog" aria-label="管理Jake Win通知设置" role="button" tabindex="0"><div role="none" data-visualcompletion="ignore"></div></div></div></div></div></div>
11
+ </div>
12
+ </body>
13
+ </html>
@@ -1,37 +1,333 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
1
+ // Facebook notifications pulls the rendered notification feed from
2
+ // `www.facebook.com/notifications` via in-page DOM walk.
3
+ //
4
+ // Replaces the legacy `pipeline:[]` form. The previous adapter had four
5
+ // overlapping silent failures the new shape resolves:
6
+ // 1. silent-bad-shape — `text.substring(0, 150)` truncated long
7
+ // notification bodies without telling the caller.
8
+ // 2. silent-bad-shape — the old time field used a dash sentinel for
9
+ // unknown values; typed unknown should be `null` so an agent caller
10
+ // does not have to learn in-band sentinel strings.
11
+ // 3. silent-column-drop — the IIFE saw the `<div>未读</div>` /
12
+ // `<div>Unread</div>` badge child and the anchor's `<a href>` URL
13
+ // but kept neither, so callers got a bare text blob with no way
14
+ // to follow up on a notification.
15
+ // 4. silent-empty-row — empty `[role="listitem"]` (login expired,
16
+ // empty inbox, FB redirected to `/login`) returned `[]` instead
17
+ // of throwing a typed error.
18
+ //
19
+ // New behavior:
20
+ // - `func` form + `Strategy.COOKIE` + `browser:true`.
21
+ // - Upfront `--limit` validation: positive integer in [1, 100], no
22
+ // silent clamp; `ArgumentError` on bad input.
23
+ // - Pure extractor `extractNotificationRowsFromDoc` is a Node-side
24
+ // export — JSDOM-against-frozen-fixture tests call it directly while
25
+ // the live IIFE embeds it via `${fn.toString()}` (mirrors hupu
26
+ // #1387 / xiaoe #1388 / dianping #1313 pattern). Helpers
27
+ // `stripMarkAsReadPrefix`, `stripAnchorChrome`, `parseNotifQuery`
28
+ // are also pure exports for the same reason.
29
+ // - Seven columns instead of three:
30
+ // index, unread (bool), text (full, no truncation),
31
+ // time (string|null), url (string), notif_id (string|null),
32
+ // notif_type (string|null)
33
+ // - Auth detection: if the IIFE runs while window.location is on a
34
+ // login / checkpoint path, throw `AUTH_REQUIRED:` sentinel which
35
+ // the Node side maps to `AuthRequiredError`.
36
+ // - Empty list (after settle + auth check passes) → `EmptyResultError`,
37
+ // never silent `[]`.
38
+
39
+ import { cli, Strategy } from '@jackwener/opencli/registry';
40
+ import {
41
+ ArgumentError,
42
+ AuthRequiredError,
43
+ CommandExecutionError,
44
+ EmptyResultError,
45
+ } from '@jackwener/opencli/errors';
46
+
47
+ export const FB_HOST = 'https://www.facebook.com';
48
+ export const NOTIFICATIONS_LIMIT_DEFAULT = 15;
49
+ export const NOTIFICATIONS_LIMIT_MAX = 100;
50
+
51
+ // Locale-specific "Mark as read" button prefixes Facebook attaches to
52
+ // the per-row mark-as-read action. We use this to recover the bare
53
+ // notification body text without the leading "未读" / "Unread" badge or
54
+ // the trailing time-ago suffix that the anchor's textContent contains.
55
+ //
56
+ // Each entry must be a complete, deterministic prefix — no regex — so a
57
+ // rare false-positive substring match elsewhere on the page does not
58
+ // silently truncate body text.
59
+ export const MARK_AS_READ_PREFIXES = [
60
+ '标记为已读,',
61
+ 'Mark as read, ',
62
+ 'Mark as Read, ',
63
+ 'Marquer comme lu, ',
64
+ 'Marcar como leído, ',
65
+ '既読にする, ',
66
+ ];
67
+
68
+ // Localised "unread" badge labels that appear inside a `<div>` inside
69
+ // the notification listitem. Used to set the typed `unread` boolean.
70
+ export const UNREAD_BADGE_LABELS = ['未读', 'Unread', 'No leído', '未読'];
71
+
72
+ export function normalizeNotificationsLimit(raw) {
73
+ if (raw === undefined || raw === null || raw === '') {
74
+ return NOTIFICATIONS_LIMIT_DEFAULT;
75
+ }
76
+ const n = typeof raw === 'number' ? raw : Number(raw);
77
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > NOTIFICATIONS_LIMIT_MAX) {
78
+ throw new ArgumentError(
79
+ `--limit must be a positive integer in [1, ${NOTIFICATIONS_LIMIT_MAX}], got ${JSON.stringify(raw)}`,
80
+ );
81
+ }
82
+ return n;
83
+ }
84
+
85
+ // Pure: strip a Facebook locale-specific "mark as read" prefix from a
86
+ // `<div role="button">` aria-label so the caller gets the bare body
87
+ // text. Returns the stripped body, or `null` when the input does not
88
+ // start with a known prefix (i.e. we do not have a mark-as-read
89
+ // aria-label and the caller should fall through to anchor text).
90
+ //
91
+ // `prefixes` is injected so the same function works in JSDOM tests
92
+ // (passed the imported `MARK_AS_READ_PREFIXES`) and in the live IIFE
93
+ // (the array is inlined into the embedded script via `JSON.stringify`).
94
+ export function stripMarkAsReadPrefix(label, prefixes) {
95
+ if (!label) return null;
96
+ const text = String(label).trim();
97
+ if (!text) return null;
98
+ for (let i = 0; i < prefixes.length; i += 1) {
99
+ const p = prefixes[i];
100
+ if (text.startsWith(p)) return text.slice(p.length).trim();
101
+ }
102
+ return null;
103
+ }
104
+
105
+ // Pure: strip the leading "未读" / "Unread" badge prefix and the trailing
106
+ // time-ago suffix (e.g. "2天" / "5 hrs") from the rendered text of a
107
+ // notification anchor. Used as a fallback when the mark-as-read
108
+ // aria-label is not present (already-read rows).
109
+ //
110
+ // `badges` is the localized unread-badge labels list; `timeStr` is the
111
+ // trailing time string previously extracted from `<abbr>` (or `null`).
112
+ // Trailing `.` / `。` that Facebook inserts between body and time are
113
+ // also stripped — but only at the very end, never mid-text.
114
+ export function stripAnchorChrome(rawText, timeStr, badges) {
115
+ if (!rawText) return '';
116
+ let text = String(rawText).trim();
117
+ for (let i = 0; i < badges.length; i += 1) {
118
+ const b = badges[i];
119
+ if (text.startsWith(b)) {
120
+ text = text.slice(b.length).trim();
121
+ break;
122
+ }
123
+ }
124
+ if (timeStr) {
125
+ const t = String(timeStr).trim();
126
+ if (t && text.endsWith(t)) {
127
+ text = text.slice(0, text.length - t.length).trim();
128
+ }
129
+ }
130
+ text = text.replace(/[.。]+$/, '').trim();
131
+ return text;
132
+ }
133
+
134
+ // Pure: extract `notif_id` and `notif_t` query params from a
135
+ // notification anchor href. Both fields stay `null` when absent so the
136
+ // caller can detect "this row has no canonical notif handle" (rare,
137
+ // but happens for some notification types FB inlines a non-canonical
138
+ // URL for) — never an in-band sentinel.
139
+ export function parseNotifQuery(rawHref, fbHost) {
140
+ const out = { notif_id: null, notif_type: null };
141
+ if (!rawHref) return out;
142
+ let url;
143
+ try {
144
+ url = new URL(rawHref, fbHost);
145
+ } catch (_) {
146
+ return out;
147
+ }
148
+ out.notif_id = url.searchParams.get('notif_id') || null;
149
+ out.notif_type = url.searchParams.get('notif_t') || null;
150
+ return out;
151
+ }
152
+
153
+ export function isFacebookAuthRedirectPath(pathname) {
154
+ return /^\/(?:login|checkpoint)(?:\.php)?(?:\/|$)/i.test(String(pathname || ''));
155
+ }
156
+
157
+ // Pure extractor: walks `[role="listitem"]` containers in `doc` and
158
+ // returns at most `limit` notification rows. Header listitems (those
159
+ // without an `<a href>` child — e.g. the "新通知" / "Earlier" section
160
+ // heading FB inserts at the top of the feed) are skipped.
161
+ //
162
+ // `helpers` carries the four pure helpers as positional refs so the
163
+ // same function works in JSDOM (test imports them) and in the live
164
+ // IIFE (embeds them via `${fn.toString()}`):
165
+ // { stripMark, stripChrome, parseQuery, fbHost,
166
+ // markPrefixes, unreadBadges }
167
+ export function extractNotificationRowsFromDoc(doc, limit, helpers) {
168
+ const out = [];
169
+ const items = doc.querySelectorAll('[role="listitem"]');
170
+ for (let i = 0; i < items.length && out.length < limit; i += 1) {
171
+ const item = items[i];
172
+ const anchor = item.querySelector('a[href]');
173
+ if (!anchor) continue;
174
+ const href = anchor.href || anchor.getAttribute('href') || '';
175
+ if (!href) continue;
176
+
177
+ const abbr = item.querySelector('abbr');
178
+ const time = abbr ? ((abbr.textContent || '').trim() || null) : null;
179
+
180
+ // Unread badge: prefer the explicit `<div>未读</div>` /
181
+ // `<div>Unread</div>` child; fall back to anchor text prefix.
182
+ let unread = false;
183
+ const allDivs = item.querySelectorAll('div');
184
+ for (let j = 0; j < allDivs.length; j += 1) {
185
+ const t = (allDivs[j].textContent || '').trim();
186
+ if (helpers.unreadBadges.indexOf(t) !== -1) {
187
+ unread = true;
188
+ break;
189
+ }
190
+ }
191
+ if (!unread) {
192
+ const anchorText = (anchor.textContent || '').trim();
193
+ for (let k = 0; k < helpers.unreadBadges.length; k += 1) {
194
+ if (anchorText.startsWith(helpers.unreadBadges[k])) {
195
+ unread = true;
196
+ break;
197
+ }
198
+ }
199
+ }
200
+
201
+ // Body text — try every aria-label on descendants, take the
202
+ // first one that the mark-as-read prefix helper recognises.
203
+ // Fall back to the anchor's own textContent with badge / time
204
+ // / trailing punctuation chrome stripped.
205
+ let text = null;
206
+ const labelHosts = item.querySelectorAll('[aria-label]');
207
+ for (let k = 0; k < labelHosts.length; k += 1) {
208
+ const label = labelHosts[k].getAttribute('aria-label');
209
+ const stripped = helpers.stripMark(label, helpers.markPrefixes);
210
+ if (stripped !== null) {
211
+ const cleaned = stripped.replace(/[.。]+$/, '').trim();
212
+ text = cleaned || null;
213
+ break;
214
+ }
215
+ }
216
+ if (!text) {
217
+ const fallback = helpers.stripChrome(
218
+ anchor.textContent || '',
219
+ time,
220
+ helpers.unreadBadges,
221
+ );
222
+ text = fallback || null;
223
+ }
224
+ if (!text) continue;
225
+
226
+ const { notif_id, notif_type } = helpers.parseQuery(href, helpers.fbHost);
227
+ out.push({
228
+ index: out.length + 1,
229
+ unread,
230
+ text,
231
+ time,
232
+ url: href,
233
+ notif_id,
234
+ notif_type,
235
+ });
236
+ }
237
+ return out;
238
+ }
239
+
240
+ export function buildNotificationsScript(limit) {
241
+ return `
242
+ (async () => {
243
+ const FB_HOST = ${JSON.stringify(FB_HOST)};
244
+ const MARK_AS_READ_PREFIXES = ${JSON.stringify(MARK_AS_READ_PREFIXES)};
245
+ const UNREAD_BADGE_LABELS = ${JSON.stringify(UNREAD_BADGE_LABELS)};
246
+ ${isFacebookAuthRedirectPath.toString()}
247
+ if (isFacebookAuthRedirectPath(window.location.pathname || '')) {
248
+ throw new Error('AUTH_REQUIRED: facebook.com redirected to login');
249
+ }
250
+ ${stripMarkAsReadPrefix.toString()}
251
+ ${stripAnchorChrome.toString()}
252
+ ${parseNotifQuery.toString()}
253
+ ${extractNotificationRowsFromDoc.toString()}
254
+ // Wait briefly for [role="listitem"] rows to render in case the SPA
255
+ // is still hydrating. 8s ceiling keeps a slow network from hanging
256
+ // the command — empty list after that surfaces as EmptyResultError
257
+ // on the Node side.
258
+ const start = Date.now();
259
+ while (document.querySelectorAll('[role="listitem"]').length === 0 && Date.now() - start < 8000) {
260
+ await new Promise(function(r) { setTimeout(r, 200); });
261
+ }
262
+ return extractNotificationRowsFromDoc(document, ${JSON.stringify(limit)}, {
263
+ stripMark: stripMarkAsReadPrefix,
264
+ stripChrome: stripAnchorChrome,
265
+ parseQuery: parseNotifQuery,
266
+ fbHost: FB_HOST,
267
+ markPrefixes: MARK_AS_READ_PREFIXES,
268
+ unreadBadges: UNREAD_BADGE_LABELS,
269
+ });
270
+ })()
271
+ `;
272
+ }
273
+
274
+ async function getFacebookNotifications(page, args) {
275
+ const limit = normalizeNotificationsLimit(args.limit);
276
+ try {
277
+ await page.goto(`${FB_HOST}/notifications`, { waitUntil: 'load', settleMs: 3000 });
278
+ } catch (error) {
279
+ const message = error instanceof Error ? error.message : String(error);
280
+ throw new CommandExecutionError(
281
+ `Failed to navigate to facebook notifications: ${message}`,
282
+ 'facebook.com may be unreachable',
283
+ );
284
+ }
285
+ let rows;
286
+ try {
287
+ rows = await page.evaluate(buildNotificationsScript(limit));
288
+ } catch (error) {
289
+ const message = error instanceof Error ? error.message : String(error);
290
+ if (/AUTH_REQUIRED/i.test(message)) {
291
+ throw new AuthRequiredError(
292
+ 'facebook.com',
293
+ 'Open Chrome and log in to Facebook before retrying',
294
+ );
295
+ }
296
+ throw new CommandExecutionError(
297
+ `Failed to read facebook notifications: ${message}`,
298
+ 'facebook.com page may not have rendered or markup may have changed',
299
+ );
300
+ }
301
+ if (!Array.isArray(rows) || rows.length === 0) {
302
+ throw new EmptyResultError(
303
+ 'facebook/notifications',
304
+ 'No notifications found — login session may have expired or you have no recent notifications',
305
+ );
306
+ }
307
+ return rows;
308
+ }
309
+
310
+ export const notificationsCommand = cli({
3
311
  site: 'facebook',
4
312
  name: 'notifications',
5
313
  access: 'read',
6
- description: 'Get recent Facebook notifications',
314
+ description: 'Get recent Facebook notifications (含 unread / time / url / notif_id / notif_type 列)',
7
315
  domain: 'www.facebook.com',
316
+ strategy: Strategy.COOKIE,
317
+ browser: true,
318
+ navigateBefore: false,
8
319
  args: [
9
- { name: 'limit', type: 'int', default: 15, help: 'Number of notifications' },
10
- ],
11
- columns: ['index', 'text', 'time'],
12
- pipeline: [
13
- { navigate: { url: 'https://www.facebook.com/notifications', settleMs: 3000 } },
14
- { evaluate: `(() => {
15
- const limit = \${{ args.limit }};
16
- const items = document.querySelectorAll('[role="listitem"]');
17
- return Array.from(items)
18
- .filter(el => el.querySelectorAll('a').length > 0)
19
- .slice(0, limit)
20
- .map((el, i) => {
21
- const raw = el.textContent.trim().replace(/\\s+/g, ' ');
22
- // Remove leading "未读" and trailing "标记为已读"
23
- const cleaned = raw.replace(/^未读/, '').replace(/标记为已读$/, '').replace(/^Unread/, '').replace(/Mark as read$/, '').trim();
24
- // Try to extract time (last segment like "11小时", "5天", "1周")
25
- const timeMatch = cleaned.match(/(\\d+\\s*(?:分钟|小时|天|周|个月|minutes?|hours?|days?|weeks?|months?))\\s*$/);
26
- const time = timeMatch ? timeMatch[1] : '';
27
- const text = timeMatch ? cleaned.slice(0, -timeMatch[0].length).trim() : cleaned;
28
- return {
29
- index: i + 1,
30
- text: text.substring(0, 150),
31
- time: time || '-',
32
- };
33
- });
34
- })()
35
- ` },
320
+ {
321
+ name: 'limit',
322
+ type: 'int',
323
+ default: NOTIFICATIONS_LIMIT_DEFAULT,
324
+ help: `Number of notifications (1-${NOTIFICATIONS_LIMIT_MAX})`,
325
+ },
36
326
  ],
327
+ columns: ['index', 'unread', 'text', 'time', 'url', 'notif_id', 'notif_type'],
328
+ func: getFacebookNotifications,
37
329
  });
330
+
331
+ export const __test__ = {
332
+ buildNotificationsScript,
333
+ };