@jackwener/opencli 1.5.5 → 1.5.7

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 (540) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +31 -4
  3. package/README.zh-CN.md +40 -5
  4. package/SKILL.md +1 -1
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +11 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.d.ts +6 -0
  13. package/dist/browser/page.js +37 -2
  14. package/dist/browser/page.test.d.ts +1 -0
  15. package/dist/browser/page.test.js +44 -0
  16. package/dist/browser/stealth.js +198 -0
  17. package/dist/browser/stealth.test.d.ts +1 -0
  18. package/dist/browser/stealth.test.js +134 -0
  19. package/dist/browser.test.js +1 -1
  20. package/dist/build-manifest.d.ts +1 -0
  21. package/dist/build-manifest.js +5 -1
  22. package/dist/build-manifest.test.js +2 -0
  23. package/dist/cli-manifest.json +1821 -252
  24. package/dist/cli.js +20 -3
  25. package/dist/clis/antigravity/serve.d.ts +1 -1
  26. package/dist/clis/antigravity/serve.js +5 -8
  27. package/dist/clis/band/bands.d.ts +1 -0
  28. package/dist/clis/band/bands.js +72 -0
  29. package/dist/clis/band/mentions.d.ts +1 -0
  30. package/dist/clis/band/mentions.js +127 -0
  31. package/dist/clis/band/post.d.ts +1 -0
  32. package/dist/clis/band/post.js +175 -0
  33. package/dist/clis/band/posts.d.ts +1 -0
  34. package/dist/clis/band/posts.js +94 -0
  35. package/dist/clis/bilibili/subtitle.js +4 -0
  36. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  37. package/dist/clis/bilibili/subtitle.test.js +48 -0
  38. package/dist/clis/chatwise/ask.js +0 -2
  39. package/dist/clis/chatwise/export.js +0 -2
  40. package/dist/clis/chatwise/history.js +0 -2
  41. package/dist/clis/chatwise/model.js +0 -2
  42. package/dist/clis/chatwise/new.js +1 -2
  43. package/dist/clis/chatwise/read.js +0 -2
  44. package/dist/clis/chatwise/screenshot.js +1 -2
  45. package/dist/clis/chatwise/send.js +0 -2
  46. package/dist/clis/chatwise/status.js +1 -2
  47. package/dist/clis/ctrip/search.d.ts +13 -0
  48. package/dist/clis/ctrip/search.js +73 -48
  49. package/dist/clis/ctrip/search.test.d.ts +1 -0
  50. package/dist/clis/ctrip/search.test.js +64 -0
  51. package/dist/clis/doubao/detail.d.ts +1 -0
  52. package/dist/clis/doubao/detail.js +33 -0
  53. package/dist/clis/doubao/detail.test.d.ts +1 -0
  54. package/dist/clis/doubao/detail.test.js +42 -0
  55. package/dist/clis/doubao/history.d.ts +1 -0
  56. package/dist/clis/doubao/history.js +28 -0
  57. package/dist/clis/doubao/history.test.d.ts +1 -0
  58. package/dist/clis/doubao/history.test.js +37 -0
  59. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  60. package/dist/clis/doubao/meeting-summary.js +39 -0
  61. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  62. package/dist/clis/doubao/meeting-transcript.js +36 -0
  63. package/dist/clis/doubao/utils.d.ts +27 -0
  64. package/dist/clis/doubao/utils.js +317 -0
  65. package/dist/clis/doubao/utils.test.d.ts +1 -0
  66. package/dist/clis/doubao/utils.test.js +24 -0
  67. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  68. package/dist/clis/douyin/_shared/public-api.js +29 -0
  69. package/dist/clis/douyin/_shared/sts2.js +8 -2
  70. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  71. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  72. package/dist/clis/douyin/activities.js +4 -2
  73. package/dist/clis/douyin/activities.test.js +34 -1
  74. package/dist/clis/douyin/collections.js +1 -1
  75. package/dist/clis/douyin/collections.test.js +24 -2
  76. package/dist/clis/douyin/draft.d.ts +8 -11
  77. package/dist/clis/douyin/draft.js +302 -185
  78. package/dist/clis/douyin/draft.test.d.ts +1 -1
  79. package/dist/clis/douyin/draft.test.js +357 -2
  80. package/dist/clis/douyin/hashtag.js +9 -2
  81. package/dist/clis/douyin/hashtag.test.js +35 -2
  82. package/dist/clis/douyin/profile.js +1 -1
  83. package/dist/clis/douyin/profile.test.js +36 -1
  84. package/dist/clis/douyin/user-videos.d.ts +5 -0
  85. package/dist/clis/douyin/user-videos.js +74 -0
  86. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  87. package/dist/clis/douyin/user-videos.test.js +108 -0
  88. package/dist/clis/douyin/videos.js +22 -5
  89. package/dist/clis/douyin/videos.test.js +45 -2
  90. package/dist/clis/facebook/search.test.d.ts +5 -0
  91. package/dist/clis/facebook/search.test.js +60 -0
  92. package/dist/clis/facebook/search.yaml +4 -3
  93. package/dist/clis/instagram/download.d.ts +16 -0
  94. package/dist/clis/instagram/download.js +225 -0
  95. package/dist/clis/instagram/download.test.d.ts +1 -0
  96. package/dist/clis/instagram/download.test.js +118 -0
  97. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  98. package/dist/clis/notebooklm/bind-current.js +29 -0
  99. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  100. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  101. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  102. package/dist/clis/notebooklm/binding.test.js +44 -0
  103. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  104. package/dist/clis/notebooklm/compat.test.js +16 -0
  105. package/dist/clis/notebooklm/current.d.ts +1 -0
  106. package/dist/clis/notebooklm/current.js +28 -0
  107. package/dist/clis/notebooklm/get.d.ts +1 -0
  108. package/dist/clis/notebooklm/get.js +37 -0
  109. package/dist/clis/notebooklm/history.d.ts +1 -0
  110. package/dist/clis/notebooklm/history.js +25 -0
  111. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  112. package/dist/clis/notebooklm/history.test.js +58 -0
  113. package/dist/clis/notebooklm/list.d.ts +1 -0
  114. package/dist/clis/notebooklm/list.js +35 -0
  115. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  116. package/dist/clis/notebooklm/note-list.js +28 -0
  117. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  118. package/dist/clis/notebooklm/note-list.test.js +56 -0
  119. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  120. package/dist/clis/notebooklm/notes-get.js +47 -0
  121. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  122. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  123. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  124. package/dist/clis/notebooklm/rpc.js +189 -0
  125. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  126. package/dist/clis/notebooklm/rpc.test.js +105 -0
  127. package/dist/clis/notebooklm/shared.d.ts +87 -0
  128. package/dist/clis/notebooklm/shared.js +3 -0
  129. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  130. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  131. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  132. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  133. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  134. package/dist/clis/notebooklm/source-get.js +40 -0
  135. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  136. package/dist/clis/notebooklm/source-get.test.js +84 -0
  137. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  138. package/dist/clis/notebooklm/source-guide.js +44 -0
  139. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  140. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  141. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  142. package/dist/clis/notebooklm/source-list.js +30 -0
  143. package/dist/clis/notebooklm/status.d.ts +1 -0
  144. package/dist/clis/notebooklm/status.js +31 -0
  145. package/dist/clis/notebooklm/summary.d.ts +1 -0
  146. package/dist/clis/notebooklm/summary.js +30 -0
  147. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  148. package/dist/clis/notebooklm/summary.test.js +78 -0
  149. package/dist/clis/notebooklm/utils.d.ts +37 -0
  150. package/dist/clis/notebooklm/utils.js +739 -0
  151. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  152. package/dist/clis/notebooklm/utils.test.js +390 -0
  153. package/dist/clis/ones/common.d.ts +32 -0
  154. package/dist/clis/ones/common.js +144 -0
  155. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  156. package/dist/clis/ones/enrich-tasks.js +37 -0
  157. package/dist/clis/ones/login.d.ts +1 -0
  158. package/dist/clis/ones/login.js +80 -0
  159. package/dist/clis/ones/logout.d.ts +1 -0
  160. package/dist/clis/ones/logout.js +17 -0
  161. package/dist/clis/ones/me.d.ts +1 -0
  162. package/dist/clis/ones/me.js +30 -0
  163. package/dist/clis/ones/my-tasks.d.ts +1 -0
  164. package/dist/clis/ones/my-tasks.js +120 -0
  165. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  166. package/dist/clis/ones/resolve-labels.js +64 -0
  167. package/dist/clis/ones/task-helpers.d.ts +29 -0
  168. package/dist/clis/ones/task-helpers.js +212 -0
  169. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  170. package/dist/clis/ones/task-helpers.test.js +12 -0
  171. package/dist/clis/ones/task.d.ts +1 -0
  172. package/dist/clis/ones/task.js +66 -0
  173. package/dist/clis/ones/tasks.d.ts +1 -0
  174. package/dist/clis/ones/tasks.js +79 -0
  175. package/dist/clis/ones/token-info.d.ts +1 -0
  176. package/dist/clis/ones/token-info.js +42 -0
  177. package/dist/clis/ones/worklog.d.ts +11 -0
  178. package/dist/clis/ones/worklog.js +267 -0
  179. package/dist/clis/ones/worklog.test.d.ts +1 -0
  180. package/dist/clis/ones/worklog.test.js +20 -0
  181. package/dist/clis/spotify/spotify.d.ts +1 -0
  182. package/dist/clis/spotify/spotify.js +316 -0
  183. package/dist/clis/spotify/utils.d.ts +21 -0
  184. package/dist/clis/spotify/utils.js +66 -0
  185. package/dist/clis/spotify/utils.test.d.ts +1 -0
  186. package/dist/clis/spotify/utils.test.js +67 -0
  187. package/dist/clis/substack/utils.d.ts +4 -0
  188. package/dist/clis/substack/utils.js +8 -2
  189. package/dist/clis/substack/utils.test.d.ts +1 -0
  190. package/dist/clis/substack/utils.test.js +46 -0
  191. package/dist/clis/tieba/commands.test.d.ts +4 -0
  192. package/dist/clis/tieba/commands.test.js +79 -0
  193. package/dist/clis/tieba/hot.d.ts +1 -0
  194. package/dist/clis/tieba/hot.js +48 -0
  195. package/dist/clis/tieba/posts.d.ts +1 -0
  196. package/dist/clis/tieba/posts.js +85 -0
  197. package/dist/clis/tieba/read.d.ts +1 -0
  198. package/dist/clis/tieba/read.js +140 -0
  199. package/dist/clis/tieba/search.d.ts +1 -0
  200. package/dist/clis/tieba/search.js +108 -0
  201. package/dist/clis/tieba/utils.d.ts +101 -0
  202. package/dist/clis/tieba/utils.js +240 -0
  203. package/dist/clis/tieba/utils.test.d.ts +1 -0
  204. package/dist/clis/tieba/utils.test.js +290 -0
  205. package/dist/clis/v2ex/hot.yaml +4 -1
  206. package/dist/clis/v2ex/latest.yaml +4 -1
  207. package/dist/clis/v2ex/topic.yaml +6 -1
  208. package/dist/clis/weixin/download.d.ts +9 -0
  209. package/dist/clis/weixin/download.js +76 -6
  210. package/dist/clis/weread/book.js +206 -13
  211. package/dist/clis/weread/commands.test.js +331 -0
  212. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  213. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  214. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  215. package/dist/clis/weread/search-regression.test.js +407 -0
  216. package/dist/clis/weread/search.js +143 -7
  217. package/dist/clis/weread/shelf.js +13 -95
  218. package/dist/clis/weread/utils.d.ts +56 -0
  219. package/dist/clis/weread/utils.js +234 -7
  220. package/dist/clis/weread/utils.test.js +71 -1
  221. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  222. package/dist/clis/xiaohongshu/comments.js +76 -17
  223. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  224. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  225. package/dist/clis/xiaohongshu/download.js +83 -22
  226. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  227. package/dist/clis/xiaohongshu/download.test.js +75 -0
  228. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  229. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  230. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  231. package/dist/clis/xiaohongshu/note.js +76 -0
  232. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  233. package/dist/clis/xiaohongshu/note.test.js +136 -0
  234. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  235. package/dist/clis/xiaohongshu/publish.js +78 -31
  236. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  237. package/dist/clis/xiaohongshu/search.js +9 -0
  238. package/dist/clis/xiaohongshu/search.test.js +10 -4
  239. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  240. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  241. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  242. package/dist/clis/xueqiu/comments.d.ts +118 -0
  243. package/dist/clis/xueqiu/comments.js +354 -0
  244. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  245. package/dist/clis/xueqiu/comments.test.js +696 -0
  246. package/dist/clis/youtube/search.js +57 -17
  247. package/dist/clis/youtube/transcript.js +2 -4
  248. package/dist/clis/youtube/utils.d.ts +9 -0
  249. package/dist/clis/youtube/utils.js +67 -3
  250. package/dist/clis/youtube/utils.test.d.ts +1 -0
  251. package/dist/clis/youtube/utils.test.js +37 -0
  252. package/dist/clis/youtube/video.js +16 -15
  253. package/dist/clis/zhihu/question.js +19 -17
  254. package/dist/clis/zhihu/question.test.d.ts +1 -0
  255. package/dist/clis/zhihu/question.test.js +54 -0
  256. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  257. package/dist/clis/zsxq/dynamics.js +47 -0
  258. package/dist/clis/zsxq/groups.d.ts +1 -0
  259. package/dist/clis/zsxq/groups.js +32 -0
  260. package/dist/clis/zsxq/search.d.ts +1 -0
  261. package/dist/clis/zsxq/search.js +43 -0
  262. package/dist/clis/zsxq/search.test.d.ts +1 -0
  263. package/dist/clis/zsxq/search.test.js +24 -0
  264. package/dist/clis/zsxq/topic.d.ts +1 -0
  265. package/dist/clis/zsxq/topic.js +47 -0
  266. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  267. package/dist/clis/zsxq/topic.test.js +29 -0
  268. package/dist/clis/zsxq/topics.d.ts +1 -0
  269. package/dist/clis/zsxq/topics.js +25 -0
  270. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  271. package/dist/clis/zsxq/topics.test.js +24 -0
  272. package/dist/clis/zsxq/utils.d.ts +97 -0
  273. package/dist/clis/zsxq/utils.js +230 -0
  274. package/dist/commanderAdapter.js +10 -1
  275. package/dist/commanderAdapter.test.js +64 -0
  276. package/dist/commands/daemon.d.ts +9 -0
  277. package/dist/commands/daemon.js +124 -0
  278. package/dist/commands/daemon.test.d.ts +1 -0
  279. package/dist/commands/daemon.test.js +185 -0
  280. package/dist/completion.js +3 -1
  281. package/dist/constants.d.ts +2 -0
  282. package/dist/constants.js +2 -0
  283. package/dist/daemon.d.ts +1 -1
  284. package/dist/daemon.js +25 -14
  285. package/dist/daemon.test.d.ts +1 -0
  286. package/dist/daemon.test.js +65 -0
  287. package/dist/discovery.d.ts +9 -0
  288. package/dist/discovery.js +47 -2
  289. package/dist/electron-apps.d.ts +29 -0
  290. package/dist/electron-apps.js +65 -0
  291. package/dist/electron-apps.test.d.ts +1 -0
  292. package/dist/electron-apps.test.js +43 -0
  293. package/dist/engine.test.js +41 -9
  294. package/dist/execution.js +20 -16
  295. package/dist/external-clis.yaml +17 -0
  296. package/dist/idle-manager.d.ts +19 -0
  297. package/dist/idle-manager.js +54 -0
  298. package/dist/launcher.d.ts +36 -0
  299. package/dist/launcher.js +152 -0
  300. package/dist/launcher.test.d.ts +1 -0
  301. package/dist/launcher.test.js +57 -0
  302. package/dist/main.js +3 -3
  303. package/dist/registry.d.ts +1 -0
  304. package/dist/registry.js +31 -3
  305. package/dist/registry.test.js +13 -0
  306. package/dist/runtime.d.ts +5 -3
  307. package/dist/runtime.js +12 -5
  308. package/dist/serialization.d.ts +1 -0
  309. package/dist/serialization.js +3 -0
  310. package/dist/serialization.test.js +17 -1
  311. package/dist/tui.d.ts +7 -0
  312. package/dist/tui.js +52 -0
  313. package/dist/tui.test.d.ts +1 -0
  314. package/dist/tui.test.js +19 -0
  315. package/dist/types.d.ts +5 -0
  316. package/dist/weixin-download.test.js +14 -0
  317. package/docs/.vitepress/config.mts +4 -0
  318. package/docs/adapters/browser/band.md +63 -0
  319. package/docs/adapters/browser/notebooklm.md +69 -0
  320. package/docs/adapters/browser/ones.md +59 -0
  321. package/docs/adapters/browser/spotify.md +62 -0
  322. package/docs/adapters/browser/tieba.md +45 -0
  323. package/docs/adapters/browser/xiaohongshu.md +19 -10
  324. package/docs/adapters/browser/xueqiu.md +5 -0
  325. package/docs/adapters/browser/zsxq.md +49 -0
  326. package/docs/adapters/index.md +67 -63
  327. package/docs/adapters-doc/ones.md +32 -0
  328. package/docs/guide/browser-bridge.md +12 -0
  329. package/docs/guide/troubleshooting.md +9 -4
  330. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  331. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  332. package/docs/zh/guide/browser-bridge.md +12 -0
  333. package/extension/dist/background.js +794 -513
  334. package/extension/src/background.test.ts +202 -2
  335. package/extension/src/background.ts +189 -10
  336. package/extension/src/cdp.ts +54 -0
  337. package/extension/src/protocol.ts +11 -5
  338. package/package.json +1 -1
  339. package/scripts/postinstall.js +16 -0
  340. package/src/browser/cdp.ts +24 -17
  341. package/src/browser/daemon-client.ts +11 -1
  342. package/src/browser/dom-helpers.test.ts +15 -1
  343. package/src/browser/dom-helpers.ts +1 -0
  344. package/src/browser/mcp.ts +18 -13
  345. package/src/browser/page.test.ts +58 -0
  346. package/src/browser/page.ts +34 -2
  347. package/src/browser/stealth.test.ts +153 -0
  348. package/src/browser/stealth.ts +198 -0
  349. package/src/browser.test.ts +1 -1
  350. package/src/build-manifest.test.ts +2 -0
  351. package/src/build-manifest.ts +6 -1
  352. package/src/cli.ts +21 -3
  353. package/src/clis/antigravity/SKILL.md +3 -12
  354. package/src/clis/antigravity/serve.ts +5 -10
  355. package/src/clis/band/bands.ts +76 -0
  356. package/src/clis/band/mentions.ts +134 -0
  357. package/src/clis/band/post.ts +187 -0
  358. package/src/clis/band/posts.ts +106 -0
  359. package/src/clis/bilibili/subtitle.test.ts +60 -0
  360. package/src/clis/bilibili/subtitle.ts +4 -0
  361. package/src/clis/chatwise/ask.ts +0 -2
  362. package/src/clis/chatwise/export.ts +0 -2
  363. package/src/clis/chatwise/history.ts +0 -2
  364. package/src/clis/chatwise/model.ts +0 -2
  365. package/src/clis/chatwise/new.ts +1 -2
  366. package/src/clis/chatwise/read.ts +0 -2
  367. package/src/clis/chatwise/screenshot.ts +1 -2
  368. package/src/clis/chatwise/send.ts +0 -2
  369. package/src/clis/chatwise/status.ts +1 -2
  370. package/src/clis/ctrip/search.test.ts +73 -0
  371. package/src/clis/ctrip/search.ts +97 -47
  372. package/src/clis/doubao/detail.test.ts +53 -0
  373. package/src/clis/doubao/detail.ts +41 -0
  374. package/src/clis/doubao/history.test.ts +45 -0
  375. package/src/clis/doubao/history.ts +32 -0
  376. package/src/clis/doubao/meeting-summary.ts +53 -0
  377. package/src/clis/doubao/meeting-transcript.ts +48 -0
  378. package/src/clis/doubao/utils.test.ts +45 -0
  379. package/src/clis/doubao/utils.ts +371 -0
  380. package/src/clis/douyin/_shared/public-api.ts +84 -0
  381. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  382. package/src/clis/douyin/_shared/sts2.ts +11 -3
  383. package/src/clis/douyin/activities.test.ts +41 -1
  384. package/src/clis/douyin/activities.ts +12 -3
  385. package/src/clis/douyin/collections.test.ts +35 -2
  386. package/src/clis/douyin/collections.ts +1 -1
  387. package/src/clis/douyin/draft.test.ts +444 -2
  388. package/src/clis/douyin/draft.ts +382 -218
  389. package/src/clis/douyin/hashtag.test.ts +42 -2
  390. package/src/clis/douyin/hashtag.ts +11 -3
  391. package/src/clis/douyin/profile.test.ts +43 -1
  392. package/src/clis/douyin/profile.ts +9 -2
  393. package/src/clis/douyin/user-videos.test.ts +122 -0
  394. package/src/clis/douyin/user-videos.ts +101 -0
  395. package/src/clis/douyin/videos.test.ts +52 -2
  396. package/src/clis/douyin/videos.ts +49 -15
  397. package/src/clis/facebook/search.test.ts +70 -0
  398. package/src/clis/facebook/search.yaml +4 -3
  399. package/src/clis/instagram/download.test.ts +159 -0
  400. package/src/clis/instagram/download.ts +286 -0
  401. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  402. package/src/clis/notebooklm/bind-current.ts +36 -0
  403. package/src/clis/notebooklm/binding.test.ts +53 -0
  404. package/src/clis/notebooklm/compat.test.ts +19 -0
  405. package/src/clis/notebooklm/current.ts +38 -0
  406. package/src/clis/notebooklm/get.ts +53 -0
  407. package/src/clis/notebooklm/history.test.ts +70 -0
  408. package/src/clis/notebooklm/history.ts +36 -0
  409. package/src/clis/notebooklm/list.ts +40 -0
  410. package/src/clis/notebooklm/note-list.test.ts +64 -0
  411. package/src/clis/notebooklm/note-list.ts +42 -0
  412. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  413. package/src/clis/notebooklm/notes-get.ts +67 -0
  414. package/src/clis/notebooklm/rpc.test.ts +126 -0
  415. package/src/clis/notebooklm/rpc.ts +286 -0
  416. package/src/clis/notebooklm/shared.ts +98 -0
  417. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  418. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  419. package/src/clis/notebooklm/source-get.test.ts +100 -0
  420. package/src/clis/notebooklm/source-get.ts +60 -0
  421. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  422. package/src/clis/notebooklm/source-guide.ts +69 -0
  423. package/src/clis/notebooklm/source-list.ts +45 -0
  424. package/src/clis/notebooklm/status.ts +34 -0
  425. package/src/clis/notebooklm/summary.test.ts +94 -0
  426. package/src/clis/notebooklm/summary.ts +45 -0
  427. package/src/clis/notebooklm/utils.test.ts +446 -0
  428. package/src/clis/notebooklm/utils.ts +893 -0
  429. package/src/clis/ones/common.ts +187 -0
  430. package/src/clis/ones/enrich-tasks.ts +47 -0
  431. package/src/clis/ones/login.ts +103 -0
  432. package/src/clis/ones/logout.ts +19 -0
  433. package/src/clis/ones/me.ts +34 -0
  434. package/src/clis/ones/my-tasks.ts +148 -0
  435. package/src/clis/ones/resolve-labels.ts +80 -0
  436. package/src/clis/ones/task-helpers.test.ts +14 -0
  437. package/src/clis/ones/task-helpers.ts +214 -0
  438. package/src/clis/ones/task.ts +79 -0
  439. package/src/clis/ones/tasks.ts +92 -0
  440. package/src/clis/ones/token-info.ts +46 -0
  441. package/src/clis/ones/worklog.test.ts +24 -0
  442. package/src/clis/ones/worklog.ts +306 -0
  443. package/src/clis/spotify/spotify.ts +328 -0
  444. package/src/clis/spotify/utils.test.ts +87 -0
  445. package/src/clis/spotify/utils.ts +92 -0
  446. package/src/clis/substack/utils.test.ts +54 -0
  447. package/src/clis/substack/utils.ts +10 -2
  448. package/src/clis/tieba/commands.test.ts +86 -0
  449. package/src/clis/tieba/hot.ts +52 -0
  450. package/src/clis/tieba/posts.ts +108 -0
  451. package/src/clis/tieba/read.ts +158 -0
  452. package/src/clis/tieba/search.ts +119 -0
  453. package/src/clis/tieba/utils.test.ts +322 -0
  454. package/src/clis/tieba/utils.ts +348 -0
  455. package/src/clis/v2ex/hot.yaml +4 -1
  456. package/src/clis/v2ex/latest.yaml +4 -1
  457. package/src/clis/v2ex/topic.yaml +6 -1
  458. package/src/clis/weixin/download.ts +95 -6
  459. package/src/clis/weread/book.ts +256 -13
  460. package/src/clis/weread/commands.test.ts +409 -0
  461. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  462. package/src/clis/weread/search-regression.test.ts +440 -0
  463. package/src/clis/weread/search.ts +189 -9
  464. package/src/clis/weread/shelf.ts +20 -122
  465. package/src/clis/weread/utils.test.ts +81 -1
  466. package/src/clis/weread/utils.ts +293 -7
  467. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  468. package/src/clis/xiaohongshu/comments.ts +76 -17
  469. package/src/clis/xiaohongshu/download.test.ts +96 -0
  470. package/src/clis/xiaohongshu/download.ts +83 -22
  471. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  472. package/src/clis/xiaohongshu/note.test.ts +164 -0
  473. package/src/clis/xiaohongshu/note.ts +86 -0
  474. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  475. package/src/clis/xiaohongshu/publish.ts +84 -30
  476. package/src/clis/xiaohongshu/search.test.ts +11 -4
  477. package/src/clis/xiaohongshu/search.ts +13 -0
  478. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  479. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  480. package/src/clis/xueqiu/comments.test.ts +823 -0
  481. package/src/clis/xueqiu/comments.ts +461 -0
  482. package/src/clis/youtube/search.ts +57 -17
  483. package/src/clis/youtube/transcript.ts +2 -4
  484. package/src/clis/youtube/utils.test.ts +43 -0
  485. package/src/clis/youtube/utils.ts +69 -0
  486. package/src/clis/youtube/video.ts +16 -15
  487. package/src/clis/zhihu/question.test.ts +71 -0
  488. package/src/clis/zhihu/question.ts +27 -15
  489. package/src/clis/zsxq/dynamics.ts +60 -0
  490. package/src/clis/zsxq/groups.ts +41 -0
  491. package/src/clis/zsxq/search.test.ts +29 -0
  492. package/src/clis/zsxq/search.ts +54 -0
  493. package/src/clis/zsxq/topic.test.ts +34 -0
  494. package/src/clis/zsxq/topic.ts +68 -0
  495. package/src/clis/zsxq/topics.test.ts +29 -0
  496. package/src/clis/zsxq/topics.ts +36 -0
  497. package/src/clis/zsxq/utils.ts +351 -0
  498. package/src/commanderAdapter.test.ts +77 -0
  499. package/src/commanderAdapter.ts +8 -1
  500. package/src/commands/daemon.test.ts +238 -0
  501. package/src/commands/daemon.ts +135 -0
  502. package/src/completion.ts +2 -1
  503. package/src/constants.ts +3 -0
  504. package/src/daemon.test.ts +88 -0
  505. package/src/daemon.ts +26 -14
  506. package/src/discovery.ts +52 -2
  507. package/src/electron-apps.test.ts +50 -0
  508. package/src/electron-apps.ts +89 -0
  509. package/src/engine.test.ts +45 -9
  510. package/src/execution.ts +24 -19
  511. package/src/external-clis.yaml +17 -0
  512. package/src/idle-manager.ts +60 -0
  513. package/src/launcher.test.ts +67 -0
  514. package/src/launcher.ts +185 -0
  515. package/src/main.ts +3 -2
  516. package/src/registry.test.ts +15 -0
  517. package/src/registry.ts +32 -3
  518. package/src/runtime.ts +13 -7
  519. package/src/serialization.test.ts +19 -1
  520. package/src/serialization.ts +2 -0
  521. package/src/tui.test.ts +23 -0
  522. package/src/tui.ts +65 -0
  523. package/src/types.ts +5 -0
  524. package/src/weixin-download.test.ts +27 -0
  525. package/tests/e2e/band-auth.test.ts +20 -0
  526. package/tests/e2e/browser-auth-helpers.ts +18 -0
  527. package/tests/e2e/browser-auth.test.ts +35 -47
  528. package/tests/e2e/browser-public-extended.test.ts +6 -2
  529. package/tests/e2e/browser-public.test.ts +288 -0
  530. package/tests/e2e/management.test.ts +1 -1
  531. package/tests/e2e/plugin-management.test.ts +1 -1
  532. package/vitest.config.ts +1 -0
  533. package/chatwise-opencli.ps1 +0 -82
  534. package/dist/clis/chatwise/shared.d.ts +0 -2
  535. package/dist/clis/chatwise/shared.js +0 -6
  536. package/dist/weread-private-api-regression.test.d.ts +0 -1
  537. package/dist/weread-search-regression.test.d.ts +0 -1
  538. package/dist/weread-search-regression.test.js +0 -39
  539. package/src/clis/chatwise/shared.ts +0 -8
  540. package/src/weread-search-regression.test.ts +0 -44
