@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,148 @@
1
+ import * as fs from 'node:fs';
2
+
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import type { BrowserCookie, IPage } from '../../../types.js';
6
+ import {
7
+ buildInstallInstagramProtocolCaptureJs,
8
+ buildReadInstagramProtocolCaptureJs,
9
+ dumpInstagramProtocolCaptureIfEnabled,
10
+ instagramPrivateApiFetch,
11
+ installInstagramProtocolCapture,
12
+ readInstagramProtocolCapture,
13
+ } from './protocol-capture.js';
14
+
15
+ describe('instagram protocol capture helpers', () => {
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ delete process.env.OPENCLI_INSTAGRAM_CAPTURE;
19
+ try { fs.rmSync('/tmp/instagram_post_protocol_trace.json', { force: true }); } catch {}
20
+ });
21
+
22
+ it('installs the protocol capture patch in page context', async () => {
23
+ const evaluate = vi.fn().mockResolvedValue({ ok: true });
24
+ const page = { evaluate } as unknown as IPage;
25
+
26
+ await installInstagramProtocolCapture(page);
27
+
28
+ expect(evaluate).toHaveBeenCalledTimes(1);
29
+ expect(String(evaluate.mock.calls[0]?.[0] || '')).toContain('__opencli_ig_protocol_capture');
30
+ expect(String(evaluate.mock.calls[0]?.[0] || '')).toContain('/media/configure_sidecar/');
31
+ });
32
+
33
+ it('prefers native page network capture when available', async () => {
34
+ const startNetworkCapture = vi.fn().mockResolvedValue(undefined);
35
+ const evaluate = vi.fn();
36
+ const page = { startNetworkCapture, evaluate } as unknown as IPage;
37
+
38
+ await installInstagramProtocolCapture(page);
39
+
40
+ expect(startNetworkCapture).toHaveBeenCalledTimes(1);
41
+ expect(evaluate).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it('reads and normalizes captured protocol entries', async () => {
45
+ const evaluate = vi.fn().mockResolvedValue({
46
+ data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/' }],
47
+ errors: ['ignored'],
48
+ });
49
+ const page = { evaluate } as unknown as IPage;
50
+
51
+ const result = await readInstagramProtocolCapture(page);
52
+
53
+ expect(String(evaluate.mock.calls[0]?.[0] || '')).toContain('__opencli_ig_protocol_capture');
54
+ expect(result).toEqual({
55
+ data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/' }],
56
+ errors: ['ignored'],
57
+ });
58
+ });
59
+
60
+ it('prefers native page network capture reads when available', async () => {
61
+ const readNetworkCapture = vi.fn().mockResolvedValue([
62
+ { kind: 'cdp', url: 'https://www.instagram.com/rupload_igphoto/test', method: 'POST' },
63
+ ]);
64
+ const evaluate = vi.fn();
65
+ const page = { readNetworkCapture, evaluate } as unknown as IPage;
66
+
67
+ const result = await readInstagramProtocolCapture(page);
68
+
69
+ expect(readNetworkCapture).toHaveBeenCalledTimes(1);
70
+ expect(evaluate).not.toHaveBeenCalled();
71
+ expect(result).toEqual({
72
+ data: [{ kind: 'cdp', url: 'https://www.instagram.com/rupload_igphoto/test', method: 'POST' }],
73
+ errors: [],
74
+ });
75
+ });
76
+
77
+ it('dumps protocol traces to /tmp only when capture env is enabled', async () => {
78
+ process.env.OPENCLI_INSTAGRAM_CAPTURE = '1';
79
+ const page = {
80
+ evaluate: vi.fn().mockResolvedValue({
81
+ data: [{ kind: 'fetch', url: 'https://www.instagram.com/rupload_igphoto/test' }],
82
+ errors: [],
83
+ }),
84
+ } as unknown as IPage;
85
+
86
+ await dumpInstagramProtocolCaptureIfEnabled(page);
87
+
88
+ const raw = fs.readFileSync('/tmp/instagram_post_protocol_trace.json', 'utf8');
89
+ expect(raw).toContain('rupload_igphoto');
90
+ });
91
+
92
+ it('does not dump protocol traces when capture env is disabled', async () => {
93
+ const page = {
94
+ evaluate: vi.fn(),
95
+ } as unknown as IPage;
96
+
97
+ await dumpInstagramProtocolCaptureIfEnabled(page);
98
+
99
+ expect(page.evaluate).not.toHaveBeenCalled();
100
+ expect(fs.existsSync('/tmp/instagram_post_protocol_trace.json')).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe('instagram private api fetch', () => {
105
+ afterEach(() => {
106
+ vi.restoreAllMocks();
107
+ });
108
+
109
+ it('uses browser cookies to build instagram private api requests', async () => {
110
+ const getCookies = vi.fn()
111
+ .mockResolvedValueOnce([{ name: 'sessionid', value: 'sess', domain: '.instagram.com' } satisfies BrowserCookie])
112
+ .mockResolvedValueOnce([
113
+ { name: 'csrftoken', value: 'csrf', domain: '.instagram.com' } satisfies BrowserCookie,
114
+ { name: 'sessionid', value: 'sess', domain: '.instagram.com' } satisfies BrowserCookie,
115
+ ]);
116
+ const evaluate = vi.fn().mockResolvedValue({
117
+ appId: 'dynamic-app-id',
118
+ csrfToken: 'csrf',
119
+ instagramAjax: 'dynamic-rollout',
120
+ });
121
+ const page = { getCookies, evaluate } as unknown as IPage;
122
+ const fetchMock = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }));
123
+ vi.stubGlobal('fetch', fetchMock);
124
+
125
+ await instagramPrivateApiFetch(page, 'https://www.instagram.com/api/v1/media/configure/', {
126
+ method: 'POST',
127
+ body: 'caption=test',
128
+ });
129
+
130
+ expect(fetchMock).toHaveBeenCalledWith(
131
+ 'https://www.instagram.com/api/v1/media/configure/',
132
+ expect.objectContaining({
133
+ method: 'POST',
134
+ headers: expect.objectContaining({
135
+ 'X-CSRFToken': 'csrf',
136
+ 'X-IG-App-ID': 'dynamic-app-id',
137
+ 'Cookie': expect.stringContaining('sessionid=sess'),
138
+ }),
139
+ body: 'caption=test',
140
+ }),
141
+ );
142
+ });
143
+
144
+ it('exposes stable browser-side JS builders', () => {
145
+ expect(buildInstallInstagramProtocolCaptureJs()).toContain('/rupload_igphoto/');
146
+ expect(buildReadInstagramProtocolCaptureJs()).toContain('__opencli_ig_protocol_capture');
147
+ });
148
+ });
@@ -0,0 +1,321 @@
1
+ import * as fs from 'node:fs';
2
+
3
+ import type { BrowserCookie, IPage } from '../../../types.js';
4
+ import { resolveInstagramRuntimeInfo } from './runtime-info.js';
5
+
6
+ const DEFAULT_CAPTURE_VAR = '__opencli_ig_protocol_capture';
7
+ const DEFAULT_CAPTURE_ERRORS_VAR = '__opencli_ig_protocol_capture_errors';
8
+ const TRACE_OUTPUT_PATH = '/tmp/instagram_post_protocol_trace.json';
9
+ const INSTAGRAM_PROTOCOL_CAPTURE_PATTERN = [
10
+ '/rupload_igphoto/',
11
+ '/rupload_igvideo/',
12
+ '/api/v1/',
13
+ '/media/configure/',
14
+ '/media/configure_sidecar/',
15
+ '/media/configure_to_story/',
16
+ '/api/graphql/',
17
+ ].join('|');
18
+
19
+ export interface InstagramProtocolCaptureEntry {
20
+ kind: 'fetch' | 'xhr';
21
+ url: string;
22
+ method: string;
23
+ requestHeaders?: Record<string, string>;
24
+ requestBodyKind?: string;
25
+ requestBodyPreview?: string;
26
+ responseStatus?: number;
27
+ responseContentType?: string;
28
+ responsePreview?: string;
29
+ timestamp: number;
30
+ }
31
+
32
+ export function buildInstallInstagramProtocolCaptureJs(
33
+ captureVar: string = DEFAULT_CAPTURE_VAR,
34
+ captureErrorsVar: string = DEFAULT_CAPTURE_ERRORS_VAR,
35
+ ): string {
36
+ return `
37
+ (() => {
38
+ const CAPTURE_VAR = ${JSON.stringify(captureVar)};
39
+ const CAPTURE_ERRORS_VAR = ${JSON.stringify(captureErrorsVar)};
40
+ const PATCH_GUARD = CAPTURE_VAR + '_patched';
41
+ const FILTERS = [
42
+ '/rupload_igphoto/',
43
+ '/rupload_igvideo/',
44
+ '/api/v1/',
45
+ '/media/configure/',
46
+ '/media/configure_sidecar/',
47
+ '/media/configure_to_story/',
48
+ '/api/graphql/',
49
+ ];
50
+
51
+ const shouldCapture = (url) => {
52
+ const value = String(url || '');
53
+ return FILTERS.some((filter) => value.includes(filter));
54
+ };
55
+
56
+ const normalizeHeaders = (headersLike) => {
57
+ const out = {};
58
+ try {
59
+ if (!headersLike) return out;
60
+ if (headersLike instanceof Headers) {
61
+ headersLike.forEach((value, key) => { out[key] = value; });
62
+ return out;
63
+ }
64
+ if (Array.isArray(headersLike)) {
65
+ for (const pair of headersLike) {
66
+ if (Array.isArray(pair) && pair.length >= 2) out[String(pair[0])] = String(pair[1]);
67
+ }
68
+ return out;
69
+ }
70
+ if (typeof headersLike === 'object') {
71
+ for (const [key, value] of Object.entries(headersLike)) out[key] = String(value);
72
+ }
73
+ } catch {}
74
+ return out;
75
+ };
76
+
77
+ const summarizeBody = async (body) => {
78
+ if (body == null) return { kind: 'empty', preview: '' };
79
+ try {
80
+ if (typeof body === 'string') {
81
+ return { kind: 'string', preview: body.slice(0, 1000) };
82
+ }
83
+ if (body instanceof URLSearchParams) {
84
+ return { kind: 'urlencoded', preview: body.toString().slice(0, 1000) };
85
+ }
86
+ if (body instanceof FormData) {
87
+ const parts = [];
88
+ for (const [key, value] of body.entries()) {
89
+ if (value instanceof File) {
90
+ parts.push(key + '=File(' + value.name + ',' + value.type + ',' + value.size + ')');
91
+ } else {
92
+ parts.push(key + '=' + String(value));
93
+ }
94
+ }
95
+ return { kind: 'formdata', preview: parts.join('&').slice(0, 2000) };
96
+ }
97
+ if (body instanceof Blob) {
98
+ return { kind: 'blob', preview: 'Blob(' + body.type + ',' + body.size + ')' };
99
+ }
100
+ if (body instanceof ArrayBuffer) {
101
+ return { kind: 'arraybuffer', preview: 'ArrayBuffer(' + body.byteLength + ')' };
102
+ }
103
+ if (ArrayBuffer.isView(body)) {
104
+ return { kind: 'typed-array', preview: body.constructor.name + '(' + body.byteLength + ')' };
105
+ }
106
+ return { kind: typeof body, preview: String(body).slice(0, 1000) };
107
+ } catch (error) {
108
+ return { kind: 'unknown', preview: 'body-preview-error:' + String(error) };
109
+ }
110
+ };
111
+
112
+ const capture = async (kind, url, method, headers, body, response) => {
113
+ if (!shouldCapture(url)) return;
114
+ try {
115
+ const bodyInfo = await summarizeBody(body);
116
+ const contentType = response?.headers?.get?.('content-type') || '';
117
+ let responsePreview = '';
118
+ try {
119
+ if (response && typeof response.clone === 'function') {
120
+ const clone = response.clone();
121
+ responsePreview = (await clone.text()).slice(0, 4000);
122
+ }
123
+ } catch (error) {
124
+ responsePreview = 'response-preview-error:' + String(error);
125
+ }
126
+ window[CAPTURE_VAR].push({
127
+ kind,
128
+ url: String(url || ''),
129
+ method: String(method || 'GET').toUpperCase(),
130
+ requestHeaders: normalizeHeaders(headers),
131
+ requestBodyKind: bodyInfo.kind,
132
+ requestBodyPreview: bodyInfo.preview,
133
+ responseStatus: response?.status,
134
+ responseContentType: contentType,
135
+ responsePreview,
136
+ timestamp: Date.now(),
137
+ });
138
+ } catch (error) {
139
+ window[CAPTURE_ERRORS_VAR].push(String(error));
140
+ }
141
+ };
142
+
143
+ if (!Array.isArray(window[CAPTURE_VAR])) window[CAPTURE_VAR] = [];
144
+ if (!Array.isArray(window[CAPTURE_ERRORS_VAR])) window[CAPTURE_ERRORS_VAR] = [];
145
+ if (window[PATCH_GUARD]) return { ok: true };
146
+
147
+ const origFetch = window.fetch;
148
+ window.fetch = async function(...args) {
149
+ const input = args[0];
150
+ const init = args[1] || {};
151
+ const url = typeof input === 'string'
152
+ ? input
153
+ : input instanceof Request
154
+ ? input.url
155
+ : String(input || '');
156
+ const method = init.method || (input instanceof Request ? input.method : 'GET');
157
+ const headers = init.headers || (input instanceof Request ? input.headers : undefined);
158
+ const body = init.body || (input instanceof Request ? input.body : undefined);
159
+ const response = await origFetch.apply(this, args);
160
+ capture('fetch', url, method, headers, body, response);
161
+ return response;
162
+ };
163
+
164
+ const origOpen = XMLHttpRequest.prototype.open;
165
+ const origSend = XMLHttpRequest.prototype.send;
166
+ const origSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
167
+
168
+ XMLHttpRequest.prototype.open = function(method, url) {
169
+ this.__opencli_method = method;
170
+ this.__opencli_url = url;
171
+ this.__opencli_headers = {};
172
+ return origOpen.apply(this, arguments);
173
+ };
174
+ XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
175
+ try {
176
+ this.__opencli_headers = this.__opencli_headers || {};
177
+ this.__opencli_headers[String(name)] = String(value);
178
+ } catch {}
179
+ return origSetRequestHeader.apply(this, arguments);
180
+ };
181
+ XMLHttpRequest.prototype.send = function(body) {
182
+ this.addEventListener('load', () => {
183
+ if (!shouldCapture(this.__opencli_url)) return;
184
+ try {
185
+ window[CAPTURE_VAR].push({
186
+ kind: 'xhr',
187
+ url: String(this.__opencli_url || ''),
188
+ method: String(this.__opencli_method || 'GET').toUpperCase(),
189
+ requestHeaders: this.__opencli_headers || {},
190
+ requestBodyKind: body == null ? 'empty' : (body instanceof FormData ? 'formdata' : typeof body),
191
+ requestBodyPreview: body == null ? '' : (body instanceof FormData ? '[formdata]' : String(body).slice(0, 2000)),
192
+ responseStatus: this.status,
193
+ responseContentType: this.getResponseHeader('content-type') || '',
194
+ responsePreview: String(this.responseText || '').slice(0, 4000),
195
+ timestamp: Date.now(),
196
+ });
197
+ } catch (error) {
198
+ window[CAPTURE_ERRORS_VAR].push(String(error));
199
+ }
200
+ });
201
+ return origSend.apply(this, arguments);
202
+ };
203
+
204
+ window[PATCH_GUARD] = true;
205
+ return { ok: true };
206
+ })()
207
+ `;
208
+ }
209
+
210
+ export function buildReadInstagramProtocolCaptureJs(
211
+ captureVar: string = DEFAULT_CAPTURE_VAR,
212
+ captureErrorsVar: string = DEFAULT_CAPTURE_ERRORS_VAR,
213
+ ): string {
214
+ return `
215
+ (() => {
216
+ const data = Array.isArray(window[${JSON.stringify(captureVar)}]) ? window[${JSON.stringify(captureVar)}] : [];
217
+ const errors = Array.isArray(window[${JSON.stringify(captureErrorsVar)}]) ? window[${JSON.stringify(captureErrorsVar)}] : [];
218
+ window[${JSON.stringify(captureVar)}] = [];
219
+ window[${JSON.stringify(captureErrorsVar)}] = [];
220
+ return { data, errors };
221
+ })()
222
+ `;
223
+ }
224
+
225
+ export async function installInstagramProtocolCapture(page: IPage): Promise<void> {
226
+ if (typeof page.startNetworkCapture === 'function') {
227
+ try {
228
+ await page.startNetworkCapture(INSTAGRAM_PROTOCOL_CAPTURE_PATTERN);
229
+ return;
230
+ } catch (error) {
231
+ const message = error instanceof Error ? error.message : String(error);
232
+ if (!message.includes('Unknown action') && !message.includes('network-capture')) {
233
+ throw error;
234
+ }
235
+ }
236
+ }
237
+ await page.evaluate(buildInstallInstagramProtocolCaptureJs());
238
+ }
239
+
240
+ export async function readInstagramProtocolCapture(page: IPage): Promise<{
241
+ data: InstagramProtocolCaptureEntry[];
242
+ errors: string[];
243
+ }> {
244
+ if (typeof page.readNetworkCapture === 'function') {
245
+ try {
246
+ const data = await page.readNetworkCapture();
247
+ return {
248
+ data: Array.isArray(data) ? data as InstagramProtocolCaptureEntry[] : [],
249
+ errors: [],
250
+ };
251
+ } catch (error) {
252
+ const message = error instanceof Error ? error.message : String(error);
253
+ if (!message.includes('Unknown action') && !message.includes('network-capture')) {
254
+ throw error;
255
+ }
256
+ }
257
+ }
258
+ const result = await page.evaluate(buildReadInstagramProtocolCaptureJs()) as {
259
+ data?: InstagramProtocolCaptureEntry[];
260
+ errors?: string[];
261
+ };
262
+ return {
263
+ data: Array.isArray(result?.data) ? result.data : [],
264
+ errors: Array.isArray(result?.errors) ? result.errors : [],
265
+ };
266
+ }
267
+
268
+ export async function dumpInstagramProtocolCaptureIfEnabled(page: IPage): Promise<void> {
269
+ if (process.env.OPENCLI_INSTAGRAM_CAPTURE !== '1') return;
270
+ const payload = await readInstagramProtocolCapture(page);
271
+ fs.writeFileSync(TRACE_OUTPUT_PATH, JSON.stringify(payload, null, 2));
272
+ }
273
+
274
+ function buildCookieHeader(cookies: BrowserCookie[]): string {
275
+ return cookies
276
+ .filter((cookie) => cookie?.name && cookie?.value)
277
+ .map((cookie) => `${cookie.name}=${cookie.value}`)
278
+ .join('; ');
279
+ }
280
+
281
+ export async function instagramPrivateApiFetch(
282
+ page: IPage,
283
+ input: string | URL,
284
+ init: {
285
+ method?: 'GET' | 'POST';
286
+ headers?: Record<string, string>;
287
+ body?: unknown;
288
+ } = {},
289
+ ): Promise<Response> {
290
+ const url = String(input);
291
+ const [urlCookies, domainCookies] = await Promise.all([
292
+ page.getCookies({ url }),
293
+ page.getCookies({ domain: 'instagram.com' }),
294
+ ]);
295
+ const merged = new Map<string, BrowserCookie>();
296
+ for (const cookie of domainCookies) merged.set(cookie.name, cookie);
297
+ for (const cookie of urlCookies) merged.set(cookie.name, cookie);
298
+ const cookieHeader = buildCookieHeader(Array.from(merged.values()));
299
+ const csrf = merged.get('csrftoken')?.value || '';
300
+ const initHeaders = init.headers ?? {};
301
+ const requestedAppIdHeader = Object.entries(initHeaders).find(([key]) => key.toLowerCase() === 'x-ig-app-id')?.[1] || '';
302
+ const runtimeInfo = requestedAppIdHeader ? null : await resolveInstagramRuntimeInfo(page);
303
+ const appId = requestedAppIdHeader || runtimeInfo?.appId || '';
304
+ const hasContentType = Object.keys(init.headers ?? {}).some((key) => key.toLowerCase() === 'content-type');
305
+
306
+ return fetch(url, {
307
+ method: init.method ?? 'GET',
308
+ headers: {
309
+ 'Accept': 'application/json, text/plain, */*',
310
+ 'X-CSRFToken': csrf,
311
+ 'X-Requested-With': 'XMLHttpRequest',
312
+ 'Origin': 'https://www.instagram.com',
313
+ 'Referer': 'https://www.instagram.com/',
314
+ ...(appId ? { 'X-IG-App-ID': appId } : {}),
315
+ ...(cookieHeader ? { 'Cookie': cookieHeader } : {}),
316
+ ...(typeof init.body === 'string' && !hasContentType ? { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } : {}),
317
+ ...initHeaders,
318
+ },
319
+ ...(init.body !== undefined ? { body: init.body as BodyInit } : {}),
320
+ });
321
+ }
@@ -0,0 +1,91 @@
1
+ import type { BrowserCookie, IPage } from '../../../types.js';
2
+
3
+ export interface InstagramRuntimeInfo {
4
+ appId: string;
5
+ csrfToken: string;
6
+ instagramAjax: string;
7
+ }
8
+
9
+ function pickMatch(input: string, patterns: RegExp[]): string {
10
+ for (const pattern of patterns) {
11
+ const match = input.match(pattern);
12
+ if (!match) continue;
13
+ for (let index = 1; index < match.length; index += 1) {
14
+ if (match[index]) return match[index]!;
15
+ }
16
+ return match[0] || '';
17
+ }
18
+ return '';
19
+ }
20
+
21
+ export function extractInstagramRuntimeInfo(html: string): InstagramRuntimeInfo {
22
+ return {
23
+ appId: pickMatch(html, [
24
+ /"X-IG-App-ID":"(\d+)"/,
25
+ /"appId":"(\d+)"/,
26
+ /"app_id":"(\d+)"/,
27
+ /"instagramWebAppId":"(\d+)"/,
28
+ ]),
29
+ csrfToken: pickMatch(html, [
30
+ /"csrf_token":"([^"]+)"/,
31
+ /"csrfToken":"([^"]+)"/,
32
+ ]),
33
+ instagramAjax: pickMatch(html, [
34
+ /"rollout_hash":"([^"]+)"/,
35
+ /"X-Instagram-AJAX":"([^"]+)"/,
36
+ /"Instagram-AJAX":"([^"]+)"/,
37
+ ]),
38
+ };
39
+ }
40
+
41
+ export function buildReadInstagramRuntimeInfoJs(): string {
42
+ return `
43
+ (() => {
44
+ const html = document.documentElement?.outerHTML || '';
45
+ const pick = (patterns) => {
46
+ for (const pattern of patterns) {
47
+ const match = html.match(new RegExp(pattern, 'i'));
48
+ if (!match) continue;
49
+ for (let index = 1; index < match.length; index += 1) {
50
+ if (match[index]) return match[index];
51
+ }
52
+ return match[0] || '';
53
+ }
54
+ return '';
55
+ };
56
+ return {
57
+ appId: pick([
58
+ '"X-IG-App-ID":"(\\\\d+)"',
59
+ '"appId":"(\\\\d+)"',
60
+ '"app_id":"(\\\\d+)"',
61
+ '"instagramWebAppId":"(\\\\d+)"',
62
+ ]),
63
+ csrfToken: pick([
64
+ '"csrf_token":"([^"]+)"',
65
+ '"csrfToken":"([^"]+)"',
66
+ ]),
67
+ instagramAjax: pick([
68
+ '"rollout_hash":"([^"]+)"',
69
+ '"X-Instagram-AJAX":"([^"]+)"',
70
+ '"Instagram-AJAX":"([^"]+)"',
71
+ ]),
72
+ };
73
+ })()
74
+ `;
75
+ }
76
+
77
+ function getCookieValue(cookies: BrowserCookie[], name: string): string {
78
+ return cookies.find((cookie) => cookie.name === name)?.value || '';
79
+ }
80
+
81
+ export async function resolveInstagramRuntimeInfo(page: IPage): Promise<InstagramRuntimeInfo> {
82
+ const [runtime, cookies] = await Promise.all([
83
+ page.evaluate(buildReadInstagramRuntimeInfoJs()) as Promise<InstagramRuntimeInfo>,
84
+ page.getCookies({ domain: 'instagram.com' }),
85
+ ]);
86
+ return {
87
+ appId: runtime?.appId || '',
88
+ csrfToken: runtime?.csrfToken || getCookieValue(cookies, 'csrftoken') || '',
89
+ instagramAjax: runtime?.instagramAjax || '',
90
+ };
91
+ }
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { ArgumentError } from '../../errors.js';
4
+ import { getRegistry } from '../../registry.js';
5
+ import type { IPage } from '../../types.js';
6
+ import './note.js';
7
+
8
+ function createPageMock(): IPage {
9
+ return {
10
+ goto: vi.fn().mockResolvedValue(undefined),
11
+ evaluate: vi.fn().mockResolvedValue(undefined),
12
+ getCookies: vi.fn().mockResolvedValue([]),
13
+ snapshot: vi.fn().mockResolvedValue(undefined),
14
+ click: vi.fn().mockResolvedValue(undefined),
15
+ typeText: vi.fn().mockResolvedValue(undefined),
16
+ pressKey: vi.fn().mockResolvedValue(undefined),
17
+ scrollTo: vi.fn().mockResolvedValue(undefined),
18
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
19
+ wait: vi.fn().mockResolvedValue(undefined),
20
+ tabs: vi.fn().mockResolvedValue([]),
21
+ closeTab: vi.fn().mockResolvedValue(undefined),
22
+ newTab: vi.fn().mockResolvedValue(undefined),
23
+ selectTab: vi.fn().mockResolvedValue(undefined),
24
+ networkRequests: vi.fn().mockResolvedValue([]),
25
+ consoleMessages: vi.fn().mockResolvedValue([]),
26
+ scroll: vi.fn().mockResolvedValue(undefined),
27
+ autoScroll: vi.fn().mockResolvedValue(undefined),
28
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
29
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
30
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
31
+ screenshot: vi.fn().mockResolvedValue(''),
32
+ setFileInput: vi.fn().mockResolvedValue(undefined),
33
+ insertText: vi.fn().mockResolvedValue(undefined),
34
+ getCurrentUrl: vi.fn().mockResolvedValue(null),
35
+ };
36
+ }
37
+
38
+ describe('instagram note registration', () => {
39
+ beforeEach(() => {
40
+ vi.restoreAllMocks();
41
+ });
42
+
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ it('registers the note command with a required positional content arg', () => {
48
+ const cmd = getRegistry().get('instagram/note');
49
+ expect(cmd).toBeDefined();
50
+ expect(cmd?.browser).toBe(true);
51
+ expect(cmd?.args.some((arg) => arg.name === 'content' && arg.positional && arg.required)).toBe(true);
52
+ });
53
+
54
+ it('rejects missing note content before browser work', async () => {
55
+ const page = createPageMock();
56
+ const cmd = getRegistry().get('instagram/note');
57
+
58
+ await expect(cmd!.func!(page, {})).rejects.toThrow(ArgumentError);
59
+ expect(page.goto).not.toHaveBeenCalled();
60
+ });
61
+
62
+ it('rejects blank note content before browser work', async () => {
63
+ const page = createPageMock();
64
+ const cmd = getRegistry().get('instagram/note');
65
+
66
+ await expect(cmd!.func!(page, { content: ' ' })).rejects.toThrow(ArgumentError);
67
+ expect(page.goto).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it('rejects note content longer than 60 characters before browser work', async () => {
71
+ const page = createPageMock();
72
+ const cmd = getRegistry().get('instagram/note');
73
+
74
+ await expect(cmd!.func!(page, { content: 'x'.repeat(61) })).rejects.toThrow(ArgumentError);
75
+ expect(page.goto).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it('publishes a note through the web inbox mutation', async () => {
79
+ const page = createPageMock();
80
+ const cmd = getRegistry().get('instagram/note');
81
+ vi.mocked(page.evaluate).mockResolvedValue({
82
+ ok: true,
83
+ noteId: '17849203563031468',
84
+ });
85
+
86
+ const rows = await cmd!.func!(page, { content: 'hello note' }) as Array<Record<string, string>>;
87
+
88
+ expect(page.goto).toHaveBeenCalledWith('https://www.instagram.com/direct/inbox/');
89
+ expect(page.evaluate).toHaveBeenCalledTimes(1);
90
+ expect(rows).toEqual([{
91
+ status: '✅ Posted',
92
+ detail: 'Instagram note published successfully',
93
+ noteId: '17849203563031468',
94
+ }]);
95
+ });
96
+ });