@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
@@ -73,6 +73,12 @@ function createChromeMock() {
73
73
  if (!tab) throw new Error(`Unknown tab ${tabId}`);
74
74
  return tab;
75
75
  }),
76
+ move: vi.fn(async (tabId: number, moveProps: { windowId: number; index: number }) => {
77
+ const tab = tabs.find((entry) => entry.id === tabId);
78
+ if (!tab) throw new Error(`Unknown tab ${tabId}`);
79
+ tab.windowId = moveProps.windowId;
80
+ return tab;
81
+ }),
76
82
  onUpdated: { addListener: vi.fn(), removeListener: vi.fn() } as Listener<(id: number, info: chrome.tabs.TabChangeInfo) => void>,
77
83
  },
78
84
  windows: {
@@ -219,6 +225,39 @@ describe('background tab isolation', () => {
219
225
  }));
220
226
  });
221
227
 
228
+ it('moves drifted tab back to automation window instead of creating a new one', async () => {
229
+ const { chrome, tabs } = createChromeMock();
230
+ // Tab 1 belongs to automation window 1 but drifted to window 2
231
+ tabs[0].windowId = 2;
232
+ tabs[0].url = 'https://twitter.com/home';
233
+ vi.stubGlobal('chrome', chrome);
234
+
235
+ const mod = await import('./background');
236
+ mod.__test__.setAutomationWindowId('site:twitter', 1);
237
+
238
+ const tabId = await mod.__test__.resolveTabId(1, 'site:twitter');
239
+
240
+ // Should have moved tab 1 back to window 1 and reused it
241
+ expect(chrome.tabs.move).toHaveBeenCalledWith(1, { windowId: 1, index: -1 });
242
+ expect(tabId).toBe(1);
243
+ });
244
+
245
+ it('falls through to re-resolve when drifted tab move fails', async () => {
246
+ const { chrome, tabs } = createChromeMock();
247
+ tabs[0].windowId = 2;
248
+ tabs[0].url = 'https://twitter.com/home';
249
+ // Make move fail
250
+ chrome.tabs.move = vi.fn(async () => { throw new Error('Cannot move tab'); });
251
+ vi.stubGlobal('chrome', chrome);
252
+
253
+ const mod = await import('./background');
254
+ mod.__test__.setAutomationWindowId('site:twitter', 1);
255
+
256
+ // Should still resolve (by finding/creating a tab in the correct window)
257
+ const tabId = await mod.__test__.resolveTabId(1, 'site:twitter');
258
+ expect(typeof tabId).toBe('number');
259
+ });
260
+
222
261
  it('idle timeout closes the automation window for site:notebooklm', async () => {
223
262
  const { chrome, tabs } = createChromeMock();
224
263
  tabs[0].url = 'https://notebooklm.google.com/';
@@ -117,6 +117,8 @@ type AutomationSession = {
117
117
  windowId: number;
118
118
  idleTimer: ReturnType<typeof setTimeout> | null;
119
119
  idleDeadlineAt: number;
120
+ owned: boolean;
121
+ preferredTabId: number | null;
120
122
  };
121
123
 
122
124
  const automationSessions = new Map<string, AutomationSession>();
@@ -134,6 +136,11 @@ function resetWindowIdleTimer(workspace: string): void {
134
136
  session.idleTimer = setTimeout(async () => {
135
137
  const current = automationSessions.get(workspace);
136
138
  if (!current) return;
139
+ if (!current.owned) {
140
+ console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`);
141
+ automationSessions.delete(workspace);
142
+ return;
143
+ }
137
144
  try {
138
145
  await chrome.windows.remove(current.windowId);
139
146
  console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
@@ -144,8 +151,11 @@ function resetWindowIdleTimer(workspace: string): void {
144
151
  }, WINDOW_IDLE_TIMEOUT);
145
152
  }
146
153
 
147
- /** Get or create the dedicated automation window. */
148
- async function getAutomationWindow(workspace: string): Promise<number> {
154
+ /** Get or create the dedicated automation window.
155
+ * @param initialUrl — if provided (http/https), used as the initial page instead of about:blank.
156
+ * This avoids an extra blank-page→target-domain navigation on first command.
157
+ */
158
+ async function getAutomationWindow(workspace: string, initialUrl?: string): Promise<number> {
149
159
  // Check if our window is still alive
150
160
  const existing = automationSessions.get(workspace);
151
161
  if (existing) {
@@ -158,12 +168,13 @@ async function getAutomationWindow(workspace: string): Promise<number> {
158
168
  }
159
169
  }
160
170
 
161
- // Create a new window with a data: URI that New Tab Override extensions cannot intercept.
162
- // Using about:blank would be hijacked by extensions like "New Tab Override".
171
+ // Use the target URL directly if it's a safe navigation URL, otherwise fall back to about:blank.
172
+ const startUrl = (initialUrl && isSafeNavigationUrl(initialUrl)) ? initialUrl : BLANK_PAGE;
173
+
163
174
  // Note: Do NOT set `state` parameter here. Chrome 146+ rejects 'normal' as an invalid
164
175
  // state value for windows.create(). The window defaults to 'normal' state anyway.
165
176
  const win = await chrome.windows.create({
166
- url: BLANK_PAGE,
177
+ url: startUrl,
167
178
  focused: false,
168
179
  width: 1280,
169
180
  height: 900,
@@ -173,12 +184,33 @@ async function getAutomationWindow(workspace: string): Promise<number> {
173
184
  windowId: win.id!,
174
185
  idleTimer: null,
175
186
  idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
187
+ owned: true,
188
+ preferredTabId: null,
176
189
  };
177
190
  automationSessions.set(workspace, session);
178
- console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
191
+ console.log(`[opencli] Created automation window ${session.windowId} (${workspace}, start=${startUrl})`);
179
192
  resetWindowIdleTimer(workspace);
180
- // Brief delay to let Chrome load the initial data: URI tab
181
- await new Promise(resolve => setTimeout(resolve, 200));
193
+ // Wait for the initial tab to finish loading instead of a fixed 200ms sleep.
194
+ const tabs = await chrome.tabs.query({ windowId: win.id! });
195
+ if (tabs[0]?.id) {
196
+ await new Promise<void>((resolve) => {
197
+ const timeout = setTimeout(resolve, 500); // fallback cap
198
+ const listener = (tabId: number, info: chrome.tabs.TabChangeInfo) => {
199
+ if (tabId === tabs[0].id && info.status === 'complete') {
200
+ chrome.tabs.onUpdated.removeListener(listener);
201
+ clearTimeout(timeout);
202
+ resolve();
203
+ }
204
+ };
205
+ // Check if already complete before listening
206
+ if (tabs[0].status === 'complete') {
207
+ clearTimeout(timeout);
208
+ resolve();
209
+ } else {
210
+ chrome.tabs.onUpdated.addListener(listener);
211
+ }
212
+ });
213
+ }
182
214
  return session.windowId;
183
215
  }
184
216
 
@@ -256,6 +288,14 @@ async function handleCommand(cmd: Command): Promise<Result> {
256
288
  return await handleSessions(cmd);
257
289
  case 'set-file-input':
258
290
  return await handleSetFileInput(cmd, workspace);
291
+ case 'insert-text':
292
+ return await handleInsertText(cmd, workspace);
293
+ case 'bind-current':
294
+ return await handleBindCurrent(cmd, workspace);
295
+ case 'network-capture-start':
296
+ return await handleNetworkCaptureStart(cmd, workspace);
297
+ case 'network-capture-read':
298
+ return await handleNetworkCaptureRead(cmd, workspace);
259
299
  default:
260
300
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
261
301
  }
@@ -303,7 +343,31 @@ function isTargetUrl(currentUrl: string | undefined, targetUrl: string): boolean
303
343
  return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
304
344
  }
305
345
 
306
- function setWorkspaceSession(workspace: string, session: Pick<AutomationSession, 'windowId'>): void {
346
+ function matchesDomain(url: string | undefined, domain: string): boolean {
347
+ if (!url) return false;
348
+ try {
349
+ const parsed = new URL(url);
350
+ return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+
356
+ function matchesBindCriteria(tab: chrome.tabs.Tab, cmd: Command): boolean {
357
+ if (!tab.id || !isDebuggableUrl(tab.url)) return false;
358
+ if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false;
359
+ if (cmd.matchPathPrefix) {
360
+ try {
361
+ const parsed = new URL(tab.url!);
362
+ if (!parsed.pathname.startsWith(cmd.matchPathPrefix)) return false;
363
+ } catch {
364
+ return false;
365
+ }
366
+ }
367
+ return true;
368
+ }
369
+
370
+ function setWorkspaceSession(workspace: string, session: Omit<AutomationSession, 'idleTimer' | 'idleDeadlineAt'>): void {
307
371
  const existing = automationSessions.get(workspace);
308
372
  if (existing?.idleTimer) clearTimeout(existing.idleTimer);
309
373
  automationSessions.set(workspace, {
@@ -313,50 +377,69 @@ function setWorkspaceSession(workspace: string, session: Pick<AutomationSession,
313
377
  });
314
378
  }
315
379
 
380
+ type ResolvedTab = { tabId: number; tab: chrome.tabs.Tab | null };
381
+
316
382
  /**
317
- * Resolve target tab in the automation window.
318
- * If explicit tabId is given, use that directly.
319
- * Otherwise, find or create a tab in the dedicated automation window.
383
+ * Resolve target tab in the automation window, returning both the tabId and
384
+ * the Tab object (when available) so callers can skip a redundant chrome.tabs.get().
320
385
  */
321
- async function resolveTabId(tabId: number | undefined, workspace: string): Promise<number> {
386
+ async function resolveTab(tabId: number | undefined, workspace: string, initialUrl?: string): Promise<ResolvedTab> {
322
387
  // Even when an explicit tabId is provided, validate it is still debuggable.
323
- // This prevents issues when extensions hijack the tab URL to chrome-extension://
324
- // or when the tab has been closed by the user.
325
388
  if (tabId !== undefined) {
326
389
  try {
327
390
  const tab = await chrome.tabs.get(tabId);
328
391
  const session = automationSessions.get(workspace);
329
- const matchesSession = session ? tab.windowId === session.windowId : false;
330
- if (isDebuggableUrl(tab.url) && matchesSession) return tabId;
331
- if (session && !matchesSession) {
332
- console.warn(`[opencli] Tab ${tabId} is not bound to workspace ${workspace}, re-resolving`);
392
+ const matchesSession = session
393
+ ? (session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId)
394
+ : false;
395
+ if (isDebuggableUrl(tab.url) && matchesSession) return { tabId, tab };
396
+ if (session && !matchesSession && session.preferredTabId === null && isDebuggableUrl(tab.url)) {
397
+ // Tab drifted to another window but content is still valid.
398
+ // Try to move it back instead of abandoning it.
399
+ console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`);
400
+ try {
401
+ await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });
402
+ const moved = await chrome.tabs.get(tabId);
403
+ if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) {
404
+ return { tabId, tab: moved };
405
+ }
406
+ } catch (moveErr) {
407
+ console.warn(`[opencli] Failed to move tab back: ${moveErr}`);
408
+ }
333
409
  } else if (!isDebuggableUrl(tab.url)) {
334
- // Tab exists but URL is not debuggable — fall through to auto-resolve
335
410
  console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
336
411
  }
337
412
  } catch {
338
- // Tab was closed — fall through to auto-resolve
339
413
  console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
340
414
  }
