@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
@@ -1,44 +1,146 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
1
+ // TikTok friend / who-to-follow suggestions via page-context API.
2
+ //
3
+ // Replaces legacy DOM-link scraping (`querySelectorAll('a[href*="/@"]')` plus
4
+ // fragile text filters that mistook UI labels for handles). We pull the
5
+ // suggestion list from `__UNIVERSAL_DATA_FOR_REHYDRATION__` first, then
6
+ // top up via `/api/recommend/user/` if the warm snapshot is short.
7
+
8
+ import { cli, Strategy } from '@jackwener/opencli/registry';
9
+ import {
10
+ EmptyResultError,
11
+ } from '@jackwener/opencli/errors';
12
+ import {
13
+ BROWSER_HELPERS,
14
+ MAX_PAGES,
15
+ SERVER_PAGE_MAX,
16
+ TIKTOK_AID,
17
+ USER_ITEM_NORMALIZER,
18
+ requireLimit,
19
+ throwTikTokPageContextError,
20
+ } from './utils.js';
21
+
22
+ const DEFAULT_LIMIT = 20;
23
+ const MAX_LIMIT = 100;
24
+
25
+ function buildFriendsScript(limit) {
26
+ return `
27
+ (async () => {
28
+ const limit = ${Number(limit)};
29
+ const maxPages = ${MAX_PAGES};
30
+ const SERVER_PAGE_MAX = ${SERVER_PAGE_MAX};
31
+ const pageSize = limit < SERVER_PAGE_MAX ? limit : SERVER_PAGE_MAX;
32
+ const aid = ${JSON.stringify(TIKTOK_AID)};
33
+
34
+ ${BROWSER_HELPERS}
35
+ ${USER_ITEM_NORMALIZER}
36
+
37
+ function collectUsersFromState(root) {
38
+ if (!root) return [];
39
+ const out = [];
40
+ walkObjects(root, (node) => {
41
+ if (Array.isArray(node)) return false;
42
+ if (Array.isArray(node.userList)) {
43
+ for (const entry of node.userList) {
44
+ out.push(entry?.user || entry);
45
+ }
46
+ }
47
+ if (Array.isArray(node.suggestList)) {
48
+ for (const entry of node.suggestList) out.push(entry?.user || entry);
49
+ }
50
+ return false;
51
+ });
52
+ return out;
53
+ }
54
+
55
+ const dedup = new Map();
56
+ const universal = findUniversalData();
57
+ for (const user of collectUsersFromState(universal)) {
58
+ const row = normalizeUserRow(user, dedup.size + 1);
59
+ if (row && !dedup.has(row.username)) dedup.set(row.username, row);
60
+ }
61
+
62
+ let apiFailure = null;
63
+ const msToken = getCookie('msToken');
64
+ if (dedup.size < limit) {
65
+ let cursor = 0;
66
+ for (let page = 0; page < maxPages && dedup.size < limit; page += 1) {
67
+ const params = new URLSearchParams({
68
+ aid,
69
+ count: String(pageSize),
70
+ cursor: String(cursor),
71
+ scene: '15',
72
+ });
73
+ if (msToken) params.set('msToken', msToken);
74
+ try {
75
+ const data = await fetchJson('/api/recommend/user/?' + params.toString());
76
+ assertTikTokApiSuccess(data, 'recommend-user');
77
+ const list = Array.isArray(data.userList)
78
+ ? data.userList
79
+ : (Array.isArray(data.user_list) ? data.user_list : []);
80
+ if (list.length === 0) break;
81
+ for (const entry of list) {
82
+ const row = normalizeUserRow(entry?.user || entry, dedup.size + 1);
83
+ if (row && !dedup.has(row.username)) dedup.set(row.username, row);
84
+ }
85
+ if (data.hasMore !== true) break;
86
+ cursor = asNumber(data.cursor) ?? cursor + list.length;
87
+ } catch (error) {
88
+ apiFailure = error instanceof Error ? error.message : String(error);
89
+ break;
90
+ }
91
+ }
92
+ }
93
+
94
+ const rows = Array.from(dedup.values())
95
+ .slice(0, limit)
96
+ .map((row, index) => ({ ...row, index: index + 1 }));
97
+
98
+ if (rows.length === 0) {
99
+ const suffix = apiFailure ? ' (recommend-user API failed: ' + apiFailure + ')' : '';
100
+ throw new Error('No friend suggestions returned by TikTok' + suffix);
101
+ }
102
+ return rows;
103
+ })()
104
+ `;
105
+ }
106
+
107
+ async function listFriends(page, args) {
108
+ const limit = requireLimit(args.limit, { fallback: DEFAULT_LIMIT, max: MAX_LIMIT });
109
+ await page.goto('https://www.tiktok.com/friends', { waitUntil: 'load', settleMs: 5000 });
110
+ let rows;
111
+ try {
112
+ rows = await page.evaluate(buildFriendsScript(limit));
113
+ } catch (error) {
114
+ throwTikTokPageContextError(error, {
115
+ authMessage: 'TikTok requires browser access to load friend suggestions',
116
+ emptyPattern: /No friend suggestions/,
117
+ emptyTarget: 'tiktok friends',
118
+ failureMessage: 'Failed to load TikTok friend suggestions',
119
+ });
120
+ }
121
+ if (!Array.isArray(rows) || rows.length === 0) {
122
+ throw new EmptyResultError('tiktok friends', 'TikTok returned no friend suggestions');
123
+ }
124
+ return rows;
125
+ }
126
+
127
+ export const friendsCommand = cli({
3
128
  site: 'tiktok',
4
129
  name: 'friends',
5
130
  access: 'read',
6
- description: 'Get TikTok friend suggestions',
131
+ description: 'Get TikTok friend / who-to-follow suggestions via page-context APIs',
7
132
  domain: 'www.tiktok.com',
133
+ strategy: Strategy.COOKIE,
134
+ browser: true,
8
135
  args: [
9
- { name: 'limit', type: 'int', default: 20, help: 'Number of suggestions' },
10
- ],
11
- columns: ['index', 'username', 'name'],
12
- pipeline: [
13
- { navigate: { url: 'https://www.tiktok.com/friends', settleMs: 5000 } },
14
- { evaluate: `(() => {
15
- const limit = \${{ args.limit }};
16
- const links = Array.from(document.querySelectorAll('a[href*="/@"]'))
17
- .filter(function(a) {
18
- const text = a.textContent.trim();
19
- return text.length > 1 && text.length < 80 &&
20
- !text.includes('Profile') && !text.includes('More') && !text.includes('Upload');
21
- });
22
-
23
- const seen = {};
24
- const results = [];
25
- for (const a of links) {
26
- const match = a.href.match(/@([^/]+)/);
27
- const username = match ? match[1] : '';
28
- if (!username || seen[username]) continue;
29
- seen[username] = true;
30
- const raw = a.textContent.trim();
31
- const hasFollow = raw.includes('Follow');
32
- const name = raw.replace('Follow', '').replace(username, '').replace('@', '').trim();
33
- results.push({
34
- index: results.length + 1,
35
- username: username,
36
- name: name || username,
37
- });
38
- if (results.length >= limit) break;
39
- }
40
- return results;
41
- })()
42
- ` },
136
+ { name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of suggestions (max ${MAX_LIMIT})` },
43
137
  ],
138
+ columns: ['index', 'username', 'name', 'secUid', 'verified', 'followers', 'following', 'url'],
139
+ func: listFriends,
44
140
  });
141
+
142
+ export const __test__ = {
143
+ buildFriendsScript,
144
+ DEFAULT_LIMIT,
145
+ MAX_LIMIT,
146
+ };
@@ -1,48 +1,144 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
1
+ // Browse TikTok live streams via page-context API.
2
+ //
3
+ // Replaces legacy DOM card scraping (`[data-e2e="live-side-nav-item"]` plus a
4
+ // regex pulling viewer counts from card text). We extract from the live-discover
5
+ // state injected on `/live`, then top up via `/api/live/discover/get/`.
6
+
7
+ import { cli, Strategy } from '@jackwener/opencli/registry';
8
+ import {
9
+ EmptyResultError,
10
+ } from '@jackwener/opencli/errors';
11
+ import {
12
+ BROWSER_HELPERS,
13
+ LIVE_ITEM_NORMALIZER,
14
+ SERVER_PAGE_MAX,
15
+ TIKTOK_AID,
16
+ requireLimit,
17
+ throwTikTokPageContextError,
18
+ } from './utils.js';
19
+
20
+ const DEFAULT_LIMIT = 10;
21
+ const MAX_LIMIT = 60;
22
+
23
+ function buildLiveScript(limit) {
24
+ return `
25
+ (async () => {
26
+ const limit = ${Number(limit)};
27
+ const SERVER_PAGE_MAX = ${SERVER_PAGE_MAX};
28
+ const pageSize = limit < SERVER_PAGE_MAX ? limit : SERVER_PAGE_MAX;
29
+ const aid = ${JSON.stringify(TIKTOK_AID)};
30
+
31
+ ${BROWSER_HELPERS}
32
+ ${LIVE_ITEM_NORMALIZER}
33
+
34
+ function collectFromState(root) {
35
+ if (!root) return [];
36
+ const out = [];
37
+ walkObjects(root, (node) => {
38
+ if (Array.isArray(node)) return false;
39
+ if (Array.isArray(node.liveList)) {
40
+ for (const entry of node.liveList) out.push(entry);
41
+ }
42
+ if (Array.isArray(node.live_list)) {
43
+ for (const entry of node.live_list) out.push(entry);
44
+ }
45
+ if (Array.isArray(node.discoverList)) {
46
+ for (const entry of node.discoverList) out.push(entry);
47
+ }
48
+ return false;
49
+ });
50
+ return out;
51
+ }
52
+
53
+ const dedup = new Map();
54
+ const universal = findUniversalData();
55
+ for (const item of collectFromState(universal)) {
56
+ const row = normalizeLiveItem(item, dedup.size + 1);
57
+ if (row) {
58
+ const key = row.streamer || row.url;
59
+ if (key && !dedup.has(key)) dedup.set(key, row);
60
+ }
61
+ }
62
+
63
+ let apiFailure = null;
64
+ const msToken = getCookie('msToken');
65
+ if (dedup.size < limit) {
66
+ const params = new URLSearchParams({
67
+ aid,
68
+ count: String(pageSize),
69
+ from_page: 'live_discover',
70
+ });
71
+ if (msToken) params.set('msToken', msToken);
72
+ try {
73
+ const data = await fetchJson('/api/live/discover/get/?' + params.toString());
74
+ assertTikTokApiSuccess(data, 'live-discover');
75
+ const list = Array.isArray(data.data?.list)
76
+ ? data.data.list
77
+ : (Array.isArray(data.list)
78
+ ? data.list
79
+ : (Array.isArray(data.live_list) ? data.live_list : []));
80
+ for (const entry of list) {
81
+ const row = normalizeLiveItem(entry, dedup.size + 1);
82
+ if (row) {
83
+ const key = row.streamer || row.url;
84
+ if (key && !dedup.has(key)) dedup.set(key, row);
85
+ }
86
+ }
87
+ } catch (error) {
88
+ apiFailure = error instanceof Error ? error.message : String(error);
89
+ }
90
+ }
91
+
92
+ const rows = Array.from(dedup.values())
93
+ .slice(0, limit)
94
+ .map((row, index) => ({ ...row, index: index + 1 }));
95
+
96
+ if (rows.length === 0) {
97
+ const suffix = apiFailure ? ' (live-discover API failed: ' + apiFailure + ')' : '';
98
+ throw new Error('No live streams returned' + suffix);
99
+ }
100
+ return rows;
101
+ })()
102
+ `;
103
+ }
104
+
105
+ async function listLive(page, args) {
106
+ const limit = requireLimit(args.limit, { fallback: DEFAULT_LIMIT, max: MAX_LIMIT });
107
+ await page.goto('https://www.tiktok.com/live', { waitUntil: 'load', settleMs: 5000 });
108
+ let rows;
109
+ try {
110
+ rows = await page.evaluate(buildLiveScript(limit));
111
+ } catch (error) {
112
+ throwTikTokPageContextError(error, {
113
+ authMessage: 'TikTok requires browser access to load live streams',
114
+ emptyPattern: /No live streams returned/,
115
+ emptyTarget: 'tiktok live',
116
+ failureMessage: 'Failed to load TikTok live streams',
117
+ });
118
+ }
119
+ if (!Array.isArray(rows) || rows.length === 0) {
120
+ throw new EmptyResultError('tiktok live', 'TikTok returned no live streams');
121
+ }
122
+ return rows;
123
+ }
124
+
125
+ export const liveCommand = cli({
3
126
  site: 'tiktok',
4
127
  name: 'live',
5
128
  access: 'read',
6
- description: 'Browse live streams on TikTok',
129
+ description: 'Browse TikTok live streams via page-context APIs',
7
130
  domain: 'www.tiktok.com',
131
+ strategy: Strategy.COOKIE,
132
+ browser: true,
8
133
  args: [
9
- { name: 'limit', type: 'int', default: 10, help: 'Number of streams' },
10
- ],
11
- columns: ['index', 'streamer', 'viewers', 'url'],
12
- pipeline: [
13
- { navigate: { url: 'https://www.tiktok.com/live', settleMs: 5000 } },
14
- { evaluate: `(() => {
15
- const limit = \${{ args.limit }};
16
- // Sidebar live list has structured data
17
- const items = document.querySelectorAll('[data-e2e="live-side-nav-item"]');
18
- const sidebar = Array.from(items).slice(0, limit).map(function(el, i) {
19
- const nameEl = el.querySelector('[data-e2e="live-side-nav-name"]');
20
- const countEl = el.querySelector('[data-e2e="person-count"]');
21
- const link = el.querySelector('a');
22
- return {
23
- index: i + 1,
24
- streamer: nameEl ? nameEl.textContent.trim() : '',
25
- viewers: countEl ? countEl.textContent.trim() : '-',
26
- url: link ? link.href : '',
27
- };
28
- });
29
-
30
- if (sidebar.length > 0) return sidebar;
31
-
32
- // Fallback: main content cards
33
- const cards = document.querySelectorAll('[data-e2e="discover-list-live-card"]');
34
- return Array.from(cards).slice(0, limit).map(function(card, i) {
35
- const text = card.textContent.trim().replace(/\\s+/g, ' ');
36
- const link = card.querySelector('a[href*="/live"]');
37
- const viewerMatch = text.match(/(\\d[\\d,.]*)\\s*watching/);
38
- return {
39
- index: i + 1,
40
- streamer: text.replace(/LIVE.*$/, '').trim().substring(0, 40),
41
- viewers: viewerMatch ? viewerMatch[1] : '-',
42
- url: link ? link.href : '',
43
- };
44
- });
45
- })()
46
- ` },
134
+ { name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of streams (max ${MAX_LIMIT})` },
47
135
  ],
136
+ columns: ['index', 'streamer', 'name', 'title', 'viewers', 'likes', 'secUid', 'url'],
137
+ func: listLive,
48
138
  });
