@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
@@ -0,0 +1,108 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { fetchDouyinUserVideosMock, fetchDouyinCommentsMock } = vi.hoisted(() => ({
3
+ fetchDouyinUserVideosMock: vi.fn(),
4
+ fetchDouyinCommentsMock: vi.fn(),
5
+ }));
6
+ vi.mock('./_shared/public-api.js', () => ({
7
+ fetchDouyinUserVideos: fetchDouyinUserVideosMock,
8
+ fetchDouyinComments: fetchDouyinCommentsMock,
9
+ }));
10
+ import { getRegistry } from '../../registry.js';
11
+ import { DEFAULT_COMMENT_LIMIT, MAX_USER_VIDEOS_LIMIT, normalizeCommentLimit, normalizeUserVideosLimit } from './user-videos.js';
12
+ describe('douyin user-videos', () => {
13
+ beforeEach(() => {
14
+ fetchDouyinUserVideosMock.mockReset();
15
+ fetchDouyinCommentsMock.mockReset();
16
+ });
17
+ it('registers the command', () => {
18
+ const registry = getRegistry();
19
+ const values = [...registry.values()];
20
+ const command = values.find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
21
+ expect(command).toBeDefined();
22
+ });
23
+ it('clamps limit to a safe maximum', () => {
24
+ expect(normalizeUserVideosLimit(100)).toBe(MAX_USER_VIDEOS_LIMIT);
25
+ expect(normalizeUserVideosLimit(0)).toBe(1);
26
+ expect(normalizeCommentLimit(99)).toBe(DEFAULT_COMMENT_LIMIT);
27
+ });
28
+ it('uses shared public-api helpers and applies clamped limits', async () => {
29
+ const registry = getRegistry();
30
+ const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
31
+ expect(command?.func).toBeDefined();
32
+ if (!command?.func)
33
+ throw new Error('douyin user-videos command not registered');
34
+ fetchDouyinUserVideosMock.mockResolvedValueOnce([
35
+ {
36
+ aweme_id: '1',
37
+ desc: 'test video',
38
+ video: { duration: 1234, play_addr: { url_list: ['https://example.com/video.mp4'] } },
39
+ statistics: { digg_count: 9 },
40
+ },
41
+ ]);
42
+ fetchDouyinCommentsMock.mockResolvedValueOnce([
43
+ { text: 'nice', digg_count: 3, nickname: 'alice' },
44
+ ]);
45
+ const page = {
46
+ goto: vi.fn().mockResolvedValue(undefined),
47
+ wait: vi.fn().mockResolvedValue(undefined),
48
+ };
49
+ const rows = await command.func(page, {
50
+ sec_uid: 'MS4w-test',
51
+ limit: 100,
52
+ comment_limit: 99,
53
+ with_comments: true,
54
+ });
55
+ expect(fetchDouyinUserVideosMock).toHaveBeenCalledWith(page, 'MS4w-test', MAX_USER_VIDEOS_LIMIT);
56
+ expect(fetchDouyinCommentsMock).toHaveBeenCalledWith(page, '1', DEFAULT_COMMENT_LIMIT);
57
+ expect(rows).toEqual([
58
+ {
59
+ index: 1,
60
+ aweme_id: '1',
61
+ title: 'test video',
62
+ duration: 1,
63
+ digg_count: 9,
64
+ play_url: 'https://example.com/video.mp4',
65
+ top_comments: [
66
+ { text: 'nice', digg_count: 3, nickname: 'alice' },
67
+ ],
68
+ },
69
+ ]);
70
+ });
71
+ it('skips comment enrichment when with_comments is false', async () => {
72
+ const registry = getRegistry();
73
+ const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
74
+ expect(command?.func).toBeDefined();
75
+ if (!command?.func)
76
+ throw new Error('douyin user-videos command not registered');
77
+ fetchDouyinUserVideosMock.mockResolvedValueOnce([
78
+ {
79
+ aweme_id: '2',
80
+ desc: 'plain video',
81
+ video: { duration: 2000, play_addr: { url_list: ['https://example.com/plain.mp4'] } },
82
+ statistics: { digg_count: 1 },
83
+ },
84
+ ]);
85
+ const page = {
86
+ goto: vi.fn().mockResolvedValue(undefined),
87
+ wait: vi.fn().mockResolvedValue(undefined),
88
+ };
89
+ const rows = await command.func(page, {
90
+ sec_uid: 'MS4w-test',
91
+ limit: 3,
92
+ with_comments: false,
93
+ comment_limit: 5,
94
+ });
95
+ expect(fetchDouyinCommentsMock).not.toHaveBeenCalled();
96
+ expect(rows).toEqual([
97
+ {
98
+ index: 1,
99
+ aweme_id: '2',
100
+ title: 'plain video',
101
+ duration: 2,
102
+ digg_count: 1,
103
+ play_url: 'https://example.com/plain.mp4',
104
+ top_comments: [],
105
+ },
106
+ ]);
107
+ });
108
+ });
@@ -1,5 +1,22 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import { browserFetch } from './_shared/browser-fetch.js';
3
+ function normalizeVideoStatus(status, publicTime) {
4
+ if (typeof status === 'number')
5
+ return status;
6
+ if (!status)
7
+ return publicTime && publicTime > Date.now() / 1000 ? 'scheduled' : 'published';
8
+ if (status.is_delete)
9
+ return 'deleted';
10
+ if (status.is_prohibited)
11
+ return 'prohibited';
12
+ if (status.in_reviewing)
13
+ return 'reviewing';
14
+ if (status.is_private)
15
+ return 'private';
16
+ if (publicTime && publicTime > Date.now() / 1000)
17
+ return 'scheduled';
18
+ return 'published';
19
+ }
3
20
  cli({
4
21
  site: 'douyin',
5
22
  name: 'videos',
@@ -17,18 +34,18 @@ cli({
17
34
  const statusNum = statusMap[kwargs.status] ?? 0;
18
35
  const url = `https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=${kwargs.limit}&page_num=${kwargs.page}&status=${statusNum}`;
19
36
  const res = (await browserFetch(page, 'GET', url));
20
- let items = res.data?.work_list ?? [];
37
+ let items = res.data?.work_list ?? res.aweme_list ?? [];
21
38
  // The API has a bug with status=16 for scheduled, so filter client-side
22
39
  if (kwargs.status === 'scheduled') {
23
- items = items.filter((v) => v.public_time > Date.now() / 1000);
40
+ items = items.filter((v) => (v.public_time ?? 0) > Date.now() / 1000);
24
41
  }
25
42
  return items.map((v) => ({
26
43
  aweme_id: v.aweme_id,
27
- title: v.desc,
28
- status: v.status,
44
+ title: v.desc ?? '',
45
+ status: normalizeVideoStatus(v.status, v.public_time),
29
46
  play_count: v.statistics?.play_count ?? 0,
30
47
  digg_count: v.statistics?.digg_count ?? 0,
31
- create_time: new Date(v.create_time * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
48
+ create_time: new Date((v.create_time ?? v.public_time ?? 0) * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
32
49
  }));
33
50
  },
34
51
  });
@@ -1,11 +1,54 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { browserFetchMock } = vi.hoisted(() => ({
3
+ browserFetchMock: vi.fn(),
4
+ }));
5
+ vi.mock('./_shared/browser-fetch.js', () => ({
6
+ browserFetch: browserFetchMock,
7
+ }));
2
8
  import { getRegistry } from '../../registry.js';
3
9
  import './videos.js';
4
- describe('douyin videos registration', () => {
10
+ describe('douyin videos', () => {
11
+ beforeEach(() => {
12
+ browserFetchMock.mockReset();
13
+ });
5
14
  it('registers the videos command', () => {
6
15
  const registry = getRegistry();
7
16
  const values = [...registry.values()];
8
17
  const cmd = values.find(c => c.site === 'douyin' && c.name === 'videos');
9
18
  expect(cmd).toBeDefined();
10
19
  });
20
+ it('parses the current creator work_list api shape', async () => {
21
+ const registry = getRegistry();
22
+ const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'videos');
23
+ expect(command?.func).toBeDefined();
24
+ if (!command?.func)
25
+ throw new Error('douyin videos command not registered');
26
+ browserFetchMock.mockResolvedValueOnce({
27
+ aweme_list: [
28
+ {
29
+ aweme_id: '7000000000000000001',
30
+ desc: '测试视频标题',
31
+ create_time: 1581571130,
32
+ statistics: {
33
+ play_count: 0,
34
+ digg_count: 12,
35
+ },
36
+ status: {
37
+ is_private: true,
38
+ },
39
+ },
40
+ ],
41
+ });
42
+ const rows = await command.func({}, { limit: 5, page: 1, status: 'all' });
43
+ expect(rows).toEqual([
44
+ {
45
+ aweme_id: '7000000000000000001',
46
+ title: '测试视频标题',
47
+ status: 'private',
48
+ play_count: 0,
49
+ digg_count: 12,
50
+ create_time: new Date(1581571130 * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' }),
51
+ },
52
+ ]);
53
+ });
11
54
  });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Regression test for issue #625.
3
+ * Facebook search must navigate in the pipeline before DOM extraction.
4
+ */
5
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Regression test for issue #625.
3
+ * Facebook search must navigate in the pipeline before DOM extraction.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import yaml from 'js-yaml';
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import { executePipeline } from '../../pipeline/index.js';
11
+ /**
12
+ * Minimal browser mock for pipeline execution tests.
13
+ * Only methods touched by this adapter path are implemented.
14
+ */
15
+ function createMockPage() {
16
+ return {
17
+ goto: vi.fn(),
18
+ evaluate: vi.fn().mockResolvedValue([]),
19
+ getCookies: vi.fn().mockResolvedValue([]),
20
+ snapshot: vi.fn().mockResolvedValue(''),
21
+ click: vi.fn(),
22
+ typeText: vi.fn(),
23
+ pressKey: vi.fn(),
24
+ scrollTo: vi.fn(),
25
+ getFormState: vi.fn().mockResolvedValue({}),
26
+ wait: vi.fn(),
27
+ tabs: vi.fn().mockResolvedValue([]),
28
+ closeTab: vi.fn(),
29
+ newTab: vi.fn(),
30
+ selectTab: vi.fn(),
31
+ networkRequests: vi.fn().mockResolvedValue([]),
32
+ consoleMessages: vi.fn().mockResolvedValue(''),
33
+ scroll: vi.fn(),
34
+ autoScroll: vi.fn(),
35
+ installInterceptor: vi.fn(),
36
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
37
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
38
+ screenshot: vi.fn().mockResolvedValue(''),
39
+ };
40
+ }
41
+ describe('facebook search pipeline', () => {
42
+ it('navigates to search results before extracting DOM data', async () => {
43
+ // Load the YAML adapter directly so the regression test covers the shipped command definition.
44
+ const filePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'search.yaml');
45
+ const raw = fs.readFileSync(filePath, 'utf8');
46
+ const command = yaml.load(raw);
47
+ const pipeline = command.pipeline ?? [];
48
+ const page = createMockPage();
49
+ await executePipeline(page, pipeline, {
50
+ args: { query: 'AI agent', limit: 3 },
51
+ });
52
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://www.facebook.com');
53
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://www.facebook.com/search/top?q=AI%20agent', {
54
+ waitUntil: undefined,
55
+ settleMs: 4000,
56
+ });
57
+ expect(page.evaluate).toHaveBeenCalledTimes(1);
58
+ expect(String(page.evaluate.mock.calls[0]?.[0] ?? '')).not.toContain('window.location.href');
59
+ });
60
+ });
@@ -17,12 +17,13 @@ args:
17
17
  pipeline:
18
18
  - navigate: https://www.facebook.com
19
19
 
20
+ - navigate:
21
+ url: https://www.facebook.com/search/top?q=${{ args.query | urlencode }}
22
+ settleMs: 4000
23
+
20
24
  - evaluate: |
21
25
  (async () => {
22
- const query = ${{ args.query | json }};
23
26
  const limit = ${{ args.limit }};
24
- window.location.href = 'https://www.facebook.com/search/top?q=' + encodeURIComponent(query);
25
- await new Promise(r => setTimeout(r, 4000));
26
27
  // Search results are typically in role="article" or role="listitem"
27
28
  let items = document.querySelectorAll('[role="article"]');
28
29
  if (items.length === 0) {
@@ -0,0 +1,16 @@
1
+ export interface InstagramMediaTarget {
2
+ kind: 'p' | 'reel' | 'tv';
3
+ shortcode: string;
4
+ canonicalUrl: string;
5
+ }
6
+ interface InstagramPageMediaItem {
7
+ type: 'image' | 'video';
8
+ url: string;
9
+ }
10
+ interface DownloadedMediaItem extends InstagramPageMediaItem {
11
+ filename: string;
12
+ }
13
+ export declare function parseInstagramMediaTarget(input: string): InstagramMediaTarget;
14
+ export declare function buildInstagramDownloadItems(shortcode: string, items: InstagramPageMediaItem[]): DownloadedMediaItem[];
15
+ export declare function buildInstagramFetchScript(shortcode: string): string;
16
+ export {};
@@ -0,0 +1,225 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError, EXIT_CODES } from '../../errors.js';
6
+ import { httpDownload } from '../../download/index.js';
7
+ const INSTAGRAM_GRAPHQL_DOC_ID = '8845758582119845';
8
+ const INSTAGRAM_GRAPHQL_APP_ID = '936619743392459';
9
+ const INSTAGRAM_HOST_SUFFIX = 'instagram.com';
10
+ const SUPPORTED_KINDS = new Set(['p', 'reel', 'tv']);
11
+ function displayPath(filePath) {
12
+ const home = os.homedir();
13
+ return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
14
+ }
15
+ export function parseInstagramMediaTarget(input) {
16
+ const raw = String(input || '').trim();
17
+ if (!raw) {
18
+ throw new ArgumentError('Instagram URL is required', 'Expected https://www.instagram.com/p/... or https://www.instagram.com/reel/...');
19
+ }
20
+ let url;
21
+ try {
22
+ url = new URL(raw);
23
+ }
24
+ catch {
25
+ throw new ArgumentError(`Invalid Instagram URL: ${raw}`, 'Expected https://www.instagram.com/p/<shortcode>/ or /reel/<shortcode>/');
26
+ }
27
+ if (!['http:', 'https:'].includes(url.protocol)) {
28
+ throw new ArgumentError(`Unsupported URL protocol: ${url.protocol}`);
29
+ }
30
+ const host = url.hostname.toLowerCase();
31
+ if (host !== INSTAGRAM_HOST_SUFFIX && !host.endsWith(`.${INSTAGRAM_HOST_SUFFIX}`)) {
32
+ throw new ArgumentError(`Unsupported host: ${host}`, 'Only instagram.com URLs are supported');
33
+ }
34
+ const segments = url.pathname.split('/').filter(Boolean);
35
+ let kind;
36
+ let shortcode;
37
+ if (segments.length >= 2 && SUPPORTED_KINDS.has(segments[0])) {
38
+ kind = segments[0];
39
+ shortcode = segments[1];
40
+ }
41
+ else if (segments.length >= 3 && SUPPORTED_KINDS.has(segments[1])) {
42
+ kind = segments[1];
43
+ shortcode = segments[2];
44
+ }
45
+ if (!kind || !shortcode) {
46
+ throw new ArgumentError(`Unsupported Instagram media URL: ${raw}`, 'Only /p/<shortcode>/, /reel/<shortcode>/, and /tv/<shortcode>/ links are supported');
47
+ }
48
+ return {
49
+ kind: kind,
50
+ shortcode,
51
+ canonicalUrl: `https://www.instagram.com/${kind}/${shortcode}/`,
52
+ };
53
+ }
54
+ export function buildInstagramDownloadItems(shortcode, items) {
55
+ return items
56
+ .filter((item) => item?.url)
57
+ .map((item, index) => {
58
+ const fallbackExt = item.type === 'video' ? '.mp4' : '.jpg';
59
+ let ext = fallbackExt;
60
+ try {
61
+ const pathname = new URL(item.url).pathname;
62
+ const candidateExt = path.extname(pathname).toLowerCase();
63
+ if (candidateExt && candidateExt.length <= 8)
64
+ ext = candidateExt;
65
+ }
66
+ catch {
67
+ ext = fallbackExt;
68
+ }
69
+ return {
70
+ type: item.type,
71
+ url: item.url,
72
+ filename: `${shortcode}_${String(index + 1).padStart(2, '0')}${ext}`,
73
+ };
74
+ });
75
+ }
76
+ export function buildInstagramFetchScript(shortcode) {
77
+ return `
78
+ (async () => {
79
+ const shortcode = ${JSON.stringify(shortcode)};
80
+ const docId = ${JSON.stringify(INSTAGRAM_GRAPHQL_DOC_ID)};
81
+ const variables = {
82
+ shortcode,
83
+ fetch_tagged_user_count: null,
84
+ hoisted_comment_id: null,
85
+ hoisted_reply_id: null,
86
+ };
87
+ const url = 'https://www.instagram.com/graphql/query/?doc_id=' + docId + '&variables=' + encodeURIComponent(JSON.stringify(variables));
88
+ const res = await fetch(url, {
89
+ credentials: 'include',
90
+ headers: {
91
+ 'Accept': 'application/json,text/plain,*/*',
92
+ 'X-IG-App-ID': ${JSON.stringify(INSTAGRAM_GRAPHQL_APP_ID)},
93
+ },
94
+ });
95
+ const rawText = await res.text();
96
+
97
+ let data = null;
98
+ try {
99
+ data = rawText ? JSON.parse(rawText) : null;
100
+ } catch {
101
+ return {
102
+ ok: false,
103
+ errorCode: 'COMMAND_EXEC',
104
+ error: 'Instagram returned non-JSON content while fetching media metadata',
105
+ };
106
+ }
107
+
108
+ const message = typeof data?.message === 'string' ? data.message : '';
109
+ const lowered = (message || '').toLowerCase();
110
+
111
+ if (!res.ok) {
112
+ if (res.status === 401 || res.status === 403 || data?.require_login) {
113
+ return { ok: false, errorCode: 'AUTH_REQUIRED', error: message || ('HTTP ' + res.status) };
114
+ }
115
+ if (res.status === 429) {
116
+ return { ok: false, errorCode: 'RATE_LIMITED', error: message || 'HTTP 429' };
117
+ }
118
+ if (res.status === 404 || res.status === 410) {
119
+ return { ok: false, errorCode: 'PRIVATE_OR_UNAVAILABLE', error: message || ('HTTP ' + res.status) };
120
+ }
121
+ return { ok: false, errorCode: 'COMMAND_EXEC', error: message || ('HTTP ' + res.status) };
122
+ }
123
+
124
+ if (data?.require_login) {
125
+ return { ok: false, errorCode: 'AUTH_REQUIRED', error: message || 'Instagram login required' };
126
+ }
127
+ if (lowered.includes('wait a few minutes') || lowered.includes('rate')) {
128
+ return { ok: false, errorCode: 'RATE_LIMITED', error: message || 'Instagram rate limit triggered' };
129
+ }
130
+
131
+ const media = data?.data?.xdt_shortcode_media;
132
+ if (!media) {
133
+ return {
134
+ ok: false,
135
+ errorCode: 'PRIVATE_OR_UNAVAILABLE',
136
+ error: message || 'Post may be private, unavailable, or inaccessible to the current browser session',
137
+ };
138
+ }
139
+
140
+ const nodes = Array.isArray(media?.edge_sidecar_to_children?.edges) && media.edge_sidecar_to_children.edges.length > 0
141
+ ? media.edge_sidecar_to_children.edges.map((edge) => edge?.node).filter(Boolean)
142
+ : [media];
143
+
144
+ const items = nodes
145
+ .map((node) => ({
146
+ type: node?.is_video ? 'video' : 'image',
147
+ url: String(node?.is_video ? (node?.video_url || '') : (node?.display_url || '')),
148
+ }))
149
+ .filter((item) => item.url);
150
+
151
+ return {
152
+ ok: true,
153
+ shortcode: media.shortcode || shortcode,
154
+ owner: media?.owner?.username || '',
155
+ items,
156
+ };
157
+ })()
158
+ `;
159
+ }
160
+ function ensurePage(page) {
161
+ if (!page)
162
+ throw new CommandExecutionError('Browser session required');
163
+ return page;
164
+ }
165
+ function normalizeFetchResult(result) {
166
+ if (!result || typeof result !== 'object') {
167
+ throw new CommandExecutionError('Failed to fetch Instagram media metadata');
168
+ }
169
+ return result;
170
+ }
171
+ function handleFetchFailure(result) {
172
+ const message = result.error || 'Instagram media fetch failed';
173
+ if (result.errorCode === 'AUTH_REQUIRED') {
174
+ throw new AuthRequiredError('instagram.com', message);
175
+ }
176
+ if (result.errorCode === 'RATE_LIMITED') {
177
+ throw new CliError('RATE_LIMITED', message, 'Wait a few minutes and retry, or switch to a browser session with a warmer Instagram login state.', EXIT_CODES.TEMPFAIL);
178
+ }
179
+ if (result.errorCode === 'PRIVATE_OR_UNAVAILABLE') {
180
+ throw new CommandExecutionError(message, 'Open the post in a logged-in browser session and retry');
181
+ }
182
+ throw new CommandExecutionError(message);
183
+ }
184
+ async function downloadInstagramMedia(items, outputDir) {
185
+ fs.mkdirSync(outputDir, { recursive: true });
186
+ for (const item of items) {
187
+ const destPath = path.join(outputDir, item.filename);
188
+ const result = await httpDownload(item.url, destPath, {
189
+ timeout: item.type === 'video' ? 120000 : 60000,
190
+ });
191
+ if (!result.success) {
192
+ throw new CommandExecutionError(`Failed to download ${item.filename}: ${result.error || 'unknown error'}`);
193
+ }
194
+ }
195
+ }
196
+ cli({
197
+ site: 'instagram',
198
+ name: 'download',
199
+ description: 'Download images and videos from Instagram posts and reels',
200
+ domain: 'www.instagram.com',
201
+ strategy: Strategy.COOKIE,
202
+ navigateBefore: false,
203
+ args: [
204
+ { name: 'url', positional: true, required: true, help: 'Instagram post / reel / tv URL' },
205
+ { name: 'path', default: path.join(os.homedir(), 'Downloads', 'Instagram'), help: 'Download directory' },
206
+ ],
207
+ func: async (page, kwargs) => {
208
+ const browserPage = ensurePage(page);
209
+ const target = parseInstagramMediaTarget(String(kwargs.url ?? ''));
210
+ const outputRoot = String(kwargs.path ?? path.join(os.homedir(), 'Downloads', 'Instagram'));
211
+ await browserPage.goto(target.canonicalUrl);
212
+ const fetchResult = normalizeFetchResult(await browserPage.evaluate(buildInstagramFetchScript(target.shortcode)));
213
+ if (!fetchResult.ok)
214
+ handleFetchFailure(fetchResult);
215
+ const shortcode = fetchResult.shortcode || target.shortcode;
216
+ const mediaItems = buildInstagramDownloadItems(shortcode, fetchResult.items || []);
217
+ if (mediaItems.length === 0) {
218
+ throw new CommandExecutionError('No downloadable media found');
219
+ }
220
+ const savedDir = path.join(outputRoot, shortcode);
221
+ await downloadInstagramMedia(mediaItems, savedDir);
222
+ console.log(`📁 saved: ${displayPath(savedDir)}`);
223
+ return null;
224
+ },
225
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,118 @@
1
+ import * as os from 'node:os';
2
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { getRegistry } from '../../registry.js';
4
+ import { ArgumentError, AuthRequiredError, CommandExecutionError } from '../../errors.js';
5
+ const { mockHttpDownload, logSpy } = vi.hoisted(() => ({
6
+ mockHttpDownload: vi.fn(),
7
+ logSpy: vi.spyOn(console, 'log').mockImplementation(() => undefined),
8
+ }));
9
+ vi.mock('../../download/index.js', async () => {
10
+ const actual = await vi.importActual('../../download/index.js');
11
+ return { ...actual, httpDownload: mockHttpDownload };
12
+ });
13
+ const { buildInstagramDownloadItems, parseInstagramMediaTarget, } = await import('./download.js');
14
+ let cmd;
15
+ beforeAll(() => {
16
+ cmd = getRegistry().get('instagram/download');
17
+ expect(cmd?.func).toBeTypeOf('function');
18
+ });
19
+ function createPageMock(evaluateResult) {
20
+ return {
21
+ goto: vi.fn().mockResolvedValue(undefined),
22
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
23
+ };
24
+ }
25
+ describe('instagram download helpers', () => {
26
+ it('parses canonical and username-prefixed Instagram media URLs', () => {
27
+ expect(parseInstagramMediaTarget('https://www.instagram.com/reel/DWg8NuZEj9p/?utm_source=ig_web_copy_link')).toEqual({
28
+ kind: 'reel',
29
+ shortcode: 'DWg8NuZEj9p',
30
+ canonicalUrl: 'https://www.instagram.com/reel/DWg8NuZEj9p/',
31
+ });
32
+ expect(parseInstagramMediaTarget('https://www.instagram.com/nasa/p/DWUR_azCWbN/?img_index=1')).toEqual({
33
+ kind: 'p',
34
+ shortcode: 'DWUR_azCWbN',
35
+ canonicalUrl: 'https://www.instagram.com/p/DWUR_azCWbN/',
36
+ });
37
+ });
38
+ it('rejects unsupported URLs early', () => {
39
+ expect(() => parseInstagramMediaTarget('https://example.com/p/abc')).toThrow(ArgumentError);
40
+ expect(() => parseInstagramMediaTarget('https://www.instagram.com/stories/abc/123')).toThrow(ArgumentError);
41
+ });
42
+ it('builds padded filenames and preserves known file extensions', () => {
43
+ expect(buildInstagramDownloadItems('DWUR_azCWbN', [
44
+ { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' },
45
+ { type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2' },
46
+ { type: 'image', url: 'not-a-valid-url' },
47
+ ])).toEqual([
48
+ { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1', filename: 'DWUR_azCWbN_01.webp' },
49
+ { type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2', filename: 'DWUR_azCWbN_02.mp4' },
50
+ { type: 'image', url: 'not-a-valid-url', filename: 'DWUR_azCWbN_03.jpg' },
51
+ ]);
52
+ });
53
+ });
54
+ describe('instagram download command', () => {
55
+ beforeEach(() => {
56
+ mockHttpDownload.mockReset();
57
+ logSpy.mockClear();
58
+ });
59
+ it('rejects invalid URLs before browser work', async () => {
60
+ const page = createPageMock({ ok: true, items: [] });
61
+ await expect(cmd.func(page, { url: 'https://example.com/not-instagram' })).rejects.toThrow(ArgumentError);
62
+ expect(page.goto.mock.calls).toHaveLength(0);
63
+ });
64
+ it('maps auth failures to AuthRequiredError', async () => {
65
+ const page = createPageMock({ ok: false, errorCode: 'AUTH_REQUIRED', error: 'Instagram login required' });
66
+ await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(AuthRequiredError);
67
+ expect(mockHttpDownload).not.toHaveBeenCalled();
68
+ });
69
+ it('maps rate limit failures to CliError with RATE_LIMITED code', async () => {
70
+ const page = createPageMock({ ok: false, errorCode: 'RATE_LIMITED', error: 'Please wait a few minutes' });
71
+ await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toMatchObject({ code: 'RATE_LIMITED' });
72
+ expect(mockHttpDownload).not.toHaveBeenCalled();
73
+ });
74
+ it('maps private/unavailable failures to CommandExecutionError', async () => {
75
+ const page = createPageMock({ ok: false, errorCode: 'PRIVATE_OR_UNAVAILABLE', error: 'Post may be private' });
76
+ await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(CommandExecutionError);
77
+ expect(mockHttpDownload).not.toHaveBeenCalled();
78
+ });
79
+ it('throws when no downloadable media is found', async () => {
80
+ const page = createPageMock({ ok: true, shortcode: 'DWUR_azCWbN', items: [] });
81
+ await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(CommandExecutionError);
82
+ expect(mockHttpDownload).not.toHaveBeenCalled();
83
+ });
84
+ it('downloads media and prints saved directory', async () => {
85
+ mockHttpDownload
86
+ .mockResolvedValueOnce({ success: true, size: 120_000 })
87
+ .mockResolvedValueOnce({ success: true, size: 8_200_000 });
88
+ const page = createPageMock({
89
+ ok: true,
90
+ shortcode: 'DWUR_azCWbN',
91
+ items: [
92
+ { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' },
93
+ { type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2' },
94
+ ],
95
+ });
96
+ const result = await cmd.func(page, {
97
+ url: 'https://www.instagram.com/nasa/p/DWUR_azCWbN/?img_index=1',
98
+ path: './instagram-test',
99
+ });
100
+ expect(result).toBeNull();
101
+ expect(page.goto.mock.calls[0]?.[0]).toBe('https://www.instagram.com/p/DWUR_azCWbN/');
102
+ expect(mockHttpDownload).toHaveBeenNthCalledWith(1, 'https://cdn.example.com/photo.webp?foo=1', expect.stringContaining('instagram-test/DWUR_azCWbN/DWUR_azCWbN_01.webp'), expect.objectContaining({ timeout: 60000 }));
103
+ expect(mockHttpDownload).toHaveBeenNthCalledWith(2, 'https://cdn.example.com/video.mp4?bar=2', expect.stringContaining('instagram-test/DWUR_azCWbN/DWUR_azCWbN_02.mp4'), expect.objectContaining({ timeout: 120000 }));
104
+ expect(logSpy).toHaveBeenCalledWith('📁 saved: instagram-test/DWUR_azCWbN');
105
+ });
106
+ it('uses a cross-platform Downloads default when path is omitted', async () => {
107
+ mockHttpDownload.mockResolvedValueOnce({ success: true, size: 120_000 });
108
+ const page = createPageMock({
109
+ ok: true,
110
+ shortcode: 'DWUR_azCWbN',
111
+ items: [
112
+ { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' },
113
+ ],
114
+ });
115
+ await cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' });
116
+ expect(mockHttpDownload).toHaveBeenCalledWith('https://cdn.example.com/photo.webp?foo=1', expect.stringContaining(`${os.homedir()}/Downloads/Instagram/DWUR_azCWbN/DWUR_azCWbN_01.webp`), expect.objectContaining({ timeout: 60000 }));
117
+ });
118
+ });
@@ -0,0 +1 @@
1
+ export {};