@jackwener/opencli 1.6.1 → 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 (384) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +27 -45
  3. package/README.zh-CN.md +32 -34
  4. package/autoresearch/browse-tasks.json +18 -20
  5. package/autoresearch/commands/debug.ts +163 -0
  6. package/autoresearch/commands/fix.ts +145 -0
  7. package/autoresearch/commands/plan.ts +88 -0
  8. package/autoresearch/commands/run.ts +138 -0
  9. package/autoresearch/config.ts +82 -0
  10. package/autoresearch/engine.ts +359 -0
  11. package/autoresearch/eval-all.ts +127 -0
  12. package/autoresearch/eval-browse.ts +1 -1
  13. package/autoresearch/eval-publish.ts +238 -0
  14. package/autoresearch/eval-save.ts +249 -0
  15. package/autoresearch/eval-skill.ts +14 -8
  16. package/autoresearch/eval-v2ex.ts +220 -0
  17. package/autoresearch/eval-zhihu.ts +230 -0
  18. package/autoresearch/logger.ts +69 -0
  19. package/autoresearch/presets/combined-reliability.ts +27 -0
  20. package/autoresearch/presets/index.ts +23 -0
  21. package/autoresearch/presets/operate-reliability.ts +24 -0
  22. package/autoresearch/presets/save-reliability.ts +26 -0
  23. package/autoresearch/presets/skill-quality.ts +20 -0
  24. package/autoresearch/presets/v2ex-reliability.ts +24 -0
  25. package/autoresearch/presets/zhihu-reliability.ts +25 -0
  26. package/autoresearch/publish-tasks.json +345 -0
  27. package/autoresearch/run-save.sh +11 -0
  28. package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
  29. package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
  30. package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
  31. package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
  32. package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
  33. package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
  34. package/autoresearch/save-tasks.json +281 -0
  35. package/autoresearch/v2ex-tasks.json +899 -0
  36. package/autoresearch/zhihu-tasks.json +848 -0
  37. package/dist/browser/base-page.d.ts +4 -2
  38. package/dist/browser/base-page.js +37 -4
  39. package/dist/browser/bridge.js +10 -8
  40. package/dist/browser/cdp.js +2 -6
  41. package/dist/browser/daemon-client.d.ts +11 -1
  42. package/dist/browser/daemon-client.js +3 -0
  43. package/dist/browser/dom-helpers.d.ts +4 -2
  44. package/dist/browser/dom-helpers.js +42 -31
  45. package/dist/browser/dom-snapshot.js +23 -1
  46. package/dist/browser/page.d.ts +7 -2
  47. package/dist/browser/page.js +112 -30
  48. package/dist/browser.test.js +1 -1
  49. package/dist/build-manifest.d.ts +1 -0
  50. package/dist/build-manifest.js +1 -0
  51. package/dist/cli-manifest.json +1135 -184
  52. package/dist/cli.d.ts +2 -0
  53. package/dist/cli.js +48 -7
  54. package/dist/cli.test.d.ts +1 -0
  55. package/dist/cli.test.js +88 -0
  56. package/dist/clis/1688/item.d.ts +70 -0
  57. package/dist/clis/1688/item.js +187 -0
  58. package/dist/clis/1688/item.test.d.ts +1 -0
  59. package/dist/clis/1688/item.test.js +67 -0
  60. package/dist/clis/1688/search.d.ts +56 -0
  61. package/dist/clis/1688/search.js +309 -0
  62. package/dist/clis/1688/search.test.d.ts +1 -0
  63. package/dist/clis/1688/search.test.js +75 -0
  64. package/dist/clis/1688/shared.d.ts +112 -0
  65. package/dist/clis/1688/shared.js +514 -0
  66. package/dist/clis/1688/shared.test.d.ts +1 -0
  67. package/dist/clis/1688/shared.test.js +57 -0
  68. package/dist/clis/1688/store.d.ts +45 -0
  69. package/dist/clis/1688/store.js +226 -0
  70. package/dist/clis/1688/store.test.d.ts +1 -0
  71. package/dist/clis/1688/store.test.js +62 -0
  72. package/dist/clis/amazon/bestsellers.d.ts +0 -20
  73. package/dist/clis/amazon/bestsellers.js +6 -129
  74. package/dist/clis/amazon/bestsellers.test.js +12 -3
  75. package/dist/clis/amazon/movers-shakers.d.ts +1 -0
  76. package/dist/clis/amazon/movers-shakers.js +7 -0
  77. package/dist/clis/amazon/new-releases.d.ts +1 -0
  78. package/dist/clis/amazon/new-releases.js +7 -0
  79. package/dist/clis/amazon/rankings.d.ts +59 -0
  80. package/dist/clis/amazon/rankings.js +226 -0
  81. package/dist/clis/amazon/rankings.test.d.ts +1 -0
  82. package/dist/clis/amazon/rankings.test.js +41 -0
  83. package/dist/clis/amazon/shared.d.ts +11 -0
  84. package/dist/clis/amazon/shared.js +121 -11
  85. package/dist/clis/amazon/shared.test.js +11 -0
  86. package/dist/clis/bilibili/comments.js +2 -2
  87. package/dist/clis/bilibili/comments.test.js +3 -2
  88. package/dist/clis/bilibili/download.js +2 -1
  89. package/dist/clis/bilibili/subtitle.js +4 -3
  90. package/dist/clis/bilibili/subtitle.test.js +2 -1
  91. package/dist/clis/bilibili/utils.d.ts +5 -0
  92. package/dist/clis/bilibili/utils.js +30 -0
  93. package/dist/clis/bilibili/utils.test.d.ts +1 -0
  94. package/dist/clis/bilibili/utils.test.js +17 -0
  95. package/dist/clis/douban/marks.js +1 -1
  96. package/dist/clis/douban/subject.yaml +50 -19
  97. package/dist/clis/doubao/utils.js +32 -12
  98. package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
  99. package/dist/clis/douyin/_shared/transcode.test.js +0 -2
  100. package/dist/clis/douyin/draft.test.js +0 -2
  101. package/dist/clis/facebook/search.test.js +0 -2
  102. package/dist/clis/gemini/ask.js +9 -3
  103. package/dist/clis/gemini/ask.test.d.ts +1 -0
  104. package/dist/clis/gemini/ask.test.js +100 -0
  105. package/dist/clis/gemini/reply-state.test.d.ts +1 -0
  106. package/dist/clis/gemini/reply-state.test.js +641 -0
  107. package/dist/clis/gemini/utils.d.ts +44 -1
  108. package/dist/clis/gemini/utils.js +528 -61
  109. package/dist/clis/gemini/utils.test.js +149 -2
  110. package/dist/clis/hupu/detail.d.ts +1 -0
  111. package/dist/clis/hupu/detail.js +72 -0
  112. package/dist/clis/hupu/hot.yaml +43 -0
  113. package/dist/clis/hupu/like.d.ts +1 -0
  114. package/dist/clis/hupu/like.js +75 -0
  115. package/dist/clis/hupu/reply.d.ts +1 -0
  116. package/dist/clis/hupu/reply.js +71 -0
  117. package/dist/clis/hupu/search.d.ts +1 -0
  118. package/dist/clis/hupu/search.js +59 -0
  119. package/dist/clis/hupu/unlike.d.ts +1 -0
  120. package/dist/clis/hupu/unlike.js +75 -0
  121. package/dist/clis/hupu/utils.d.ts +20 -0
  122. package/dist/clis/hupu/utils.js +319 -0
  123. package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
  124. package/dist/clis/instagram/_shared/private-publish.js +1030 -0
  125. package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
  126. package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
  127. package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
  128. package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
  129. package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
  130. package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
  131. package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
  132. package/dist/clis/instagram/_shared/runtime-info.js +81 -0
  133. package/dist/clis/instagram/note.d.ts +1 -0
  134. package/dist/clis/instagram/note.js +222 -0
  135. package/dist/clis/instagram/note.test.d.ts +1 -0
  136. package/dist/clis/instagram/note.test.js +81 -0
  137. package/dist/clis/instagram/post.d.ts +4 -0
  138. package/dist/clis/instagram/post.js +1496 -0
  139. package/dist/clis/instagram/post.test.d.ts +1 -0
  140. package/dist/clis/instagram/post.test.js +1647 -0
  141. package/dist/clis/instagram/reel.d.ts +1 -0
  142. package/dist/clis/instagram/reel.js +826 -0
  143. package/dist/clis/instagram/reel.test.d.ts +1 -0
  144. package/dist/clis/instagram/reel.test.js +167 -0
  145. package/dist/clis/instagram/story.d.ts +1 -0
  146. package/dist/clis/instagram/story.js +115 -0
  147. package/dist/clis/instagram/story.test.d.ts +1 -0
  148. package/dist/clis/instagram/story.test.js +167 -0
  149. package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
  150. package/dist/clis/sinafinance/stock-rank.js +65 -0
  151. package/dist/clis/substack/utils.test.js +0 -2
  152. package/dist/clis/twitter/post.js +72 -45
  153. package/dist/clis/twitter/post.test.d.ts +1 -0
  154. package/dist/clis/twitter/post.test.js +116 -0
  155. package/dist/clis/twitter/reply.d.ts +12 -0
  156. package/dist/clis/twitter/reply.js +257 -35
  157. package/dist/clis/twitter/reply.test.d.ts +1 -0
  158. package/dist/clis/twitter/reply.test.js +151 -0
  159. package/dist/clis/xianyu/chat.d.ts +7 -0
  160. package/dist/clis/xianyu/chat.js +146 -0
  161. package/dist/clis/xianyu/chat.test.d.ts +1 -0
  162. package/dist/clis/xianyu/chat.test.js +15 -0
  163. package/dist/clis/xianyu/item.d.ts +7 -0
  164. package/dist/clis/xianyu/item.js +152 -0
  165. package/dist/clis/xianyu/item.test.d.ts +1 -0
  166. package/dist/clis/xianyu/item.test.js +56 -0
  167. package/dist/clis/xianyu/search.d.ts +10 -0
  168. package/dist/clis/xianyu/search.js +134 -0
  169. package/dist/clis/xianyu/search.test.d.ts +1 -0
  170. package/dist/clis/xianyu/search.test.js +17 -0
  171. package/dist/clis/xianyu/utils.d.ts +1 -0
  172. package/dist/clis/xianyu/utils.js +8 -0
  173. package/dist/clis/xiaoe/catalog.yaml +129 -0
  174. package/dist/clis/xiaoe/content.yaml +43 -0
  175. package/dist/clis/xiaoe/courses.yaml +73 -0
  176. package/dist/clis/xiaoe/detail.yaml +39 -0
  177. package/dist/clis/xiaoe/play-url.yaml +124 -0
  178. package/dist/clis/xiaohongshu/comments.test.js +0 -2
  179. package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
  180. package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
  181. package/dist/clis/xiaohongshu/download.test.js +0 -2
  182. package/dist/clis/xiaohongshu/note.test.js +0 -2
  183. package/dist/clis/xiaohongshu/publish.test.js +0 -2
  184. package/dist/clis/xiaohongshu/search.js +29 -20
  185. package/dist/clis/xiaohongshu/search.test.js +56 -48
  186. package/dist/clis/yuanbao/ask.d.ts +21 -0
  187. package/dist/clis/yuanbao/ask.js +427 -0
  188. package/dist/clis/yuanbao/ask.test.d.ts +1 -0
  189. package/dist/clis/yuanbao/ask.test.js +124 -0
  190. package/dist/clis/yuanbao/new.d.ts +1 -0
  191. package/dist/clis/yuanbao/new.js +70 -0
  192. package/dist/clis/yuanbao/new.test.d.ts +1 -0
  193. package/dist/clis/yuanbao/new.test.js +30 -0
  194. package/dist/clis/yuanbao/shared.d.ts +13 -0
  195. package/dist/clis/yuanbao/shared.js +49 -0
  196. package/dist/clis/zhihu/question.js +30 -19
  197. package/dist/clis/zhihu/question.test.js +34 -16
  198. package/dist/commanderAdapter.js +8 -4
  199. package/dist/commanderAdapter.test.js +42 -0
  200. package/dist/completion.js +3 -1
  201. package/dist/completion.test.d.ts +1 -0
  202. package/dist/completion.test.js +23 -0
  203. package/dist/doctor.js +1 -1
  204. package/dist/electron-apps.d.ts +2 -0
  205. package/dist/electron-apps.js +7 -1
  206. package/dist/errors.js +1 -1
  207. package/dist/execution.js +25 -35
  208. package/dist/explore.js +1 -1
  209. package/dist/launcher.d.ts +4 -0
  210. package/dist/launcher.js +64 -8
  211. package/dist/launcher.test.js +88 -7
  212. package/dist/output.d.ts +2 -0
  213. package/dist/output.js +10 -1
  214. package/dist/output.test.d.ts +0 -3
  215. package/dist/output.test.js +59 -92
  216. package/dist/pipeline/executor.test.js +0 -2
  217. package/dist/pipeline/steps/download.test.js +0 -2
  218. package/dist/registry.d.ts +2 -0
  219. package/dist/serialization.d.ts +1 -0
  220. package/dist/serialization.js +1 -0
  221. package/dist/types.d.ts +9 -2
  222. package/docs/.vitepress/config.mts +4 -0
  223. package/docs/adapters/browser/1688.md +52 -0
  224. package/docs/adapters/browser/36kr.md +2 -1
  225. package/docs/adapters/browser/doubao.md +5 -1
  226. package/docs/adapters/browser/hupu.md +53 -0
  227. package/docs/adapters/browser/sinafinance.md +32 -2
  228. package/docs/adapters/browser/weibo.md +6 -1
  229. package/docs/adapters/browser/wikipedia.md +2 -0
  230. package/docs/adapters/browser/xianyu.md +42 -0
  231. package/docs/adapters/browser/xiaoe.md +44 -0
  232. package/docs/adapters/browser/yuanbao.md +64 -0
  233. package/docs/adapters/index.md +14 -5
  234. package/docs/comparison.md +1 -1
  235. package/docs/developer/ai-workflow.md +2 -2
  236. package/docs/developer/contributing.md +1 -1
  237. package/docs/developer/testing.md +2 -0
  238. package/docs/guide/plugins.md +1 -0
  239. package/docs/guide/troubleshooting.md +11 -0
  240. package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
  241. package/docs/zh/guide/plugins.md +1 -0
  242. package/extension/dist/background.js +1127 -0
  243. package/extension/src/background.test.ts +39 -0
  244. package/extension/src/background.ts +223 -34
  245. package/extension/src/cdp.ts +194 -4
  246. package/extension/src/protocol.ts +22 -1
  247. package/package.json +3 -2
  248. package/scripts/postinstall.js +1 -1
  249. package/skills/opencli-explorer/SKILL.md +1 -1
  250. package/skills/opencli-oneshot/SKILL.md +2 -2
  251. package/skills/opencli-operate/SKILL.md +120 -27
  252. package/skills/opencli-usage/SKILL.md +31 -20
  253. package/skills/opencli-usage/browser.md +114 -16
  254. package/skills/opencli-usage/public-api.md +32 -3
  255. package/skills/smart-search/SKILL.md +156 -0
  256. package/skills/smart-search/references/sources-ai.md +74 -0
  257. package/skills/smart-search/references/sources-info.md +43 -0
  258. package/skills/smart-search/references/sources-media.md +50 -0
  259. package/skills/smart-search/references/sources-other.md +42 -0
  260. package/skills/smart-search/references/sources-shopping.md +31 -0
  261. package/skills/smart-search/references/sources-social.md +51 -0
  262. package/skills/smart-search/references/sources-tech.md +42 -0
  263. package/skills/smart-search/references/sources-travel.md +20 -0
  264. package/src/browser/base-page.ts +41 -6
  265. package/src/browser/bridge.ts +11 -8
  266. package/src/browser/cdp.ts +1 -8
  267. package/src/browser/daemon-client.ts +11 -1
  268. package/src/browser/dom-helpers.ts +43 -31
  269. package/src/browser/dom-snapshot.ts +23 -1
  270. package/src/browser/page.ts +115 -31
  271. package/src/browser.test.ts +1 -1
  272. package/src/build-manifest.ts +2 -0
  273. package/src/cli.test.ts +133 -0
  274. package/src/cli.ts +73 -11
  275. package/src/clis/1688/item.test.ts +69 -0
  276. package/src/clis/1688/item.ts +282 -0
  277. package/src/clis/1688/search.test.ts +81 -0
  278. package/src/clis/1688/search.ts +402 -0
  279. package/src/clis/1688/shared.test.ts +75 -0
  280. package/src/clis/1688/shared.ts +623 -0
  281. package/src/clis/1688/store.test.ts +69 -0
  282. package/src/clis/1688/store.ts +300 -0
  283. package/src/clis/amazon/bestsellers.test.ts +12 -3
  284. package/src/clis/amazon/bestsellers.ts +6 -178
  285. package/src/clis/amazon/movers-shakers.ts +8 -0
  286. package/src/clis/amazon/new-releases.ts +8 -0
  287. package/src/clis/amazon/rankings.test.ts +47 -0
  288. package/src/clis/amazon/rankings.ts +312 -0
  289. package/src/clis/amazon/shared.test.ts +16 -0
  290. package/src/clis/amazon/shared.ts +134 -12
  291. package/src/clis/bilibili/comments.test.ts +4 -3
  292. package/src/clis/bilibili/comments.ts +2 -2
  293. package/src/clis/bilibili/download.ts +2 -1
  294. package/src/clis/bilibili/subtitle.test.ts +2 -1
  295. package/src/clis/bilibili/subtitle.ts +4 -3
  296. package/src/clis/bilibili/utils.test.ts +21 -0
  297. package/src/clis/bilibili/utils.ts +27 -0
  298. package/src/clis/douban/marks.ts +1 -1
  299. package/src/clis/douban/subject.yaml +50 -19
  300. package/src/clis/doubao/utils.ts +32 -12
  301. package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
  302. package/src/clis/douyin/_shared/transcode.test.ts +0 -2
  303. package/src/clis/douyin/draft.test.ts +0 -2
  304. package/src/clis/facebook/search.test.ts +0 -2
  305. package/src/clis/gemini/ask.test.ts +116 -0
  306. package/src/clis/gemini/ask.ts +10 -3
  307. package/src/clis/gemini/reply-state.test.ts +708 -0
  308. package/src/clis/gemini/utils.test.ts +184 -2
  309. package/src/clis/gemini/utils.ts +588 -60
  310. package/src/clis/hupu/detail.ts +126 -0
  311. package/src/clis/hupu/hot.yaml +43 -0
  312. package/src/clis/hupu/like.ts +76 -0
  313. package/src/clis/hupu/reply.ts +76 -0
  314. package/src/clis/hupu/search.ts +95 -0
  315. package/src/clis/hupu/unlike.ts +76 -0
  316. package/src/clis/hupu/utils.ts +381 -0
  317. package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
  318. package/src/clis/instagram/_shared/private-publish.ts +1303 -0
  319. package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
  320. package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
  321. package/src/clis/instagram/_shared/runtime-info.ts +91 -0
  322. package/src/clis/instagram/note.test.ts +96 -0
  323. package/src/clis/instagram/note.ts +254 -0
  324. package/src/clis/instagram/post.test.ts +1716 -0
  325. package/src/clis/instagram/post.ts +1620 -0
  326. package/src/clis/instagram/reel.test.ts +191 -0
  327. package/src/clis/instagram/reel.ts +886 -0
  328. package/src/clis/instagram/story.test.ts +191 -0
  329. package/src/clis/instagram/story.ts +151 -0
  330. package/src/clis/sinafinance/stock-rank.ts +68 -0
  331. package/src/clis/substack/utils.test.ts +0 -2
  332. package/src/clis/twitter/post.test.ts +157 -0
  333. package/src/clis/twitter/post.ts +82 -48
  334. package/src/clis/twitter/reply.test.ts +177 -0
  335. package/src/clis/twitter/reply.ts +285 -39
  336. package/src/clis/xianyu/chat.test.ts +20 -0
  337. package/src/clis/xianyu/chat.ts +175 -0
  338. package/src/clis/xianyu/item.test.ts +67 -0
  339. package/src/clis/xianyu/item.ts +172 -0
  340. package/src/clis/xianyu/search.test.ts +22 -0
  341. package/src/clis/xianyu/search.ts +151 -0
  342. package/src/clis/xianyu/utils.ts +9 -0
  343. package/src/clis/xiaoe/catalog.yaml +129 -0
  344. package/src/clis/xiaoe/content.yaml +43 -0
  345. package/src/clis/xiaoe/courses.yaml +73 -0
  346. package/src/clis/xiaoe/detail.yaml +39 -0
  347. package/src/clis/xiaoe/play-url.yaml +124 -0
  348. package/src/clis/xiaohongshu/comments.test.ts +0 -2
  349. package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
  350. package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
  351. package/src/clis/xiaohongshu/download.test.ts +0 -2
  352. package/src/clis/xiaohongshu/note.test.ts +0 -2
  353. package/src/clis/xiaohongshu/publish.test.ts +0 -2
  354. package/src/clis/xiaohongshu/search.test.ts +59 -48
  355. package/src/clis/xiaohongshu/search.ts +31 -21
  356. package/src/clis/yuanbao/ask.test.ts +156 -0
  357. package/src/clis/yuanbao/ask.ts +522 -0
  358. package/src/clis/yuanbao/new.test.ts +36 -0
  359. package/src/clis/yuanbao/new.ts +81 -0
  360. package/src/clis/yuanbao/shared.ts +57 -0
  361. package/src/clis/zhihu/question.test.ts +42 -17
  362. package/src/clis/zhihu/question.ts +31 -26
  363. package/src/commanderAdapter.test.ts +51 -0
  364. package/src/commanderAdapter.ts +8 -4
  365. package/src/completion.test.ts +30 -0
  366. package/src/completion.ts +3 -1
  367. package/src/doctor.ts +1 -1
  368. package/src/electron-apps.ts +9 -1
  369. package/src/errors.ts +1 -1
  370. package/src/execution.ts +26 -30
  371. package/src/explore.ts +1 -1
  372. package/src/launcher.test.ts +121 -7
  373. package/src/launcher.ts +87 -9
  374. package/src/output.test.ts +50 -90
  375. package/src/output.ts +10 -1
  376. package/src/pipeline/executor.test.ts +0 -2
  377. package/src/pipeline/steps/download.test.ts +0 -2
  378. package/src/registry.ts +2 -0
  379. package/src/serialization.ts +2 -0
  380. package/src/types.ts +9 -2
  381. package/tests/e2e/browser-auth.test.ts +9 -0
  382. package/CLI-EXPLORER.md +0 -724
  383. package/CLI-ONESHOT.md +0 -216
  384. package/SKILL.md +0 -59
