@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,827 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+
5
+ import { afterAll, describe, expect, it } from 'vitest';
6
+
7
+ import type { InstagramProtocolCaptureEntry } from './protocol-capture.js';
8
+ import {
9
+ buildConfigureBody,
10
+ buildConfigureSidecarPayload,
11
+ buildConfigureToStoryPhotoPayload,
12
+ buildConfigureToStoryVideoPayload,
13
+ deriveInstagramJazoest,
14
+ derivePrivateApiContextFromCapture,
15
+ extractInstagramRuntimeInfo,
16
+ getInstagramFeedNormalizedDimensions,
17
+ getInstagramStoryNormalizedDimensions,
18
+ isInstagramFeedAspectRatioAllowed,
19
+ isInstagramStoryAspectRatioAllowed,
20
+ publishStoryViaPrivateApi,
21
+ publishMediaViaPrivateApi,
22
+ publishImagesViaPrivateApi,
23
+ readImageAsset,
24
+ resolveInstagramPrivatePublishConfig,
25
+ } from './private-publish.js';
26
+
27
+ const tempDirs: string[] = [];
28
+
29
+ function createTempFile(name: string, bytes: Buffer): string {
30
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-private-'));
31
+ tempDirs.push(dir);
32
+ const filePath = path.join(dir, name);
33
+ fs.writeFileSync(filePath, bytes);
34
+ return filePath;
35
+ }
36
+
37
+ afterAll(() => {
38
+ for (const dir of tempDirs) {
39
+ fs.rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ describe('instagram private publish helpers', () => {
44
+ it('derives the private API context from captured instagram request headers', () => {
45
+ const entries: InstagramProtocolCaptureEntry[] = [
46
+ {
47
+ kind: 'cdp' as never,
48
+ url: 'https://www.instagram.com/api/v1/feed/timeline/',
49
+ method: 'GET',
50
+ requestHeaders: {
51
+ 'X-ASBD-ID': '359341',
52
+ 'X-CSRFToken': 'csrf-token',
53
+ 'X-IG-App-ID': '936619743392459',
54
+ 'X-IG-WWW-Claim': 'hmac.claim',
55
+ 'X-Instagram-AJAX': '1036517563',
56
+ 'X-Web-Session-ID': 'abc:def:ghi',
57
+ },
58
+ timestamp: Date.now(),
59
+ },
60
+ ];
61
+
62
+ expect(derivePrivateApiContextFromCapture(entries)).toEqual({
63
+ asbdId: '359341',
64
+ csrfToken: 'csrf-token',
65
+ igAppId: '936619743392459',
66
+ igWwwClaim: 'hmac.claim',
67
+ instagramAjax: '1036517563',
68
+ webSessionId: 'abc:def:ghi',
69
+ });
70
+ });
71
+
72
+ it('derives jazoest from the csrf token', () => {
73
+ expect(deriveInstagramJazoest('SJ_btbvfkpAVFKCN_tJstW')).toBe('22047');
74
+ });
75
+
76
+ it('extracts app id, rollout hash, and csrf token from instagram html', () => {
77
+ const html = `
78
+ <html>
79
+ <head>
80
+ <script type="application/json">
81
+ {"csrf_token":"csrf-from-html","rollout_hash":"1036523242","X-IG-App-ID":"936619743392459"}
82
+ </script>
83
+ </head>
84
+ </html>
85
+ `;
86
+ expect(extractInstagramRuntimeInfo(html)).toEqual({
87
+ appId: '936619743392459',
88
+ csrfToken: 'csrf-from-html',
89
+ instagramAjax: '1036523242',
90
+ });
91
+ });
92
+
93
+ it('resolves private publish config from capture, runtime html, and cookies', async () => {
94
+ const entries: InstagramProtocolCaptureEntry[] = [
95
+ {
96
+ kind: 'cdp' as never,
97
+ url: 'https://www.instagram.com/api/v1/feed/timeline/',
98
+ method: 'GET',
99
+ requestHeaders: {
100
+ 'X-ASBD-ID': '359341',
101
+ 'X-IG-WWW-Claim': 'hmac.claim',
102
+ 'X-Web-Session-ID': 'abc:def:ghi',
103
+ },
104
+ timestamp: Date.now(),
105
+ },
106
+ ];
107
+ const page = {
108
+ goto: async () => undefined,
109
+ wait: async () => undefined,
110
+ getCookies: async () => [{ name: 'csrftoken', value: 'csrf-cookie', domain: 'instagram.com' }],
111
+ startNetworkCapture: async () => undefined,
112
+ readNetworkCapture: async () => entries,
113
+ evaluate: async () => ({
114
+ appId: '936619743392459',
115
+ csrfToken: 'csrf-from-html',
116
+ instagramAjax: '1036523242',
117
+ }),
118
+ } as any;
119
+
120
+ await expect(resolveInstagramPrivatePublishConfig(page)).resolves.toEqual({
121
+ apiContext: {
122
+ asbdId: '359341',
123
+ csrfToken: 'csrf-from-html',
124
+ igAppId: '936619743392459',
125
+ igWwwClaim: 'hmac.claim',
126
+ instagramAjax: '1036523242',
127
+ webSessionId: 'abc:def:ghi',
128
+ },
129
+ jazoest: deriveInstagramJazoest('csrf-from-html'),
130
+ });
131
+ });
132
+
133
+ it('retries transient private publish config resolution failures and then succeeds', async () => {
134
+ const entries: InstagramProtocolCaptureEntry[] = [
135
+ {
136
+ kind: 'cdp' as never,
137
+ url: 'https://www.instagram.com/api/v1/feed/timeline/',
138
+ method: 'GET',
139
+ requestHeaders: {
140
+ 'X-ASBD-ID': '359341',
141
+ 'X-IG-WWW-Claim': 'hmac.claim',
142
+ 'X-Web-Session-ID': 'abc:def:ghi',
143
+ },
144
+ timestamp: Date.now(),
145
+ },
146
+ ];
147
+ let evaluateAttempts = 0;
148
+ const page = {
149
+ goto: async () => undefined,
150
+ wait: async () => undefined,
151
+ getCookies: async () => [{ name: 'csrftoken', value: 'csrf-cookie', domain: 'instagram.com' }],
152
+ startNetworkCapture: async () => undefined,
153
+ readNetworkCapture: async () => entries,
154
+ evaluate: async () => {
155
+ evaluateAttempts += 1;
156
+ if (evaluateAttempts === 1) {
157
+ throw new TypeError('fetch failed');
158
+ }
159
+ return {
160
+ appId: '936619743392459',
161
+ csrfToken: 'csrf-from-html',
162
+ instagramAjax: '1036523242',
163
+ };
164
+ },
165
+ } as any;
166
+
167
+ await expect(resolveInstagramPrivatePublishConfig(page)).resolves.toEqual({
168
+ apiContext: {
169
+ asbdId: '359341',
170
+ csrfToken: 'csrf-from-html',
171
+ igAppId: '936619743392459',
172
+ igWwwClaim: 'hmac.claim',
173
+ instagramAjax: '1036523242',
174
+ webSessionId: 'abc:def:ghi',
175
+ },
176
+ jazoest: deriveInstagramJazoest('csrf-from-html'),
177
+ });
178
+ expect(evaluateAttempts).toBe(2);
179
+ });
180
+
181
+ it('builds the single-image configure form body', () => {
182
+ expect(buildConfigureBody({
183
+ uploadId: '1775134280303',
184
+ caption: 'hello private route',
185
+ jazoest: '22047',
186
+ })).toBe(
187
+ 'archive_only=false&caption=hello+private+route&clips_share_preview_to_feed=1'
188
+ + '&disable_comments=0&disable_oa_reuse=false&igtv_share_preview_to_feed=1'
189
+ + '&is_meta_only_post=0&is_unified_video=1&like_and_view_counts_disabled=0'
190
+ + '&media_share_flow=creation_flow&share_to_facebook=&share_to_fb_destination_type=USER'
191
+ + '&source_type=library&upload_id=1775134280303&video_subtitles_enabled=0&jazoest=22047'
192
+ );
193
+ });
194
+
195
+ it('builds the carousel configure_sidecar JSON payload', () => {
196
+ expect(buildConfigureSidecarPayload({
197
+ uploadIds: ['1', '3', '2'],
198
+ caption: 'hello carousel',
199
+ clientSidecarId: '1775134574348',
200
+ jazoest: '22047',
201
+ })).toEqual({
202
+ archive_only: false,
203
+ caption: 'hello carousel',
204
+ children_metadata: [
205
+ { upload_id: '1' },
206
+ { upload_id: '3' },
207
+ { upload_id: '2' },
208
+ ],
209
+ client_sidecar_id: '1775134574348',
210
+ disable_comments: '0',
211
+ is_meta_only_post: false,
212
+ is_open_to_public_submission: false,
213
+ like_and_view_counts_disabled: 0,
214
+ media_share_flow: 'creation_flow',
215
+ share_to_facebook: '',
216
+ share_to_fb_destination_type: 'USER',
217
+ source_type: 'library',
218
+ jazoest: '22047',
219
+ });
220
+ });
221
+
222
+ it('reads png and jpeg image assets with mime type and dimensions', () => {
223
+ const png = createTempFile('sample.png', Buffer.from(
224
+ '89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082',
225
+ 'hex',
226
+ ));
227
+ const jpeg = createTempFile('sample.jpg', Buffer.from(
228
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
229
+ 'hex',
230
+ ));
231
+
232
+ expect(readImageAsset(png)).toMatchObject({
233
+ mimeType: 'image/png',
234
+ width: 3,
235
+ height: 5,
236
+ });
237
+ expect(readImageAsset(jpeg)).toMatchObject({
238
+ mimeType: 'image/jpeg',
239
+ width: 6,
240
+ height: 4,
241
+ });
242
+ });
243
+
244
+ it('computes feed-safe aspect-ratio normalization targets', () => {
245
+ expect(isInstagramFeedAspectRatioAllowed(1080, 1350)).toBe(true);
246
+ expect(isInstagramFeedAspectRatioAllowed(1179, 2556)).toBe(false);
247
+ expect(getInstagramFeedNormalizedDimensions(1179, 2556)).toEqual({
248
+ width: 2045,
249
+ height: 2556,
250
+ });
251
+ expect(getInstagramFeedNormalizedDimensions(2120, 1140)).toBeNull();
252
+ });
253
+
254
+ it('computes story-safe aspect-ratio normalization targets', () => {
255
+ expect(isInstagramStoryAspectRatioAllowed(1080, 1920)).toBe(true);
256
+ expect(isInstagramStoryAspectRatioAllowed(1080, 1080)).toBe(false);
257
+ expect(getInstagramStoryNormalizedDimensions(1080, 1080)).toEqual({
258
+ width: 1080,
259
+ height: 1440,
260
+ });
261
+ });
262
+
263
+ it('builds the single-photo configure_to_story payload', () => {
264
+ expect(buildConfigureToStoryPhotoPayload({
265
+ uploadId: '1775134280303',
266
+ width: 1080,
267
+ height: 1920,
268
+ now: () => 1_775_134_280_303,
269
+ jazoest: '22047',
270
+ })).toMatchObject({
271
+ source_type: '4',
272
+ upload_id: '1775134280303',
273
+ configure_mode: 1,
274
+ edits: {
275
+ crop_original_size: [1080, 1920],
276
+ crop_center: [0, 0],
277
+ crop_zoom: 1.3333334,
278
+ },
279
+ extra: {
280
+ source_width: 1080,
281
+ source_height: 1920,
282
+ },
283
+ jazoest: '22047',
284
+ });
285
+ });
286
+
287
+ it('builds the single-video configure_to_story payload', () => {
288
+ expect(buildConfigureToStoryVideoPayload({
289
+ uploadId: '1775134280303',
290
+ width: 1080,
291
+ height: 1920,
292
+ durationMs: 12500,
293
+ now: () => 1_775_134_280_303,
294
+ jazoest: '22047',
295
+ })).toMatchObject({
296
+ source_type: '4',
297
+ upload_id: '1775134280303',
298
+ configure_mode: 1,
299
+ poster_frame_index: 0,
300
+ length: 12.5,
301
+ extra: {
302
+ source_width: 1080,
303
+ source_height: 1920,
304
+ },
305
+ jazoest: '22047',
306
+ });
307
+ });
308
+
309
+ it('publishes a single image through rupload + configure', async () => {
310
+ const jpeg = createTempFile('private-single.jpg', Buffer.from(
311
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
312
+ 'hex',
313
+ ));
314
+ const calls: Array<{ url: string; init?: { method?: string; headers?: Record<string, string>; body?: unknown } }> = [];
315
+ const fetcher = async (url: string | URL, init?: { method?: string; headers?: Record<string, string>; body?: unknown }) => {
316
+ calls.push({ url: String(url), init });
317
+ if (String(url).includes('/rupload_igphoto/')) {
318
+ return new Response('{"upload_id":"111","status":"ok"}', { status: 200 });
319
+ }
320
+ return new Response('{"media":{"code":"ABC123"}}', { status: 200 });
321
+ };
322
+
323
+ const response = await publishImagesViaPrivateApi({
324
+ page: {} as never,
325
+ imagePaths: [jpeg],
326
+ caption: 'private single',
327
+ apiContext: {
328
+ asbdId: '359341',
329
+ csrfToken: 'csrf-token',
330
+ igAppId: '936619743392459',
331
+ igWwwClaim: 'hmac.claim',
332
+ instagramAjax: '1036517563',
333
+ webSessionId: 'abc:def:ghi',
334
+ },
335
+ jazoest: '22047',
336
+ now: () => 111,
337
+ fetcher,
338
+ });
339
+
340
+ expect(calls).toHaveLength(2);
341
+ expect(calls[0]?.url).toContain('https://i.instagram.com/rupload_igphoto/fb_uploader_111');
342
+ expect(calls[0]?.init?.headers).toMatchObject({
343
+ 'Content-Type': 'image/jpeg',
344
+ 'X-Entity-Length': String(fs.statSync(jpeg).size),
345
+ 'X-Entity-Name': 'fb_uploader_111',
346
+ 'X-IG-App-ID': '936619743392459',
347
+ });
348
+ expect(calls[1]?.url).toBe('https://www.instagram.com/api/v1/media/configure/');
349
+ expect(String(calls[1]?.init?.body || '')).toContain('upload_id=111');
350
+ expect(response).toEqual({ code: 'ABC123', uploadIds: ['111'] });
351
+ });
352
+
353
+ it('publishes a single image story through rupload + configure_to_story', async () => {
354
+ const jpeg = createTempFile('private-story.jpg', Buffer.from(
355
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080010000903012200021101031101FFD9',
356
+ 'hex',
357
+ ));
358
+ const calls: Array<{ url: string; init?: { method?: string; headers?: Record<string, string>; body?: unknown } }> = [];
359
+ const fetcher = async (url: string | URL, init?: { method?: string; headers?: Record<string, string>; body?: unknown }) => {
360
+ calls.push({ url: String(url), init });
361
+ if (String(url).includes('/rupload_igphoto/')) {
362
+ return new Response('{"upload_id":"111","status":"ok"}', { status: 200 });
363
+ }
364
+ return new Response('{"media":{"pk":"1234567890"}}', { status: 200 });
365
+ };
366
+
367
+ const response = await publishStoryViaPrivateApi({
368
+ page: {} as never,
369
+ mediaItem: { type: 'image', filePath: jpeg },
370
+ content: '',
371
+ currentUserId: '61236465677',
372
+ apiContext: {
373
+ asbdId: '359341',
374
+ csrfToken: 'csrf-token',
375
+ igAppId: '936619743392459',
376
+ igWwwClaim: 'hmac.claim',
377
+ instagramAjax: '1036517563',
378
+ webSessionId: 'abc:def:ghi',
379
+ },
380
+ jazoest: '22047',
381
+ now: () => 111,
382
+ fetcher,
383
+ prepareMediaAsset: async () => ({
384
+ type: 'image',
385
+ asset: {
386
+ filePath: jpeg,
387
+ fileName: path.basename(jpeg),
388
+ mimeType: 'image/jpeg',
389
+ width: 1080,
390
+ height: 1920,
391
+ byteLength: fs.statSync(jpeg).size,
392
+ bytes: fs.readFileSync(jpeg),
393
+ },
394
+ }),
395
+ });
396
+
397
+ expect(calls).toHaveLength(2);
398
+ expect(calls[0]?.url).toContain('/rupload_igphoto/fb_uploader_111');
399
+ expect(calls[1]?.url).toBe('https://i.instagram.com/api/v1/media/configure_to_story/');
400
+ expect(String(calls[1]?.init?.body || '')).toContain('signed_body=');
401
+ expect(response).toEqual({ mediaPk: '1234567890', uploadId: '111' });
402
+ });
403
+
404
+ it('publishes a single video story through rupload + cover + configure_to_story?video=1', async () => {
405
+ const video = createTempFile('private-story.mp4', Buffer.from('story-video'));
406
+ const coverBytes = Buffer.from(
407
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080010000903012200021101031101FFD9',
408
+ 'hex',
409
+ );
410
+ const calls: Array<{ url: string; init?: { method?: string; headers?: Record<string, string>; body?: unknown } }> = [];
411
+ const fetcher = async (url: string | URL, init?: { method?: string; headers?: Record<string, string>; body?: unknown }) => {
412
+ calls.push({ url: String(url), init });
413
+ if (String(url).includes('/rupload_igvideo/')) {
414
+ return new Response('{"upload_id":"222","status":"ok"}', { status: 200 });
415
+ }
416
+ if (String(url).includes('/rupload_igphoto/')) {
417
+ return new Response('{"upload_id":"222","status":"ok"}', { status: 200 });
418
+ }
419
+ return new Response('{"media":{"pk":"9988776655"}}', { status: 200 });
420
+ };
421
+
422
+ const response = await publishStoryViaPrivateApi({
423
+ page: {} as never,
424
+ mediaItem: { type: 'video', filePath: video },
425
+ content: '',
426
+ currentUserId: '61236465677',
427
+ apiContext: {
428
+ asbdId: '359341',
429
+ csrfToken: 'csrf-token',
430
+ igAppId: '936619743392459',
431
+ igWwwClaim: 'hmac.claim',
432
+ instagramAjax: '1036517563',
433
+ webSessionId: 'abc:def:ghi',
434
+ },
435
+ jazoest: '22047',
436
+ now: () => 222,
437
+ fetcher,
438
+ prepareMediaAsset: async () => ({
439
+ type: 'video',
440
+ asset: {
441
+ filePath: video,
442
+ fileName: path.basename(video),
443
+ mimeType: 'video/mp4',
444
+ width: 1080,
445
+ height: 1920,
446
+ durationMs: 12500,
447
+ byteLength: fs.statSync(video).size,
448
+ bytes: fs.readFileSync(video),
449
+ coverImage: {
450
+ filePath: '/tmp/cover.jpg',
451
+ fileName: 'cover.jpg',
452
+ mimeType: 'image/jpeg',
453
+ width: 1080,
454
+ height: 1920,
455
+ byteLength: coverBytes.length,
456
+ bytes: coverBytes,
457
+ },
458
+ },
459
+ }),
460
+ });
461
+
462
+ expect(calls).toHaveLength(4);
463
+ expect(calls[0]?.url).toContain('/rupload_igvideo/fb_uploader_222');
464
+ expect(calls[1]?.url).toContain('/rupload_igphoto/fb_uploader_222');
465
+ expect(calls[2]?.url).toBe('https://i.instagram.com/api/v1/media/configure_to_story/');
466
+ expect(calls[3]?.url).toBe('https://i.instagram.com/api/v1/media/configure_to_story/?video=1');
467
+ expect(String(calls[2]?.init?.body || '')).toContain('signed_body=');
468
+ expect(String(calls[3]?.init?.body || '')).toContain('signed_body=');
469
+ expect(response).toEqual({ mediaPk: '9988776655', uploadId: '222' });
470
+ });
471
+
472
+ it('publishes a carousel through rupload + configure_sidecar', async () => {
473
+ const first = createTempFile('private-carousel-1.jpg', Buffer.from(
474
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
475
+ 'hex',
476
+ ));
477
+ const second = createTempFile('private-carousel-2.png', Buffer.from(
478
+ '89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082',
479
+ 'hex',
480
+ ));
481
+ const calls: Array<{ url: string; init?: { method?: string; headers?: Record<string, string>; body?: unknown } }> = [];
482
+ let uploadCounter = 0;
483
+ const fetcher = async (url: string | URL, init?: { method?: string; headers?: Record<string, string>; body?: unknown }) => {
484
+ calls.push({ url: String(url), init });
485
+ if (String(url).includes('/rupload_igphoto/')) {
486
+ uploadCounter += 1;
487
+ return new Response(JSON.stringify({ upload_id: String(200 + uploadCounter), status: 'ok' }), { status: 200 });
488
+ }
489
+ return new Response('{"media":{"code":"SIDE123"}}', { status: 200 });
490
+ };
491
+
492
+ const response = await publishImagesViaPrivateApi({
493
+ page: {} as never,
494
+ imagePaths: [first, second],
495
+ caption: 'private carousel',
496
+ apiContext: {
497
+ asbdId: '359341',
498
+ csrfToken: 'csrf-token',
499
+ igAppId: '936619743392459',
500
+ igWwwClaim: 'hmac.claim',
501
+ instagramAjax: '1036517563',
502
+ webSessionId: 'abc:def:ghi',
503
+ },
504
+ jazoest: '22047',
505
+ now: () => 200,
506
+ fetcher,
507
+ prepareAsset: async (filePath) => readImageAsset(filePath),
508
+ });
509
+
510
+ expect(calls).toHaveLength(3);
511
+ expect(calls[2]?.url).toBe('https://www.instagram.com/api/v1/media/configure_sidecar/');
512
+ expect(JSON.parse(String(calls[2]?.init?.body || '{}'))).toMatchObject({
513
+ caption: 'private carousel',
514
+ client_sidecar_id: '200',
515
+ children_metadata: [{ upload_id: '201' }, { upload_id: '202' }],
516
+ });
517
+ expect(response).toEqual({ code: 'SIDE123', uploadIds: ['201', '202'] });
518
+ });
519
+
520
+ it('uses prepared assets when private carousel upload needs aspect-ratio normalization', async () => {
521
+ const first = createTempFile('private-carousel-normalize-1.jpg', Buffer.from(
522
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
523
+ 'hex',
524
+ ));
525
+ const second = createTempFile('private-carousel-normalize-2.png', Buffer.from(
526
+ '89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082',
527
+ 'hex',
528
+ ));
529
+ const calls: Array<{ url: string; init?: { method?: string; headers?: Record<string, string>; body?: unknown } }> = [];
530
+ let uploadCounter = 0;
531
+ const fetcher = async (url: string | URL, init?: { method?: string; headers?: Record<string, string>; body?: unknown }) => {
532
+ calls.push({ url: String(url), init });
533
+ if (String(url).includes('/rupload_igphoto/')) {
534
+ uploadCounter += 1;
535
+ return new Response(JSON.stringify({ upload_id: String(400 + uploadCounter), status: 'ok' }), { status: 200 });
536
+ }
537
+ return new Response('{"media":{"code":"SIDEPAD"}}', { status: 200 });
538
+ };
539
+
540
+ const preparedBytes = Buffer.from(
541
+ '89504E470D0A1A0A0000000D49484452000007FD000009FC08060000008D6F26E50000000049454E44AE426082',
542
+ 'hex',
543
+ );
544
+
545
+ const response = await publishImagesViaPrivateApi({
546
+ page: {} as never,
547
+ imagePaths: [first, second],
548
+ caption: 'private carousel normalized',
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: () => 400,
559
+ fetcher,
560
+ prepareAsset: async (filePath) => {
561
+ if (filePath === second) {
562
+ return {
563
+ filePath: '/tmp/normalized.png',
564
+ fileName: 'normalized.png',
565
+ mimeType: 'image/png',
566
+ width: 2045,
567
+ height: 2556,
568
+ byteLength: preparedBytes.length,
569
+ bytes: preparedBytes,
570
+ cleanupPath: '/tmp/normalized.png',
571
+ };
572
+ }
573
+ return readImageAsset(filePath);
574
+ },
575
+ });
576
+
577
+ const secondUploadHeaders = calls[1]?.init?.headers ?? {};
578
+ expect(JSON.parse(String(secondUploadHeaders['X-Instagram-Rupload-Params'] || '{}'))).toMatchObject({
579
+ upload_media_width: 2045,
580
+ upload_media_height: 2556,
581
+ });
582
+ expect(response).toEqual({ code: 'SIDEPAD', uploadIds: ['401', '402'] });
583
+ });
584
+
585
+ it('includes the response body when configure_sidecar returns a 400', async () => {
586
+ const first = createTempFile('private-carousel-error-1.jpg', Buffer.from(
587
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
588
+ 'hex',
589
+ ));
590
+ const second = createTempFile('private-carousel-error-2.png', Buffer.from(
591
+ '89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082',
592
+ 'hex',
593
+ ));
594
+ let uploadCounter = 0;
595
+ const fetcher = async (url: string | URL) => {
596
+ if (String(url).includes('/rupload_igphoto/')) {
597
+ uploadCounter += 1;
598
+ return new Response(JSON.stringify({ upload_id: String(300 + uploadCounter), status: 'ok' }), { status: 200 });
599
+ }
600
+ return new Response('{"message":"children_metadata invalid"}', { status: 400 });
601
+ };
602
+
603
+ await expect(publishImagesViaPrivateApi({
604
+ page: {} as never,
605
+ imagePaths: [first, second],
606
+ caption: 'private carousel',
607
+ apiContext: {
608
+ asbdId: '359341',
609
+ csrfToken: 'csrf-token',
610
+ igAppId: '936619743392459',
611
+ igWwwClaim: 'hmac.claim',
612
+ instagramAjax: '1036517563',
613
+ webSessionId: 'abc:def:ghi',
614
+ },
615
+ jazoest: '22047',
616
+ now: () => 300,
617
+ fetcher,
618
+ prepareAsset: async (filePath) => readImageAsset(filePath),
619
+ })).rejects.toThrow('children_metadata invalid');
620
+ });
621
+
622
+ it('retries transient rupload fetch failures and still completes the carousel publish', async () => {
623
+ const first = createTempFile('private-carousel-retry-1.jpg', Buffer.from(
624
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
625
+ 'hex',
626
+ ));
627
+ const second = createTempFile('private-carousel-retry-2.png', Buffer.from(
628
+ '89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082',
629
+ 'hex',
630
+ ));
631
+ const calls: string[] = [];
632
+ let firstUploadAttempts = 0;
633
+ let uploadCounter = 0;
634
+ const fetcher = async (url: string | URL) => {
635
+ const value = String(url);
636
+ calls.push(value);
637
+ if (value.includes('/rupload_igphoto/')) {
638
+ firstUploadAttempts += value.includes('fb_uploader_501') ? 1 : 0;
639
+ if (value.includes('fb_uploader_501') && firstUploadAttempts === 1) {
640
+ throw new TypeError('fetch failed');
641
+ }
642
+ uploadCounter += 1;
643
+ return new Response(JSON.stringify({ upload_id: String(500 + uploadCounter), status: 'ok' }), { status: 200 });
644
+ }
645
+ return new Response('{"media":{"code":"SIDERETRY"}}', { status: 200 });
646
+ };
647
+
648
+ const response = await publishImagesViaPrivateApi({
649
+ page: {} as never,
650
+ imagePaths: [first, second],
651
+ caption: 'private carousel retry',
652
+ apiContext: {
653
+ asbdId: '359341',
654
+ csrfToken: 'csrf-token',
655
+ igAppId: '936619743392459',
656
+ igWwwClaim: 'hmac.claim',
657
+ instagramAjax: '1036517563',
658
+ webSessionId: 'abc:def:ghi',
659
+ },
660
+ jazoest: '22047',
661
+ now: () => 500,
662
+ fetcher,
663
+ prepareAsset: async (filePath) => readImageAsset(filePath),
664
+ });
665
+
666
+ expect(calls.filter((url) => url.includes('fb_uploader_501'))).toHaveLength(2);
667
+ expect(response).toEqual({ code: 'SIDERETRY', uploadIds: ['501', '502'] });
668
+ });
669
+
670
+ it('does not retry transient configure_sidecar fetch failures to avoid duplicate posts', async () => {
671
+ const first = createTempFile('private-carousel-no-retry-1.jpg', Buffer.from(
672
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
673
+ 'hex',
674
+ ));
675
+ const second = createTempFile('private-carousel-no-retry-2.png', Buffer.from(
676
+ '89504E470D0A1A0A0000000D49484452000000030000000508060000008D6F26E50000000049454E44AE426082',
677
+ 'hex',
678
+ ));
679
+ const calls: string[] = [];
680
+ let uploadCounter = 0;
681
+ const fetcher = async (url: string | URL) => {
682
+ const value = String(url);
683
+ calls.push(value);
684
+ if (value.includes('/rupload_igphoto/')) {
685
+ uploadCounter += 1;
686
+ return new Response(JSON.stringify({ upload_id: String(600 + uploadCounter), status: 'ok' }), { status: 200 });
687
+ }
688
+ throw new TypeError('fetch failed');
689
+ };
690
+
691
+ await expect(publishImagesViaPrivateApi({
692
+ page: {} as never,
693
+ imagePaths: [first, second],
694
+ caption: 'private no retry configure',
695
+ apiContext: {
696
+ asbdId: '359341',
697
+ csrfToken: 'csrf-token',
698
+ igAppId: '936619743392459',
699
+ igWwwClaim: 'hmac.claim',
700
+ instagramAjax: '1036517563',
701
+ webSessionId: 'abc:def:ghi',
702
+ },
703
+ jazoest: '22047',
704
+ now: () => 600,
705
+ fetcher,
706
+ prepareAsset: async (filePath) => readImageAsset(filePath),
707
+ })).rejects.toThrow('fetch failed');
708
+
709
+ expect(calls.filter((url) => url.includes('configure_sidecar'))).toHaveLength(1);
710
+ });
711
+
712
+ it('publishes a mixed image/video carousel and polls configure_sidecar until transcoding finishes', async () => {
713
+ const image = createTempFile('mixed-private-image.jpg', Buffer.from(
714
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080004000603012200021101031101FFD9',
715
+ 'hex',
716
+ ));
717
+ const video = createTempFile('mixed-private-video.mp4', Buffer.from('video-binary'));
718
+ const coverBytes = Buffer.from(
719
+ 'FFD8FFE000104A46494600010100000100010000FFC00011080168028003012200021101031101FFD9',
720
+ 'hex',
721
+ );
722
+ const calls: Array<{ url: string; init?: { method?: string; headers?: Record<string, string>; body?: unknown } }> = [];
723
+ let configureAttempts = 0;
724
+ const fetcher = async (url: string | URL, init?: { method?: string; headers?: Record<string, string>; body?: unknown }) => {
725
+ const value = String(url);
726
+ calls.push({ url: value, init });
727
+ if (value.includes('/rupload_igphoto/') && value.includes('fb_uploader_701')) {
728
+ return new Response('{"upload_id":"701","status":"ok"}', { status: 200 });
729
+ }
730
+ if (value.includes('/rupload_igvideo/') && value.includes('fb_uploader_702')) {
731
+ return new Response('{"media_id":17944674009157009,"status":"ok"}', { status: 200 });
732
+ }
733
+ if (value.includes('/rupload_igphoto/') && value.includes('fb_uploader_702')) {
734
+ return new Response('{"upload_id":"702","status":"ok"}', { status: 200 });
735
+ }
736
+ configureAttempts += 1;
737
+ if (configureAttempts === 1) {
738
+ return new Response('{"message":"Transcode not finished yet.","status":"fail"}', { status: 202 });
739
+ }
740
+ return new Response('{"status":"ok","media":{"code":"MIXEDSIDE123"}}', { status: 200 });
741
+ };
742
+
743
+ const response = await publishMediaViaPrivateApi({
744
+ page: {} as never,
745
+ mediaItems: [
746
+ { type: 'image', filePath: image },
747
+ { type: 'video', filePath: video },
748
+ ],
749
+ caption: 'mixed private carousel',
750
+ apiContext: {
751
+ asbdId: '359341',
752
+ csrfToken: 'csrf-token',
753
+ igAppId: '936619743392459',
754
+ igWwwClaim: 'hmac.claim',
755
+ instagramAjax: '1036517563',
756
+ webSessionId: 'abc:def:ghi',
757
+ },
758
+ jazoest: '22047',
759
+ now: () => 700,
760
+ fetcher,
761
+ prepareMediaAsset: async (item) => {
762
+ if (item.type === 'image') {
763
+ return {
764
+ type: 'image' as const,
765
+ asset: readImageAsset(item.filePath),
766
+ };
767
+ }
768
+ return {
769
+ type: 'video' as const,
770
+ asset: {
771
+ filePath: item.filePath,
772
+ fileName: 'mixed-private-video.mp4',
773
+ mimeType: 'video/mp4',
774
+ width: 640,
775
+ height: 360,
776
+ durationMs: 28245,
777
+ byteLength: 12,
778
+ bytes: Buffer.from('video-binary'),
779
+ coverImage: {
780
+ filePath: '/tmp/mixed-private-cover.jpg',
781
+ fileName: 'mixed-private-cover.jpg',
782
+ mimeType: 'image/jpeg',
783
+ width: 640,
784
+ height: 360,
785
+ byteLength: coverBytes.length,
786
+ bytes: coverBytes,
787
+ },
788
+ },
789
+ };
790
+ },
791
+ waitMs: async () => undefined,
792
+ });
793
+
794
+ expect(calls).toHaveLength(5);
795
+ expect(calls[0]?.url).toContain('/rupload_igphoto/fb_uploader_701');
796
+ expect(calls[1]?.url).toContain('/rupload_igvideo/fb_uploader_702');
797
+ expect(calls[2]?.url).toContain('/rupload_igphoto/fb_uploader_702');
798
+ expect(JSON.parse(String(calls[1]?.init?.headers?.['X-Instagram-Rupload-Params'] || '{}'))).toMatchObject({
799
+ media_type: 2,
800
+ upload_id: '702',
801
+ upload_media_width: 640,
802
+ upload_media_height: 360,
803
+ upload_media_duration_ms: 28245,
804
+ video_edit_params: {
805
+ crop_width: 360,
806
+ crop_height: 360,
807
+ crop_x1: 140,
808
+ crop_y1: 0,
809
+ trim_start: 0,
810
+ trim_end: 28.245,
811
+ mute: false,
812
+ },
813
+ });
814
+ expect(JSON.parse(String(calls[2]?.init?.headers?.['X-Instagram-Rupload-Params'] || '{}'))).toMatchObject({
815
+ media_type: 2,
816
+ upload_id: '702',
817
+ upload_media_width: 640,
818
+ upload_media_height: 360,
819
+ });
820
+ expect(JSON.parse(String(calls[3]?.init?.body || '{}'))).toMatchObject({
821
+ caption: 'mixed private carousel',
822
+ client_sidecar_id: '700',
823
+ children_metadata: [{ upload_id: '701' }, { upload_id: '702' }],
824
+ });
825
+ expect(response).toEqual({ code: 'MIXEDSIDE123', uploadIds: ['701', '702'] });
826
+ });
827
+ });