341
415
  }
342
416
 
417
+ const existingSession = automationSessions.get(workspace);
418
+ if (existingSession?.preferredTabId !== null) {
419
+ try {
420
+ const preferredTab = await chrome.tabs.get(existingSession.preferredTabId);
421
+ if (isDebuggableUrl(preferredTab.url)) return { tabId: preferredTab.id!, tab: preferredTab };
422
+ } catch {
423
+ automationSessions.delete(workspace);
424
+ }
425
+ }
426
+
343
427
  // Get (or create) the automation window
344
- const windowId = await getAutomationWindow(workspace);
428
+ const windowId = await getAutomationWindow(workspace, initialUrl);
345
429
 
346
430
  // Prefer an existing debuggable tab
347
431
  const tabs = await chrome.tabs.query({ windowId });
348
432
  const debuggableTab = tabs.find(t => t.id && isDebuggableUrl(t.url));
349
- if (debuggableTab?.id) return debuggableTab.id;
433
+ if (debuggableTab?.id) return { tabId: debuggableTab.id, tab: debuggableTab };
350
434
 
351
435
  // No debuggable tab — another extension may have hijacked the tab URL.
352
- // Try to reuse by navigating to a data: URI (not interceptable by New Tab Override).
353
436
  const reuseTab = tabs.find(t => t.id);
