@jackwener/opencli 1.5.6 → 1.5.7

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 (334) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +4 -2
  3. package/README.zh-CN.md +4 -1
  4. package/SKILL.md +879 -0
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +7 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.js +22 -2
  13. package/dist/browser/page.test.d.ts +1 -0
  14. package/dist/browser/page.test.js +44 -0
  15. package/dist/browser/stealth.js +198 -0
  16. package/dist/browser/stealth.test.d.ts +1 -0
  17. package/dist/browser/stealth.test.js +134 -0
  18. package/dist/browser.test.js +1 -1
  19. package/dist/build-manifest.d.ts +1 -0
  20. package/dist/build-manifest.js +5 -1
  21. package/dist/build-manifest.test.js +2 -0
  22. package/dist/cli-manifest.json +544 -137
  23. package/dist/cli.js +20 -3
  24. package/dist/clis/antigravity/serve.d.ts +1 -1
  25. package/dist/clis/antigravity/serve.js +5 -8
  26. package/dist/clis/bilibili/subtitle.js +4 -0
  27. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  28. package/dist/clis/bilibili/subtitle.test.js +48 -0
  29. package/dist/clis/chatwise/ask.js +0 -2
  30. package/dist/clis/chatwise/export.js +0 -2
  31. package/dist/clis/chatwise/history.js +0 -2
  32. package/dist/clis/chatwise/model.js +0 -2
  33. package/dist/clis/chatwise/new.js +1 -2
  34. package/dist/clis/chatwise/read.js +0 -2
  35. package/dist/clis/chatwise/screenshot.js +1 -2
  36. package/dist/clis/chatwise/send.js +0 -2
  37. package/dist/clis/chatwise/status.js +1 -2
  38. package/dist/clis/ctrip/search.d.ts +13 -0
  39. package/dist/clis/ctrip/search.js +73 -48
  40. package/dist/clis/ctrip/search.test.d.ts +1 -0
  41. package/dist/clis/ctrip/search.test.js +64 -0
  42. package/dist/clis/douyin/_shared/sts2.js +8 -2
  43. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  44. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  45. package/dist/clis/douyin/activities.js +4 -2
  46. package/dist/clis/douyin/activities.test.js +34 -1
  47. package/dist/clis/douyin/collections.js +1 -1
  48. package/dist/clis/douyin/collections.test.js +24 -2
  49. package/dist/clis/douyin/draft.d.ts +8 -11
  50. package/dist/clis/douyin/draft.js +302 -185
  51. package/dist/clis/douyin/draft.test.d.ts +1 -1
  52. package/dist/clis/douyin/draft.test.js +357 -2
  53. package/dist/clis/douyin/hashtag.js +9 -2
  54. package/dist/clis/douyin/hashtag.test.js +35 -2
  55. package/dist/clis/douyin/profile.js +1 -1
  56. package/dist/clis/douyin/profile.test.js +36 -1
  57. package/dist/clis/douyin/videos.js +22 -5
  58. package/dist/clis/douyin/videos.test.js +45 -2
  59. package/dist/clis/facebook/search.test.d.ts +5 -0
  60. package/dist/clis/facebook/search.test.js +60 -0
  61. package/dist/clis/facebook/search.yaml +4 -3
  62. package/dist/clis/instagram/download.d.ts +16 -0
  63. package/dist/clis/instagram/download.js +225 -0
  64. package/dist/clis/instagram/download.test.d.ts +1 -0
  65. package/dist/clis/instagram/download.test.js +118 -0
  66. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  67. package/dist/clis/notebooklm/bind-current.js +29 -0
  68. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  69. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  70. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  71. package/dist/clis/notebooklm/binding.test.js +44 -0
  72. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  73. package/dist/clis/notebooklm/compat.test.js +16 -0
  74. package/dist/clis/notebooklm/current.d.ts +1 -0
  75. package/dist/clis/notebooklm/current.js +28 -0
  76. package/dist/clis/notebooklm/get.d.ts +1 -0
  77. package/dist/clis/notebooklm/get.js +37 -0
  78. package/dist/clis/notebooklm/history.d.ts +1 -0
  79. package/dist/clis/notebooklm/history.js +25 -0
  80. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  81. package/dist/clis/notebooklm/history.test.js +58 -0
  82. package/dist/clis/notebooklm/list.d.ts +1 -0
  83. package/dist/clis/notebooklm/list.js +35 -0
  84. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  85. package/dist/clis/notebooklm/note-list.js +28 -0
  86. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  87. package/dist/clis/notebooklm/note-list.test.js +56 -0
  88. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  89. package/dist/clis/notebooklm/notes-get.js +47 -0
  90. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  91. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  92. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  93. package/dist/clis/notebooklm/rpc.js +189 -0
  94. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  95. package/dist/clis/notebooklm/rpc.test.js +105 -0
  96. package/dist/clis/notebooklm/shared.d.ts +87 -0
  97. package/dist/clis/notebooklm/shared.js +3 -0
  98. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  99. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  100. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  101. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  102. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  103. package/dist/clis/notebooklm/source-get.js +40 -0
  104. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  105. package/dist/clis/notebooklm/source-get.test.js +84 -0
  106. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  107. package/dist/clis/notebooklm/source-guide.js +44 -0
  108. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  109. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  110. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  111. package/dist/clis/notebooklm/source-list.js +30 -0
  112. package/dist/clis/notebooklm/status.d.ts +1 -0
  113. package/dist/clis/notebooklm/status.js +31 -0
  114. package/dist/clis/notebooklm/summary.d.ts +1 -0
  115. package/dist/clis/notebooklm/summary.js +30 -0
  116. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  117. package/dist/clis/notebooklm/summary.test.js +78 -0
  118. package/dist/clis/notebooklm/utils.d.ts +37 -0
  119. package/dist/clis/notebooklm/utils.js +739 -0
  120. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  121. package/dist/clis/notebooklm/utils.test.js +390 -0
  122. package/dist/clis/substack/utils.d.ts +4 -0
  123. package/dist/clis/substack/utils.js +8 -2
  124. package/dist/clis/substack/utils.test.d.ts +1 -0
  125. package/dist/clis/substack/utils.test.js +46 -0
  126. package/dist/clis/v2ex/hot.yaml +4 -1
  127. package/dist/clis/v2ex/latest.yaml +4 -1
  128. package/dist/clis/v2ex/topic.yaml +6 -1
  129. package/dist/clis/weixin/download.d.ts +9 -0
  130. package/dist/clis/weixin/download.js +76 -6
  131. package/dist/clis/weread/book.js +108 -2
  132. package/dist/clis/weread/commands.test.js +262 -152
  133. package/dist/clis/weread/utils.d.ts +10 -0
  134. package/dist/clis/weread/utils.js +27 -7
  135. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  136. package/dist/clis/xiaohongshu/comments.js +76 -17
  137. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  138. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  139. package/dist/clis/xiaohongshu/download.js +83 -22
  140. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  141. package/dist/clis/xiaohongshu/download.test.js +75 -0
  142. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  143. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  144. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/note.js +76 -0
  146. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  147. package/dist/clis/xiaohongshu/note.test.js +136 -0
  148. package/dist/clis/xiaohongshu/search.js +9 -0
  149. package/dist/clis/xiaohongshu/search.test.js +10 -4
  150. package/dist/clis/youtube/search.js +57 -17
  151. package/dist/clis/zhihu/question.js +19 -17
  152. package/dist/clis/zhihu/question.test.d.ts +1 -0
  153. package/dist/clis/zhihu/question.test.js +54 -0
  154. package/dist/commanderAdapter.js +9 -0
  155. package/dist/commanderAdapter.test.js +25 -0
  156. package/dist/commands/daemon.d.ts +9 -0
  157. package/dist/commands/daemon.js +124 -0
  158. package/dist/commands/daemon.test.d.ts +1 -0
  159. package/dist/commands/daemon.test.js +185 -0
  160. package/dist/completion.js +3 -1
  161. package/dist/constants.d.ts +2 -0
  162. package/dist/constants.js +2 -0
  163. package/dist/daemon.d.ts +1 -1
  164. package/dist/daemon.js +25 -14
  165. package/dist/daemon.test.d.ts +1 -0
  166. package/dist/daemon.test.js +65 -0
  167. package/dist/discovery.d.ts +9 -0
  168. package/dist/discovery.js +47 -2
  169. package/dist/electron-apps.d.ts +29 -0
  170. package/dist/electron-apps.js +65 -0
  171. package/dist/electron-apps.test.d.ts +1 -0
  172. package/dist/electron-apps.test.js +43 -0
  173. package/dist/engine.test.js +41 -9
  174. package/dist/execution.js +20 -16
  175. package/dist/idle-manager.d.ts +19 -0
  176. package/dist/idle-manager.js +54 -0
  177. package/dist/launcher.d.ts +36 -0
  178. package/dist/launcher.js +152 -0
  179. package/dist/launcher.test.d.ts +1 -0
  180. package/dist/launcher.test.js +57 -0
  181. package/dist/main.js +3 -3
  182. package/dist/registry.d.ts +1 -0
  183. package/dist/registry.js +31 -3
  184. package/dist/registry.test.js +13 -0
  185. package/dist/runtime.d.ts +5 -3
  186. package/dist/runtime.js +12 -5
  187. package/dist/serialization.d.ts +1 -0
  188. package/dist/serialization.js +3 -0
  189. package/dist/serialization.test.js +17 -1
  190. package/dist/tui.d.ts +7 -0
  191. package/dist/tui.js +52 -0
  192. package/dist/tui.test.d.ts +1 -0
  193. package/dist/tui.test.js +19 -0
  194. package/dist/weixin-download.test.js +14 -0
  195. package/docs/.vitepress/config.mts +1 -0
  196. package/docs/adapters/browser/notebooklm.md +69 -0
  197. package/docs/adapters/browser/xiaohongshu.md +19 -10
  198. package/docs/adapters/index.md +67 -66
  199. package/docs/guide/browser-bridge.md +12 -0
  200. package/docs/guide/troubleshooting.md +9 -4
  201. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  202. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  203. package/docs/zh/guide/browser-bridge.md +12 -0
  204. package/extension/dist/background.js +794 -513
  205. package/extension/src/background.test.ts +202 -2
  206. package/extension/src/background.ts +174 -10
  207. package/extension/src/cdp.ts +12 -0
  208. package/extension/src/protocol.ts +7 -5
  209. package/package.json +1 -1
  210. package/src/browser/cdp.ts +24 -17
  211. package/src/browser/daemon-client.ts +7 -1
  212. package/src/browser/dom-helpers.test.ts +15 -1
  213. package/src/browser/dom-helpers.ts +1 -0
  214. package/src/browser/mcp.ts +18 -13
  215. package/src/browser/page.test.ts +58 -0
  216. package/src/browser/page.ts +18 -2
  217. package/src/browser/stealth.test.ts +153 -0
  218. package/src/browser/stealth.ts +198 -0
  219. package/src/browser.test.ts +1 -1
  220. package/src/build-manifest.test.ts +2 -0
  221. package/src/build-manifest.ts +6 -1
  222. package/src/cli.ts +21 -3
  223. package/src/clis/antigravity/SKILL.md +3 -12
  224. package/src/clis/antigravity/serve.ts +5 -10
  225. package/src/clis/bilibili/subtitle.test.ts +60 -0
  226. package/src/clis/bilibili/subtitle.ts +4 -0
  227. package/src/clis/chatwise/ask.ts +0 -2
  228. package/src/clis/chatwise/export.ts +0 -2
  229. package/src/clis/chatwise/history.ts +0 -2
  230. package/src/clis/chatwise/model.ts +0 -2
  231. package/src/clis/chatwise/new.ts +1 -2
  232. package/src/clis/chatwise/read.ts +0 -2
  233. package/src/clis/chatwise/screenshot.ts +1 -2
  234. package/src/clis/chatwise/send.ts +0 -2
  235. package/src/clis/chatwise/status.ts +1 -2
  236. package/src/clis/ctrip/search.test.ts +73 -0
  237. package/src/clis/ctrip/search.ts +97 -47
  238. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  239. package/src/clis/douyin/_shared/sts2.ts +11 -3
  240. package/src/clis/douyin/activities.test.ts +41 -1
  241. package/src/clis/douyin/activities.ts +12 -3
  242. package/src/clis/douyin/collections.test.ts +35 -2
  243. package/src/clis/douyin/collections.ts +1 -1
  244. package/src/clis/douyin/draft.test.ts +444 -2
  245. package/src/clis/douyin/draft.ts +382 -218
  246. package/src/clis/douyin/hashtag.test.ts +42 -2
  247. package/src/clis/douyin/hashtag.ts +11 -3
  248. package/src/clis/douyin/profile.test.ts +43 -1
  249. package/src/clis/douyin/profile.ts +9 -2
  250. package/src/clis/douyin/videos.test.ts +52 -2
  251. package/src/clis/douyin/videos.ts +49 -15
  252. package/src/clis/facebook/search.test.ts +70 -0
  253. package/src/clis/facebook/search.yaml +4 -3
  254. package/src/clis/instagram/download.test.ts +159 -0
  255. package/src/clis/instagram/download.ts +286 -0
  256. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  257. package/src/clis/notebooklm/bind-current.ts +36 -0
  258. package/src/clis/notebooklm/binding.test.ts +53 -0
  259. package/src/clis/notebooklm/compat.test.ts +19 -0
  260. package/src/clis/notebooklm/current.ts +38 -0
  261. package/src/clis/notebooklm/get.ts +53 -0
  262. package/src/clis/notebooklm/history.test.ts +70 -0
  263. package/src/clis/notebooklm/history.ts +36 -0
  264. package/src/clis/notebooklm/list.ts +40 -0
  265. package/src/clis/notebooklm/note-list.test.ts +64 -0
  266. package/src/clis/notebooklm/note-list.ts +42 -0
  267. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  268. package/src/clis/notebooklm/notes-get.ts +67 -0
  269. package/src/clis/notebooklm/rpc.test.ts +126 -0
  270. package/src/clis/notebooklm/rpc.ts +286 -0
  271. package/src/clis/notebooklm/shared.ts +98 -0
  272. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  273. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  274. package/src/clis/notebooklm/source-get.test.ts +100 -0
  275. package/src/clis/notebooklm/source-get.ts +60 -0
  276. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  277. package/src/clis/notebooklm/source-guide.ts +69 -0
  278. package/src/clis/notebooklm/source-list.ts +45 -0
  279. package/src/clis/notebooklm/status.ts +34 -0
  280. package/src/clis/notebooklm/summary.test.ts +94 -0
  281. package/src/clis/notebooklm/summary.ts +45 -0
  282. package/src/clis/notebooklm/utils.test.ts +446 -0
  283. package/src/clis/notebooklm/utils.ts +893 -0
  284. package/src/clis/substack/utils.test.ts +54 -0
  285. package/src/clis/substack/utils.ts +10 -2
  286. package/src/clis/v2ex/hot.yaml +4 -1
  287. package/src/clis/v2ex/latest.yaml +4 -1
  288. package/src/clis/v2ex/topic.yaml +6 -1
  289. package/src/clis/weixin/download.ts +95 -6
  290. package/src/clis/weread/book.ts +142 -2
  291. package/src/clis/weread/commands.test.ts +314 -154
  292. package/src/clis/weread/utils.ts +33 -4
  293. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  294. package/src/clis/xiaohongshu/comments.ts +76 -17
  295. package/src/clis/xiaohongshu/download.test.ts +96 -0
  296. package/src/clis/xiaohongshu/download.ts +83 -22
  297. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  298. package/src/clis/xiaohongshu/note.test.ts +164 -0
  299. package/src/clis/xiaohongshu/note.ts +86 -0
  300. package/src/clis/xiaohongshu/search.test.ts +11 -4
  301. package/src/clis/xiaohongshu/search.ts +13 -0
  302. package/src/clis/youtube/search.ts +57 -17
  303. package/src/clis/zhihu/question.test.ts +71 -0
  304. package/src/clis/zhihu/question.ts +27 -15
  305. package/src/commanderAdapter.test.ts +30 -0
  306. package/src/commanderAdapter.ts +7 -0
  307. package/src/commands/daemon.test.ts +238 -0
  308. package/src/commands/daemon.ts +135 -0
  309. package/src/completion.ts +2 -1
  310. package/src/constants.ts +3 -0
  311. package/src/daemon.test.ts +88 -0
  312. package/src/daemon.ts +26 -14
  313. package/src/discovery.ts +52 -2
  314. package/src/electron-apps.test.ts +50 -0
  315. package/src/electron-apps.ts +89 -0
  316. package/src/engine.test.ts +45 -9
  317. package/src/execution.ts +24 -19
  318. package/src/idle-manager.ts +60 -0
  319. package/src/launcher.test.ts +67 -0
  320. package/src/launcher.ts +185 -0
  321. package/src/main.ts +3 -2
  322. package/src/registry.test.ts +15 -0
  323. package/src/registry.ts +32 -3
  324. package/src/runtime.ts +13 -7
  325. package/src/serialization.test.ts +19 -1
  326. package/src/serialization.ts +2 -0
  327. package/src/tui.test.ts +23 -0
  328. package/src/tui.ts +65 -0
  329. package/src/weixin-download.test.ts +27 -0
  330. package/tests/e2e/browser-public-extended.test.ts +6 -2
  331. package/chatwise-opencli.ps1 +0 -82
  332. package/dist/clis/chatwise/shared.d.ts +0 -2
  333. package/dist/clis/chatwise/shared.js +0 -6
  334. package/src/clis/chatwise/shared.ts +0 -8