@@ -0,0 +1,1127 @@
1
+ //#region src/protocol.ts
2
+ /** Default daemon port */
3
+ var DAEMON_PORT = 19825;
4
+ var DAEMON_HOST = "localhost";
5
+ var DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
6
+ /** Lightweight health-check endpoint — probed before each WebSocket attempt. */
7
+ var DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
8
+ /** Base reconnect delay for extension WebSocket (ms) */
9
+ var WS_RECONNECT_BASE_DELAY = 2e3;
10
+ /** Max reconnect delay (ms) — kept short since daemon is long-lived */
11
+ var WS_RECONNECT_MAX_DELAY = 5e3;
12
+ //#endregion
13
+ //#region src/cdp.ts
14
+ /**
15
+ * CDP execution via chrome.debugger API.
16
+ *
17
+ * chrome.debugger only needs the "debugger" permission — no host_permissions.
18
+ * It can attach to any http/https tab. Avoid chrome:// and chrome-extension://
19
+ * tabs (resolveTabId in background.ts filters them).
20
+ */
21
+ var attached = /* @__PURE__ */ new Set();
22
+ var networkCaptures = /* @__PURE__ */ new Map();
23
+ /** Check if a URL can be attached via CDP — only allow http(s) and blank pages. */
24
+ function isDebuggableUrl$1(url) {
25
+ if (!url) return true;
26
+ return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:");
27
+ }
28
+ async function ensureAttached(tabId, aggressiveRetry = false) {
29
+ try {
30
+ const tab = await chrome.tabs.get(tabId);
31
+ if (!isDebuggableUrl$1(tab.url)) {
32
+ attached.delete(tabId);
33
+ throw new Error(`Cannot debug tab ${tabId}: URL is ${tab.url ?? "unknown"}`);
34
+ }
35
+ } catch (e) {
36
+ if (e instanceof Error && e.message.startsWith("Cannot debug tab")) throw e;
37
+ attached.delete(tabId);
38
+ throw new Error(`Tab ${tabId} no longer exists`);
39
+ }
40
+ if (attached.has(tabId)) try {
41
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
42
+ expression: "1",
43
+ returnByValue: true
44
+ });
45
+ return;
46
+ } catch {
47
+ attached.delete(tabId);
48
+ }
49
+ const MAX_ATTACH_RETRIES = aggressiveRetry ? 5 : 2;
50
+ const RETRY_DELAY_MS = aggressiveRetry ? 1500 : 500;
51
+ let lastError = "";
52
+ for (let attempt = 1; attempt <= MAX_ATTACH_RETRIES; attempt++) try {
53
+ try {
54
+ await chrome.debugger.detach({ tabId });
55
+ } catch {}
56
+ await chrome.debugger.attach({ tabId }, "1.3");
57
+ lastError = "";
58
+ break;
59
+ } catch (e) {
60
+ lastError = e instanceof Error ? e.message : String(e);
61
+ if (attempt < MAX_ATTACH_RETRIES) {
62
+ console.warn(`[opencli] attach attempt ${attempt}/${MAX_ATTACH_RETRIES} failed: ${lastError}, retrying in ${RETRY_DELAY_MS}ms...`);
63
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
64
+ try {
65
+ const tab = await chrome.tabs.get(tabId);
66
+ if (!isDebuggableUrl$1(tab.url)) {
67
+ lastError = `Tab URL changed to ${tab.url} during retry`;
68
+ break;
69
+ }
70
+ } catch {
71
+ lastError = `Tab ${tabId} no longer exists`;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ if (lastError) {
77
+ let finalUrl = "unknown";
78
+ let finalWindowId = "unknown";
79
+ try {
80
+ const tab = await chrome.tabs.get(tabId);
81
+ finalUrl = tab.url ?? "undefined";
82
+ finalWindowId = String(tab.windowId);
83
+ } catch {}
84
+ console.warn(`[opencli] attach failed for tab ${tabId}: url=${finalUrl}, windowId=${finalWindowId}, error=${lastError}`);
85
+ const hint = lastError.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : "";
86
+ throw new Error(`attach failed: ${lastError}${hint}`);
87
+ }
88
+ attached.add(tabId);
89
+ try {
90
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
91
+ } catch {}
92
+ }
93
+ async function evaluate(tabId, expression, aggressiveRetry = false) {
94
+ const MAX_EVAL_RETRIES = aggressiveRetry ? 3 : 2;
95
+ for (let attempt = 1; attempt <= MAX_EVAL_RETRIES; attempt++) try {
96
+ await ensureAttached(tabId, aggressiveRetry);
97
+ const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
98
+ expression,
99
+ returnByValue: true,
100
+ awaitPromise: true
101
+ });
102
+ if (result.exceptionDetails) {
103
+ const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
104
+ throw new Error(errMsg);
105
+ }
106
+ return result.result?.value;
107
+ } catch (e) {
108
+ const msg = e instanceof Error ? e.message : String(e);
109
+ const isNavigateError = msg.includes("Inspected target navigated") || msg.includes("Target closed");
110
+ if ((isNavigateError || msg.includes("attach failed") || msg.includes("Debugger is not attached") || msg.includes("chrome-extension://")) && attempt < MAX_EVAL_RETRIES) {
111
+ attached.delete(tabId);
112
+ const retryMs = isNavigateError ? 200 : 500;
113
+ await new Promise((resolve) => setTimeout(resolve, retryMs));
114
+ continue;
115
+ }
116
+ throw e;
117
+ }
118
+ throw new Error("evaluate: max retries exhausted");
119
+ }
120
+ var evaluateAsync = evaluate;
121
+ /**
122
+ * Capture a screenshot via CDP Page.captureScreenshot.
123
+ * Returns base64-encoded image data.
124
+ */
125
+ async function screenshot(tabId, options = {}) {
126
+ await ensureAttached(tabId);
127
+ const format = options.format ?? "png";
128
+ if (options.fullPage) {
129
+ const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics");
130
+ const size = metrics.cssContentSize || metrics.contentSize;
131
+ if (size) await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
132
+ mobile: false,
133
+ width: Math.ceil(size.width),
134
+ height: Math.ceil(size.height),
135
+ deviceScaleFactor: 1
136
+ });
137
+ }
138
+ try {
139
+ const params = { format };
140
+ if (format === "jpeg" && options.quality !== void 0) params.quality = Math.max(0, Math.min(100, options.quality));
141
+ return (await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params)).data;
142
+ } finally {
143
+ if (options.fullPage) await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {});
144
+ }
145
+ }
146
+ /**
147
+ * Set local file paths on a file input element via CDP DOM.setFileInputFiles.
148
+ * This bypasses the need to send large base64 payloads through the message channel —
149
+ * Chrome reads the files directly from the local filesystem.
150
+ *
151
+ * @param tabId - Target tab ID
152
+ * @param files - Array of absolute local file paths
153
+ * @param selector - CSS selector to find the file input (optional, defaults to first file input)
154
+ */
155
+ async function setFileInputFiles(tabId, files, selector) {
156
+ await ensureAttached(tabId);
157
+ await chrome.debugger.sendCommand({ tabId }, "DOM.enable");
158
+ const doc = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument");
159
+ const query = selector || "input[type=\"file\"]";
160
+ const result = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", {
161
+ nodeId: doc.root.nodeId,
162
+ selector: query
163
+ });
164
+ if (!result.nodeId) throw new Error(`No element found matching selector: ${query}`);
165
+ await chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", {
166
+ files,
167
+ nodeId: result.nodeId
168
+ });
169
+ }
170
+ async function insertText(tabId, text) {
171
+ await ensureAttached(tabId);
172
+ await chrome.debugger.sendCommand({ tabId }, "Input.insertText", { text });
173
+ }
174
+ function normalizeCapturePatterns(pattern) {
175
+ return String(pattern || "").split("|").map((part) => part.trim()).filter(Boolean);
176
+ }
177
+ function shouldCaptureUrl(url, patterns) {
178
+ if (!url) return false;
179
+ if (!patterns.length) return true;
180
+ return patterns.some((pattern) => url.includes(pattern));
181
+ }
182
+ function normalizeHeaders(headers) {
183
+ if (!headers || typeof headers !== "object") return {};
184
+ const out = {};
185
+ for (const [key, value] of Object.entries(headers)) out[String(key)] = String(value);
186
+ return out;
187
+ }
188
+ function getOrCreateNetworkCaptureEntry(tabId, requestId, fallback) {
189
+ const state = networkCaptures.get(tabId);
190
+ if (!state) return null;
191
+ const existingIndex = state.requestToIndex.get(requestId);
192
+ if (existingIndex !== void 0) return state.entries[existingIndex] || null;
193
+ const url = fallback?.url || "";
194
+ if (!shouldCaptureUrl(url, state.patterns)) return null;
195
+ const entry = {
196
+ kind: "cdp",
197
+ url,
198
+ method: fallback?.method || "GET",
199
+ requestHeaders: fallback?.requestHeaders || {},
200
+ timestamp: Date.now()
201
+ };
202
+ state.entries.push(entry);
203
+ state.requestToIndex.set(requestId, state.entries.length - 1);
204
+ return entry;
205
+ }
206
+ async function startNetworkCapture(tabId, pattern) {
207
+ await ensureAttached(tabId);
208
+ await chrome.debugger.sendCommand({ tabId }, "Network.enable");
209
+ networkCaptures.set(tabId, {
210
+ patterns: normalizeCapturePatterns(pattern),
211
+ entries: [],
212
+ requestToIndex: /* @__PURE__ */ new Map()
213
+ });
214
+ }
215
+ async function readNetworkCapture(tabId) {
216
+ const state = networkCaptures.get(tabId);
217
+ if (!state) return [];
218
+ const entries = state.entries.slice();
219
+ state.entries = [];
220
+ state.requestToIndex.clear();
221
+ return entries;
222
+ }
223
+ async function detach(tabId) {
224
+ if (!attached.has(tabId)) return;
225
+ attached.delete(tabId);
226
+ networkCaptures.delete(tabId);
227
+ try {
228
+ await chrome.debugger.detach({ tabId });
229
+ } catch {}
230
+ }
231
+ function registerListeners() {
232
+ chrome.tabs.onRemoved.addListener((tabId) => {
233
+ attached.delete(tabId);
234
+ networkCaptures.delete(tabId);
235
+ });
236
+ chrome.debugger.onDetach.addListener((source) => {
237
+ if (source.tabId) {
238
+ attached.delete(source.tabId);
239
+ networkCaptures.delete(source.tabId);
240
+ }
241
+ });
242
+ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
243
+ if (info.url && !isDebuggableUrl$1(info.url)) await detach(tabId);
244
+ });
245
+ chrome.debugger.onEvent.addListener(async (source, method, params) => {
246
+ const tabId = source.tabId;
247
+ if (!tabId) return;
248
+ const state = networkCaptures.get(tabId);
249
+ if (!state) return;
250
+ if (method === "Network.requestWillBeSent") {
251
+ const requestId = String(params?.requestId || "");
252
+ const request = params?.request;
253
+ const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, {
254
+ url: request?.url,
255
+ method: request?.method,
256
+ requestHeaders: normalizeHeaders(request?.headers)
257
+ });
258
+ if (!entry) return;
259
+ entry.requestBodyKind = request?.hasPostData ? "string" : "empty";
260
+ entry.requestBodyPreview = String(request?.postData || "").slice(0, 4e3);
261
+ try {
262
+ const postData = await chrome.debugger.sendCommand({ tabId }, "Network.getRequestPostData", { requestId });
263
+ if (postData?.postData) {
264
+ entry.requestBodyKind = "string";
265
+ entry.requestBodyPreview = postData.postData.slice(0, 4e3);
266
+ }
267
+ } catch {}
268
+ return;
269
+ }
270
+ if (method === "Network.responseReceived") {
271
+ const requestId = String(params?.requestId || "");
272
+ const response = params?.response;
273
+ const entry = getOrCreateNetworkCaptureEntry(tabId, requestId, { url: response?.url });
274
+ if (!entry) return;
275
+ entry.responseStatus = response?.status;
276
+ entry.responseContentType = response?.mimeType || "";
277
+ entry.responseHeaders = normalizeHeaders(response?.headers);
278
+ return;
279
+ }
280
+ if (method === "Network.loadingFinished") {
281
+ const requestId = String(params?.requestId || "");
282
+ const stateEntryIndex = state.requestToIndex.get(requestId);
283
+ if (stateEntryIndex === void 0) return;
284
+ const entry = state.entries[stateEntryIndex];
285
+ if (!entry) return;
286
+ try {
287
+ const body = await chrome.debugger.sendCommand({ tabId }, "Network.getResponseBody", { requestId });
288
+ if (typeof body?.body === "string") entry.responsePreview = body.base64Encoded ? `base64:${body.body.slice(0, 4e3)}` : body.body.slice(0, 4e3);
289
+ } catch {}
290
+ }
291
+ });
292
+ }
293
+ //#endregion
294
+ //#region src/background.ts
295
+ var ws = null;
296
+ var reconnectTimer = null;
297
+ var reconnectAttempts = 0;
298
+ var _origLog = console.log.bind(console);
299
+ var _origWarn = console.warn.bind(console);
300
+ var _origError = console.error.bind(console);
301
+ function forwardLog(level, args) {
302
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
303
+ try {
304
+ const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
305
+ ws.send(JSON.stringify({
306
+ type: "log",
307
+ level,
308
+ msg,
309
+ ts: Date.now()
310
+ }));
311
+ } catch {}
312
+ }
313
+ console.log = (...args) => {
314
+ _origLog(...args);
315
+ forwardLog("info", args);
316
+ };
317
+ console.warn = (...args) => {
318
+ _origWarn(...args);
319
+ forwardLog("warn", args);
320
+ };
321
+ console.error = (...args) => {
322
+ _origError(...args);
323
+ forwardLog("error", args);
324
+ };
325
+ /**
326
+ * Probe the daemon via its /ping HTTP endpoint before attempting a WebSocket
327
+ * connection. fetch() failures are silently catchable; new WebSocket() is not
328
+ * — Chrome logs ERR_CONNECTION_REFUSED to the extension error page before any
329
+ * JS handler can intercept it. By keeping the probe inside connect() every
330
+ * call site remains unchanged and the guard can never be accidentally skipped.
331
+ */
332
+ async function connect() {
333
+ if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
334
+ try {
335
+ if (!(await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) })).ok) return;
336
+ } catch {
337
+ return;
338
+ }
339
+ try {
340
+ ws = new WebSocket(DAEMON_WS_URL);
341
+ } catch {
342
+ scheduleReconnect();
343
+ return;
344
+ }
345
+ ws.onopen = () => {
346
+ console.log("[opencli] Connected to daemon");
347
+ reconnectAttempts = 0;
348
+ if (reconnectTimer) {
349
+ clearTimeout(reconnectTimer);
350
+ reconnectTimer = null;
351
+ }
352
+ ws?.send(JSON.stringify({
353
+ type: "hello",
354
+ version: chrome.runtime.getManifest().version
355
+ }));
356
+ };
357
+ ws.onmessage = async (event) => {
358
+ try {
359
+ const result = await handleCommand(JSON.parse(event.data));
360
+ ws?.send(JSON.stringify(result));
361
+ } catch (err) {
362
+ console.error("[opencli] Message handling error:", err);
363
+ }
364
+ };
365
+ ws.onclose = () => {
366
+ console.log("[opencli] Disconnected from daemon");
367
+ ws = null;
368
+ scheduleReconnect();
369
+ };
370
+ ws.onerror = () => {
371
+ ws?.close();
372
+ };
373
+ }
374
+ /**
375
+ * After MAX_EAGER_ATTEMPTS (reaching 60s backoff), stop scheduling reconnects.
376
+ * The keepalive alarm (~24s) will still call connect() periodically, but at a
377
+ * much lower frequency — reducing console noise when the daemon is not running.
378
+ */
379
+ var MAX_EAGER_ATTEMPTS = 6;
380
+ function scheduleReconnect() {
381
+ if (reconnectTimer) return;
382
+ reconnectAttempts++;
383
+ if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return;
384
+ const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
385
+ reconnectTimer = setTimeout(() => {
386
+ reconnectTimer = null;
387
+ connect();
388
+ }, delay);
389
+ }
390
+ var automationSessions = /* @__PURE__ */ new Map();
391
+ var WINDOW_IDLE_TIMEOUT = 3e4;
392
+ function getWorkspaceKey(workspace) {
393
+ return workspace?.trim() || "default";
394
+ }
395
+ function resetWindowIdleTimer(workspace) {
396
+ const session = automationSessions.get(workspace);
397
+ if (!session) return;
398
+ if (session.idleTimer) clearTimeout(session.idleTimer);
399
+ session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
400
+ session.idleTimer = setTimeout(async () => {
401
+ const current = automationSessions.get(workspace);
402
+ if (!current) return;
403
+ if (!current.owned) {
404
+ console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`);
405
+ automationSessions.delete(workspace);
406
+ return;
407
+ }
408
+ try {
409
+ await chrome.windows.remove(current.windowId);
410
+ console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
411
+ } catch {}
412
+ automationSessions.delete(workspace);
413
+ }, WINDOW_IDLE_TIMEOUT);
414
+ }
415
+ /** Get or create the dedicated automation window.
416
+ * @param initialUrl — if provided (http/https), used as the initial page instead of about:blank.
417
+ * This avoids an extra blank-page→target-domain navigation on first command.
418
+ */
419
+ async function getAutomationWindow(workspace, initialUrl) {
420
+ const existing = automationSessions.get(workspace);
421
+ if (existing) try {
422
+ await chrome.windows.get(existing.windowId);
423
+ return existing.windowId;
424
+ } catch {
425
+ automationSessions.delete(workspace);
426
+ }
427
+ const startUrl = initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE;
428
+ const win = await chrome.windows.create({
429
+ url: startUrl,
430
+ focused: false,
431
+ width: 1280,
432
+ height: 900,
433
+ type: "normal"
434
+ });
435
+ const session = {
436
+ windowId: win.id,
437
+ idleTimer: null,
438
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
439
+ owned: true,
440
+ preferredTabId: null
441
+ };
442
+ automationSessions.set(workspace, session);
443
+ console.log(`[opencli] Created automation window ${session.windowId} (${workspace}, start=${startUrl})`);
444
+ resetWindowIdleTimer(workspace);
445
+ const tabs = await chrome.tabs.query({ windowId: win.id });
446
+ if (tabs[0]?.id) await new Promise((resolve) => {
447
+ const timeout = setTimeout(resolve, 500);
448
+ const listener = (tabId, info) => {
449
+ if (tabId === tabs[0].id && info.status === "complete") {
450
+ chrome.tabs.onUpdated.removeListener(listener);
451
+ clearTimeout(timeout);
452
+ resolve();
453
+ }
454
+ };
455
+ if (tabs[0].status === "complete") {
456
+ clearTimeout(timeout);
457
+ resolve();
458
+ } else chrome.tabs.onUpdated.addListener(listener);
459
+ });
460
+ return session.windowId;
461
+ }
462
+ chrome.windows.onRemoved.addListener((windowId) => {
463
+ for (const [workspace, session] of automationSessions.entries()) if (session.windowId === windowId) {
464
+ console.log(`[opencli] Automation window closed (${workspace})`);
465
+ if (session.idleTimer) clearTimeout(session.idleTimer);
466
+ automationSessions.delete(workspace);
467
+ }
468
+ });
469
+ var initialized = false;
470
+ function initialize() {
471
+ if (initialized) return;
472
+ initialized = true;
473
+ chrome.alarms.create("keepalive", { periodInMinutes: .4 });
474
+ registerListeners();
475
+ connect();
476
+ console.log("[opencli] OpenCLI extension initialized");
477
+ }
478
+ chrome.runtime.onInstalled.addListener(() => {
479
+ initialize();
480
+ });
481
+ chrome.runtime.onStartup.addListener(() => {
482
+ initialize();
483
+ });
484
+ chrome.alarms.onAlarm.addListener((alarm) => {
485
+ if (alarm.name === "keepalive") connect();
486
+ });
487
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
488
+ if (msg?.type === "getStatus") sendResponse({
489
+ connected: ws?.readyState === WebSocket.OPEN,
490
+ reconnecting: reconnectTimer !== null
491
+ });
492
+ return false;
493
+ });
494
+ async function handleCommand(cmd) {
495
+ const workspace = getWorkspaceKey(cmd.workspace);
496
+ resetWindowIdleTimer(workspace);
497
+ try {
498
+ switch (cmd.action) {
499
+ case "exec": return await handleExec(cmd, workspace);
500
+ case "navigate": return await handleNavigate(cmd, workspace);
501
+ case "tabs": return await handleTabs(cmd, workspace);
502
+ case "cookies": return await handleCookies(cmd);
503
+ case "screenshot": return await handleScreenshot(cmd, workspace);
504
+ case "close-window": return await handleCloseWindow(cmd, workspace);
505
+ case "cdp": return await handleCdp(cmd, workspace);
506
+ case "sessions": return await handleSessions(cmd);
507
+ case "set-file-input": return await handleSetFileInput(cmd, workspace);
508
+ case "insert-text": return await handleInsertText(cmd, workspace);
509
+ case "bind-current": return await handleBindCurrent(cmd, workspace);
510
+ case "network-capture-start": return await handleNetworkCaptureStart(cmd, workspace);
511
+ case "network-capture-read": return await handleNetworkCaptureRead(cmd, workspace);
512
+ default: return {
513
+ id: cmd.id,
514
+ ok: false,
515
+ error: `Unknown action: ${cmd.action}`
516
+ };
517
+ }
518
+ } catch (err) {
519
+ return {
520
+ id: cmd.id,
521
+ ok: false,
522
+ error: err instanceof Error ? err.message : String(err)
523
+ };
524
+ }
525
+ }
526
+ /** Internal blank page used when no user URL is provided. */
527
+ var BLANK_PAGE = "about:blank";
528
+ /** Check if a URL can be attached via CDP — only allow http(s) and blank pages. */
529
+ function isDebuggableUrl(url) {
530
+ if (!url) return true;
531
+ return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:");
532
+ }
533
+ /** Check if a URL is safe for user-facing navigation (http/https only). */
534
+ function isSafeNavigationUrl(url) {
535
+ return url.startsWith("http://") || url.startsWith("https://");
536
+ }
537
+ /** Minimal URL normalization for same-page comparison: root slash + default port only. */
538
+ function normalizeUrlForComparison(url) {
539
+ if (!url) return "";
540
+ try {
541
+ const parsed = new URL(url);
542
+ if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") parsed.port = "";
543
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname;
544
+ return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`;
545
+ } catch {
546
+ return url;
547
+ }
548
+ }
549
+ function isTargetUrl(currentUrl, targetUrl) {
550
+ return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
551
+ }
552
+ function matchesDomain(url, domain) {
553
+ if (!url) return false;
554
+ try {
555
+ const parsed = new URL(url);
556
+ return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
557
+ } catch {
558
+ return false;
559
+ }
560
+ }
561
+ function matchesBindCriteria(tab, cmd) {
562
+ if (!tab.id || !isDebuggableUrl(tab.url)) return false;
563
+ if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false;
564
+ if (cmd.matchPathPrefix) try {
565
+ if (!new URL(tab.url).pathname.startsWith(cmd.matchPathPrefix)) return false;
566
+ } catch {
567
+ return false;
568
+ }
569
+ return true;
570
+ }
571
+ function setWorkspaceSession(workspace, session) {
572
+ const existing = automationSessions.get(workspace);
573
+ if (existing?.idleTimer) clearTimeout(existing.idleTimer);
574
+ automationSessions.set(workspace, {
575
+ ...session,
576
+ idleTimer: null,
577
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
578
+ });
579
+ }
580
+ /**
581
+ * Resolve target tab in the automation window, returning both the tabId and
582
+ * the Tab object (when available) so callers can skip a redundant chrome.tabs.get().
583
+ */
584
+ async function resolveTab(tabId, workspace, initialUrl) {
585
+ if (tabId !== void 0) try {
586
+ const tab = await chrome.tabs.get(tabId);
587
+ const session = automationSessions.get(workspace);
588
+ const matchesSession = session ? session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId : false;
589
+ if (isDebuggableUrl(tab.url) && matchesSession) return {
590
+ tabId,
591
+ tab
592
+ };
593
+ if (session && !matchesSession && session.preferredTabId === null && isDebuggableUrl(tab.url)) {
594
+ console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`);
595
+ try {
596
+ await chrome.tabs.move(tabId, {
597
+ windowId: session.windowId,
598
+ index: -1
599
+ });
600
+ const moved = await chrome.tabs.get(tabId);
601
+ if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) return {
602
+ tabId,
603
+ tab: moved
604
+ };
605
+ } catch (moveErr) {
606
+ console.warn(`[opencli] Failed to move tab back: ${moveErr}`);
607
+ }
608
+ } else if (!isDebuggableUrl(tab.url)) console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
609
+ } catch {
610
+ console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
611
+ }
612
+ const existingSession = automationSessions.get(workspace);
613
+ if (existingSession?.preferredTabId !== null) try {
614
+ const preferredTab = await chrome.tabs.get(existingSession.preferredTabId);
615
+ if (isDebuggableUrl(preferredTab.url)) return {
616
+ tabId: preferredTab.id,
617
+ tab: preferredTab
618
+ };
619
+ } catch {
620
+ automationSessions.delete(workspace);
621
+ }
622
+ const windowId = await getAutomationWindow(workspace, initialUrl);
623
+ const tabs = await chrome.tabs.query({ windowId });
624
+ const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
625
+ if (debuggableTab?.id) return {
626
+ tabId: debuggableTab.id,
627
+ tab: debuggableTab
628
+ };
629
+ const reuseTab = tabs.find((t) => t.id);
630
+ if (reuseTab?.id) {
631
+ await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
632
+ await new Promise((resolve) => setTimeout(resolve, 300));
633
+ try {
634
+ const updated = await chrome.tabs.get(reuseTab.id);
635
+ if (isDebuggableUrl(updated.url)) return {
636
+ tabId: reuseTab.id,
637
+ tab: updated
638
+ };
639
+ console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
640
+ } catch {}
641
+ }
642
+ const newTab = await chrome.tabs.create({
643
+ windowId,
644
+ url: BLANK_PAGE,
645
+ active: true
646
+ });
647
+ if (!newTab.id) throw new Error("Failed to create tab in automation window");
648
+ return {
649
+ tabId: newTab.id,
650
+ tab: newTab
651
+ };
652
+ }
653
+ /** Convenience wrapper returning just the tabId (used by most handlers) */
654
+ async function resolveTabId(tabId, workspace, initialUrl) {
655
+ return (await resolveTab(tabId, workspace, initialUrl)).tabId;
656
+ }
657
+ async function listAutomationTabs(workspace) {
658
+ const session = automationSessions.get(workspace);
659
+ if (!session) return [];
660
+ if (session.preferredTabId !== null) try {
661
+ return [await chrome.tabs.get(session.preferredTabId)];
662
+ } catch {
663
+ automationSessions.delete(workspace);
664
+ return [];
665
+ }
666
+ try {
667
+ return await chrome.tabs.query({ windowId: session.windowId });
668
+ } catch {
669
+ automationSessions.delete(workspace);
670
+ return [];
671
+ }
672
+ }
673
+ async function listAutomationWebTabs(workspace) {
674
+ return (await listAutomationTabs(workspace)).filter((tab) => isDebuggableUrl(tab.url));
675
+ }
676
+ async function handleExec(cmd, workspace) {
677
+ if (!cmd.code) return {
678
+ id: cmd.id,
679
+ ok: false,
680
+ error: "Missing code"
681
+ };
682
+ const tabId = await resolveTabId(cmd.tabId, workspace);
683
+ try {
684
+ const aggressive = workspace.startsWith("operate:");
685
+ const data = await evaluateAsync(tabId, cmd.code, aggressive);
686
+ return {
687
+ id: cmd.id,
688
+ ok: true,
689
+ data
690
+ };
691
+ } catch (err) {
692
+ return {
693
+ id: cmd.id,
694
+ ok: false,
695
+ error: err instanceof Error ? err.message : String(err)
696
+ };
697
+ }
698
+ }
699
+ async function handleNavigate(cmd, workspace) {
700
+ if (!cmd.url) return {
701
+ id: cmd.id,
702
+ ok: false,
703
+ error: "Missing url"
704
+ };
705
+ if (!isSafeNavigationUrl(cmd.url)) return {
706
+ id: cmd.id,
707
+ ok: false,
708
+ error: "Blocked URL scheme -- only http:// and https:// are allowed"
709
+ };
710
+ const resolved = await resolveTab(cmd.tabId, workspace, cmd.url);
711
+ const tabId = resolved.tabId;
712
+ const beforeTab = resolved.tab ?? await chrome.tabs.get(tabId);
713
+ const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
714
+ const targetUrl = cmd.url;
715
+ if (beforeTab.status === "complete" && isTargetUrl(beforeTab.url, targetUrl)) return {
716
+ id: cmd.id,
717
+ ok: true,
718
+ data: {
719
+ title: beforeTab.title,
720
+ url: beforeTab.url,
721
+ tabId,
722
+ timedOut: false
723
+ }
724
+ };
725
+ await detach(tabId);
726
+ await chrome.tabs.update(tabId, { url: targetUrl });
727
+ let timedOut = false;
728
+ await new Promise((resolve) => {
729
+ let settled = false;
730
+ let checkTimer = null;
731
+ let timeoutTimer = null;
732
+ const finish = () => {
733
+ if (settled) return;
734
+ settled = true;
735
+ chrome.tabs.onUpdated.removeListener(listener);
736
+ if (checkTimer) clearTimeout(checkTimer);
737
+ if (timeoutTimer) clearTimeout(timeoutTimer);
738
+ resolve();
739
+ };
740
+ const isNavigationDone = (url) => {
741
+ return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized;
742
+ };
743
+ const listener = (id, info, tab) => {
744
+ if (id !== tabId) return;
745
+ if (info.status === "complete" && isNavigationDone(tab.url ?? info.url)) finish();
746
+ };
747
+ chrome.tabs.onUpdated.addListener(listener);
748
+ checkTimer = setTimeout(async () => {
749
+ try {
750
+ const currentTab = await chrome.tabs.get(tabId);
751
+ if (currentTab.status === "complete" && isNavigationDone(currentTab.url)) finish();
752
+ } catch {}
753
+ }, 100);
754
+ timeoutTimer = setTimeout(() => {
755
+ timedOut = true;
756
+ console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`);
757
+ finish();
758
+ }, 15e3);
759
+ });
760
+ let tab = await chrome.tabs.get(tabId);
761
+ const session = automationSessions.get(workspace);
762
+ if (session && tab.windowId !== session.windowId) {
763
+ console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId} during navigation, moving back to ${session.windowId}`);
764
+ try {
765
+ await chrome.tabs.move(tabId, {
766
+ windowId: session.windowId,
767
+ index: -1
768
+ });
769
+ tab = await chrome.tabs.get(tabId);
770
+ } catch (moveErr) {
771
+ console.warn(`[opencli] Failed to recover drifted tab: ${moveErr}`);
772
+ }
773
+ }
774
+ return {
775
+ id: cmd.id,
776
+ ok: true,
777
+ data: {
778
+ title: tab.title,
779
+ url: tab.url,
780
+ tabId,
781
+ timedOut
782
+ }
783
+ };
784
+ }
785
+ async function handleTabs(cmd, workspace) {
786
+ switch (cmd.op) {
787
+ case "list": {
788
+ const data = (await listAutomationWebTabs(workspace)).map((t, i) => ({
789
+ index: i,
790
+ tabId: t.id,
791
+ url: t.url,
792
+ title: t.title,
793
+ active: t.active
794
+ }));
795
+ return {
796
+ id: cmd.id,
797
+ ok: true,
798
+ data
799
+ };
800
+ }
801
+ case "new": {
802
+ if (cmd.url && !isSafeNavigationUrl(cmd.url)) return {
803
+ id: cmd.id,
804
+ ok: false,
805
+ error: "Blocked URL scheme -- only http:// and https:// are allowed"
806
+ };
807
+ const windowId = await getAutomationWindow(workspace);
808
+ const tab = await chrome.tabs.create({
809
+ windowId,
810
+ url: cmd.url ?? BLANK_PAGE,
811
+ active: true
812
+ });
813
+ return {
814
+ id: cmd.id,
815
+ ok: true,
816
+ data: {
817
+ tabId: tab.id,
818
+ url: tab.url
819
+ }
820
+ };
821
+ }
822
+ case "close": {
823
+ if (cmd.index !== void 0) {
824
+ const target = (await listAutomationWebTabs(workspace))[cmd.index];
825
+ if (!target?.id) return {
826
+ id: cmd.id,
827
+ ok: false,
828
+ error: `Tab index ${cmd.index} not found`
829
+ };
830
+ await chrome.tabs.remove(target.id);
831
+ await detach(target.id);
832
+ return {
833
+ id: cmd.id,
834
+ ok: true,
835
+ data: { closed: target.id }
836
+ };
837
+ }
838
+ const tabId = await resolveTabId(cmd.tabId, workspace);
839
+ await chrome.tabs.remove(tabId);
840
+ await detach(tabId);
841
+ return {
842
+ id: cmd.id,
843
+ ok: true,
844
+ data: { closed: tabId }
845
+ };
846
+ }
847
+ case "select": {
848
+ if (cmd.index === void 0 && cmd.tabId === void 0) return {
849
+ id: cmd.id,
850
+ ok: false,
851
+ error: "Missing index or tabId"
852
+ };
853
+ if (cmd.tabId !== void 0) {
854
+ const session = automationSessions.get(workspace);
855
+ let tab;
856
+ try {
857
+ tab = await chrome.tabs.get(cmd.tabId);
858
+ } catch {
859
+ return {
860
+ id: cmd.id,
861
+ ok: false,
862
+ error: `Tab ${cmd.tabId} no longer exists`
863
+ };
864
+ }
865
+ if (!session || tab.windowId !== session.windowId) return {
866
+ id: cmd.id,
867
+ ok: false,
868
+ error: `Tab ${cmd.tabId} is not in the automation window`
869
+ };
870
+ await chrome.tabs.update(cmd.tabId, { active: true });
871
+ return {
872
+ id: cmd.id,
873
+ ok: true,
874
+ data: { selected: cmd.tabId }
875
+ };
876
+ }
877
+ const target = (await listAutomationWebTabs(workspace))[cmd.index];
878
+ if (!target?.id) return {
879
+ id: cmd.id,
880
+ ok: false,
881
+ error: `Tab index ${cmd.index} not found`
882
+ };
883
+ await chrome.tabs.update(target.id, { active: true });
884
+ return {
885
+ id: cmd.id,
886
+ ok: true,
887
+ data: { selected: target.id }
888
+ };
889
+ }
890
+ default: return {
891
+ id: cmd.id,
892
+ ok: false,
893
+ error: `Unknown tabs op: ${cmd.op}`
894
+ };
895
+ }
896
+ }
897
+ async function handleCookies(cmd) {
898
+ if (!cmd.domain && !cmd.url) return {
899
+ id: cmd.id,
900
+ ok: false,
901
+ error: "Cookie scope required: provide domain or url to avoid dumping all cookies"
902
+ };
903
+ const details = {};
904
+ if (cmd.domain) details.domain = cmd.domain;
905
+ if (cmd.url) details.url = cmd.url;
906
+ const data = (await chrome.cookies.getAll(details)).map((c) => ({
907
+ name: c.name,
908
+ value: c.value,
909
+ domain: c.domain,
910
+ path: c.path,
911
+ secure: c.secure,
912
+ httpOnly: c.httpOnly,
913
+ expirationDate: c.expirationDate
914
+ }));
915
+ return {
916
+ id: cmd.id,
917
+ ok: true,
918
+ data
919
+ };
920
+ }
921
+ async function handleScreenshot(cmd, workspace) {
922
+ const tabId = await resolveTabId(cmd.tabId, workspace);
923
+ try {
924
+ const data = await screenshot(tabId, {
925
+ format: cmd.format,
926
+ quality: cmd.quality,
927
+ fullPage: cmd.fullPage
928
+ });
929
+ return {
930
+ id: cmd.id,
931
+ ok: true,
932
+ data
933
+ };
934
+ } catch (err) {
935
+ return {
936
+ id: cmd.id,
937
+ ok: false,
938
+ error: err instanceof Error ? err.message : String(err)
939
+ };
940
+ }
941
+ }
942
+ /** CDP methods permitted via the 'cdp' passthrough action. */
943
+ var CDP_ALLOWLIST = new Set([
944
+ "Accessibility.getFullAXTree",
945
+ "DOM.getDocument",
946
+ "DOM.getBoxModel",
947
+ "DOM.getContentQuads",
948
+ "DOM.querySelectorAll",
949
+ "DOM.scrollIntoViewIfNeeded",
950
+ "DOMSnapshot.captureSnapshot",
951
+ "Input.dispatchMouseEvent",
952
+ "Input.dispatchKeyEvent",
953
+ "Input.insertText",
954
+ "Page.getLayoutMetrics",
955
+ "Page.captureScreenshot",
956
+ "Runtime.enable",
957
+ "Emulation.setDeviceMetricsOverride",
958
+ "Emulation.clearDeviceMetricsOverride"
959
+ ]);
960
+ async function handleCdp(cmd, workspace) {
961
+ if (!cmd.cdpMethod) return {
962
+ id: cmd.id,
963
+ ok: false,
964
+ error: "Missing cdpMethod"
965
+ };
966
+ if (!CDP_ALLOWLIST.has(cmd.cdpMethod)) return {
967
+ id: cmd.id,
968
+ ok: false,
969
+ error: `CDP method not permitted: ${cmd.cdpMethod}`
970
+ };
971
+ const tabId = await resolveTabId(cmd.tabId, workspace);
972
+ try {
973
+ await ensureAttached(tabId, workspace.startsWith("operate:"));
974
+ const data = await chrome.debugger.sendCommand({ tabId }, cmd.cdpMethod, cmd.cdpParams ?? {});
975
+ return {
976
+ id: cmd.id,
977
+ ok: true,
978
+ data
979
+ };
980
+ } catch (err) {
981
+ return {
982
+ id: cmd.id,
983
+ ok: false,
984
+ error: err instanceof Error ? err.message : String(err)
985
+ };
986
+ }
987
+ }
988
+ async function handleCloseWindow(cmd, workspace) {
989
+ const session = automationSessions.get(workspace);
990
+ if (session) {
991
+ if (session.owned) try {
992
+ await chrome.windows.remove(session.windowId);
993
+ } catch {}
994
+ if (session.idleTimer) clearTimeout(session.idleTimer);
995
+ automationSessions.delete(workspace);
996
+ }
997
+ return {
998
+ id: cmd.id,
999
+ ok: true,
1000
+ data: { closed: true }
1001
+ };
1002
+ }
1003
+ async function handleSetFileInput(cmd, workspace) {
1004
+ if (!cmd.files || !Array.isArray(cmd.files) || cmd.files.length === 0) return {
1005
+ id: cmd.id,
1006
+ ok: false,
1007
+ error: "Missing or empty files array"
1008
+ };
1009
+ const tabId = await resolveTabId(cmd.tabId, workspace);
1010
+ try {
1011
+ await setFileInputFiles(tabId, cmd.files, cmd.selector);
1012
+ return {
1013
+ id: cmd.id,
1014
+ ok: true,
1015
+ data: { count: cmd.files.length }
1016
+ };
1017
+ } catch (err) {
1018
+ return {
1019
+ id: cmd.id,
1020
+ ok: false,
1021
+ error: err instanceof Error ? err.message : String(err)
1022
+ };
1023
+ }
1024
+ }
1025
+ async function handleInsertText(cmd, workspace) {
1026
+ if (typeof cmd.text !== "string") return {
1027
+ id: cmd.id,
1028
+ ok: false,
1029
+ error: "Missing text payload"
1030
+ };
1031
+ const tabId = await resolveTabId(cmd.tabId, workspace);
1032
+ try {
1033
+ await insertText(tabId, cmd.text);
1034
+ return {
1035
+ id: cmd.id,
1036
+ ok: true,
1037
+ data: { inserted: true }
1038
+ };
1039
+ } catch (err) {
1040
+ return {
1041
+ id: cmd.id,
1042
+ ok: false,
1043
+ error: err instanceof Error ? err.message : String(err)
1044
+ };
1045
+ }
1046
+ }
1047
+ async function handleNetworkCaptureStart(cmd, workspace) {
1048
+ const tabId = await resolveTabId(cmd.tabId, workspace);
1049
+ try {
1050
+ await startNetworkCapture(tabId, cmd.pattern);
1051
+ return {
1052
+ id: cmd.id,
1053
+ ok: true,
1054
+ data: { started: true }
1055
+ };
1056
+ } catch (err) {
1057
+ return {
1058
+ id: cmd.id,
1059
+ ok: false,
1060
+ error: err instanceof Error ? err.message : String(err)
1061
+ };
1062
+ }
1063
+ }
1064
+ async function handleNetworkCaptureRead(cmd, workspace) {
1065
+ const tabId = await resolveTabId(cmd.tabId, workspace);
1066
+ try {
1067
+ const data = await readNetworkCapture(tabId);
1068
+ return {
1069
+ id: cmd.id,
1070
+ ok: true,
1071
+ data
1072
+ };
1073
+ } catch (err) {
1074
+ return {
1075
+ id: cmd.id,
1076
+ ok: false,
1077
+ error: err instanceof Error ? err.message : String(err)
1078
+ };
1079
+ }
1080
+ }
1081
+ async function handleSessions(cmd) {
1082
+ const now = Date.now();
1083
+ const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
1084
+ workspace,
1085
+ windowId: session.windowId,
1086
+ tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length,
1087
+ idleMsRemaining: Math.max(0, session.idleDeadlineAt - now)
1088
+ })));
1089
+ return {
1090
+ id: cmd.id,
1091
+ ok: true,
1092
+ data
1093
+ };
1094
+ }
1095
+ async function handleBindCurrent(cmd, workspace) {
1096
+ const activeTabs = await chrome.tabs.query({
1097
+ active: true,
1098
+ lastFocusedWindow: true
1099
+ });
1100
+ const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
1101
+ const allTabs = await chrome.tabs.query({});
1102
+ const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd));
1103
+ if (!boundTab?.id) return {
1104
+ id: cmd.id,
1105
+ ok: false,
1106
+ error: cmd.matchDomain || cmd.matchPathPrefix ? `No visible tab matching ${cmd.matchDomain ?? "domain"}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ""}` : "No active debuggable tab found"
1107
+ };
1108
+ setWorkspaceSession(workspace, {
1109
+ windowId: boundTab.windowId,
1110
+ owned: false,
1111
+ preferredTabId: boundTab.id
1112
+ });
1113
+ resetWindowIdleTimer(workspace);
1114
+ console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
1115
+ return {
1116
+ id: cmd.id,
1117
+ ok: true,
1118
+ data: {
1119
+ tabId: boundTab.id,
1120
+ windowId: boundTab.windowId,
1121
+ url: boundTab.url,
1122
+ title: boundTab.title,
1123
+ workspace
1124
+ }
1125
+ };
1126
+ }
1127
+ //#endregion