@jackwener/opencli 1.6.0 → 1.6.2

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 (390) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -45
  4. package/README.zh-CN.md +32 -34
  5. package/autoresearch/browse-tasks.json +18 -20
  6. package/autoresearch/commands/debug.ts +163 -0
  7. package/autoresearch/commands/fix.ts +145 -0
  8. package/autoresearch/commands/plan.ts +88 -0
  9. package/autoresearch/commands/run.ts +138 -0
  10. package/autoresearch/config.ts +82 -0
  11. package/autoresearch/engine.ts +359 -0
  12. package/autoresearch/eval-all.ts +127 -0
  13. package/autoresearch/eval-browse.ts +1 -1
  14. package/autoresearch/eval-publish.ts +238 -0
  15. package/autoresearch/eval-save.ts +249 -0
  16. package/autoresearch/eval-skill.ts +14 -8
  17. package/autoresearch/eval-v2ex.ts +220 -0
  18. package/autoresearch/eval-zhihu.ts +230 -0
  19. package/autoresearch/logger.ts +69 -0
  20. package/autoresearch/presets/combined-reliability.ts +27 -0
  21. package/autoresearch/presets/index.ts +23 -0
  22. package/autoresearch/presets/operate-reliability.ts +24 -0
  23. package/autoresearch/presets/save-reliability.ts +26 -0
  24. package/autoresearch/presets/skill-quality.ts +20 -0
  25. package/autoresearch/presets/v2ex-reliability.ts +24 -0
  26. package/autoresearch/presets/zhihu-reliability.ts +25 -0
  27. package/autoresearch/publish-tasks.json +345 -0
  28. package/autoresearch/run-save.sh +11 -0
  29. package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
  30. package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
  31. package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
  32. package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
  33. package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
  34. package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
  35. package/autoresearch/save-tasks.json +281 -0
  36. package/autoresearch/v2ex-tasks.json +899 -0
  37. package/autoresearch/zhihu-tasks.json +848 -0
  38. package/bun.lock +615 -0
  39. package/dist/browser/base-page.d.ts +4 -2
  40. package/dist/browser/base-page.js +37 -4
  41. package/dist/browser/bridge.js +10 -8
  42. package/dist/browser/cdp.js +2 -6
  43. package/dist/browser/daemon-client.d.ts +11 -1
  44. package/dist/browser/daemon-client.js +3 -0
  45. package/dist/browser/dom-helpers.d.ts +4 -2
  46. package/dist/browser/dom-helpers.js +42 -31
  47. package/dist/browser/dom-snapshot.js +23 -1
  48. package/dist/browser/page.d.ts +7 -2
  49. package/dist/browser/page.js +112 -30
  50. package/dist/browser.test.js +1 -1
  51. package/dist/build-manifest.d.ts +1 -0
  52. package/dist/build-manifest.js +1 -0
  53. package/dist/cli-manifest.json +1133 -182
  54. package/dist/cli.d.ts +2 -0
  55. package/dist/cli.js +48 -7
  56. package/dist/cli.test.d.ts +1 -0
  57. package/dist/cli.test.js +88 -0
  58. package/dist/clis/1688/item.d.ts +70 -0
  59. package/dist/clis/1688/item.js +187 -0
  60. package/dist/clis/1688/item.test.d.ts +1 -0
  61. package/dist/clis/1688/item.test.js +67 -0
  62. package/dist/clis/1688/search.d.ts +56 -0
  63. package/dist/clis/1688/search.js +309 -0
  64. package/dist/clis/1688/search.test.d.ts +1 -0
  65. package/dist/clis/1688/search.test.js +75 -0
  66. package/dist/clis/1688/shared.d.ts +112 -0
  67. package/dist/clis/1688/shared.js +514 -0
  68. package/dist/clis/1688/shared.test.d.ts +1 -0
  69. package/dist/clis/1688/shared.test.js +57 -0
  70. package/dist/clis/1688/store.d.ts +45 -0
  71. package/dist/clis/1688/store.js +226 -0
  72. package/dist/clis/1688/store.test.d.ts +1 -0
  73. package/dist/clis/1688/store.test.js +62 -0
  74. package/dist/clis/amazon/bestsellers.d.ts +0 -20
  75. package/dist/clis/amazon/bestsellers.js +6 -129
  76. package/dist/clis/amazon/bestsellers.test.js +12 -3
  77. package/dist/clis/amazon/movers-shakers.d.ts +1 -0
  78. package/dist/clis/amazon/movers-shakers.js +7 -0
  79. package/dist/clis/amazon/new-releases.d.ts +1 -0
  80. package/dist/clis/amazon/new-releases.js +7 -0
  81. package/dist/clis/amazon/rankings.d.ts +59 -0
  82. package/dist/clis/amazon/rankings.js +226 -0
  83. package/dist/clis/amazon/rankings.test.d.ts +1 -0
  84. package/dist/clis/amazon/rankings.test.js +41 -0
  85. package/dist/clis/amazon/shared.d.ts +11 -0
  86. package/dist/clis/amazon/shared.js +121 -11
  87. package/dist/clis/amazon/shared.test.js +11 -0
  88. package/dist/clis/bilibili/comments.js +2 -2
  89. package/dist/clis/bilibili/comments.test.js +3 -2
  90. package/dist/clis/bilibili/download.js +2 -1
  91. package/dist/clis/bilibili/subtitle.js +4 -3
  92. package/dist/clis/bilibili/subtitle.test.js +2 -1
  93. package/dist/clis/bilibili/utils.d.ts +5 -0
  94. package/dist/clis/bilibili/utils.js +30 -0
  95. package/dist/clis/bilibili/utils.test.d.ts +1 -0
  96. package/dist/clis/bilibili/utils.test.js +17 -0
  97. package/dist/clis/douban/marks.js +1 -1
  98. package/dist/clis/douban/subject.yaml +50 -19
  99. package/dist/clis/doubao/utils.js +32 -12
  100. package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
  101. package/dist/clis/douyin/_shared/transcode.test.js +0 -2
  102. package/dist/clis/douyin/draft.test.js +0 -2
  103. package/dist/clis/facebook/search.test.js +0 -2
  104. package/dist/clis/gemini/ask.js +9 -3
  105. package/dist/clis/gemini/ask.test.d.ts +1 -0
  106. package/dist/clis/gemini/ask.test.js +100 -0
  107. package/dist/clis/gemini/reply-state.test.d.ts +1 -0
  108. package/dist/clis/gemini/reply-state.test.js +641 -0
  109. package/dist/clis/gemini/utils.d.ts +44 -1
  110. package/dist/clis/gemini/utils.js +528 -61
  111. package/dist/clis/gemini/utils.test.js +149 -2
  112. package/dist/clis/hupu/detail.d.ts +1 -0
  113. package/dist/clis/hupu/detail.js +72 -0
  114. package/dist/clis/hupu/hot.yaml +43 -0
  115. package/dist/clis/hupu/like.d.ts +1 -0
  116. package/dist/clis/hupu/like.js +75 -0
  117. package/dist/clis/hupu/reply.d.ts +1 -0
  118. package/dist/clis/hupu/reply.js +71 -0
  119. package/dist/clis/hupu/search.d.ts +1 -0
  120. package/dist/clis/hupu/search.js +59 -0
  121. package/dist/clis/hupu/unlike.d.ts +1 -0
  122. package/dist/clis/hupu/unlike.js +75 -0
  123. package/dist/clis/hupu/utils.d.ts +20 -0
  124. package/dist/clis/hupu/utils.js +319 -0
  125. package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
  126. package/dist/clis/instagram/_shared/private-publish.js +1030 -0
  127. package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
  128. package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
  129. package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
  130. package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
  131. package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
  132. package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
  133. package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
  134. package/dist/clis/instagram/_shared/runtime-info.js +81 -0
  135. package/dist/clis/instagram/note.d.ts +1 -0
  136. package/dist/clis/instagram/note.js +222 -0
  137. package/dist/clis/instagram/note.test.d.ts +1 -0
  138. package/dist/clis/instagram/note.test.js +81 -0
  139. package/dist/clis/instagram/post.d.ts +4 -0
  140. package/dist/clis/instagram/post.js +1496 -0
  141. package/dist/clis/instagram/post.test.d.ts +1 -0
  142. package/dist/clis/instagram/post.test.js +1647 -0
  143. package/dist/clis/instagram/reel.d.ts +1 -0
  144. package/dist/clis/instagram/reel.js +826 -0
  145. package/dist/clis/instagram/reel.test.d.ts +1 -0
  146. package/dist/clis/instagram/reel.test.js +167 -0
  147. package/dist/clis/instagram/story.d.ts +1 -0
  148. package/dist/clis/instagram/story.js +115 -0
  149. package/dist/clis/instagram/story.test.d.ts +1 -0
  150. package/dist/clis/instagram/story.test.js +167 -0
  151. package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
  152. package/dist/clis/sinafinance/stock-rank.js +65 -0
  153. package/dist/clis/substack/utils.test.js +0 -2
  154. package/dist/clis/twitter/post.js +72 -45
  155. package/dist/clis/twitter/post.test.d.ts +1 -0
  156. package/dist/clis/twitter/post.test.js +116 -0
  157. package/dist/clis/twitter/reply.d.ts +12 -0
  158. package/dist/clis/twitter/reply.js +257 -35
  159. package/dist/clis/twitter/reply.test.d.ts +1 -0
  160. package/dist/clis/twitter/reply.test.js +151 -0
  161. package/dist/clis/twitter/search.js +67 -5
  162. package/dist/clis/twitter/search.test.js +83 -5
  163. package/dist/clis/xianyu/chat.d.ts +7 -0
  164. package/dist/clis/xianyu/chat.js +146 -0
  165. package/dist/clis/xianyu/chat.test.d.ts +1 -0
  166. package/dist/clis/xianyu/chat.test.js +15 -0
  167. package/dist/clis/xianyu/item.d.ts +7 -0
  168. package/dist/clis/xianyu/item.js +152 -0
  169. package/dist/clis/xianyu/item.test.d.ts +1 -0
  170. package/dist/clis/xianyu/item.test.js +56 -0
  171. package/dist/clis/xianyu/search.d.ts +10 -0
  172. package/dist/clis/xianyu/search.js +134 -0
  173. package/dist/clis/xianyu/search.test.d.ts +1 -0
  174. package/dist/clis/xianyu/search.test.js +17 -0
  175. package/dist/clis/xianyu/utils.d.ts +1 -0
  176. package/dist/clis/xianyu/utils.js +8 -0
  177. package/dist/clis/xiaoe/catalog.yaml +129 -0
  178. package/dist/clis/xiaoe/content.yaml +43 -0
  179. package/dist/clis/xiaoe/courses.yaml +73 -0
  180. package/dist/clis/xiaoe/detail.yaml +39 -0
  181. package/dist/clis/xiaoe/play-url.yaml +124 -0
  182. package/dist/clis/xiaohongshu/comments.test.js +0 -2
  183. package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
  184. package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
  185. package/dist/clis/xiaohongshu/download.test.js +0 -2
  186. package/dist/clis/xiaohongshu/note.test.js +0 -2
  187. package/dist/clis/xiaohongshu/publish.test.js +0 -2
  188. package/dist/clis/xiaohongshu/search.js +29 -20
  189. package/dist/clis/xiaohongshu/search.test.js +56 -48
  190. package/dist/clis/yuanbao/ask.d.ts +21 -0
  191. package/dist/clis/yuanbao/ask.js +427 -0
  192. package/dist/clis/yuanbao/ask.test.d.ts +1 -0
  193. package/dist/clis/yuanbao/ask.test.js +124 -0
  194. package/dist/clis/yuanbao/new.d.ts +1 -0
  195. package/dist/clis/yuanbao/new.js +70 -0
  196. package/dist/clis/yuanbao/new.test.d.ts +1 -0
  197. package/dist/clis/yuanbao/new.test.js +30 -0
  198. package/dist/clis/yuanbao/shared.d.ts +13 -0
  199. package/dist/clis/yuanbao/shared.js +49 -0
  200. package/dist/clis/zhihu/question.js +30 -19
  201. package/dist/clis/zhihu/question.test.js +34 -16
  202. package/dist/commanderAdapter.js +8 -4
  203. package/dist/commanderAdapter.test.js +42 -0
  204. package/dist/completion.js +3 -1
  205. package/dist/completion.test.d.ts +1 -0
  206. package/dist/completion.test.js +23 -0
  207. package/dist/doctor.js +1 -1
  208. package/dist/electron-apps.d.ts +2 -0
  209. package/dist/electron-apps.js +7 -1
  210. package/dist/errors.js +1 -1
  211. package/dist/execution.js +25 -35
  212. package/dist/explore.js +1 -1
  213. package/dist/launcher.d.ts +4 -0
  214. package/dist/launcher.js +64 -8
  215. package/dist/launcher.test.js +88 -7
  216. package/dist/output.d.ts +2 -0
  217. package/dist/output.js +10 -1
  218. package/dist/output.test.d.ts +0 -3
  219. package/dist/output.test.js +59 -92
  220. package/dist/pipeline/executor.test.js +0 -2
  221. package/dist/pipeline/steps/download.test.js +0 -2
  222. package/dist/registry.d.ts +2 -0
  223. package/dist/serialization.d.ts +1 -0
  224. package/dist/serialization.js +1 -0
  225. package/dist/types.d.ts +9 -2
  226. package/docs/.vitepress/config.mts +4 -0
  227. package/docs/adapters/browser/1688.md +52 -0
  228. package/docs/adapters/browser/36kr.md +2 -1
  229. package/docs/adapters/browser/doubao.md +5 -1
  230. package/docs/adapters/browser/hupu.md +53 -0
  231. package/docs/adapters/browser/sinafinance.md +32 -2
  232. package/docs/adapters/browser/weibo.md +6 -1
  233. package/docs/adapters/browser/wikipedia.md +2 -0
  234. package/docs/adapters/browser/xianyu.md +42 -0
  235. package/docs/adapters/browser/xiaoe.md +44 -0
  236. package/docs/adapters/browser/yuanbao.md +64 -0
  237. package/docs/adapters/index.md +14 -5
  238. package/docs/comparison.md +1 -1
  239. package/docs/developer/ai-workflow.md +2 -2
  240. package/docs/developer/contributing.md +1 -1
  241. package/docs/developer/testing.md +2 -0
  242. package/docs/guide/plugins.md +1 -0
  243. package/docs/guide/troubleshooting.md +11 -0
  244. package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
  245. package/docs/zh/guide/plugins.md +1 -0
  246. package/extension/dist/background.js +1127 -0
  247. package/extension/src/background.test.ts +39 -0
  248. package/extension/src/background.ts +223 -34
  249. package/extension/src/cdp.ts +194 -4
  250. package/extension/src/protocol.ts +22 -1
  251. package/package.json +3 -2
  252. package/scripts/postinstall.js +1 -1
  253. package/skills/opencli-explorer/SKILL.md +1 -1
  254. package/skills/opencli-oneshot/SKILL.md +2 -2
  255. package/skills/opencli-operate/SKILL.md +120 -27
  256. package/skills/opencli-usage/SKILL.md +31 -20
  257. package/skills/opencli-usage/browser.md +114 -16
  258. package/skills/opencli-usage/public-api.md +32 -3
  259. package/skills/smart-search/SKILL.md +156 -0
  260. package/skills/smart-search/references/sources-ai.md +74 -0
  261. package/skills/smart-search/references/sources-info.md +43 -0
  262. package/skills/smart-search/references/sources-media.md +50 -0
  263. package/skills/smart-search/references/sources-other.md +42 -0
  264. package/skills/smart-search/references/sources-shopping.md +31 -0
  265. package/skills/smart-search/references/sources-social.md +51 -0
  266. package/skills/smart-search/references/sources-tech.md +42 -0
  267. package/skills/smart-search/references/sources-travel.md +20 -0
  268. package/src/browser/base-page.ts +41 -6
  269. package/src/browser/bridge.ts +11 -8
  270. package/src/browser/cdp.ts +1 -8
  271. package/src/browser/daemon-client.ts +11 -1
  272. package/src/browser/dom-helpers.ts +43 -31
  273. package/src/browser/dom-snapshot.ts +23 -1
  274. package/src/browser/page.ts +115 -31
  275. package/src/browser.test.ts +1 -1
  276. package/src/build-manifest.ts +2 -0
  277. package/src/cli.test.ts +133 -0
  278. package/src/cli.ts +73 -11
  279. package/src/clis/1688/item.test.ts +69 -0
  280. package/src/clis/1688/item.ts +282 -0
  281. package/src/clis/1688/search.test.ts +81 -0
  282. package/src/clis/1688/search.ts +402 -0
  283. package/src/clis/1688/shared.test.ts +75 -0
  284. package/src/clis/1688/shared.ts +623 -0
  285. package/src/clis/1688/store.test.ts +69 -0
  286. package/src/clis/1688/store.ts +300 -0
  287. package/src/clis/amazon/bestsellers.test.ts +12 -3
  288. package/src/clis/amazon/bestsellers.ts +6 -178
  289. package/src/clis/amazon/movers-shakers.ts +8 -0
  290. package/src/clis/amazon/new-releases.ts +8 -0
  291. package/src/clis/amazon/rankings.test.ts +47 -0
  292. package/src/clis/amazon/rankings.ts +312 -0
  293. package/src/clis/amazon/shared.test.ts +16 -0
  294. package/src/clis/amazon/shared.ts +134 -12
  295. package/src/clis/bilibili/comments.test.ts +4 -3
  296. package/src/clis/bilibili/comments.ts +2 -2
  297. package/src/clis/bilibili/download.ts +2 -1
  298. package/src/clis/bilibili/subtitle.test.ts +2 -1
  299. package/src/clis/bilibili/subtitle.ts +4 -3
  300. package/src/clis/bilibili/utils.test.ts +21 -0
  301. package/src/clis/bilibili/utils.ts +27 -0
  302. package/src/clis/douban/marks.ts +1 -1
  303. package/src/clis/douban/subject.yaml +50 -19
  304. package/src/clis/doubao/utils.ts +32 -12
  305. package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
  306. package/src/clis/douyin/_shared/transcode.test.ts +0 -2
  307. package/src/clis/douyin/draft.test.ts +0 -2
  308. package/src/clis/facebook/search.test.ts +0 -2
  309. package/src/clis/gemini/ask.test.ts +116 -0
  310. package/src/clis/gemini/ask.ts +10 -3
  311. package/src/clis/gemini/reply-state.test.ts +708 -0
  312. package/src/clis/gemini/utils.test.ts +184 -2
  313. package/src/clis/gemini/utils.ts +588 -60
  314. package/src/clis/hupu/detail.ts +126 -0
  315. package/src/clis/hupu/hot.yaml +43 -0
  316. package/src/clis/hupu/like.ts +76 -0
  317. package/src/clis/hupu/reply.ts +76 -0
  318. package/src/clis/hupu/search.ts +95 -0
  319. package/src/clis/hupu/unlike.ts +76 -0
  320. package/src/clis/hupu/utils.ts +381 -0
  321. package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
  322. package/src/clis/instagram/_shared/private-publish.ts +1303 -0
  323. package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
  324. package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
  325. package/src/clis/instagram/_shared/runtime-info.ts +91 -0
  326. package/src/clis/instagram/note.test.ts +96 -0
  327. package/src/clis/instagram/note.ts +254 -0
  328. package/src/clis/instagram/post.test.ts +1716 -0
  329. package/src/clis/instagram/post.ts +1620 -0
  330. package/src/clis/instagram/reel.test.ts +191 -0
  331. package/src/clis/instagram/reel.ts +886 -0
  332. package/src/clis/instagram/story.test.ts +191 -0
  333. package/src/clis/instagram/story.ts +151 -0
  334. package/src/clis/sinafinance/stock-rank.ts +68 -0
  335. package/src/clis/substack/utils.test.ts +0 -2
  336. package/src/clis/twitter/post.test.ts +157 -0
  337. package/src/clis/twitter/post.ts +82 -48
  338. package/src/clis/twitter/reply.test.ts +177 -0
  339. package/src/clis/twitter/reply.ts +285 -39
  340. package/src/clis/twitter/search.test.ts +88 -5
  341. package/src/clis/twitter/search.ts +68 -5
  342. package/src/clis/xianyu/chat.test.ts +20 -0
  343. package/src/clis/xianyu/chat.ts +175 -0
  344. package/src/clis/xianyu/item.test.ts +67 -0
  345. package/src/clis/xianyu/item.ts +172 -0
  346. package/src/clis/xianyu/search.test.ts +22 -0
  347. package/src/clis/xianyu/search.ts +151 -0
  348. package/src/clis/xianyu/utils.ts +9 -0
  349. package/src/clis/xiaoe/catalog.yaml +129 -0
  350. package/src/clis/xiaoe/content.yaml +43 -0
  351. package/src/clis/xiaoe/courses.yaml +73 -0
  352. package/src/clis/xiaoe/detail.yaml +39 -0
  353. package/src/clis/xiaoe/play-url.yaml +124 -0
  354. package/src/clis/xiaohongshu/comments.test.ts +0 -2
  355. package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
  356. package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
  357. package/src/clis/xiaohongshu/download.test.ts +0 -2
  358. package/src/clis/xiaohongshu/note.test.ts +0 -2
  359. package/src/clis/xiaohongshu/publish.test.ts +0 -2
  360. package/src/clis/xiaohongshu/search.test.ts +59 -48
  361. package/src/clis/xiaohongshu/search.ts +31 -21
  362. package/src/clis/yuanbao/ask.test.ts +156 -0
  363. package/src/clis/yuanbao/ask.ts +522 -0
  364. package/src/clis/yuanbao/new.test.ts +36 -0
  365. package/src/clis/yuanbao/new.ts +81 -0
  366. package/src/clis/yuanbao/shared.ts +57 -0
  367. package/src/clis/zhihu/question.test.ts +42 -17
  368. package/src/clis/zhihu/question.ts +31 -26
  369. package/src/commanderAdapter.test.ts +51 -0
  370. package/src/commanderAdapter.ts +8 -4
  371. package/src/completion.test.ts +30 -0
  372. package/src/completion.ts +3 -1
  373. package/src/doctor.ts +1 -1
  374. package/src/electron-apps.ts +9 -1
  375. package/src/errors.ts +1 -1
  376. package/src/execution.ts +26 -30
  377. package/src/explore.ts +1 -1
  378. package/src/launcher.test.ts +121 -7
  379. package/src/launcher.ts +87 -9
  380. package/src/output.test.ts +50 -90
  381. package/src/output.ts +10 -1
  382. package/src/pipeline/executor.test.ts +0 -2
  383. package/src/pipeline/steps/download.test.ts +0 -2
  384. package/src/registry.ts +2 -0
  385. package/src/serialization.ts +2 -0
  386. package/src/types.ts +9 -2
  387. package/tests/e2e/browser-auth.test.ts +9 -0
  388. package/CLI-EXPLORER.md +0 -724
  389. package/CLI-ONESHOT.md +0 -216
  390. package/SKILL.md +0 -59