354
437
  if (reuseTab?.id) {
355
438
  await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
356
439
  await new Promise(resolve => setTimeout(resolve, 300));
357
440
  try {
358
441
  const updated = await chrome.tabs.get(reuseTab.id);
359
- if (isDebuggableUrl(updated.url)) return reuseTab.id;
442
+ if (isDebuggableUrl(updated.url)) return { tabId: reuseTab.id, tab: updated };
360
443
  console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
361
444
  } catch {
362
445
  // Tab was closed during navigation
@@ -366,12 +449,26 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
366
449
  // Fallback: create a new tab
367
450
  const newTab = await chrome.tabs.create({ windowId, url: BLANK_PAGE, active: true });
368
451
  if (!newTab.id) throw new Error('Failed to create tab in automation window');
369
- return newTab.id;
452
+ return { tabId: newTab.id, tab: newTab };
453
+ }
454
+
455
+ /** Convenience wrapper returning just the tabId (used by most handlers) */
456
+ async function resolveTabId(tabId: number | undefined, workspace: string, initialUrl?: string): Promise<number> {
457
+ const resolved = await resolveTab(tabId, workspace, initialUrl);
458
+ return resolved.tabId;
370
459
  }
371
460
 
372
461
  async function listAutomationTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
373
462
  const session = automationSessions.get(workspace);
374
463
  if (!session) return [];
464
+ if (session.preferredTabId !== null) {
465
+ try {
466
+ return [await chrome.tabs.get(session.preferredTabId)];
467
+ } catch {
468
+ automationSessions.delete(workspace);
469
+ return [];
470
+ }
471
+ }
375
472
  try {
376
473
  return await chrome.tabs.query({ windowId: session.windowId });
377
474
  } catch {
@@ -402,9 +499,11 @@ async function handleNavigate(cmd: Command, workspace: string): Promise<Result>
402
499
  if (!isSafeNavigationUrl(cmd.url)) {
403
500
  return { id: cmd.id, ok: false, error: 'Blocked URL scheme -- only http:// and https:// are allowed' };
404
501
  }
405
- const tabId = await resolveTabId(cmd.tabId, workspace);
502
+ // Pass target URL so that first-time window creation can start on the right domain
503
+ const resolved = await resolveTab(cmd.tabId, workspace, cmd.url);
504
+ const tabId = resolved.tabId;
406
505
 
407
- const beforeTab = await chrome.tabs.get(tabId);
506
+ const beforeTab = resolved.tab ?? await chrome.tabs.get(tabId);
408
507
  const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
409
508
  const targetUrl = cmd.url;
410
509
 
@@ -475,7 +574,22 @@ async function handleNavigate(cmd: Command, workspace: string): Promise<Result>
475
574
  }, 15000);