@@ -35,8 +35,12 @@ function createChromeMock() {
35
35
  { id: 3, windowId: 1, url: 'chrome://extensions', title: 'chrome', active: false, status: 'complete' },
36
36
  ];
37
37
 
38
- const query = vi.fn(async (queryInfo: { windowId?: number } = {}) => {
39
- return tabs.filter((tab) => queryInfo.windowId === undefined || tab.windowId === queryInfo.windowId);
38
+ const query = vi.fn(async (queryInfo: { windowId?: number; active?: boolean } = {}) => {
39
+ return tabs.filter((tab) => {
40
+ if (queryInfo.windowId !== undefined && tab.windowId !== queryInfo.windowId) return false;
41
+ if (queryInfo.active !== undefined && !!tab.active !== queryInfo.active) return false;
42
+ return true;
43
+ });
40
44
  });
41
45
  const create = vi.fn(async ({ windowId, url, active }: { windowId?: number; url?: string; active?: boolean }) => {
42
46
  const tab: MockTab = {
@@ -84,6 +88,8 @@ function createChromeMock() {
84
88
  runtime: {
85
89
  onInstalled: { addListener: vi.fn() } as Listener<() => void>,
86
90
  onStartup: { addListener: vi.fn() } as Listener<() => void>,
91
+ onMessage: { addListener: vi.fn() } as Listener<(msg: unknown, sender: unknown, sendResponse: (value: unknown) => void) => void>,
92
+ getManifest: vi.fn(() => ({ version: 'test-version' })),
87
93
  },
88
94
  cookies: {
89
95
  getAll: vi.fn(async () => []),
@@ -193,4 +199,198 @@ describe('background tab isolation', () => {
193
199
  expect.objectContaining({ workspace: 'site:zhihu', windowId: 2 }),
194
200
  ]));
195
201
  });
202
+
203
+ it('rebinds site:notebooklm to the active notebook tab instead of a home tab', async () => {
204
+ const { chrome, tabs } = createChromeMock();
205
+ tabs[0].url = 'https://notebooklm.google.com/';
206
+ tabs[0].title = 'NotebookLM Home';
207
+ tabs[1].url = 'https://notebooklm.google.com/notebook/nb-live';
208
+ tabs[1].title = 'Live Notebook';
209
+ vi.stubGlobal('chrome', chrome);
210
+
211
+ const mod = await import('./background');
212
+ mod.__test__.setAutomationWindowId('site:notebooklm', 1);
213
+
214
+ const tabId = await mod.__test__.resolveTabId(undefined, 'site:notebooklm');
215
+
216
+ expect(tabId).toBe(2);
217
+ expect(mod.__test__.getSession('site:notebooklm')).toEqual(expect.objectContaining({
218
+ windowId: 2,
219
+ preferredTabId: 2,
220
+ owned: false,
221
+ }));
222
+ });
223
+
224
+ it('prefers a notebook tab over an active home tab for site:notebooklm', async () => {
225
+ const { chrome, tabs } = createChromeMock();
226
+ tabs[0].url = 'https://notebooklm.google.com/';
227
+ tabs[0].title = 'NotebookLM Home';
228
+ tabs[0].active = true;
229
+ tabs[1].url = 'https://notebooklm.google.com/notebook/nb-passive';
230
+ tabs[1].title = 'Notebook';
231
+ tabs[1].active = false;
232
+ vi.stubGlobal('chrome', chrome);
233
+
234
+ const mod = await import('./background');
235
+ mod.__test__.setAutomationWindowId('site:notebooklm', 1);
236
+
237
+ const tabId = await mod.__test__.resolveTabId(undefined, 'site:notebooklm');
238
+
239
+ expect(tabId).toBe(2);
240
+ expect(mod.__test__.getSession('site:notebooklm')).toEqual(expect.objectContaining({
241
+ windowId: 2,
242
+ preferredTabId: 2,
243
+ owned: false,
244
+ }));
245
+ });
246
+
247
+ it('detaches an adopted workspace session on idle instead of closing the user window', async () => {
248
+ const { chrome } = createChromeMock();
249
+ vi.stubGlobal('chrome', chrome);
250
+ vi.useFakeTimers();
251
+
252
+ const mod = await import('./background');
253
+ mod.__test__.setSession('site:notebooklm', {
254
+ windowId: 2,
255
+ preferredTabId: 2,
256
+ owned: false,
257
+ });
258
+
259
+ mod.__test__.resetWindowIdleTimer('site:notebooklm');
260
+ await vi.advanceTimersByTimeAsync(30001);
261
+
262
+ expect(chrome.windows.remove).not.toHaveBeenCalled();
263
+ expect(mod.__test__.getSession('site:notebooklm')).toBeNull();
264
+ });
265
+
266
+ it('binds the active NotebookLM tab into the workspace explicitly', async () => {
267
+ const { chrome, tabs } = createChromeMock();
268
+ tabs[1].url = 'https://notebooklm.google.com/notebook/nb-active';
269
+ tabs[1].title = 'Bound Notebook';
270
+ tabs[1].active = true;
271
+ vi.stubGlobal('chrome', chrome);
272
+
273
+ const mod = await import('./background');
274
+ const result = await mod.__test__.handleBindCurrent(
275
+ {
276
+ id: 'bind-current',
277
+ action: 'bind-current',
278
+ workspace: 'site:notebooklm',
279
+ matchDomain: 'notebooklm.google.com',
280
+ matchPathPrefix: '/notebook/',
281
+ },
282
+ 'site:notebooklm',
283
+ );
284
+
285
+ expect(result).toEqual({
286
+ id: 'bind-current',
287
+ ok: true,
288
+ data: expect.objectContaining({
289
+ tabId: 2,
290
+ windowId: 2,
291
+ url: 'https://notebooklm.google.com/notebook/nb-active',
292
+ title: 'Bound Notebook',
293
+ workspace: 'site:notebooklm',
294
+ }),
295
+ });
296
+ expect(mod.__test__.getSession('site:notebooklm')).toEqual(expect.objectContaining({
297
+ windowId: 2,
298
+ preferredTabId: 2,
299
+ owned: false,
300
+ }));
301
+ });
302
+
303
+ it('bind-current falls back to another matching notebook tab in the current window', async () => {
304
+ const { chrome, tabs } = createChromeMock();
305
+ tabs[0].windowId = 2;
306
+ tabs[0].url = 'https://notebooklm.google.com/';
307
+ tabs[0].title = 'NotebookLM Home';
308
+ tabs[0].active = true;
309
+ tabs[1].url = 'https://notebooklm.google.com/notebook/nb-passive';
310
+ tabs[1].title = 'Passive Notebook';
311
+ tabs[1].active = false;
312
+ vi.stubGlobal('chrome', chrome);
313
+
314
+ const mod = await import('./background');
315
+ const result = await mod.__test__.handleBindCurrent(
316
+ {
317
+ id: 'bind-fallback',
318
+ action: 'bind-current',
319
+ workspace: 'site:notebooklm',
320
+ matchDomain: 'notebooklm.google.com',
321
+ matchPathPrefix: '/notebook/',
322
+ },
323
+ 'site:notebooklm',
324
+ );
325
+
326
+ expect(result).toEqual({
327
+ id: 'bind-fallback',
328
+ ok: true,
329
+ data: expect.objectContaining({
330
+ tabId: 2,
331
+ windowId: 2,
332
+ url: 'https://notebooklm.google.com/notebook/nb-passive',
333
+ title: 'Passive Notebook',
334
+ }),
335
+ });
336
+ });
337
+
338
+ it('bind-current falls back to a matching notebook tab in another window of the same profile', async () => {
339
+ const { chrome, tabs } = createChromeMock();
340
+ tabs[0].windowId = 3;
341
+ tabs[0].url = 'https://notebooklm.google.com/';
342
+ tabs[0].title = 'NotebookLM Home';
343
+ tabs[0].active = true;
344
+ tabs[1].windowId = 2;
345
+ tabs[1].url = 'https://notebooklm.google.com/notebook/nb-other-window';
346
+ tabs[1].title = 'Notebook In Other Window';
347
+ tabs[1].active = false;
348
+ vi.stubGlobal('chrome', chrome);
349
+
350
+ const mod = await import('./background');
351
+ const result = await mod.__test__.handleBindCurrent(
352
+ {
353
+ id: 'bind-cross-window',
354
+ action: 'bind-current',
355
+ workspace: 'site:notebooklm',
356
+ matchDomain: 'notebooklm.google.com',
357
+ matchPathPrefix: '/notebook/',
358
+ },
359
+ 'site:notebooklm',
360
+ );
361
+
362
+ expect(result).toEqual({
363
+ id: 'bind-cross-window',
364
+ ok: true,
365
+ data: expect.objectContaining({
366
+ tabId: 2,
367
+ windowId: 2,
368
+ url: 'https://notebooklm.google.com/notebook/nb-other-window',
369
+ title: 'Notebook In Other Window',
370
+ }),
371
+ });
372
+ });
373
+
374
+ it('rejects bind-current when the active tab is not NotebookLM', async () => {
375
+ const { chrome } = createChromeMock();
376
+ vi.stubGlobal('chrome', chrome);
377
+
378
+ const mod = await import('./background');
379
+ const result = await mod.__test__.handleBindCurrent(
380
+ {
381
+ id: 'bind-miss',
382
+ action: 'bind-current',
383
+ workspace: 'site:notebooklm',
384
+ matchDomain: 'notebooklm.google.com',
385
+ matchPathPrefix: '/notebook/',
386
+ },
387
+ 'site:notebooklm',
388
+ );
389
+
390
+ expect(result).toEqual({
391
+ id: 'bind-miss',
392
+ ok: false,
393
+ error: 'No visible tab matching notebooklm.google.com /notebook/',
394
+ });
395
+ });
196
396
  });
@@ -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)`);
@@ -173,6 +180,8 @@ async function getAutomationWindow(workspace: string): Promise<number> {
173
180
  windowId: win.id!,
174
181
  idleTimer: null,
175
182
  idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
183
+ owned: true,
184
+ preferredTabId: null,
176
185
  };
177
186
  automationSessions.set(workspace, session);
178
187
  console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
@@ -254,6 +263,8 @@ async function handleCommand(cmd: Command): Promise<Result> {
254
263
  return await handleSessions(cmd);
255
264
  case 'set-file-input':
256
265
  return await handleSetFileInput(cmd, workspace);
266
+ case 'bind-current':
267
+ return await handleBindCurrent(cmd, workspace);
257
268
  default:
258
269
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
259
270
  }
@@ -301,6 +312,89 @@ function isTargetUrl(currentUrl: string | undefined, targetUrl: string): boolean
301
312
  return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
302
313
  }
303
314
 
315
+ function matchesDomain(url: string | undefined, domain: string): boolean {
316
+ if (!url) return false;
317
+ try {
318
+ const parsed = new URL(url);
319
+ return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+
325
+ function matchesBindCriteria(tab: chrome.tabs.Tab, cmd: Command): boolean {
326
+ if (!tab.id || !isDebuggableUrl(tab.url)) return false;
327
+ if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false;
328
+ if (cmd.matchPathPrefix) {
329
+ try {
330
+ const parsed = new URL(tab.url!);
331
+ if (!parsed.pathname.startsWith(cmd.matchPathPrefix)) return false;
332
+ } catch {
333
+ return false;
334
+ }
335
+ }
336
+ return true;
337
+ }
338
+
339
+ function isNotebooklmWorkspace(workspace: string): boolean {
340
+ return workspace === 'site:notebooklm';
341
+ }
342
+
343
+ function classifyNotebooklmUrl(url?: string): 'notebook' | 'home' | 'other' {
344
+ if (!url) return 'other';
345
+ try {
346
+ const parsed = new URL(url);
347
+ if (parsed.hostname !== 'notebooklm.google.com') return 'other';
348
+ return parsed.pathname.startsWith('/notebook/') ? 'notebook' : 'home';
349
+ } catch {
350
+ return 'other';
351
+ }
352
+ }
353
+
354
+ function scoreWorkspaceTab(workspace: string, tab: chrome.tabs.Tab): number {
355
+ if (!tab.id || !isDebuggableUrl(tab.url)) return -1;
356
+ if (isNotebooklmWorkspace(workspace)) {
357
+ const kind = classifyNotebooklmUrl(tab.url);
358
+ if (kind === 'other') return -1;
359
+ if (kind === 'notebook') return tab.active ? 400 : 300;
360
+ return tab.active ? 200 : 100;
361
+ }
362
+ return -1;
363
+ }
364
+
365
+ function setWorkspaceSession(workspace: string, session: Omit<AutomationSession, 'idleTimer' | 'idleDeadlineAt'>): void {
366
+ const existing = automationSessions.get(workspace);
367
+ if (existing?.idleTimer) clearTimeout(existing.idleTimer);
368
+ automationSessions.set(workspace, {
369
+ ...session,
370
+ idleTimer: null,
371
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
372
+ });
373
+ }
374
+
375
+ async function maybeBindWorkspaceToExistingTab(workspace: string): Promise<number | null> {
376
+ if (!isNotebooklmWorkspace(workspace)) return null;
377
+ const tabs = await chrome.tabs.query({});
378
+ let bestTab: chrome.tabs.Tab | null = null;
379
+ let bestScore = -1;
380
+ for (const tab of tabs) {
381
+ const score = scoreWorkspaceTab(workspace, tab);
382
+ if (score > bestScore) {
383
+ bestScore = score;
384
+ bestTab = tab;
385
+ }
386
+ }
387
+ if (!bestTab?.id || bestScore < 0) return null;
388
+ setWorkspaceSession(workspace, {
389
+ windowId: bestTab.windowId,
390
+ owned: false,
391
+ preferredTabId: bestTab.id,
392
+ });
393
+ console.log(`[opencli] Workspace ${workspace} bound to existing tab ${bestTab.id} in window ${bestTab.windowId}`);
394
+ resetWindowIdleTimer(workspace);
395
+ return bestTab.id;
396
+ }
397
+
304
398
  /**
305
399
  * Resolve target tab in the automation window.
306
400
  * If explicit tabId is given, use that directly.
@@ -314,9 +408,12 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
314
408
  try {
315
409
  const tab = await chrome.tabs.get(tabId);
316
410
  const session = automationSessions.get(workspace);
317
- if (isDebuggableUrl(tab.url) && session && tab.windowId === session.windowId) return tabId;
318
- if (session && tab.windowId !== session.windowId) {
319
- console.warn(`[opencli] Tab ${tabId} belongs to window ${tab.windowId}, not automation window ${session.windowId}, re-resolving`);
411
+ const matchesSession = session
412
+ ? (session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId)
413
+ : false;
414
+ if (isDebuggableUrl(tab.url) && matchesSession) return tabId;
415
+ if (session && !matchesSession) {
416
+ console.warn(`[opencli] Tab ${tabId} is not bound to workspace ${workspace}, re-resolving`);
320
417
  } else if (!isDebuggableUrl(tab.url)) {
321
418
  // Tab exists but URL is not debuggable — fall through to auto-resolve
322
419
  console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
@@ -327,6 +424,19 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
327
424
  }
328
425
  }
329
426
 
427
+ const adoptedTabId = await maybeBindWorkspaceToExistingTab(workspace);
428
+ if (adoptedTabId !== null) return adoptedTabId;
429
+
430
+ const existingSession = automationSessions.get(workspace);
431
+ if (existingSession?.preferredTabId !== null) {
432
+ try {
433
+ const preferredTab = await chrome.tabs.get(existingSession.preferredTabId);
434
+ if (isDebuggableUrl(preferredTab.url)) return preferredTab.id!;
435
+ } catch {
436
+ automationSessions.delete(workspace);
437
+ }
438
+ }
439
+
330
440
  // Get (or create) the automation window
331
441
  const windowId = await getAutomationWindow(workspace);
332
442
 
@@ -359,6 +469,14 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
359
469
  async function listAutomationTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
360
470
  const session = automationSessions.get(workspace);
361
471
  if (!session) return [];
472
+ if (session.preferredTabId !== null) {
473
+ try {
474
+ return [await chrome.tabs.get(session.preferredTabId)];
475
+ } catch {
476
+ automationSessions.delete(workspace);
477
+ return [];
478
+ }
479
+ }
362
480
  try {
363
481
  return await chrome.tabs.query({ windowId: session.windowId });
364
482
  } catch {
@@ -570,10 +688,12 @@ async function handleScreenshot(cmd: Command, workspace: string): Promise<Result
570
688
  async function handleCloseWindow(cmd: Command, workspace: string): Promise<Result> {
571
689
  const session = automationSessions.get(workspace);
572
690
  if (session) {
573
- try {
574
- await chrome.windows.remove(session.windowId);
575
- } catch {
576
- // Window may already be closed
691
+ if (session.owned) {
692
+ try {
693
+ await chrome.windows.remove(session.windowId);
694
+ } catch {
695
+ // Window may already be closed
696
+ }
577
697
  }
578
698
  if (session.idleTimer) clearTimeout(session.idleTimer);
579
699
  automationSessions.delete(workspace);
@@ -605,11 +725,52 @@ async function handleSessions(cmd: Command): Promise<Result> {
605
725
  return { id: cmd.id, ok: true, data };
606
726
  }
607
727
 
728
+ async function handleBindCurrent(cmd: Command, workspace: string): Promise<Result> {
729
+ const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
730
+ const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
731
+ const allTabs = await chrome.tabs.query({});
732
+ const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd))
733
+ ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd))
734
+ ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd));
735
+ if (!boundTab?.id) {
736
+ return {
737
+ id: cmd.id,
738
+ ok: false,
739
+ error: cmd.matchDomain || cmd.matchPathPrefix
740
+ ? `No visible tab matching ${cmd.matchDomain ?? 'domain'}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ''}`
741
+ : 'No active debuggable tab found',
742
+ };
743
+ }
744
+
745
+ setWorkspaceSession(workspace, {
746
+ windowId: boundTab.windowId,
747
+ owned: false,
748
+ preferredTabId: boundTab.id,
749
+ });
750
+ resetWindowIdleTimer(workspace);
751
+ console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
752
+ return {
753
+ id: cmd.id,
754
+ ok: true,
755
+ data: {
756
+ tabId: boundTab.id,
757
+ windowId: boundTab.windowId,
758
+ url: boundTab.url,
759
+ title: boundTab.title,
760
+ workspace,
761
+ },
762
+ };
763
+ }
764
+
608
765
  export const __test__ = {
609
766
  handleNavigate,
610
767
  isTargetUrl,
611
768
  handleTabs,
612
769
  handleSessions,
770
+ handleBindCurrent,
771
+ resolveTabId,
772
+ resetWindowIdleTimer,
773
+ getSession: (workspace: string = 'default') => automationSessions.get(workspace) ?? null,
613
774
  getAutomationWindowId: (workspace: string = 'default') => automationSessions.get(workspace)?.windowId ?? null,
614
775
  setAutomationWindowId: (workspace: string, windowId: number | null) => {
615
776
  if (windowId === null) {
@@ -618,10 +779,13 @@ export const __test__ = {
618
779
  automationSessions.delete(workspace);
619
780
  return;
620
781
  }
621
- automationSessions.set(workspace, {
782
+ setWorkspaceSession(workspace, {
622
783
  windowId,
623
- idleTimer: null,
624
- idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
784
+ owned: true,
785
+ preferredTabId: null,
625
786
  });
626
787
  },
788
+ setSession: (workspace: string, session: { windowId: number; owned: boolean; preferredTabId: number | null }) => {
789
+ setWorkspaceSession(workspace, session);
790
+ },
627
791
  };
@@ -71,6 +71,18 @@ async function ensureAttached(tabId: number): Promise<void> {
71
71
  } catch {
72
72
  // Some pages may not need explicit enable
73
73
  }
74
+
75
+ // Disable breakpoints so that `debugger;` statements in page code don't
76
+ // pause execution. Anti-bot scripts use `debugger;` traps to detect CDP —
77
+ // they measure the time gap caused by the pause. Deactivating breakpoints
78
+ // makes the engine skip `debugger;` entirely, neutralising the timing
79
+ // side-channel without patching page JS.
80
+ try {
81
+ await chrome.debugger.sendCommand({ tabId }, 'Debugger.enable');
82
+ await chrome.debugger.sendCommand({ tabId }, 'Debugger.setBreakpointsActive', { active: false });
83
+ } catch {
84
+ // Non-fatal: best-effort hardening
85
+ }
74
86
  }
75
87
 
76
88
  export async function evaluate(tabId: number, expression: string): Promise<unknown> {
@@ -5,7 +5,7 @@
5
5
  * Everything else is just JS code sent via 'exec'.
6
6
  */
7
7
 
8
- export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input';
8
+ export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'bind-current';
9
9
 
10
10
  export interface Command {
11
11
  /** Unique request ID */
@@ -26,6 +26,10 @@ export interface Command {
26
26
  index?: number;
27
27
  /** Cookie domain filter */
28
28
  domain?: string;
29
+ /** Optional hostname/domain to require for current-tab binding */
30
+ matchDomain?: string;
31
+ /** Optional pathname prefix to require for current-tab binding */
32
+ matchPathPrefix?: string;
29
33
  /** Screenshot format: png (default) or jpeg */
30
34
  format?: 'png' | 'jpeg';
31
35
  /** JPEG quality (0-100), only for jpeg format */
@@ -58,7 +62,5 @@ export const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
58
62
 
59
63
  /** Base reconnect delay for extension WebSocket (ms) */
60
64
  export const WS_RECONNECT_BASE_DELAY = 2000;
61
- /** Max reconnect delay (ms) */
62
- export const WS_RECONNECT_MAX_DELAY = 60000;
63
- /** Idle timeout before daemon auto-exits (ms) */
64
- export const DAEMON_IDLE_TIMEOUT = 5 * 60 * 1000;
65
+ /** Max reconnect delay (ms) — kept short since daemon is long-lived */
66
+ export const WS_RECONNECT_MAX_DELAY = 5000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,6 +29,7 @@ import {
29
29
  waitForSelectorJs,
30
30
  } from './dom-helpers.js';
31
31
  import { isRecord, saveBase64ToFile } from '../utils.js';
32
+ import { getAllElectronApps } from '../electron-apps.js';
32
33
 
33
34
  export interface CDPTarget {
34
35
  type?: string;
@@ -56,11 +57,11 @@ export class CDPBridge implements IBrowserFactory {
56
57
  private _pending = new Map<number, { resolve: (val: unknown) => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }>();
57
58
  private _eventListeners = new Map<string, Set<(params: unknown) => void>>();
58
59
 
59
- async connect(opts?: { timeout?: number; workspace?: string }): Promise<IPage> {
60
+ async connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string }): Promise<IPage> {
60
61
  if (this._ws) throw new Error('CDPBridge is already connected. Call close() before reconnecting.');
61
62
 
62
- const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
63
- if (!endpoint) throw new Error('OPENCLI_CDP_ENDPOINT is not set');
63
+ const endpoint = opts?.cdpEndpoint ?? process.env.OPENCLI_CDP_ENDPOINT;
64
+ if (!endpoint) throw new Error('CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)');
64
65
 
65
66
  let wsUrl = endpoint;
66
67
  if (endpoint.startsWith('http')) {
@@ -326,7 +327,17 @@ class CDPPage implements IPage {
326
327
  }
327
328
 
328
329
  async getCurrentUrl(): Promise<string | null> {
329
- return this._lastUrl;
330
+ if (this._lastUrl) return this._lastUrl;
331
+ try {
332
+ const current = await this.evaluate('window.location.href');
333
+ if (typeof current === 'string' && current) {
334
+ this._lastUrl = current;
335
+ return current;
336
+ }
337
+ } catch {
338
+ // Best-effort: direct CDP sessions may not have a ready page yet.
339
+ }
340
+ return null;
330
341
  }
331
342
 
332
343
  async installInterceptor(pattern: string): Promise<void> {
@@ -404,19 +415,15 @@ function scoreCDPTarget(target: CDPTarget, preferredPattern?: RegExp): number {
404
415
  if (url === '' || url === 'about:blank') score -= 40;
405
416
 
406
417
  if (title && title !== 'devtools') score += 25;
407
- if (title.includes('antigravity')) score += 120;
408
- if (title.includes('codex')) score += 120;
409
- if (title.includes('cursor')) score += 120;
410
- if (title.includes('chatwise')) score += 120;
411
- if (title.includes('notion')) score += 120;
412
- if (title.includes('discord')) score += 120;
413
-
414
- if (url.includes('antigravity')) score += 100;
415
- if (url.includes('codex')) score += 100;
416
- if (url.includes('cursor')) score += 100;
417
- if (url.includes('chatwise')) score += 100;
418
- if (url.includes('notion')) score += 100;
419
- if (url.includes('discord')) score += 100;
418
+
419
+ // Boost score for known Electron app names from the registry (builtin + user-defined)
420
+ const appNames = Object.values(getAllElectronApps()).map(a => (a.displayName ?? a.processName).toLowerCase());
421
+ for (const name of appNames) {
422
+ if (title.includes(name)) { score += 120; break; }
423
+ }
424
+ for (const name of appNames) {
425
+ if (url.includes(name)) { score += 100; break; }
426
+ }
420
427
 
421
428
  return score;
422
429
  }
@@ -19,7 +19,7 @@ function generateId(): string {
19
19
 
20
20
  export interface DaemonCommand {
21
21
  id: string;
22
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input';
22
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'bind-current';
23
23
  tabId?: number;
24
24
  code?: string;
25
25
  workspace?: string;
@@ -27,6 +27,8 @@ export interface DaemonCommand {
27
27
  op?: string;
28
28
  index?: number;
29
29
  domain?: string;
30
+ matchDomain?: string;
31
+ matchPathPrefix?: string;
30
32
  format?: 'png' | 'jpeg';
31
33
  quality?: number;
32
34
  fullPage?: boolean;
@@ -145,3 +147,7 @@ export async function listSessions(): Promise<BrowserSessionInfo[]> {
145
147
  return Array.isArray(result) ? result : [];
146
148
  }
147
149
 
150
+ export async function bindCurrentTab(workspace: string, opts: { matchDomain?: string; matchPathPrefix?: string } = {}): Promise<unknown> {
151
+ return sendCommand('bind-current', { workspace, ...opts });
152
+ }
153
+
@@ -1,5 +1,19 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { waitForCaptureJs, waitForSelectorJs } from './dom-helpers.js';
2
+ import { autoScrollJs, waitForCaptureJs, waitForSelectorJs } from './dom-helpers.js';
3
+
4
+ describe('autoScrollJs', () => {
5
+ it('returns early without error when document.body is null', async () => {
6
+ const g = globalThis as any;
7
+ const origDoc = g.document;
8
+ g.document = { body: null, documentElement: {} };
9
+ g.window = g;
10
+ const code = autoScrollJs(3, 500);
11
+ // Should resolve without throwing
12
+ await expect(eval(code)).resolves.not.toThrow();
13
+ g.document = origDoc;
14
+ delete g.window;
15
+ });
16
+ });
3
17
 
4
18
  describe('waitForCaptureJs', () => {
5
19
  it('returns a non-empty string', () => {
@@ -109,6 +109,7 @@ export function scrollJs(direction: string, amount: number): string {
109
109
  export function autoScrollJs(times: number, delayMs: number): string {
110
110
  return `
111
111
  (async () => {
112
+ if (!document.body) return;
112
113
  for (let i = 0; i < ${times}; i++) {
113
114
  const lastHeight = document.body.scrollHeight;
114
115
  window.scrollTo(0, lastHeight);