@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,58 +1,145 @@
1
- import { cli } from '@jackwener/opencli/registry';
2
- cli({
3
- site: 'tiktok',
4
- name: 'comment',
5
- access: 'write',
6
- description: 'Comment on a TikTok video',
7
- domain: 'www.tiktok.com',
8
- args: [
9
- { name: 'url', required: true, positional: true, help: 'TikTok video URL' },
10
- { name: 'text', required: true, positional: true, help: 'Comment text' },
11
- ],
12
- columns: ['status', 'url', 'text'],
13
- pipeline: [
14
- { navigate: { url: '${{ args.url }}', settleMs: 6000 } },
15
- { evaluate: `(async () => {
16
- const url = \${{ args.url | json }};
17
- const commentText = \${{ args.text | json }};
18
- const wait = (ms) => new Promise(r => setTimeout(r, ms));
19
-
20
- // Click comment icon to expand comment section
1
+ // Post a comment on a TikTok video via in-page button click + state
2
+ // verification.
3
+ //
4
+ // Replaces the legacy pipeline-based adapter that returned a silent
5
+ // failure row `{ status: posted ? 'Commented' : 'Comment may have failed' }`.
6
+ // Route 1 keeps the live UI button + contenteditable input as the
7
+ // trigger (TikTok's `/api/comment/publish/` write endpoint requires
8
+ // X-Bogus signing — out of scope), and every transition raises a typed
9
+ // error:
10
+ //
11
+ // ArgumentError — empty / overlong text, malformed video URL
12
+ // AuthRequiredError — not logged in
13
+ // CommandExecutionError — comment input / post button missing /
14
+ // state verification fails / rate limit
15
+ // detected (retryable=false: TikTok may have
16
+ // accepted the comment server-side even when
17
+ // our state-verify timed out)
18
+ //
19
+ // `result` row enum: `posted`. There is no idempotent fast path: TikTok
20
+ // allows multiple identical comments, so we cannot safely "detect"
21
+ // already-posted by scanning the existing list. Failures throw — never
22
+ // returned as a success row.
23
+
24
+ import { cli, Strategy } from '@jackwener/opencli/registry';
25
+ import {
26
+ BROWSER_HELPERS,
27
+ BUTTON_WALKER_HELPERS,
28
+ RETRYABLE_HINTS,
29
+ parseTikTokVideoUrl,
30
+ requireCommentText,
31
+ throwButtonWalkerError,
32
+ } from './utils.js';
33
+
34
+ function buildCommentScript(commentText) {
35
+ return `
36
+ (async () => {
37
+ const commentText = ${JSON.stringify(commentText)};
38
+
39
+ ${BROWSER_HELPERS}
40
+ ${BUTTON_WALKER_HELPERS}
41
+
42
+ ensureLoggedInOrThrow();
43
+ ensureNoRateLimitOrThrow();
44
+
45
+ // Expand the comment panel if it is collapsed (vertical feed pages).
21
46
  const commentIcon = document.querySelector('[data-e2e="comment-icon"]');
22
47
  if (commentIcon) {
23
48
  const cBtn = commentIcon.closest('button') || commentIcon.closest('[role="button"]') || commentIcon;
24
49
  cBtn.click();
25
- await wait(3000);
50
+ await waitFor(() => Boolean(
51
+ document.querySelector('[data-e2e="comment-input"] [contenteditable="true"]')
52
+ ), { timeoutMs: 4000 });
26
53
  }
27
54
 
28
- // Count existing comments for verification
29
55
  const beforeCount = document.querySelectorAll('[data-e2e="comment-level-1"]').length;
30
56
 
31
- // Find comment input
32
- const input = document.querySelector('[data-e2e="comment-input"] [contenteditable="true"]') ||
33
- document.querySelector('[contenteditable="true"]');
34
- if (!input) throw new Error('Comment input not found - make sure you are logged in');
57
+ const input = document.querySelector('[data-e2e="comment-input"] [contenteditable="true"]')
58
+ || document.querySelector('[contenteditable="true"]');
59
+ if (!input) {
60
+ throw new Error('BUTTON_NOT_FOUND: comment input not found (page not rendered, comments disabled, or selectors changed)');
61
+ }
35
62
 
36
63
  input.focus();
64
+ // execCommand is deprecated but still the only reliable way to inject
65
+ // text into TikTok's contenteditable so its React tree picks up the
66
+ // value; replicating with InputEvent fires but TikTok ignores it.
37
67
  document.execCommand('insertText', false, commentText);
38
- await wait(1000);
39
-
40
- // Click post button
41
- const btns = Array.from(document.querySelectorAll('[data-e2e="comment-post"], button'));
42
- const postBtn = btns.find(function(b) {
43
- var t = b.textContent.trim();
44
- return t === 'Post' || t === '发布' || t === '发送';
45
- });
46
- if (!postBtn) throw new Error('Post button not found');
68
+
69
+ // Wait for the post button to become enabled — TikTok disables it
70
+ // until non-empty text is detected by their input handler.
71
+ const postReady = await waitFor(() => {
72
+ const candidate = findButtonByText(['Post', '发布', '发送']);
73
+ if (!candidate) return false;
74
+ const ariaDisabled = candidate.getAttribute && candidate.getAttribute('aria-disabled');
75
+ return !candidate.disabled && ariaDisabled !== 'true';
76
+ }, { timeoutMs: 4000 });
77
+ if (!postReady) {
78
+ ensureNoRateLimitOrThrow();
79
+ throw new Error('BUTTON_NOT_FOUND: Post button never became enabled (text not registered or selectors changed)');
80
+ }
81
+
82
+ const postBtn = findButtonByText(['Post', '发布', '发送']);
47
83
  postBtn.click();
48
- await wait(3000);
49
84
 
50
- // Verify comment was posted by checking if comment count increased
51
- const afterCount = document.querySelectorAll('[data-e2e="comment-level-1"]').length;
52
- const posted = afterCount > beforeCount;
85
+ // State verification: a new comment-level-1 element should appear.
86
+ const flipped = await waitFor(
87
+ () => document.querySelectorAll('[data-e2e="comment-level-1"]').length > beforeCount,
88
+ { timeoutMs: 8000 },
89
+ );
90
+ if (!flipped) {
91
+ ensureNoRateLimitOrThrow();
92
+ throw new Error('STATE_VERIFY_FAIL: comment count did not increase within 8s; comment may or may not have been recorded server-side');
93
+ }
94
+
95
+ ensureNoRateLimitOrThrow();
53
96
 
54
- return [{ status: posted ? 'Commented' : 'Comment may have failed', url: url, text: commentText }];
97
+ return [{
98
+ url: location.href,
99
+ text: commentText,
100
+ result: 'posted',
101
+ }];
55
102
  })()
56
- ` },
103
+ `;
104
+ }
105
+
106
+ async function postComment(page, args) {
107
+ const { url } = parseTikTokVideoUrl(args.url);
108
+ const text = requireCommentText(args.text);
109
+ const throwFailure = (error) => throwButtonWalkerError(error, {
110
+ authMessage: 'TikTok requires login to post comments',
111
+ failureMessage: `Failed to post comment on ${url}`,
112
+ retryableHint: RETRYABLE_HINTS.commentFailure,
113
+ });
114
+ let rows;
115
+ try {
116
+ await page.goto(url, { waitUntil: 'load', settleMs: 6000 });
117
+ rows = await page.evaluate(buildCommentScript(text));
118
+ } catch (error) {
119
+ throwFailure(error);
120
+ }
121
+ if (!Array.isArray(rows) || rows.length === 0) {
122
+ throwFailure(new Error(`STATE_VERIFY_FAIL: comment returned no row for ${url}`));
123
+ }
124
+ return rows;
125
+ }
126
+
127
+ export const commentCommand = cli({
128
+ site: 'tiktok',
129
+ name: 'comment',
130
+ access: 'write',
131
+ description: 'Post a comment on a TikTok video',
132
+ domain: 'www.tiktok.com',
133
+ strategy: Strategy.COOKIE,
134
+ browser: true,
135
+ args: [
136
+ { name: 'url', required: true, positional: true, help: 'TikTok video URL (https://www.tiktok.com/@user/video/<id>)' },
137
+ { name: 'text', required: true, positional: true, help: 'Comment text (≤150 chars)' },
57
138
  ],
139
+ columns: ['url', 'text', 'result'],
140
+ func: postComment,
58
141
  });
142
+
143
+ export const __test__ = {
144
+ buildCommentScript,
145
+ };
@@ -0,0 +1,270 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import {
3
+ ArgumentError,
4
+ AuthRequiredError,
5
+ CommandExecutionError,
6
+ EmptyResultError,
7
+ getErrorMessage,
8
+ } from '@jackwener/opencli/errors';
9
+
10
+ const STUDIO_CONTENT_URL = 'https://www.tiktok.com/tiktokstudio/content';
11
+ const ITEM_LIST_API_PATH = '/tiktok/creator/manage/item_list/v1/';
12
+ const DEFAULT_LIMIT = 20;
13
+ const MAX_LIMIT = 250;
14
+ const SERVER_PAGE_MAX = 50;
15
+
16
+ function requirePositiveInt(value, label, defaultValue, maxValue) {
17
+ const raw = value ?? defaultValue;
18
+ const parsed = Number(raw);
19
+ if (!Number.isInteger(parsed) || parsed <= 0) {
20
+ throw new ArgumentError(`${label} must be a positive integer`, `Example: opencli tiktok creator-videos --${label} ${defaultValue}`);
21
+ }
22
+ if (parsed > maxValue) {
23
+ throw new ArgumentError(`${label} must be <= ${maxValue}`, `Example: opencli tiktok creator-videos --${label} ${maxValue}`);
24
+ }
25
+ return parsed;
26
+ }
27
+
28
+ function requireCursor(value) {
29
+ const raw = value ?? '0';
30
+ const text = String(raw).trim();
31
+ if (!/^\d+$/.test(text)) {
32
+ throw new ArgumentError('cursor must be a non-negative integer string', 'Example: opencli tiktok creator-videos --cursor 0');
33
+ }
34
+ const cursor = Number(text);
35
+ if (!Number.isSafeInteger(cursor)) {
36
+ throw new ArgumentError('cursor must be a safe integer', 'Example: opencli tiktok creator-videos --cursor 0');
37
+ }
38
+ return cursor;
39
+ }
40
+
41
+ function buildItemListRequest(cursor, size) {
42
+ return {
43
+ cursor,
44
+ size,
45
+ query: {
46
+ conditions: [],
47
+ sort_orders: [{ field_name: 'create_time', order: 2 }],
48
+ },
49
+ };
50
+ }
51
+
52
+ function buildFetchItemListScript(body) {
53
+ const request = {
54
+ url: `${ITEM_LIST_API_PATH}?aid=1988`,
55
+ body,
56
+ };
57
+ return `
58
+ (async () => {
59
+ const request = ${JSON.stringify(request)};
60
+ try {
61
+ const res = await fetch(request.url, {
62
+ method: 'POST',
63
+ credentials: 'include',
64
+ headers: {
65
+ accept: 'application/json',
66
+ 'content-type': 'application/json',
67
+ },
68
+ body: JSON.stringify(request.body),
69
+ });
70
+ const text = await res.text();
71
+ let data = null;
72
+ if (text.trim()) {
73
+ try {
74
+ data = JSON.parse(text);
75
+ } catch (error) {
76
+ return {
77
+ ok: false,
78
+ status: res.status,
79
+ statusText: res.statusText,
80
+ parseError: error instanceof Error ? error.message : String(error),
81
+ text: text.slice(0, 500),
82
+ };
83
+ }
84
+ }
85
+ return {
86
+ ok: res.ok,
87
+ status: res.status,
88
+ statusText: res.statusText,
89
+ data,
90
+ text: text.slice(0, 500),
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ ok: false,
95
+ status: 0,
96
+ statusText: '',
97
+ networkError: error instanceof Error ? error.message : String(error),
98
+ };
99
+ }
100
+ })()
101
+ `;
102
+ }
103
+
104
+ function looksAuthFailure(message) {
105
+ return /\b(auth|login|log in|permission|unauthori[sz]ed|forbidden)\b/i.test(message);
106
+ }
107
+
108
+ function unwrapPayload(data) {
109
+ if (!data || typeof data !== 'object') {
110
+ throw new CommandExecutionError('TikTok Studio item_list returned an empty response');
111
+ }
112
+ return data.data && typeof data.data === 'object' ? data.data : data;
113
+ }
114
+
115
+ function assertApiSuccess(data) {
116
+ const statusCode = data.status_code ?? data.statusCode;
117
+ const statusMsg = String(data.status_msg ?? data.statusMsg ?? '').trim();
118
+ if (statusCode !== undefined && Number(statusCode) !== 0) {
119
+ if (looksAuthFailure(statusMsg)) {
120
+ throw new AuthRequiredError('www.tiktok.com', `TikTok Studio item_list requires login: ${statusMsg || statusCode}`);
121
+ }
122
+ throw new CommandExecutionError(`TikTok Studio item_list failed: ${statusMsg || statusCode}`);
123
+ }
124
+ if (statusMsg && !/^(success|ok)$/i.test(statusMsg)) {
125
+ if (looksAuthFailure(statusMsg)) {
126
+ throw new AuthRequiredError('www.tiktok.com', `TikTok Studio item_list requires login: ${statusMsg}`);
127
+ }
128
+ throw new CommandExecutionError(`TikTok Studio item_list failed: ${statusMsg}`);
129
+ }
130
+ }
131
+
132
+ function normalizeNumber(value) {
133
+ const n = Number(value);
134
+ return Number.isFinite(n) ? n : 0;
135
+ }
136
+
137
+ function formatDate(value) {
138
+ const seconds = Number(value);
139
+ if (!Number.isFinite(seconds) || seconds <= 0) return '';
140
+ return new Date(seconds * 1000).toLocaleString('zh-CN', {
141
+ timeZone: 'Asia/Shanghai',
142
+ hour12: false,
143
+ });
144
+ }
145
+
146
+ function extractUsername(item) {
147
+ const direct = item.author?.unique_id ?? item.author?.uniqueId ?? item.author_unique_id ?? item.authorUniqueId ?? item.user_name ?? item.username;
148
+ if (direct) return String(direct);
149
+
150
+ const blobs = [
151
+ ...(Array.isArray(item.play_addr) ? item.play_addr : []),
152
+ ...(item.download_info && Array.isArray(item.download_info.download_urls) ? item.download_info.download_urls : []),
153
+ ];
154
+ for (const raw of blobs) {
155
+ try {
156
+ const match = String(raw).match(/[?&]user_text=([^&]+)/);
157
+ if (match) return decodeURIComponent(match[1]);
158
+ } catch {
159
+ // Keep scanning other candidate URLs.
160
+ }
161
+ }
162
+ return '';
163
+ }
164
+
165
+ function normalizeRow(item) {
166
+ if (!item || typeof item !== 'object') return null;
167
+ const videoId = String(item.item_id ?? item.id ?? '').trim();
168
+ if (!videoId) return null;
169
+ const username = extractUsername(item);
170
+ const url = username
171
+ ? `https://www.tiktok.com/@${encodeURIComponent(username)}/video/${encodeURIComponent(videoId)}`
172
+ : '';
173
+ return {
174
+ video_id: videoId,
175
+ title: String(item.desc ?? item.title ?? '').replace(/\s+/g, ' ').trim(),
176
+ date: formatDate(item.post_time ?? item.create_time ?? item.schedule_time),
177
+ views: normalizeNumber(item.play_count),
178
+ likes: normalizeNumber(item.like_count),
179
+ comments: normalizeNumber(item.comment_count),
180
+ saves: normalizeNumber(item.favorite_count),
181
+ shares: normalizeNumber(item.share_count),
182
+ url,
183
+ };
184
+ }
185
+
186
+ async function fetchCreatorVideosPage(page, cursor, size) {
187
+ const result = await page.evaluate(buildFetchItemListScript(buildItemListRequest(cursor, size))).catch((error) => {
188
+ throw new CommandExecutionError(`Failed to fetch TikTok Studio item_list: ${getErrorMessage(error)}`);
189
+ });
190
+ if (!result || typeof result !== 'object') {
191
+ throw new CommandExecutionError('TikTok Studio item_list returned an unreadable response');
192
+ }
193
+ if (result.networkError) {
194
+ throw new CommandExecutionError(`TikTok Studio item_list network failure: ${result.networkError}`);
195
+ }
196
+ if (result.status === 401 || result.status === 403) {
197
+ throw new AuthRequiredError('www.tiktok.com', `TikTok Studio item_list requires login (HTTP ${result.status})`);
198
+ }
199
+ if (!result.ok) {
200
+ const detail = result.parseError
201
+ ? `invalid JSON (${result.parseError})`
202
+ : `HTTP ${result.status || 0}${result.statusText ? ` ${result.statusText}` : ''}`;
203
+ throw new CommandExecutionError(`TikTok Studio item_list failed: ${detail}`, result.text ? `Response preview: ${result.text}` : undefined);
204
+ }
205
+ const payload = unwrapPayload(result.data);
206
+ assertApiSuccess(payload);
207
+ return payload;
208
+ }
209
+
210
+ async function listCreatorVideos(page, args) {
211
+ const limit = requirePositiveInt(args.limit, 'limit', DEFAULT_LIMIT, MAX_LIMIT);
212
+ let nextCursor = requireCursor(args.cursor);
213
+ const rows = [];
214
+ let skippedMissingId = 0;
215
+ const pageSize = limit > SERVER_PAGE_MAX ? SERVER_PAGE_MAX : limit;
216
+ const maxPages = Math.ceil(limit / pageSize);
217
+
218
+ await page.goto(STUDIO_CONTENT_URL, { waitUntil: 'load', settleMs: 6000 });
219
+
220
+ for (let pageIndex = 0; pageIndex < maxPages && rows.length < limit; pageIndex += 1) {
221
+ const data = await fetchCreatorVideosPage(page, nextCursor, pageSize);
222
+ const items = Array.isArray(data.item_list) ? data.item_list : [];
223
+ for (const item of items) {
224
+ const row = normalizeRow(item);
225
+ if (!row) {
226
+ skippedMissingId += 1;
227
+ continue;
228
+ }
229
+ rows.push(row);
230
+ if (rows.length >= limit) break;
231
+ }
232
+ if (!data.has_more || items.length === 0) break;
233
+ nextCursor = requireCursor(data.cursor);
234
+ await page.wait(250);
235
+ }
236
+
237
+ if (rows.length === 0 && skippedMissingId > 0) {
238
+ throw new CommandExecutionError('TikTok Studio item_list returned videos without stable video_id');
239
+ }
240
+ if (rows.length === 0) {
241
+ throw new EmptyResultError('tiktok creator-videos', 'No creator videos were returned. Confirm the current Chrome profile is logged in to TikTok Studio and has published content.');
242
+ }
243
+ return rows.slice(0, limit);
244
+ }
245
+
246
+ export const creatorVideosCommand = cli({
247
+ site: 'tiktok',
248
+ name: 'creator-videos',
249
+ access: 'read',
250
+ description: 'TikTok Studio creator content list (views/likes/comments/saves/shares)',
251
+ domain: 'www.tiktok.com',
252
+ strategy: Strategy.COOKIE,
253
+ browser: true,
254
+ navigateBefore: STUDIO_CONTENT_URL,
255
+ args: [
256
+ { name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: `Number of creator videos to return (max ${MAX_LIMIT})` },
257
+ { name: 'cursor', type: 'string', default: '0', help: 'Non-negative TikTok Studio pagination cursor' },
258
+ ],
259
+ columns: ['video_id', 'title', 'date', 'views', 'likes', 'comments', 'saves', 'shares', 'url'],
260
+ func: listCreatorVideos,
261
+ });
262
+
263
+ export const __test__ = {
264
+ buildFetchItemListScript,
265
+ buildItemListRequest,
266
+ extractUsername,
267
+ normalizeRow,
268
+ requireCursor,
269
+ requirePositiveInt,
270
+ };
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { creatorVideosCommand, __test__ } from './creator-videos.js';
4
+
5
+ function makePage(evaluateResults = []) {
6
+ const evaluate = vi.fn();
7
+ for (const result of evaluateResults) {
8
+ evaluate.mockResolvedValueOnce(result);
9
+ }
10
+ evaluate.mockResolvedValue({ ok: true, data: { item_list: [], has_more: false } });
11
+ return {
12
+ goto: vi.fn().mockResolvedValue(undefined),
13
+ wait: vi.fn().mockResolvedValue(undefined),
14
+ evaluate,
15
+ };
16
+ }
17
+
18
+ const apiItem = {
19
+ item_id: '7350000000000000000',
20
+ desc: 'hello\nworld',
21
+ create_time: 1710000000,
22
+ play_count: '123',
23
+ like_count: '12',
24
+ comment_count: '3',
25
+ favorite_count: '4',
26
+ share_count: '5',
27
+ author: { uniqueId: 'creator' },
28
+ };
29
+
30
+ describe('tiktok/creator-videos', () => {
31
+ it('registers a read-only browser command with stable video_id column', () => {
32
+ expect(creatorVideosCommand.access).toBe('read');
33
+ expect(creatorVideosCommand.browser).toBe(true);
34
+ expect(creatorVideosCommand.columns).toEqual([
35
+ 'video_id',
36
+ 'title',
37
+ 'date',
38
+ 'views',
39
+ 'likes',
40
+ 'comments',
41
+ 'saves',
42
+ 'shares',
43
+ 'url',
44
+ ]);
45
+ });
46
+
47
+ it('validates args before navigating', async () => {
48
+ const page = makePage();
49
+
50
+ await expect(creatorVideosCommand.func(page, { limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
51
+ await expect(creatorVideosCommand.func(page, { limit: 251 })).rejects.toBeInstanceOf(ArgumentError);
52
+ await expect(creatorVideosCommand.func(page, { cursor: 'abc' })).rejects.toBeInstanceOf(ArgumentError);
53
+
54
+ expect(page.goto).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it('maps TikTok Studio API rows and keeps video_id even when URL can be built', async () => {
58
+ const page = makePage([
59
+ { ok: true, data: { status_code: 0, status_msg: 'success', item_list: [apiItem], has_more: false } },
60
+ ]);
61
+
62
+ const rows = await creatorVideosCommand.func(page, { limit: 1 });
63
+
64
+ expect(page.goto).toHaveBeenCalledWith('https://www.tiktok.com/tiktokstudio/content', { waitUntil: 'load', settleMs: 6000 });
65
+ expect(rows).toEqual([{
66
+ video_id: '7350000000000000000',
67
+ title: 'hello world',
68
+ date: expect.any(String),
69
+ views: 123,
70
+ likes: 12,
71
+ comments: 3,
72
+ saves: 4,
73
+ shares: 5,
74
+ url: 'https://www.tiktok.com/@creator/video/7350000000000000000',
75
+ }]);
76
+ });
77
+
78
+ it('uses explicit cursor validation for follow-up pages instead of fallback-to-zero', async () => {
79
+ const page = makePage([
80
+ { ok: true, data: { item_list: [apiItem], has_more: true, cursor: 'bad-cursor' } },
81
+ ]);
82
+
83
+ await expect(creatorVideosCommand.func(page, { limit: 51 })).rejects.toBeInstanceOf(ArgumentError);
84
+ });
85
+
86
+ it('maps auth, API, empty, and missing-id states to typed errors', async () => {
87
+ await expect(creatorVideosCommand.func(makePage([
88
+ { ok: false, status: 403, statusText: 'Forbidden' },
89
+ ]), { limit: 1 })).rejects.toBeInstanceOf(AuthRequiredError);
90
+
91
+ await expect(creatorVideosCommand.func(makePage([
92
+ { ok: true, data: { status_code: 1001, status_msg: 'creator permission denied' } },
93
+ ]), { limit: 1 })).rejects.toBeInstanceOf(AuthRequiredError);
94
+
95
+ await expect(creatorVideosCommand.func(makePage([
96
+ { ok: true, data: { status_code: 500, status_msg: 'internal error' } },
97
+ ]), { limit: 1 })).rejects.toBeInstanceOf(CommandExecutionError);
98
+
99
+ await expect(creatorVideosCommand.func(makePage([
100
+ { ok: true, data: { item_list: [], has_more: false } },
101
+ ]), { limit: 1 })).rejects.toBeInstanceOf(EmptyResultError);
102
+
103
+ await expect(creatorVideosCommand.func(makePage([
104
+ { ok: true, data: { item_list: [{ desc: 'missing id' }], has_more: false } },
105
+ ]), { limit: 1 })).rejects.toBeInstanceOf(CommandExecutionError);
106
+ });
107
+
108
+ it('extracts username from TikTok media URLs when author fields are absent', () => {
109
+ expect(__test__.extractUsername({
110
+ play_addr: ['https://example.invalid/video?user_text=test_user&x=1'],
111
+ })).toBe('test_user');
112
+ });
113
+ });