@@ -0,0 +1,705 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterAll, describe, expect, it } from 'vitest';
5
+ import { buildConfigureBody, buildConfigureSidecarPayload, buildConfigureToStoryPhotoPayload, buildConfigureToStoryVideoPayload, deriveInstagramJazoest, derivePrivateApiContextFromCapture, extractInstagramRuntimeInfo, getInstagramFeedNormalizedDimensions, getInstagramStoryNormalizedDimensions, isInstagramFeedAspectRatioAllowed, isInstagramStoryAspectRatioAllowed, publishStoryViaPrivateApi, publishMediaViaPrivateApi, publishImagesViaPrivateApi, readImageAsset, resolveInstagramPrivatePublishConfig, } from './private-publish.js';
6
+ const tempDirs = [];
7
+ function createTempFile(name, bytes) {
8
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-private-'));
9
+ tempDirs.push(dir);
10
+ const filePath = path.join(dir, name);
11
+ fs.writeFileSync(filePath, bytes);
12
+ return filePath;
13
+ }
14
+ afterAll(() => {
15
+ for (const dir of tempDirs) {
16
+ fs.rmSync(dir, { recursive: true, force: true });
17
+ }
18
+ });
19
+ describe('instagram private publish helpers', () => {
20
+ it('derives the private API context from captured instagram request headers', () => {
21
+ const entries = [
22
+ {
23
+ kind: 'cdp',
24
+ url: 'https://www.instagram.com/api/v1/feed/timeline/',
25
+ method: 'GET',
26
+ requestHeaders: {
27
+ 'X-ASBD-ID': '359341',
28
+ 'X-CSRFToken': 'csrf-token',
29
+ 'X-IG-App-ID': '936619743392459',
30
+ 'X-IG-WWW-Claim': 'hmac.claim',
31
+ 'X-Instagram-AJAX': '1036517563',
32
+ 'X-Web-Session-ID': 'abc:def:ghi',
33
+ },
34
+ timestamp: Date.now(),
35
+ },
36
+ ];
37
+ expect(derivePrivateApiContextFromCapture(entries)).toEqual({
38
+ asbdId: '359341',
39
+ csrfToken: 'csrf-token',
40
+ igAppId: '936619743392459',
41
+ igWwwClaim: 'hmac.claim',
42
+ instagramAjax: '1036517563',
43
+ webSessionId: 'abc:def:ghi',
44
+ });
45
+ });
46
+ it('derives jazoest from the csrf token', () => {
47
+ expect(deriveInstagramJazoest('SJ_btbvfkpAVFKCN_tJstW')).toBe('22047');
48
+ });
49
+ it('extracts app id, rollout hash, and csrf token from instagram html', () => {
50
+ const html = `
51
+ <html>
52
+ <head>
53
+ <script type="application/json">
54
+ {"csrf_token":"csrf-from-html","rollout_hash":"1036523242","X-IG-App-ID":"936619743392459"}
55
+ </script>
56
+ </head>
57
+ </html>
58
+ `;
59
+ expect(extractInstagramRuntimeInfo(html)).toEqual({
60
+ appId: '936619743392459',
61
+ csrfToken: 'csrf-from-html',
62
+ instagramAjax: '1036523242',
63
+ });
64
+ });
65
+ it('resolves private publish config from capture, runtime html, and cookies', async () => {
66
+ const entries = [
67
+ {
68
+ kind: 'cdp',
69
+ url: 'https://www.instagram.com/api/v1/feed/timeline/',
70
+ method: 'GET',
71
+ requestHeaders: {
72
+ 'X-ASBD-ID': '359341',
73
+ 'X-IG-WWW-Claim': 'hmac.claim',
74
+ 'X-Web-Session-ID': 'abc:def:ghi',
75
+ },
76
+ timestamp: Date.now(),
77
+ },
78
+ ];
79
+ const page = {
80
+ goto: async () => undefined,
81
+ wait: async () => undefined,
82
+ getCookies: async () => [{ name: 'csrftoken', value: 'csrf-cookie', domain: 'instagram.com' }],
83
+ startNetworkCapture: async () => undefined,
84
+ readNetworkCapture: async () => entries,
85
+ evaluate: async () => ({
86
+ appId: '936619743392459',
87
+ csrfToken: 'csrf-from-html',
88
+ instagramAjax: '1036523242',
89
+ }),
90
+ };
91
+ await expect(resolveInstagramPrivatePublishConfig(page)).resolves.toEqual({
92
+ apiContext: {
93
+ asbdId: '359341',
94
+ csrfToken: 'csrf-from-html',
95
+ igAppId: '936619743392459',
96
+ igWwwClaim: 'hmac.claim',
97
+ instagramAjax: '1036523242',
98
+ webSessionId: 'abc:def:ghi',
99
+ },
100
+ jazoest: deriveInstagramJazoest('csrf-from-html'),
101
+ });
102
+ });
103
+ it('retries transient private publish config resolution failures and then succeeds', async () => {
104
+ const entries = [
105
+ {
106
+ kind: 'cdp',
107
+ url: 'https://www.instagram.com/api/v1/feed/timeline/',
108
+ method: 'GET',
109
+ requestHeaders: {
110
+ 'X-ASBD-ID': '359341',
111
+ 'X-IG-WWW-Claim': 'hmac.claim',
112
+ 'X-Web-Session-ID': 'abc:def:ghi',
113
+ },
114
+ timestamp: Date.now(),
115
+ },
116
+ ];
117
+ let evaluateAttempts = 0;
118
+ const page = {
119
+ goto: async () => undefined,
120
+ wait: async () => undefined,
121
+ getCookies: async () => [{ name: 'csrftoken', value: 'csrf-cookie', domain: 'instagram.com' }],
122
+ startNetworkCapture: async () => undefined,
123
+ readNetworkCapture: async () => entries,
124
+ evaluate: async () => {
125
+ evaluateAttempts += 1;
126
+ if (evaluateAttempts === 1) {
127
+ throw new TypeError('fetch failed');
128
+ }
129
+ return {
130
+ appId: '936619743392459',
131
+ csrfToken: 'csrf-from-html',
132
+ instagramAjax: '1036523242',
133
+ };
134
+ },
135
+ };
136
+ await expect(resolveInstagramPrivatePublishConfig(page)).resolves.toEqual({
137
+ apiContext: {
138
+ asbdId: '359341',
139
+ csrfToken: 'csrf-from-html',
140
+ igAppId: '936619743392459',
141
+ igWwwClaim: 'hmac.claim',
142
+ instagramAjax: '1036523242',
143
+ webSessionId: 'abc:def:ghi',
144
+ },
145
+ jazoest: deriveInstagramJazoest('csrf-from-html'),
146
+ });
147
+ expect(evaluateAttempts).toBe(2);
148
+ });
149
+ it('builds the single-image configure form body', () => {
150
+ expect(buildConfigureBody({
151
+ uploadId: '1775134280303',
152
+ caption: 'hello private route',
153
+ jazoest: '22047',
154
+ })).toBe('archive_only=false&caption=hello+private+route&clips_share_preview_to_feed=1'
155
+ + '&disable_comments=0&disable_oa_reuse=false&igtv_share_preview_to_feed=1'
156
+ + '&is_meta_only_post=0&is_unified_video=1&like_and_view_counts_disabled=0'
157
+ + '&media_share_flow=creation_flow&share_to_facebook=&share_to_fb_destination_type=USER'
158
+ + '&source_type=library&upload_id=1775134280303&video_subtitles_enabled=0&jazoest=22047');
159
+ });
160
+ it('builds the carousel configure_sidecar JSON payload', () => {
161
+ expect(buildConfigureSidecarPayload({
162
+ uploadIds: ['1', '3', '2'],
163
+ caption: 'hello carousel',
164
+ clientSidecarId: '1775134574348',
165
+ jazoest: '22047',
166
+ })).toEqual({
167
+ archive_only: false,
168
+ caption: 'hello carousel',
169
+ children_metadata: [
170
+ { upload_id: '1' },
171
+ { upload_id: '3' },
172
+ { upload_id: '2' },
173
+ ],
174
+ client_sidecar_id: '1775134574348',
175
+ disable_comments: '0',
176
+ is_meta_only_post: false,
177
+ is_open_to_public_submission: false,
178
+ like_and_view_counts_disabled: 0,
179
+ media_share_flow: 'creation_flow',
180
+ share_to_facebook: '',
181
+ share_to_fb_destination_type: 'USER',
182
+ source_type: 'library',
183
+ jazoest: '22047',
184
+ });
185
+ });
186
+ it('reads png and jpeg image assets with mime type and dimensions', () => {
187
+ const png = createTempFile('sample.png', Buffer.from('89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082', 'hex'));
188
+ const jpeg = createTempFile('sample.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
189
+ expect(readImageAsset(png)).toMatchObject({
190
+ mimeType: 'image/png',
191
+ width: 3,
192
+ height: 5,
193
+ });
194
+ expect(readImageAsset(jpeg)).toMatchObject({
195
+ mimeType: 'image/jpeg',
196
+ width: 6,
197
+ height: 4,
198
+ });
199
+ });
200
+ it('computes feed-safe aspect-ratio normalization targets', () => {
201
+ expect(isInstagramFeedAspectRatioAllowed(1080, 1350)).toBe(true);
202
+ expect(isInstagramFeedAspectRatioAllowed(1179, 2556)).toBe(false);
203
+ expect(getInstagramFeedNormalizedDimensions(1179, 2556)).toEqual({
204
+ width: 2045,
205
+ height: 2556,
206
+ });
207
+ expect(getInstagramFeedNormalizedDimensions(2120, 1140)).toBeNull();
208
+ });
209
+ it('computes story-safe aspect-ratio normalization targets', () => {
210
+ expect(isInstagramStoryAspectRatioAllowed(1080, 1920)).toBe(true);
211
+ expect(isInstagramStoryAspectRatioAllowed(1080, 1080)).toBe(false);
212
+ expect(getInstagramStoryNormalizedDimensions(1080, 1080)).toEqual({
213
+ width: 1080,
214
+ height: 1440,
215
+ });
216
+ });
217
+ it('builds the single-photo configure_to_story payload', () => {
218
+ expect(buildConfigureToStoryPhotoPayload({
219
+ uploadId: '1775134280303',
220
+ width: 1080,
221
+ height: 1920,
222
+ now: () => 1_775_134_280_303,
223
+ jazoest: '22047',
224
+ })).toMatchObject({
225
+ source_type: '4',
226
+ upload_id: '1775134280303',
227
+ configure_mode: 1,
228
+ edits: {
229
+ crop_original_size: [1080, 1920],
230
+ crop_center: [0, 0],
231
+ crop_zoom: 1.3333334,
232
+ },
233
+ extra: {
234
+ source_width: 1080,
235
+ source_height: 1920,
236
+ },
237
+ jazoest: '22047',
238
+ });
239
+ });
240
+ it('builds the single-video configure_to_story payload', () => {
241
+ expect(buildConfigureToStoryVideoPayload({
242
+ uploadId: '1775134280303',
243
+ width: 1080,
244
+ height: 1920,
245
+ durationMs: 12500,
246
+ now: () => 1_775_134_280_303,
247
+ jazoest: '22047',
248
+ })).toMatchObject({
249
+ source_type: '4',
250
+ upload_id: '1775134280303',
251
+ configure_mode: 1,
252
+ poster_frame_index: 0,
253
+ length: 12.5,
254
+ extra: {
255
+ source_width: 1080,
256
+ source_height: 1920,
257
+ },
258
+ jazoest: '22047',
259
+ });
260
+ });
261
+ it('publishes a single image through rupload + configure', async () => {
262
+ const jpeg = createTempFile('private-single.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
263
+ const calls = [];
264
+ const fetcher = async (url, init) => {
265
+ calls.push({ url: String(url), init });
266
+ if (String(url).includes('/rupload_igphoto/')) {
267
+ return new Response('{"upload_id":"111","status":"ok"}', { status: 200 });
268
+ }
269
+ return new Response('{"media":{"code":"ABC123"}}', { status: 200 });
270
+ };
271
+ const response = await publishImagesViaPrivateApi({
272
+ page: {},
273
+ imagePaths: [jpeg],
274
+ caption: 'private single',
275
+ apiContext: {
276
+ asbdId: '359341',
277
+ csrfToken: 'csrf-token',
278
+ igAppId: '936619743392459',
279
+ igWwwClaim: 'hmac.claim',
280
+ instagramAjax: '1036517563',
281
+ webSessionId: 'abc:def:ghi',
282
+ },
283
+ jazoest: '22047',
284
+ now: () => 111,
285
+ fetcher,
286
+ });
287
+ expect(calls).toHaveLength(2);
288
+ expect(calls[0]?.url).toContain('https://i.instagram.com/rupload_igphoto/fb_uploader_111');
289
+ expect(calls[0]?.init?.headers).toMatchObject({
290
+ 'Content-Type': 'image/jpeg',
291
+ 'X-Entity-Length': String(fs.statSync(jpeg).size),
292
+ 'X-Entity-Name': 'fb_uploader_111',
293
+ 'X-IG-App-ID': '936619743392459',
294
+ });
295
+ expect(calls[1]?.url).toBe('https://www.instagram.com/api/v1/media/configure/');
296
+ expect(String(calls[1]?.init?.body || '')).toContain('upload_id=111');
297
+ expect(response).toEqual({ code: 'ABC123', uploadIds: ['111'] });
298
+ });
299
+ it('publishes a single image story through rupload + configure_to_story', async () => {
300
+ const jpeg = createTempFile('private-story.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080010000903012200021101031101FFD9', 'hex'));
301
+ const calls = [];
302
+ const fetcher = async (url, init) => {
303
+ calls.push({ url: String(url), init });
304
+ if (String(url).includes('/rupload_igphoto/')) {
305
+ return new Response('{"upload_id":"111","status":"ok"}', { status: 200 });
306
+ }
307
+ return new Response('{"media":{"pk":"1234567890"}}', { status: 200 });
308
+ };
309
+ const response = await publishStoryViaPrivateApi({
310
+ page: {},
311
+ mediaItem: { type: 'image', filePath: jpeg },
312
+ content: '',
313
+ currentUserId: '61236465677',
314
+ apiContext: {
315
+ asbdId: '359341',
316
+ csrfToken: 'csrf-token',
317
+ igAppId: '936619743392459',
318
+ igWwwClaim: 'hmac.claim',
319
+ instagramAjax: '1036517563',
320
+ webSessionId: 'abc:def:ghi',
321
+ },
322
+ jazoest: '22047',
323
+ now: () => 111,
324
+ fetcher,
325
+ prepareMediaAsset: async () => ({
326
+ type: 'image',
327
+ asset: {
328
+ filePath: jpeg,
329
+ fileName: path.basename(jpeg),
330
+ mimeType: 'image/jpeg',
331
+ width: 1080,
332
+ height: 1920,
333
+ byteLength: fs.statSync(jpeg).size,
334
+ bytes: fs.readFileSync(jpeg),
335
+ },
336
+ }),
337
+ });
338
+ expect(calls).toHaveLength(2);
339
+ expect(calls[0]?.url).toContain('/rupload_igphoto/fb_uploader_111');
340
+ expect(calls[1]?.url).toBe('https://i.instagram.com/api/v1/media/configure_to_story/');
341
+ expect(String(calls[1]?.init?.body || '')).toContain('signed_body=');
342
+ expect(response).toEqual({ mediaPk: '1234567890', uploadId: '111' });
343
+ });
344
+ it('publishes a single video story through rupload + cover + configure_to_story?video=1', async () => {
345
+ const video = createTempFile('private-story.mp4', Buffer.from('story-video'));
346
+ const coverBytes = Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080010000903012200021101031101FFD9', 'hex');
347
+ const calls = [];
348
+ const fetcher = async (url, init) => {
349
+ calls.push({ url: String(url), init });
350
+ if (String(url).includes('/rupload_igvideo/')) {
351
+ return new Response('{"upload_id":"222","status":"ok"}', { status: 200 });
352
+ }
353
+ if (String(url).includes('/rupload_igphoto/')) {
354
+ return new Response('{"upload_id":"222","status":"ok"}', { status: 200 });
355
+ }
356
+ return new Response('{"media":{"pk":"9988776655"}}', { status: 200 });
357
+ };
358
+ const response = await publishStoryViaPrivateApi({
359
+ page: {},
360
+ mediaItem: { type: 'video', filePath: video },
361
+ content: '',
362
+ currentUserId: '61236465677',
363
+ apiContext: {
364
+ asbdId: '359341',
365
+ csrfToken: 'csrf-token',
366
+ igAppId: '936619743392459',
367
+ igWwwClaim: 'hmac.claim',
368
+ instagramAjax: '1036517563',
369
+ webSessionId: 'abc:def:ghi',
370
+ },
371
+ jazoest: '22047',
372
+ now: () => 222,
373
+ fetcher,
374
+ prepareMediaAsset: async () => ({
375
+ type: 'video',
376
+ asset: {
377
+ filePath: video,
378
+ fileName: path.basename(video),
379
+ mimeType: 'video/mp4',
380
+ width: 1080,
381
+ height: 1920,
382
+ durationMs: 12500,
383
+ byteLength: fs.statSync(video).size,
384
+ bytes: fs.readFileSync(video),
385
+ coverImage: {
386
+ filePath: '/tmp/cover.jpg',
387
+ fileName: 'cover.jpg',
388
+ mimeType: 'image/jpeg',
389
+ width: 1080,
390
+ height: 1920,
391
+ byteLength: coverBytes.length,
392
+ bytes: coverBytes,
393
+ },
394
+ },
395
+ }),
396
+ });
397
+ expect(calls).toHaveLength(4);
398
+ expect(calls[0]?.url).toContain('/rupload_igvideo/fb_uploader_222');
399
+ expect(calls[1]?.url).toContain('/rupload_igphoto/fb_uploader_222');
400
+ expect(calls[2]?.url).toBe('https://i.instagram.com/api/v1/media/configure_to_story/');
401
+ expect(calls[3]?.url).toBe('https://i.instagram.com/api/v1/media/configure_to_story/?video=1');
402
+ expect(String(calls[2]?.init?.body || '')).toContain('signed_body=');
403
+ expect(String(calls[3]?.init?.body || '')).toContain('signed_body=');
404
+ expect(response).toEqual({ mediaPk: '9988776655', uploadId: '222' });
405
+ });
406
+ it('publishes a carousel through rupload + configure_sidecar', async () => {
407
+ const first = createTempFile('private-carousel-1.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
408
+ const second = createTempFile('private-carousel-2.png', Buffer.from('89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082', 'hex'));
409
+ const calls = [];
410
+ let uploadCounter = 0;
411
+ const fetcher = async (url, init) => {
412
+ calls.push({ url: String(url), init });
413
+ if (String(url).includes('/rupload_igphoto/')) {
414
+ uploadCounter += 1;
415
+ return new Response(JSON.stringify({ upload_id: String(200 + uploadCounter), status: 'ok' }), { status: 200 });
416
+ }
417
+ return new Response('{"media":{"code":"SIDE123"}}', { status: 200 });
418
+ };
419
+ const response = await publishImagesViaPrivateApi({
420
+ page: {},
421
+ imagePaths: [first, second],
422
+ caption: 'private carousel',
423
+ apiContext: {
424
+ asbdId: '359341',
425
+ csrfToken: 'csrf-token',
426
+ igAppId: '936619743392459',
427
+ igWwwClaim: 'hmac.claim',
428
+ instagramAjax: '1036517563',
429
+ webSessionId: 'abc:def:ghi',
430
+ },
431
+ jazoest: '22047',
432
+ now: () => 200,
433
+ fetcher,
434
+ prepareAsset: async (filePath) => readImageAsset(filePath),
435
+ });
436
+ expect(calls).toHaveLength(3);
437
+ expect(calls[2]?.url).toBe('https://www.instagram.com/api/v1/media/configure_sidecar/');
438
+ expect(JSON.parse(String(calls[2]?.init?.body || '{}'))).toMatchObject({
439
+ caption: 'private carousel',
440
+ client_sidecar_id: '200',
441
+ children_metadata: [{ upload_id: '201' }, { upload_id: '202' }],
442
+ });
443
+ expect(response).toEqual({ code: 'SIDE123', uploadIds: ['201', '202'] });
444
+ });
445
+ it('uses prepared assets when private carousel upload needs aspect-ratio normalization', async () => {
446
+ const first = createTempFile('private-carousel-normalize-1.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
447
+ const second = createTempFile('private-carousel-normalize-2.png', Buffer.from('89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082', 'hex'));
448
+ const calls = [];
449
+ let uploadCounter = 0;
450
+ const fetcher = async (url, init) => {
451
+ calls.push({ url: String(url), init });
452
+ if (String(url).includes('/rupload_igphoto/')) {
453
+ uploadCounter += 1;
454
+ return new Response(JSON.stringify({ upload_id: String(400 + uploadCounter), status: 'ok' }), { status: 200 });
455
+ }
456
+ return new Response('{"media":{"code":"SIDEPAD"}}', { status: 200 });
457
+ };
458
+ const preparedBytes = Buffer.from('89504E470D0A1A0A0000000D49484452000007FD000009FC08060000008D6F26E50000000049454E44AE426082', 'hex');
459
+ const response = await publishImagesViaPrivateApi({
460
+ page: {},
461
+ imagePaths: [first, second],
462
+ caption: 'private carousel normalized',
463
+ apiContext: {
464
+ asbdId: '359341',
465
+ csrfToken: 'csrf-token',
466
+ igAppId: '936619743392459',
467
+ igWwwClaim: 'hmac.claim',
468
+ instagramAjax: '1036517563',
469
+ webSessionId: 'abc:def:ghi',
470
+ },
471
+ jazoest: '22047',
472
+ now: () => 400,
473
+ fetcher,
474
+ prepareAsset: async (filePath) => {
475
+ if (filePath === second) {
476
+ return {
477
+ filePath: '/tmp/normalized.png',
478
+ fileName: 'normalized.png',
479
+ mimeType: 'image/png',
480
+ width: 2045,
481
+ height: 2556,
482
+ byteLength: preparedBytes.length,
483
+ bytes: preparedBytes,
484
+ cleanupPath: '/tmp/normalized.png',
485
+ };
486
+ }
487
+ return readImageAsset(filePath);
488
+ },
489
+ });
490
+ const secondUploadHeaders = calls[1]?.init?.headers ?? {};
491
+ expect(JSON.parse(String(secondUploadHeaders['X-Instagram-Rupload-Params'] || '{}'))).toMatchObject({
492
+ upload_media_width: 2045,
493
+ upload_media_height: 2556,
494
+ });
495
+ expect(response).toEqual({ code: 'SIDEPAD', uploadIds: ['401', '402'] });
496
+ });
497
+ it('includes the response body when configure_sidecar returns a 400', async () => {
498
+ const first = createTempFile('private-carousel-error-1.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
499
+ const second = createTempFile('private-carousel-error-2.png', Buffer.from('89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082', 'hex'));
500
+ let uploadCounter = 0;
501
+ const fetcher = async (url) => {
502
+ if (String(url).includes('/rupload_igphoto/')) {
503
+ uploadCounter += 1;
504
+ return new Response(JSON.stringify({ upload_id: String(300 + uploadCounter), status: 'ok' }), { status: 200 });
505
+ }
506
+ return new Response('{"message":"children_metadata invalid"}', { status: 400 });
507
+ };
508
+ await expect(publishImagesViaPrivateApi({
509
+ page: {},
510
+ imagePaths: [first, second],
511
+ caption: 'private carousel',
512
+ apiContext: {
513
+ asbdId: '359341',
514
+ csrfToken: 'csrf-token',
515
+ igAppId: '936619743392459',
516
+ igWwwClaim: 'hmac.claim',
517
+ instagramAjax: '1036517563',
518
+ webSessionId: 'abc:def:ghi',
519
+ },
520
+ jazoest: '22047',
521
+ now: () => 300,
522
+ fetcher,
523
+ prepareAsset: async (filePath) => readImageAsset(filePath),
524
+ })).rejects.toThrow('children_metadata invalid');
525
+ });
526
+ it('retries transient rupload fetch failures and still completes the carousel publish', async () => {
527
+ const first = createTempFile('private-carousel-retry-1.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
528
+ const second = createTempFile('private-carousel-retry-2.png', Buffer.from('89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082', 'hex'));
529
+ const calls = [];
530
+ let firstUploadAttempts = 0;
531
+ let uploadCounter = 0;
532
+ const fetcher = async (url) => {
533
+ const value = String(url);
534
+ calls.push(value);
535
+ if (value.includes('/rupload_igphoto/')) {
536
+ firstUploadAttempts += value.includes('fb_uploader_501') ? 1 : 0;
537
+ if (value.includes('fb_uploader_501') && firstUploadAttempts === 1) {
538
+ throw new TypeError('fetch failed');
539
+ }
540
+ uploadCounter += 1;
541
+ return new Response(JSON.stringify({ upload_id: String(500 + uploadCounter), status: 'ok' }), { status: 200 });
542
+ }
543
+ return new Response('{"media":{"code":"SIDERETRY"}}', { status: 200 });
544
+ };
545
+ const response = await publishImagesViaPrivateApi({
546
+ page: {},
547
+ imagePaths: [first, second],
548
+ caption: 'private carousel retry',
549
+ apiContext: {
550
+ asbdId: '359341',
551
+ csrfToken: 'csrf-token',
552
+ igAppId: '936619743392459',
553
+ igWwwClaim: 'hmac.claim',
554
+ instagramAjax: '1036517563',
555
+ webSessionId: 'abc:def:ghi',
556
+ },
557
+ jazoest: '22047',
558
+ now: () => 500,
559
+ fetcher,
560
+ prepareAsset: async (filePath) => readImageAsset(filePath),
561
+ });
562
+ expect(calls.filter((url) => url.includes('fb_uploader_501'))).toHaveLength(2);
563
+ expect(response).toEqual({ code: 'SIDERETRY', uploadIds: ['501', '502'] });
564
+ });
565
+ it('does not retry transient configure_sidecar fetch failures to avoid duplicate posts', async () => {
566
+ const first = createTempFile('private-carousel-no-retry-1.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
567
+ const second = createTempFile('private-carousel-no-retry-2.png', Buffer.from('89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082', 'hex'));
568
+ const calls = [];
569
+ let uploadCounter = 0;
570
+ const fetcher = async (url) => {
571
+ const value = String(url);
572
+ calls.push(value);
573
+ if (value.includes('/rupload_igphoto/')) {
574
+ uploadCounter += 1;
575
+ return new Response(JSON.stringify({ upload_id: String(600 + uploadCounter), status: 'ok' }), { status: 200 });
576
+ }
577
+ throw new TypeError('fetch failed');
578
+ };
579
+ await expect(publishImagesViaPrivateApi({
580
+ page: {},
581
+ imagePaths: [first, second],
582
+ caption: 'private no retry configure',
583
+ apiContext: {
584
+ asbdId: '359341',
585
+ csrfToken: 'csrf-token',
586
+ igAppId: '936619743392459',
587
+ igWwwClaim: 'hmac.claim',
588
+ instagramAjax: '1036517563',
589
+ webSessionId: 'abc:def:ghi',
590
+ },
591
+ jazoest: '22047',
592
+ now: () => 600,
593
+ fetcher,
594
+ prepareAsset: async (filePath) => readImageAsset(filePath),
595
+ })).rejects.toThrow('fetch failed');
596
+ expect(calls.filter((url) => url.includes('configure_sidecar'))).toHaveLength(1);
597
+ });
598
+ it('publishes a mixed image/video carousel and polls configure_sidecar until transcoding finishes', async () => {
599
+ const image = createTempFile('mixed-private-image.jpg', Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9', 'hex'));
600
+ const video = createTempFile('mixed-private-video.mp4', Buffer.from('video-binary'));
601
+ const coverBytes = Buffer.from('FFD8FFE000104A46494600010100000100010000FFC00011080168028003012200021101031101FFD9', 'hex');
602
+ const calls = [];
603
+ let configureAttempts = 0;
604
+ const fetcher = async (url, init) => {
605
+ const value = String(url);
606
+ calls.push({ url: value, init });
607
+ if (value.includes('/rupload_igphoto/') && value.includes('fb_uploader_701')) {
608
+ return new Response('{"upload_id":"701","status":"ok"}', { status: 200 });
609
+ }
610
+ if (value.includes('/rupload_igvideo/') && value.includes('fb_uploader_702')) {
611
+ return new Response('{"media_id":17944674009157009,"status":"ok"}', { status: 200 });
612
+ }
613
+ if (value.includes('/rupload_igphoto/') && value.includes('fb_uploader_702')) {
614
+ return new Response('{"upload_id":"702","status":"ok"}', { status: 200 });
615
+ }
616
+ configureAttempts += 1;
617
+ if (configureAttempts === 1) {
618
+ return new Response('{"message":"Transcode not finished yet.","status":"fail"}', { status: 202 });
619
+ }
620
+ return new Response('{"status":"ok","media":{"code":"MIXEDSIDE123"}}', { status: 200 });
621
+ };
622
+ const response = await publishMediaViaPrivateApi({
623
+ page: {},
624
+ mediaItems: [
625
+ { type: 'image', filePath: image },
626
+ { type: 'video', filePath: video },
627
+ ],
628
+ caption: 'mixed private carousel',
629
+ apiContext: {
630
+ asbdId: '359341',
631
+ csrfToken: 'csrf-token',
632
+ igAppId: '936619743392459',
633
+ igWwwClaim: 'hmac.claim',
634
+ instagramAjax: '1036517563',
635
+ webSessionId: 'abc:def:ghi',
636
+ },
637
+ jazoest: '22047',
638
+ now: () => 700,
639
+ fetcher,
640
+ prepareMediaAsset: async (item) => {
641
+ if (item.type === 'image') {
642
+ return {
643
+ type: 'image',
644
+ asset: readImageAsset(item.filePath),
645
+ };
646
+ }
647
+ return {
648
+ type: 'video',
649
+ asset: {
650
+ filePath: item.filePath,
651
+ fileName: 'mixed-private-video.mp4',
652
+ mimeType: 'video/mp4',
653
+ width: 640,
654
+ height: 360,
655
+ durationMs: 28245,
656
+ byteLength: 12,
657
+ bytes: Buffer.from('video-binary'),
658
+ coverImage: {
659
+ filePath: '/tmp/mixed-private-cover.jpg',
660
+ fileName: 'mixed-private-cover.jpg',
661
+ mimeType: 'image/jpeg',
662
+ width: 640,
663
+ height: 360,
664
+ byteLength: coverBytes.length,
665
+ bytes: coverBytes,
666
+ },
667
+ },
668
+ };
669
+ },
670
+ waitMs: async () => undefined,
671
+ });
672
+ expect(calls).toHaveLength(5);
673
+ expect(calls[0]?.url).toContain('/rupload_igphoto/fb_uploader_701');
674
+ expect(calls[1]?.url).toContain('/rupload_igvideo/fb_uploader_702');
675
+ expect(calls[2]?.url).toContain('/rupload_igphoto/fb_uploader_702');
676
+ expect(JSON.parse(String(calls[1]?.init?.headers?.['X-Instagram-Rupload-Params'] || '{}'))).toMatchObject({
677
+ media_type: 2,
678
+ upload_id: '702',
679
+ upload_media_width: 640,
680
+ upload_media_height: 360,
681
+ upload_media_duration_ms: 28245,
682
+ video_edit_params: {
683
+ crop_width: 360,
684
+ crop_height: 360,
685
+ crop_x1: 140,
686
+ crop_y1: 0,
687
+ trim_start: 0,
688
+ trim_end: 28.245,
689
+ mute: false,
690
+ },
691
+ });
692
+ expect(JSON.parse(String(calls[2]?.init?.headers?.['X-Instagram-Rupload-Params'] || '{}'))).toMatchObject({
693
+ media_type: 2,
694
+ upload_id: '702',
695
+ upload_media_width: 640,
696
+ upload_media_height: 360,
697
+ });
698
+ expect(JSON.parse(String(calls[3]?.init?.body || '{}'))).toMatchObject({
699
+ caption: 'mixed private carousel',
700
+ client_sidecar_id: '700',
701
+ children_metadata: [{ upload_id: '701' }, { upload_id: '702' }],
702
+ });
703
+ expect(response).toEqual({ code: 'MIXEDSIDE123', uploadIds: ['701', '702'] });
704
+ });
705
+ });