476
575
  });
477
576
 
478
- const tab = await chrome.tabs.get(tabId);
577
+ let tab = await chrome.tabs.get(tabId);
578
+
579
+ // Post-navigation drift detection: if the tab moved to another window
580
+ // during navigation (e.g. a tab-management extension regrouped it),
581
+ // try to move it back to maintain session isolation.
582
+ const session = automationSessions.get(workspace);
583
+ if (session && tab.windowId !== session.windowId) {
584
+ console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId} during navigation, moving back to ${session.windowId}`);
585
+ try {
586
+ await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });
587
+ tab = await chrome.tabs.get(tabId);
588
+ } catch (moveErr) {
589
+ console.warn(`[opencli] Failed to recover drifted tab: ${moveErr}`);
590
+ }
591
+ }
592
+
479
593
  return {
480
594
  id: cmd.id,
481
595
  ok: true,
@@ -628,10 +742,12 @@ async function handleCdp(cmd: Command, workspace: string): Promise<Result> {
628
742
  async function handleCloseWindow(cmd: Command, workspace: string): Promise<Result> {
629
743
  const session = automationSessions.get(workspace);
630
744
  if (session) {
631
- try {
632
- await chrome.windows.remove(session.windowId);
633
- } catch {
634
- // Window may already be closed
745
+ if (session.owned) {
746
+ try {
747
+ await chrome.windows.remove(session.windowId);
748
+ } catch {
749
+ // Window may already be closed
750
+ }
635
751
  }
636
752
  if (session.idleTimer) clearTimeout(session.idleTimer);
637
753
  automationSessions.delete(workspace);
@@ -652,6 +768,39 @@ async function handleSetFileInput(cmd: Command, workspace: string): Promise<Resu
652
768
  }
653
769
  }
654
770
 
771
+ async function handleInsertText(cmd: Command, workspace: string): Promise<Result> {
772
+ if (typeof cmd.text !== 'string') {
773
+ return { id: cmd.id, ok: false, error: 'Missing text payload' };
774
+ }
775
+ const tabId = await resolveTabId(cmd.tabId, workspace);
776
+ try {
777
+ await executor.insertText(tabId, cmd.text);
778
+ return { id: cmd.id, ok: true, data: { inserted: true } };
779
+ } catch (err) {
780
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
781
+ }
782
+ }
783
+
784
+ async function handleNetworkCaptureStart(cmd: Command, workspace: string): Promise<Result> {
785
+ const tabId = await resolveTabId(cmd.tabId, workspace);
786
+ try {
787
+ await executor.startNetworkCapture(tabId, cmd.pattern);
788
+ return { id: cmd.id, ok: true, data: { started: true } };
789
+ } catch (err) {
790
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
791
+ }
792
+ }
793
+
794
+ async function handleNetworkCaptureRead(cmd: Command, workspace: string): Promise<Result> {
795
+ const tabId = await resolveTabId(cmd.tabId, workspace);
796
+ try {
797
+ const data = await executor.readNetworkCapture(tabId);
798
+ return { id: cmd.id, ok: true, data };
799
+ } catch (err) {
800
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
801
+ }
802
+ }
803
+
655
804
  async function handleSessions(cmd: Command): Promise<Result> {
656
805
  const now = Date.now();
657
806
  const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
@@ -663,11 +812,49 @@ async function handleSessions(cmd: Command): Promise<Result> {
663
812
  return { id: cmd.id, ok: true, data };
664
813
  }
665
814
 
815
+ async function handleBindCurrent(cmd: Command, workspace: string): Promise<Result> {
816
+ const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
817
+ const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
818
+ const allTabs = await chrome.tabs.query({});
819
+ const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd))
820
+ ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd))
821
+ ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd));
822
+ if (!boundTab?.id) {
823
+ return {
824
+ id: cmd.id,
825
+ ok: false,
826
+ error: cmd.matchDomain || cmd.matchPathPrefix
827
+ ? `No visible tab matching ${cmd.matchDomain ?? 'domain'}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ''}`
828
+ : 'No active debuggable tab found',
829
+ };
830
+ }
831
+
832
+ setWorkspaceSession(workspace, {
833
+ windowId: boundTab.windowId,
834
+ owned: false,
835
+ preferredTabId: boundTab.id,
836
+ });
837
+ resetWindowIdleTimer(workspace);
838
+ console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
839
+ return {
840
+ id: cmd.id,
841
+ ok: true,
842
+ data: {
843
+ tabId: boundTab.id,
844
+ windowId: boundTab.windowId,
845
+ url: boundTab.url,
846
+ title: boundTab.title,
847
+ workspace,
848
+ },
849
+ };
850
+ }
851
+
666
852
  export const __test__ = {
667
853
  handleNavigate,
668
854
  isTargetUrl,
669
855
  handleTabs,
670
856
  handleSessions,
857
+ handleBindCurrent,
671
858
  resolveTabId,
672
859
  resetWindowIdleTimer,
673
860
  getSession: (workspace: string = 'default') => automationSessions.get(workspace) ?? null,
@@ -681,9 +868,11 @@ export const __test__ = {
681
868
  }
682
869
  setWorkspaceSession(workspace, {
683
870
  windowId,
871
+ owned: true,
872
+ preferredTabId: null,
684
873
  });
685
874
  },
686
- setSession: (workspace: string, session: { windowId: number }) => {
875
+ setSession: (workspace: string, session: { windowId: number; owned: boolean; preferredTabId: number | null }) => {
687
876
  setWorkspaceSession(workspace, session);
688
877
  },
689
878
  };