139
+
140
+ export const __test__ = {
141
+ buildLiveScript,
142
+ DEFAULT_LIMIT,
143
+ MAX_LIMIT,
144
+ };
@@ -1,50 +1,153 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
1
+ // Read TikTok inbox notifications via page-context API.
2
+ //
3
+ // Replaces the legacy "click the bell icon then scrape `[data-e2e="inbox-list"]`
4
+ // text content" pipeline. Calls `/api/notice/multi/?notice_type=N` directly
5
+ // from inside the live page so cookies + msToken get forwarded by the browser.
6
+
7
+ import { cli, Strategy } from '@jackwener/opencli/registry';
8
+ import {
9
+ EmptyResultError,
10
+ } from '@jackwener/opencli/errors';
11
+ import {
12
+ BROWSER_HELPERS,
13
+ MAX_PAGES,
14
+ NOTIFICATION_NORMALIZER,
15
+ NOTIFICATION_TYPES,
16
+ SERVER_PAGE_MAX,
17
+ TIKTOK_AID,
18
+ requireLimit,
19
+ requireNotificationType,
20
+ throwTikTokPageContextError,
21
+ } from './utils.js';
22
+
23
+ const DEFAULT_LIMIT = 15;
24
+ const MAX_LIMIT = 100;
25
+
26
+ function buildNotificationsScript(limit, typeKey) {
27
+ const typeMeta = NOTIFICATION_TYPES[typeKey];
28
+ return `
29
+ (async () => {
30
+ const limit = ${Number(limit)};
31
+ const maxPages = ${MAX_PAGES};
32
+ const SERVER_PAGE_MAX = ${SERVER_PAGE_MAX};
33
+ const pageSize = limit < SERVER_PAGE_MAX ? limit : SERVER_PAGE_MAX;
34
+ const noticeType = ${Number(typeMeta.code)};
35
+ const noticeLabel = ${JSON.stringify(typeMeta.label)};
36
+ const aid = ${JSON.stringify(TIKTOK_AID)};
37
+
38
+ ${BROWSER_HELPERS}
39
+ ${NOTIFICATION_NORMALIZER}
40
+
41
+ function collectFromState(root) {
42
+ if (!root) return [];
43
+ const out = [];
44
+ walkObjects(root, (node) => {
45
+ if (Array.isArray(node)) return false;
46
+ if (Array.isArray(node.noticeList)) {
47
+ for (const entry of node.noticeList) out.push(entry);
48
+ }
49
+ if (Array.isArray(node.notice_list)) {
50
+ for (const entry of node.notice_list) out.push(entry);
51
+ }
52
+ return false;
53
+ });
54
+ return out;
55
+ }
56
+
57
+ const dedup = new Map();
58
+ const universal = findUniversalData();
59
+ for (const item of collectFromState(universal)) {
60
+ const row = normalizeNotification(item, dedup.size + 1);
61
+ if (row && !dedup.has(row.id)) dedup.set(row.id, row);
62
+ }
63
+
64
+ let apiFailure = null;
65
+ const msToken = getCookie('msToken');
66
+ let maxTime = 0;
67
+ for (let page = 0; page < maxPages && dedup.size < limit; page += 1) {
68
+ const params = new URLSearchParams({
69
+ aid,
70
+ notice_type: String(noticeType),
71
+ count: String(pageSize),
72
+ max_time: String(maxTime),
73
+ });
74
+ if (msToken) params.set('msToken', msToken);
75
+ try {
76
+ const data = await fetchJson('/api/notice/multi/?' + params.toString());
77
+ assertTikTokApiSuccess(data, 'notice');
78
+ const list = Array.isArray(data.notice_list_v1)
79
+ ? data.notice_list_v1
80
+ : (Array.isArray(data.noticeList) ? data.noticeList : (Array.isArray(data.notice_list) ? data.notice_list : []));
81
+ if (list.length === 0) break;
82
+ for (const entry of list) {
83
+ const row = normalizeNotification(entry, dedup.size + 1);
84
+ if (row && !dedup.has(row.id)) dedup.set(row.id, row);
85
+ }
86
+ if (data.has_more !== true && data.hasMore !== true) break;
87
+ maxTime = asNumber(data.min_time) ?? asNumber(data.maxTime) ?? maxTime + list.length;
88
+ } catch (error) {
89
+ apiFailure = error instanceof Error ? error.message : String(error);
90
+ break;
91
+ }
92
+ }
93
+
94
+ const rows = Array.from(dedup.values())
95
+ .slice(0, limit)
96
+ .map((row, index) => ({ ...row, index: index + 1 }));
97
+
98
+ if (rows.length === 0) {
99
+ const suffix = apiFailure ? ' (notice API failed: ' + apiFailure + ')' : '';
100
+ throw new Error('No notifications returned for ' + noticeLabel + suffix);
101
+ }
102
+ return rows;
103
+ })()
104
+ `;
105
+ }
106
+
107
+ async function listNotifications(page, args) {
108
+ const limit = requireLimit(args.limit, { fallback: DEFAULT_LIMIT, max: MAX_LIMIT });
109
+ const typeKey = requireNotificationType(args.type);
110
+ await page.goto('https://www.tiktok.com/foryou', { waitUntil: 'load', settleMs: 5000 });
111
+ let rows;
112
+ try {
113
+ rows = await page.evaluate(buildNotificationsScript(limit, typeKey));
114
+ } catch (error) {
115
+ throwTikTokPageContextError(error, {
116
+ authMessage: 'TikTok requires login to read notifications',
117
+ emptyPattern: /No notifications returned/,
118
+ emptyTarget: 'tiktok notifications',
119
+ failureMessage: 'Failed to load TikTok notifications',
120
+ });
121
+ }
122
+ if (!Array.isArray(rows) || rows.length === 0) {
123
+ throw new EmptyResultError('tiktok notifications', 'TikTok returned no notifications');
124
+ }
125
+ return rows;
126
+ }
127
+
128
+ export const notificationsCommand = cli({
3
129
  site: 'tiktok',
4
130
  name: 'notifications',
5
131
  access: 'read',
6
- description: 'Get TikTok notifications (likes, comments, mentions, followers)',
132
+ description: 'Read TikTok inbox notifications (likes, comments, mentions, followers) via page-context APIs',
7
133
  domain: 'www.tiktok.com',
134
+ strategy: Strategy.COOKIE,
135
+ browser: true,
8
136
  args: [
9
- { name: 'limit', type: 'int', default: 15, help: 'Number of notifications' },
137
+ { name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of notifications (max ${MAX_LIMIT})` },
10
138
  {
11
139
  name: 'type',
12
140
  default: 'all',
13
141
  help: 'Notification type',
14
- choices: ['all', 'likes', 'comments', 'mentions', 'followers'],
142
+ choices: Object.keys(NOTIFICATION_TYPES),
15
143
  },
16
144
  ],
17
- columns: ['index', 'text'],
18
- pipeline: [
19
- { navigate: { url: 'https://www.tiktok.com/following', settleMs: 5000 } },
20
- { evaluate: `(async () => {
21
- const limit = \${{ args.limit }};
22
- const type = \${{ args.type | json }};
23
- const wait = (ms) => new Promise(r => setTimeout(r, ms));
24
-
25
- // Click inbox icon to open notifications panel
26
- const inboxIcon = document.querySelector('[data-e2e="inbox-icon"]');
27
- if (inboxIcon) inboxIcon.click();
28
- await wait(1500);
29
-
30
- // Click specific tab if needed
31
- if (type !== 'all') {
32
- const tab = document.querySelector('[data-e2e="' + type + '"]');
33
- if (tab) {
34
- tab.click();
35
- await wait(1500);
36
- }
37
- }
38
-
39
- const items = document.querySelectorAll('[data-e2e="inbox-list"] > div, [data-e2e="inbox-list"] [role="button"]');
40
- return Array.from(items)
41
- .filter(el => el.textContent.trim().length > 5)
42
- .slice(0, limit)
43
- .map((el, i) => ({
44
- index: i + 1,
45
- text: el.textContent.trim().replace(/\\s+/g, ' ').substring(0, 150),
46
- }));
47
- })()
48
- ` },
49
- ],
145
+ columns: ['index', 'id', 'from', 'text', 'createTime'],
146
+ func: listNotifications,
50
147
  });
148
+
149
+ export const __test__ = {
150
+ buildNotificationsScript,
151
+ DEFAULT_LIMIT,
152
+ MAX_LIMIT,
153
+ };