@@ -11,32 +11,60 @@ cli({
11
11
  args: [
12
12
  { name: 'query', required: true, positional: true, help: 'Search query' },
13
13
  { name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
14
+ { name: 'type', default: '', help: 'Filter type: shorts, video, channel, playlist' },
15
+ { name: 'upload', default: '', help: 'Upload date: hour, today, week, month, year' },
16
+ { name: 'sort', default: '', help: 'Sort by: relevance, date, views, rating' },
14
17
  ],
15
- columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'],
18
+ columns: ['rank', 'title', 'channel', 'views', 'duration', 'published', 'url'],
16
19
  func: async (page, kwargs) => {
17
20
  const limit = Math.min(kwargs.limit || 20, 50);
18
- await page.goto('https://www.youtube.com');
19
- await page.wait(2);
21
+ const query = encodeURIComponent(kwargs.query);
22
+ // Build search URL with filter params
23
+ // YouTube uses sp= parameter for filters — we use the URL approach for reliability
24
+ const spMap = {
25
+ // type filters
26
+ 'shorts': 'EgIQCQ%3D%3D', // Shorts (type=9)
27
+ 'video': 'EgIQAQ%3D%3D',
28
+ 'channel': 'EgIQAg%3D%3D',
29
+ 'playlist': 'EgIQAw%3D%3D',
30
+ // upload date filters (can be combined with type via URL)
31
+ 'hour': 'EgIIAQ%3D%3D',
32
+ 'today': 'EgIIAg%3D%3D',
33
+ 'week': 'EgIIAw%3D%3D',
34
+ 'month': 'EgIIBA%3D%3D',
35
+ 'year': 'EgIIBQ%3D%3D',
36
+ };
37
+ const sortMap = {
38
+ 'date': 'CAI%3D',
39
+ 'views': 'CAM%3D',
40
+ 'rating': 'CAE%3D',
41
+ };
42
+ // YouTube only supports a single sp= parameter — pick the most specific filter.
43
+ // Priority: type > upload > sort (type is the most common use case)
44
+ let sp = '';
45
+ if (kwargs.type && spMap[kwargs.type])
46
+ sp = spMap[kwargs.type];
47
+ else if (kwargs.upload && spMap[kwargs.upload])
48
+ sp = spMap[kwargs.upload];
49
+ else if (kwargs.sort && sortMap[kwargs.sort])
50
+ sp = sortMap[kwargs.sort];
51
+ let url = `https://www.youtube.com/results?search_query=${query}`;
52
+ if (sp)
53
+ url += `&sp=${sp}`;
54
+ await page.goto(url);
55
+ await page.wait(3);
20
56
  const data = await page.evaluate(`
21
57
  (async () => {
22
- const cfg = window.ytcfg?.data_ || {};
23
- const apiKey = cfg.INNERTUBE_API_KEY;
24
- const context = cfg.INNERTUBE_CONTEXT;
25
- if (!apiKey || !context) return {error: 'YouTube config not found'};
58
+ const data = window.ytInitialData;
59
+ if (!data) return {error: 'YouTube data not found'};
26
60
 
27
- const resp = await fetch('/youtubei/v1/search?key=' + apiKey + '&prettyPrint=false', {
28
- method: 'POST', credentials: 'include',
29
- headers: {'Content-Type': 'application/json'},
30
- body: JSON.stringify({context, query: '${kwargs.query.replace(/'/g, "\\'")}'})
31
- });
32
- if (!resp.ok) return {error: 'HTTP ' + resp.status};
33
-
34
- const data = await resp.json();
35
61
  const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
36
62
  const videos = [];
37
63
  for (const section of contents) {
38
- for (const item of (section.itemSectionRenderer?.contents || [])) {
39
- if (item.videoRenderer && videos.length < ${limit}) {
64
+ const items = section.itemSectionRenderer?.contents || section.reelShelfRenderer?.items || [];
65
+ for (const item of items) {
66
+ if (videos.length >= ${limit}) break;
67
+ if (item.videoRenderer) {
40
68
  const v = item.videoRenderer;
41
69
  videos.push({
42
70
  rank: videos.length + 1,
@@ -44,8 +72,20 @@ cli({
44
72
  channel: v.ownerText?.runs?.[0]?.text || '',
45
73
  views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '',
46
74
  duration: v.lengthText?.simpleText || 'LIVE',
75
+ published: v.publishedTimeText?.simpleText || '',
47
76
  url: 'https://www.youtube.com/watch?v=' + v.videoId
48
77
  });
78
+ } else if (item.reelItemRenderer) {
79
+ const r = item.reelItemRenderer;
80
+ videos.push({
81
+ rank: videos.length + 1,
82
+ title: r.headline?.simpleText || '',
83
+ channel: r.navigationEndpoint?.reelWatchEndpoint?.overlay?.reelPlayerOverlayRenderer?.reelPlayerHeaderSupportedRenderers?.reelPlayerHeaderRenderer?.channelTitleText?.runs?.[0]?.text || '',
84
+ views: r.viewCountText?.simpleText || '',
85
+ duration: 'SHORT',
86
+ published: r.publishedTimeText?.simpleText || '',
87
+ url: 'https://www.youtube.com/shorts/' + r.videoId
88
+ });
49
89
  }
50
90
  }
51
91
  }
@@ -10,7 +10,7 @@
10
10
  * --mode raw: every caption segment as-is with precise timestamps
11
11
  */
12
12
  import { cli, Strategy } from '../../registry.js';
13
- import { parseVideoId } from './utils.js';
13
+ import { parseVideoId, prepareYoutubeApiPage } from './utils.js';
14
14
  import { groupTranscriptSegments, formatGroupedTranscript, } from './transcript-group.js';
15
15
  import { CommandExecutionError, EmptyResultError } from '../../errors.js';
16
16
  cli({
@@ -28,9 +28,7 @@ cli({
28
28
  // so we let the renderer auto-detect columns from the data keys.
29
29
  func: async (page, kwargs) => {
30
30
  const videoId = parseVideoId(kwargs.url);
31
- const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
32
- await page.goto(videoUrl);
33
- await page.wait(3);
31
+ await prepareYoutubeApiPage(page);
34
32
  const lang = kwargs.lang || '';
35
33
  const mode = kwargs.mode || 'grouped';
36
34
  // Step 1: Get caption track URL via Android InnerTube API
@@ -1,8 +1,17 @@
1
1
  /**
2
2
  * Shared YouTube utilities — URL parsing, video ID extraction, etc.
3
3
  */
4
+ import type { IPage } from '../../types.js';
4
5
  /**
5
6
  * Extract a YouTube video ID from a URL or bare video ID string.
6
7
  * Supports: watch?v=, youtu.be/, /shorts/, /embed/, /live/, /v/
7
8
  */
8
9
  export declare function parseVideoId(input: string): string;
10
+ /**
11
+ * Extract a JSON object assigned to a known bootstrap variable inside YouTube HTML.
12
+ */
13
+ export declare function extractJsonAssignmentFromHtml(html: string, keys: string | string[]): Record<string, unknown> | null;
14
+ /**
15
+ * Prepare a quiet YouTube API-capable page without opening the watch UI.
16
+ */
17
+ export declare function prepareYoutubeApiPage(page: IPage): Promise<void>;
@@ -1,6 +1,3 @@
1
- /**
2
- * Shared YouTube utilities — URL parsing, video ID extraction, etc.
3
- */
4
1
  /**
5
2
  * Extract a YouTube video ID from a URL or bare video ID string.
6
3
  * Supports: watch?v=, youtu.be/, /shorts/, /embed/, /live/, /v/
@@ -26,3 +23,70 @@ export function parseVideoId(input) {
26
23
  }
27
24
  return input;
28
25
  }
26
+ /**
27
+ * Extract a JSON object assigned to a known bootstrap variable inside YouTube HTML.
28
+ */
29
+ export function extractJsonAssignmentFromHtml(html, keys) {
30
+ const candidates = Array.isArray(keys) ? keys : [keys];
31
+ for (const key of candidates) {
32
+ const markers = [
33
+ `var ${key} = `,
34
+ `window["${key}"] = `,
35
+ `window.${key} = `,
36
+ `${key} = `,
37
+ ];
38
+ for (const marker of markers) {
39
+ const markerIndex = html.indexOf(marker);
40
+ if (markerIndex === -1)
41
+ continue;
42
+ const jsonStart = html.indexOf('{', markerIndex + marker.length);
43
+ if (jsonStart === -1)
44
+ continue;
45
+ let depth = 0;
46
+ let inString = false;
47
+ let escaping = false;
48
+ for (let i = jsonStart; i < html.length; i += 1) {
49
+ const ch = html[i];
50
+ if (inString) {
51
+ if (escaping) {
52
+ escaping = false;
53
+ }
54
+ else if (ch === '\\') {
55
+ escaping = true;
56
+ }
57
+ else if (ch === '"') {
58
+ inString = false;
59
+ }
60
+ continue;
61
+ }
62
+ if (ch === '"') {
63
+ inString = true;
64
+ continue;
65
+ }
66
+ if (ch === '{') {
67
+ depth += 1;
68
+ continue;
69
+ }
70
+ if (ch === '}') {
71
+ depth -= 1;
72
+ if (depth === 0) {
73
+ try {
74
+ return JSON.parse(html.slice(jsonStart, i + 1));
75
+ }
76
+ catch {
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ /**
87
+ * Prepare a quiet YouTube API-capable page without opening the watch UI.
88
+ */
89
+ export async function prepareYoutubeApiPage(page) {
90
+ await page.goto('https://www.youtube.com', { waitUntil: 'none' });
91
+ await page.wait(2);
92
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { extractJsonAssignmentFromHtml, prepareYoutubeApiPage } from './utils.js';
3
+ describe('youtube utils', () => {
4
+ it('extractJsonAssignmentFromHtml parses bootstrap objects with nested braces in strings', () => {
5
+ const html = `
6
+ <script>
7
+ var ytInitialPlayerResponse = {
8
+ "title": "brace { inside } string",
9
+ "nested": { "count": 2, "text": "quote \\"value\\"" }
10
+ };
11
+ </script>
12
+ `;
13
+ expect(extractJsonAssignmentFromHtml(html, 'ytInitialPlayerResponse')).toEqual({
14
+ title: 'brace { inside } string',
15
+ nested: { count: 2, text: 'quote "value"' },
16
+ });
17
+ });
18
+ it('extractJsonAssignmentFromHtml supports window assignments', () => {
19
+ const html = `
20
+ <script>
21
+ window["ytInitialData"] = {"contents":{"items":[1,2,3]}};
22
+ </script>
23
+ `;
24
+ expect(extractJsonAssignmentFromHtml(html, 'ytInitialData')).toEqual({
25
+ contents: { items: [1, 2, 3] },
26
+ });
27
+ });
28
+ it('prepareYoutubeApiPage loads the quiet API bootstrap page', async () => {
29
+ const page = {
30
+ goto: vi.fn().mockResolvedValue(undefined),
31
+ wait: vi.fn().mockResolvedValue(undefined),
32
+ };
33
+ await expect(prepareYoutubeApiPage(page)).resolves.toBeUndefined();
34
+ expect(page.goto).toHaveBeenCalledWith('https://www.youtube.com', { waitUntil: 'none' });
35
+ expect(page.wait).toHaveBeenCalledWith(2);
36
+ });
37
+ });
@@ -1,8 +1,8 @@
1
1
  /**
2
- * YouTube video metadata — read ytInitialPlayerResponse + ytInitialData from video page.
2
+ * YouTube video metadata — fetch watch HTML and parse bootstrap data without opening the watch UI.
3
3
  */
4
4
  import { cli, Strategy } from '../../registry.js';
5
- import { parseVideoId } from './utils.js';
5
+ import { extractJsonAssignmentFromHtml, parseVideoId, prepareYoutubeApiPage } from './utils.js';
6
6
  import { CommandExecutionError } from '../../errors.js';
7
7
  cli({
8
8
  site: 'youtube',
@@ -16,23 +16,28 @@ cli({
16
16
  columns: ['field', 'value'],
17
17
  func: async (page, kwargs) => {
18
18
  const videoId = parseVideoId(kwargs.url);
19
- const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
20
- await page.goto(videoUrl);
21
- await page.wait(3);
19
+ await prepareYoutubeApiPage(page);
22
20
  const data = await page.evaluate(`
23
21
  (async () => {
24
- const player = window.ytInitialPlayerResponse;
25
- const yt = window.ytInitialData;
26
- if (!player) return { error: 'ytInitialPlayerResponse not found' };
22
+ const extractJsonAssignmentFromHtml = ${extractJsonAssignmentFromHtml.toString()};
23
+
24
+ const watchResp = await fetch('/watch?v=' + encodeURIComponent(${JSON.stringify(videoId)}), {
25
+ credentials: 'include',
26
+ });
27
+ if (!watchResp.ok) return { error: 'Watch HTML returned HTTP ' + watchResp.status };
28
+
29
+ const html = await watchResp.text();
30
+ const player = extractJsonAssignmentFromHtml(html, 'ytInitialPlayerResponse');
31
+ const yt = extractJsonAssignmentFromHtml(html, 'ytInitialData');
32
+ if (!player) return { error: 'ytInitialPlayerResponse not found in watch HTML' };
27
33
 
28
34
  const details = player.videoDetails || {};
29
35
  const microformat = player.microformat?.playerMicroformatRenderer || {};
36
+ const contents = yt?.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
30
37
 
31
- // Try to get full description from ytInitialData
38
+ // Try to get full description from watch bootstrap data
32
39
  let fullDescription = details.shortDescription || '';
33
40
  try {
34
- const contents = yt?.contents?.twoColumnWatchNextResults
35
- ?.results?.results?.contents;
36
41
  if (contents) {
37
42
  for (const c of contents) {
38
43
  const desc = c.videoSecondaryInfoRenderer?.attributedDescription?.content;
@@ -44,8 +49,6 @@ cli({
44
49
  // Get like count if available
45
50
  let likes = '';
46
51
  try {
47
- const contents = yt?.contents?.twoColumnWatchNextResults
48
- ?.results?.results?.contents;
49
52
  if (contents) {
50
53
  for (const c of contents) {
51
54
  const buttons = c.videoPrimaryInfoRenderer?.videoActions
@@ -73,8 +76,6 @@ cli({
73
76
  // Get channel subscriber count if available
74
77
  let subscribers = '';
75
78
  try {
76
- const contents = yt?.contents?.twoColumnWatchNextResults
77
- ?.results?.results?.contents;
78
79
  if (contents) {
79
80
  for (const c of contents) {
80
81
  const owner = c.videoSecondaryInfoRenderer?.owner
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { AuthRequiredError } from '../../errors.js';
2
+ import { AuthRequiredError, CliError } from '../../errors.js';
3
3
  cli({
4
4
  site: 'zhihu',
5
5
  name: 'question',
@@ -13,23 +13,25 @@ cli({
13
13
  columns: ['rank', 'author', 'votes', 'content'],
14
14
  func: async (page, kwargs) => {
15
15
  const { id, limit = 5 } = kwargs;
16
+ const answerLimit = Number(limit);
16
17
  const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').trim();
17
- // Fetch question detail and answers in parallel via evaluate
18
- const result = await page.evaluate(`
19
- async () => {
20
- const [qResp, aResp] = await Promise.all([
21
- fetch('https://www.zhihu.com/api/v4/questions/${id}?include=data[*].detail,excerpt,answer_count,follower_count,visit_count', {credentials: 'include'}),
22
- fetch('https://www.zhihu.com/api/v4/questions/${id}/answers?limit=${limit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author', {credentials: 'include'})
23
- ]);
24
- if (!qResp.ok || !aResp.ok) return { error: true };
25
- const q = await qResp.json();
26
- const a = await aResp.json();
27
- return { question: q, answers: a.data || [] };
28
- }
29
- `);
30
- if (!result || result.error)
31
- throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
32
- const answers = (result.answers ?? []).slice(0, Number(limit)).map((a, i) => ({
18
+ // Only fetch answers here. The question detail endpoint is not used by the
19
+ // current CLI output and can fail independently, which would incorrectly
20
+ // turn a successful answers response into a login error.
21
+ const result = await page.evaluate(async ({ questionId, answerLimit }) => {
22
+ const aResp = await fetch(`https://www.zhihu.com/api/v4/questions/${questionId}/answers?limit=${answerLimit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author`, { credentials: 'include' });
23
+ if (!aResp.ok)
24
+ return { ok: false, status: aResp.status };
25
+ const a = await aResp.json();
26
+ return { ok: true, answers: Array.isArray(a?.data) ? a.data : [] };
27
+ }, { questionId: String(id), answerLimit });
28
+ if (!result?.ok) {
29
+ if (result?.status === 401 || result?.status === 403) {
30
+ throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
31
+ }
32
+ throw new CliError('FETCH_ERROR', `Zhihu question answers request failed with HTTP ${result?.status ?? 'unknown'}`, 'Try again later or rerun with -v for more detail');
33
+ }
34
+ const answers = result.answers.slice(0, answerLimit).map((a, i) => ({
33
35
  rank: i + 1,
34
36
  author: a.author?.name ?? 'anonymous',
35
37
  votes: a.voteup_count ?? 0,
@@ -0,0 +1 @@
1
+ import './question.js';
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import { AuthRequiredError } from '../../errors.js';
4
+ import './question.js';
5
+ describe('zhihu question', () => {
6
+ it('returns answers even when the unused question detail request fails', async () => {
7
+ const cmd = getRegistry().get('zhihu/question');
8
+ expect(cmd?.func).toBeTypeOf('function');
9
+ const evaluate = vi.fn().mockImplementation(async (_fn, args) => {
10
+ expect(args).toEqual({ questionId: '2021881398772981878', answerLimit: 3 });
11
+ return {
12
+ ok: true,
13
+ answers: [
14
+ {
15
+ author: { name: 'alice' },
16
+ voteup_count: 12,
17
+ content: '<p>Hello <b>Zhihu</b></p>',
18
+ },
19
+ ],
20
+ };
21
+ });
22
+ const page = {
23
+ evaluate,
24
+ };
25
+ await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).resolves.toEqual([
26
+ {
27
+ rank: 1,
28
+ author: 'alice',
29
+ votes: 12,
30
+ content: 'Hello Zhihu',
31
+ },
32
+ ]);
33
+ expect(evaluate).toHaveBeenCalledTimes(1);
34
+ });
35
+ it('maps auth-like answer failures to AuthRequiredError', async () => {
36
+ const cmd = getRegistry().get('zhihu/question');
37
+ expect(cmd?.func).toBeTypeOf('function');
38
+ const page = {
39
+ evaluate: vi.fn().mockResolvedValue({ ok: false, status: 403 }),
40
+ };
41
+ await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toBeInstanceOf(AuthRequiredError);
42
+ });
43
+ it('preserves non-auth fetch failures as CliError instead of login errors', async () => {
44
+ const cmd = getRegistry().get('zhihu/question');
45
+ expect(cmd?.func).toBeTypeOf('function');
46
+ const page = {
47
+ evaluate: vi.fn().mockResolvedValue({ ok: false, status: 500 }),
48
+ };
49
+ await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toMatchObject({
50
+ code: 'FETCH_ERROR',
51
+ message: 'Zhihu question answers request failed with HTTP 500',
52
+ });
53
+ });
54
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getTopicText, getTopicAuthor, getTopicUrl, } from './utils.js';
3
+ cli({
4
+ site: 'zsxq',
5
+ name: 'dynamics',
6
+ description: '获取所有星球的最新动态',
7
+ domain: 'wx.zsxq.com',
8
+ strategy: Strategy.COOKIE,
9
+ browser: true,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 20, help: 'Number of dynamics to return' },
12
+ ],
13
+ columns: ['time', 'group', 'author', 'title', 'comments', 'likes', 'url'],
14
+ func: async (page, kwargs) => {
15
+ await ensureZsxqPage(page);
16
+ await ensureZsxqAuth(page);
17
+ const limit = Math.max(1, Number(kwargs.limit) || 20);
18
+ const { data } = await fetchFirstJson(page, [
19
+ `https://api.zsxq.com/v2/dynamics?scope=general&count=${limit}`,
20
+ ]);
21
+ const respData = data?.resp_data || data;
22
+ const dynamics = respData?.dynamics || [];
23
+ return dynamics.slice(0, limit).map((d) => {
24
+ const topic = d.topic;
25
+ if (!topic) {
26
+ return {
27
+ time: d.create_time || '',
28
+ group: '',
29
+ author: '',
30
+ title: `[${d.action || 'unknown'}]`,
31
+ comments: 0,
32
+ likes: 0,
33
+ url: '',
34
+ };
35
+ }
36
+ return {
37
+ time: d.create_time || topic.create_time || '',
38
+ group: topic.group?.name || '',
39
+ author: getTopicAuthor(topic),
40
+ title: getTopicText(topic).slice(0, 120),
41
+ comments: topic.comments_count ?? 0,
42
+ likes: topic.likes_count ?? 0,
43
+ url: getTopicUrl(topic.topic_id),
44
+ };
45
+ });
46
+ },
47
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getGroupsFromResponse, } from './utils.js';
3
+ cli({
4
+ site: 'zsxq',
5
+ name: 'groups',
6
+ description: '列出当前账号加入的星球',
7
+ domain: 'wx.zsxq.com',
8
+ strategy: Strategy.COOKIE,
9
+ browser: true,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 50, help: 'Number of groups to return' },
12
+ ],
13
+ columns: ['group_id', 'name', 'category', 'members', 'topics', 'joined_at', 'url'],
14
+ func: async (page, kwargs) => {
15
+ await ensureZsxqPage(page);
16
+ await ensureZsxqAuth(page);
17
+ const limit = Math.max(1, Number(kwargs.limit) || 50);
18
+ const { data } = await fetchFirstJson(page, [
19
+ `https://api.zsxq.com/v2/groups`,
20
+ ]);
21
+ return getGroupsFromResponse(data).slice(0, limit).map((group) => ({
22
+ group_id: group.group_id ?? '',
23
+ name: group.name || '',
24
+ category: group.category?.title || '',
25
+ members: group.statistics?.subscriptions_count ?? 0,
26
+ topics: group.statistics?.topics_count ?? 0,
27
+ joined_at: group.user_specific?.join_time || '',
28
+ valid_until: group.user_specific?.validity?.end_time || '',
29
+ url: group.group_id ? `https://wx.zsxq.com/group/${group.group_id}` : 'https://wx.zsxq.com',
30
+ }));
31
+ },
32
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { getActiveGroupId, ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getGroupsFromResponse, getTopicsFromResponse, toTopicRow, } from './utils.js';
3
+ cli({
4
+ site: 'zsxq',
5
+ name: 'search',
6
+ description: '搜索星球内容',
7
+ domain: 'wx.zsxq.com',
8
+ strategy: Strategy.COOKIE,
9
+ browser: true,
10
+ args: [
11
+ { name: 'keyword', required: true, positional: true, help: 'Search keyword' },
12
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results to return' },
13
+ { name: 'group_id', help: 'Optional group id; defaults to the active group in Chrome' },
14
+ ],
15
+ columns: ['topic_id', 'group', 'author', 'title', 'comments', 'likes', 'time', 'url'],
16
+ func: async (page, kwargs) => {
17
+ await ensureZsxqPage(page);
18
+ await ensureZsxqAuth(page);
19
+ const keyword = String(kwargs.keyword || '').trim();
20
+ const limit = Math.max(1, Number(kwargs.limit) || 20);
21
+ const groupId = String(kwargs.group_id || await getActiveGroupId(page));
22
+ const query = encodeURIComponent(keyword);
23
+ // Resolve group name from groups API
24
+ let groupName = groupId;
25
+ try {
26
+ const { data: groupsData } = await fetchFirstJson(page, [
27
+ `https://api.zsxq.com/v2/groups`,
28
+ ]);
29
+ const groups = getGroupsFromResponse(groupsData);
30
+ const found = groups.find(g => String(g.group_id) === groupId);
31
+ if (found?.name)
32
+ groupName = found.name;
33
+ }
34
+ catch { /* ignore */ }
35
+ const { data } = await fetchFirstJson(page, [
36
+ `https://api.zsxq.com/v2/search/groups/${groupId}/topics?keyword=${query}&count=${limit}`,
37
+ ]);
38
+ return getTopicsFromResponse(data).slice(0, limit).map((topic) => ({
39
+ ...toTopicRow(topic),
40
+ group: groupName,
41
+ }));
42
+ },
43
+ });
@@ -0,0 +1 @@
1
+ import './search.js';
@@ -0,0 +1,24 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './search.js';
4
+ describe('zsxq search command', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ it('requires an explicit group_id when there is no active group context', async () => {
9
+ const command = getRegistry().get('zsxq/search');
10
+ expect(command?.func).toBeTypeOf('function');
11
+ const mockPage = {
12
+ goto: vi.fn().mockResolvedValue(undefined),
13
+ evaluate: vi.fn()
14
+ .mockResolvedValueOnce(true)
15
+ .mockResolvedValueOnce(null),
16
+ };
17
+ await expect(command.func(mockPage, { keyword: 'opencli', limit: 20 })).rejects.toMatchObject({
18
+ code: 'ARGUMENT',
19
+ message: 'Cannot determine active group_id',
20
+ });
21
+ expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com');
22
+ expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
23
+ });
24
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { browserJsonRequest, ensureZsxqAuth, ensureZsxqPage, fetchFirstJson, getCommentsFromResponse, getTopicFromResponse, getTopicUrl, summarizeComments, toTopicRow, } from './utils.js';
4
+ cli({
5
+ site: 'zsxq',
6
+ name: 'topic',
7
+ description: '获取单个话题详情和评论',
8
+ domain: 'wx.zsxq.com',
9
+ strategy: Strategy.COOKIE,
10
+ browser: true,
11
+ args: [
12
+ { name: 'id', required: true, positional: true, help: 'Topic ID' },
13
+ { name: 'comment_limit', type: 'int', default: 20, help: 'Number of comments to fetch' },
14
+ ],
15
+ columns: ['topic_id', 'type', 'author', 'title', 'comments', 'likes', 'comment_preview', 'url'],
16
+ func: async (page, kwargs) => {
17
+ await ensureZsxqPage(page);
18
+ await ensureZsxqAuth(page);
19
+ const topicId = String(kwargs.id);
20
+ const commentLimit = Math.max(1, Number(kwargs.comment_limit) || 20);
21
+ const detailUrl = `https://api.zsxq.com/v2/topics/${topicId}`;
22
+ const detailResp = await browserJsonRequest(page, detailUrl);
23
+ if (detailResp.status === 404) {
24
+ throw new CliError('NOT_FOUND', `Topic ${topicId} not found`);
25
+ }
26
+ if (!detailResp.ok) {
27
+ throw new CliError('FETCH_ERROR', detailResp.error || `Failed to fetch topic ${topicId}`, `Checked endpoint: ${detailUrl}`);
28
+ }
29
+ const commentsResp = await fetchFirstJson(page, [
30
+ `https://api.zsxq.com/v2/topics/${topicId}/comments?sort=asc&count=${commentLimit}`,
31
+ ]);
32
+ const topic = getTopicFromResponse(detailResp.data);
33
+ if (!topic)
34
+ throw new CliError('NOT_FOUND', `Topic ${topicId} not found`);
35
+ const comments = getCommentsFromResponse(commentsResp.data);
36
+ const row = toTopicRow({
37
+ ...topic,
38
+ comments,
39
+ comments_count: topic.comments_count ?? comments.length,
40
+ });
41
+ return [{
42
+ ...row,
43
+ comment_preview: summarizeComments(comments, 5),
44
+ url: getTopicUrl(topic.topic_id ?? topicId),
45
+ }];
46
+ },
47
+ });
@@ -0,0 +1 @@
1
+ import './topic.js';