@jackwener/opencli 1.5.6 → 1.5.8

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 (338) hide show
  1. package/CHANGELOG.md +34 -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/extension-manifest-regression.test.js +1 -0
  176. package/dist/idle-manager.d.ts +19 -0
  177. package/dist/idle-manager.js +54 -0
  178. package/dist/launcher.d.ts +36 -0
  179. package/dist/launcher.js +152 -0
  180. package/dist/launcher.test.d.ts +1 -0
  181. package/dist/launcher.test.js +57 -0
  182. package/dist/main.js +3 -3
  183. package/dist/registry.d.ts +1 -0
  184. package/dist/registry.js +31 -3
  185. package/dist/registry.test.js +13 -0
  186. package/dist/runtime.d.ts +5 -3
  187. package/dist/runtime.js +12 -5
  188. package/dist/serialization.d.ts +1 -0
  189. package/dist/serialization.js +3 -0
  190. package/dist/serialization.test.js +17 -1
  191. package/dist/tui.d.ts +7 -0
  192. package/dist/tui.js +52 -0
  193. package/dist/tui.test.d.ts +1 -0
  194. package/dist/tui.test.js +19 -0
  195. package/dist/weixin-download.test.js +14 -0
  196. package/docs/.vitepress/config.mts +1 -0
  197. package/docs/adapters/browser/notebooklm.md +69 -0
  198. package/docs/adapters/browser/xiaohongshu.md +19 -10
  199. package/docs/adapters/index.md +67 -66
  200. package/docs/guide/browser-bridge.md +12 -0
  201. package/docs/guide/troubleshooting.md +9 -4
  202. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  203. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  204. package/docs/zh/guide/browser-bridge.md +12 -0
  205. package/extension/dist/background.js +250 -11
  206. package/extension/manifest.json +2 -1
  207. package/extension/src/background.test.ts +202 -2
  208. package/extension/src/background.ts +175 -10
  209. package/extension/src/cdp.test.ts +75 -0
  210. package/extension/src/cdp.ts +89 -3
  211. package/extension/src/protocol.ts +7 -5
  212. package/package.json +1 -1
  213. package/src/browser/cdp.ts +24 -17
  214. package/src/browser/daemon-client.ts +7 -1
  215. package/src/browser/dom-helpers.test.ts +15 -1
  216. package/src/browser/dom-helpers.ts +1 -0
  217. package/src/browser/mcp.ts +18 -13
  218. package/src/browser/page.test.ts +58 -0
  219. package/src/browser/page.ts +18 -2
  220. package/src/browser/stealth.test.ts +153 -0
  221. package/src/browser/stealth.ts +198 -0
  222. package/src/browser.test.ts +1 -1
  223. package/src/build-manifest.test.ts +2 -0
  224. package/src/build-manifest.ts +6 -1
  225. package/src/cli.ts +21 -3
  226. package/src/clis/antigravity/SKILL.md +3 -12
  227. package/src/clis/antigravity/serve.ts +5 -10
  228. package/src/clis/bilibili/subtitle.test.ts +60 -0
  229. package/src/clis/bilibili/subtitle.ts +4 -0
  230. package/src/clis/chatwise/ask.ts +0 -2
  231. package/src/clis/chatwise/export.ts +0 -2
  232. package/src/clis/chatwise/history.ts +0 -2
  233. package/src/clis/chatwise/model.ts +0 -2
  234. package/src/clis/chatwise/new.ts +1 -2
  235. package/src/clis/chatwise/read.ts +0 -2
  236. package/src/clis/chatwise/screenshot.ts +1 -2
  237. package/src/clis/chatwise/send.ts +0 -2
  238. package/src/clis/chatwise/status.ts +1 -2
  239. package/src/clis/ctrip/search.test.ts +73 -0
  240. package/src/clis/ctrip/search.ts +97 -47
  241. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  242. package/src/clis/douyin/_shared/sts2.ts +11 -3
  243. package/src/clis/douyin/activities.test.ts +41 -1
  244. package/src/clis/douyin/activities.ts +12 -3
  245. package/src/clis/douyin/collections.test.ts +35 -2
  246. package/src/clis/douyin/collections.ts +1 -1
  247. package/src/clis/douyin/draft.test.ts +444 -2
  248. package/src/clis/douyin/draft.ts +382 -218
  249. package/src/clis/douyin/hashtag.test.ts +42 -2
  250. package/src/clis/douyin/hashtag.ts +11 -3
  251. package/src/clis/douyin/profile.test.ts +43 -1
  252. package/src/clis/douyin/profile.ts +9 -2
  253. package/src/clis/douyin/videos.test.ts +52 -2
  254. package/src/clis/douyin/videos.ts +49 -15
  255. package/src/clis/facebook/search.test.ts +70 -0
  256. package/src/clis/facebook/search.yaml +4 -3
  257. package/src/clis/instagram/download.test.ts +159 -0
  258. package/src/clis/instagram/download.ts +286 -0
  259. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  260. package/src/clis/notebooklm/bind-current.ts +36 -0
  261. package/src/clis/notebooklm/binding.test.ts +53 -0
  262. package/src/clis/notebooklm/compat.test.ts +19 -0
  263. package/src/clis/notebooklm/current.ts +38 -0
  264. package/src/clis/notebooklm/get.ts +53 -0
  265. package/src/clis/notebooklm/history.test.ts +70 -0
  266. package/src/clis/notebooklm/history.ts +36 -0
  267. package/src/clis/notebooklm/list.ts +40 -0
  268. package/src/clis/notebooklm/note-list.test.ts +64 -0
  269. package/src/clis/notebooklm/note-list.ts +42 -0
  270. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  271. package/src/clis/notebooklm/notes-get.ts +67 -0
  272. package/src/clis/notebooklm/rpc.test.ts +126 -0
  273. package/src/clis/notebooklm/rpc.ts +286 -0
  274. package/src/clis/notebooklm/shared.ts +98 -0
  275. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  276. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  277. package/src/clis/notebooklm/source-get.test.ts +100 -0
  278. package/src/clis/notebooklm/source-get.ts +60 -0
  279. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  280. package/src/clis/notebooklm/source-guide.ts +69 -0
  281. package/src/clis/notebooklm/source-list.ts +45 -0
  282. package/src/clis/notebooklm/status.ts +34 -0
  283. package/src/clis/notebooklm/summary.test.ts +94 -0
  284. package/src/clis/notebooklm/summary.ts +45 -0
  285. package/src/clis/notebooklm/utils.test.ts +446 -0
  286. package/src/clis/notebooklm/utils.ts +893 -0
  287. package/src/clis/substack/utils.test.ts +54 -0
  288. package/src/clis/substack/utils.ts +10 -2
  289. package/src/clis/v2ex/hot.yaml +4 -1
  290. package/src/clis/v2ex/latest.yaml +4 -1
  291. package/src/clis/v2ex/topic.yaml +6 -1
  292. package/src/clis/weixin/download.ts +95 -6
  293. package/src/clis/weread/book.ts +142 -2
  294. package/src/clis/weread/commands.test.ts +314 -154
  295. package/src/clis/weread/utils.ts +33 -4
  296. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  297. package/src/clis/xiaohongshu/comments.ts +76 -17
  298. package/src/clis/xiaohongshu/download.test.ts +96 -0
  299. package/src/clis/xiaohongshu/download.ts +83 -22
  300. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  301. package/src/clis/xiaohongshu/note.test.ts +164 -0
  302. package/src/clis/xiaohongshu/note.ts +86 -0
  303. package/src/clis/xiaohongshu/search.test.ts +11 -4
  304. package/src/clis/xiaohongshu/search.ts +13 -0
  305. package/src/clis/youtube/search.ts +57 -17
  306. package/src/clis/zhihu/question.test.ts +71 -0
  307. package/src/clis/zhihu/question.ts +27 -15
  308. package/src/commanderAdapter.test.ts +30 -0
  309. package/src/commanderAdapter.ts +7 -0
  310. package/src/commands/daemon.test.ts +238 -0
  311. package/src/commands/daemon.ts +135 -0
  312. package/src/completion.ts +2 -1
  313. package/src/constants.ts +3 -0
  314. package/src/daemon.test.ts +88 -0
  315. package/src/daemon.ts +26 -14
  316. package/src/discovery.ts +52 -2
  317. package/src/electron-apps.test.ts +50 -0
  318. package/src/electron-apps.ts +89 -0
  319. package/src/engine.test.ts +45 -9
  320. package/src/execution.ts +24 -19
  321. package/src/extension-manifest-regression.test.ts +1 -0
  322. package/src/idle-manager.ts +60 -0
  323. package/src/launcher.test.ts +67 -0
  324. package/src/launcher.ts +185 -0
  325. package/src/main.ts +3 -2
  326. package/src/registry.test.ts +15 -0
  327. package/src/registry.ts +32 -3
  328. package/src/runtime.ts +13 -7
  329. package/src/serialization.test.ts +19 -1
  330. package/src/serialization.ts +2 -0
  331. package/src/tui.test.ts +23 -0
  332. package/src/tui.ts +65 -0
  333. package/src/weixin-download.test.ts +27 -0
  334. package/tests/e2e/browser-public-extended.test.ts +6 -2
  335. package/chatwise-opencli.ps1 +0 -82
  336. package/dist/clis/chatwise/shared.d.ts +0 -2
  337. package/dist/clis/chatwise/shared.js +0 -6
  338. package/src/clis/chatwise/shared.ts +0 -8
@@ -0,0 +1,208 @@
1
+ # Daemon Lifecycle Redesign
2
+
3
+ ## Problem
4
+
5
+ OpenCLI's daemon auto-exits after 5 minutes of idle time. During typical development
6
+ cycles (write code → test → modify → test again), coding intervals frequently exceed
7
+ 5 minutes. Each restart incurs 2-4 seconds of overhead (process spawn + Extension
8
+ WebSocket reconnection), creating a noticeable and frustrating delay.
9
+
10
+ The current design treats the daemon as a disposable process, but the actual cost
11
+ profile doesn't justify this:
12
+
13
+ | Cost of staying alive | Cost of restarting |
14
+ |-----------------------|--------------------|
15
+ | ~12 MB memory, 0% CPU | 2-4 seconds delay per restart |
16
+
17
+ The restart cost far outweighs the idle cost.
18
+
19
+ ## Solution
20
+
21
+ Replace the aggressive 5-minute fixed timeout with a long-lived daemon model. The
22
+ daemon stays running for hours, exits only when truly abandoned, and reconnects to
23
+ the Chrome Extension faster when needed.
24
+
25
+ Four changes:
26
+
27
+ 1. Extend idle timeout from 5 minutes to 4 hours (configurable)
28
+ 2. Require dual idle condition: both no CLI requests AND no Extension connection
29
+ 3. Reduce Extension WebSocket reconnect backoff cap from 60s to 5s
30
+ 4. Add `opencli daemon status/stop/restart` commands
31
+
32
+ ## Design
33
+
34
+ ### Timeout Strategy
35
+
36
+ **Current behavior:** A single idle timer resets on each HTTP request. After 5
37
+ minutes without a request, the daemon calls `process.exit(0)`.
38
+
39
+ **New behavior:** The daemon tracks two activity signals independently:
40
+
41
+ - **CLI activity:** timestamp of the last HTTP request from any CLI invocation
42
+ - **Extension activity:** whether a WebSocket connection from the Chrome Extension
43
+ is currently open
44
+
45
+ The exit countdown starts only when BOTH conditions are met simultaneously:
46
+
47
+ - No CLI request for `IDLE_TIMEOUT` duration
48
+ - No Extension WebSocket connection
49
+
50
+ If either signal is active, the daemon stays alive. This means:
51
+
52
+ - A connected Extension keeps the daemon alive indefinitely (user has Chrome open,
53
+ likely still working)
54
+ - Recent CLI activity keeps the daemon alive even if Extension temporarily
55
+ disconnects (Chrome restarting, Extension updating)
56
+
57
+ **Timeout value:** 4 hours by default, configurable via `OPENCLI_DAEMON_TIMEOUT`
58
+ environment variable. Value in milliseconds. Set to `0` to disable timeout entirely.
59
+
60
+ ```typescript
61
+ const DEFAULT_IDLE_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
62
+ const IDLE_TIMEOUT = Number(process.env.OPENCLI_DAEMON_TIMEOUT ?? DEFAULT_IDLE_TIMEOUT);
63
+ ```
64
+
65
+ **Timer implementation:**
66
+
67
+ ```
68
+ resetIdleTimer():
69
+ clear existing timer
70
+ if Extension is connected:
71
+ do not start timer (Extension connection keeps daemon alive)
72
+ return
73
+ start timer with IDLE_TIMEOUT duration
74
+ on timeout: process.exit(0)
75
+
76
+ On CLI HTTP request:
77
+ update lastRequestTime
78
+ resetIdleTimer()
79
+
80
+ On Extension WebSocket connect:
81
+ clear timer (Extension keeps daemon alive)
82
+
83
+ On Extension WebSocket disconnect:
84
+ elapsed = now - lastRequestTime
85
+ if elapsed >= IDLE_TIMEOUT:
86
+ process.exit(0) // CLI has been idle long enough already
87
+ else:
88
+ start timer with (IDLE_TIMEOUT - elapsed) // count remaining time
89
+ ```
90
+
91
+ ### Extension Fast Reconnect
92
+
93
+ **Current behavior:** When the Extension loses its WebSocket connection to the
94
+ daemon, it reconnects with exponential backoff: 2s → 4s → 8s → 16s → 32s → 60s
95
+ (capped). In the worst case, the Extension waits up to 60 seconds before attempting
96
+ reconnection.
97
+
98
+ **New behavior:** Cap the backoff at 5 seconds instead of 60 seconds.
99
+
100
+ ```typescript
101
+ // extension/src/background.ts
102
+ const WS_RECONNECT_MAX_DELAY = 5000; // was 60000
103
+ ```
104
+
105
+ Rationale: with a 4-hour daemon timeout, the daemon is almost always running. Long
106
+ backoff intervals are unnecessary and only increase reconnection latency. A 5-second
107
+ cap means the Extension reconnects within 5 seconds of the daemon becoming available.
108
+
109
+ ### Daemon Management Commands
110
+
111
+ Add three new CLI commands for daemon lifecycle management:
112
+
113
+ **`opencli daemon status`**
114
+
115
+ Queries the daemon's `/status` endpoint (new) and displays:
116
+
117
+ ```
118
+ Daemon: running (PID 12345)
119
+ Uptime: 2h 15m
120
+ Extension: connected
121
+ Last CLI request: 8 min ago
122
+ Memory: 12.3 MB
123
+ Port: 19825
124
+ ```
125
+
126
+ If daemon is not running:
127
+
128
+ ```
129
+ Daemon: not running
130
+ ```
131
+
132
+ **`opencli daemon stop`**
133
+
134
+ Sends a `POST /shutdown` request to the daemon, which triggers a graceful shutdown:
135
+ reject pending requests with a shutdown message, close WebSocket connections, close
136
+ HTTP server, then exit.
137
+
138
+ **`opencli daemon restart`**
139
+
140
+ Equivalent to `stop` followed by spawning a new daemon. Useful when the daemon gets
141
+ into a bad state.
142
+
143
+ **Daemon-side endpoints:**
144
+
145
+ - `GET /status` — returns JSON with PID, uptime, extension connection state, last
146
+ request time, memory usage
147
+ - `POST /shutdown` — initiates graceful shutdown
148
+
149
+ Both endpoints require the same `X-OpenCLI` header as existing endpoints for CSRF
150
+ protection.
151
+
152
+ ### CLI Connection Experience
153
+
154
+ **Current behavior:** When daemon is running but Extension is not connected, the CLI
155
+ silently polls every 300ms and eventually times out with a generic error.
156
+
157
+ **New behavior:** Show a progress indicator and actionable message:
158
+
159
+ ```
160
+ ⏳ Waiting for Chrome extension to connect...
161
+ Make sure Chrome is open and the OpenCLI extension is enabled.
162
+ ```
163
+
164
+ Poll interval reduced from 300ms to 200ms for slightly faster detection.
165
+
166
+ If the daemon is not running at all (connection refused), the CLI spawns it as before
167
+ and shows:
168
+
169
+ ```
170
+ ⏳ Starting daemon...
171
+ ```
172
+
173
+ ## Files Changed
174
+
175
+ | File | Change | Estimated LOC |
176
+ |------|--------|---------------|
177
+ | `src/daemon.ts` | Dual-condition idle timeout, `/status` endpoint, `/shutdown` endpoint | ~40 |
178
+ | `extension/src/background.ts` | `WS_RECONNECT_MAX_DELAY` 60000 → 5000 | 1 |
179
+ | `src/browser/daemon-client.ts` | Better connection-waiting UX, 200ms poll interval | ~20 |
180
+ | `src/commands/daemon.ts` (new) | `status`, `stop`, `restart` subcommands | ~80 |
181
+ | `src/constants.ts` | `DEFAULT_IDLE_TIMEOUT` constant | 2 |
182
+
183
+ **Total: ~143 lines of new/changed code.**
184
+
185
+ ## Backward Compatibility
186
+
187
+ - No breaking changes to CLI commands or Extension protocol
188
+ - Existing `OPENCLI_DAEMON_PORT` environment variable continues to work
189
+ - The only observable behavior change: daemon stays alive longer
190
+ - New `daemon` subcommands are additive
191
+
192
+ ## Testing
193
+
194
+ - Unit test: idle timer starts only when both CLI and Extension are idle
195
+ - Unit test: idle timer is cleared when Extension connects
196
+ - Unit test: `/status` returns correct state
197
+ - Unit test: `/shutdown` triggers graceful exit
198
+ - Integration test: daemon survives 10+ minutes without CLI requests while Extension
199
+ is connected
200
+ - Integration test: daemon exits after configured timeout when fully idle
201
+ - Integration test: `opencli daemon status/stop/restart` work correctly
202
+
203
+ ## Out of Scope
204
+
205
+ - OS-level daemon management (launchd/systemd) — can be added later if needed
206
+ - Daemon auto-update mechanism
207
+ - Multi-daemon coordination
208
+ - Persistent daemon state across restarts
@@ -22,3 +22,15 @@ OpenCLI 通过轻量级 **Browser Bridge** Chrome 扩展 + 微守护进程连接
22
22
  ```bash
23
23
  opencli doctor # 检查扩展 + 守护进程连接
24
24
  ```
25
+
26
+ ## Daemon 生命周期
27
+
28
+ Daemon 在首次运行浏览器命令时自动启动,默认保持 **4 小时**。仅当 CLI 空闲超时**且** Chrome 扩展未连接时才会退出。
29
+
30
+ ```bash
31
+ opencli daemon status # 查看 daemon 状态(PID、运行时长、扩展连接、内存)
32
+ opencli daemon stop # 优雅关停
33
+ opencli daemon restart # 重启
34
+ ```
35
+
36
+ 通过 `OPENCLI_DAEMON_TIMEOUT` 环境变量覆盖超时时间(毫秒)。设为 `0` 则永不超时。
@@ -3,14 +3,67 @@ const DAEMON_HOST = "localhost";
3
3
  const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
4
4
  const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
5
5
  const WS_RECONNECT_BASE_DELAY = 2e3;
6
- const WS_RECONNECT_MAX_DELAY = 6e4;
6
+ const WS_RECONNECT_MAX_DELAY = 5e3;
7
7
 
8
8
  const attached = /* @__PURE__ */ new Set();
9
9
  const BLANK_PAGE$1 = "data:text/html,<html></html>";
10
+ const FOREIGN_EXTENSION_URL_PREFIX = "chrome-extension://";
11
+ const ATTACH_RECOVERY_DELAY_MS = 120;
10
12
  function isDebuggableUrl$1(url) {
11
13
  if (!url) return true;
12
14
  return url.startsWith("http://") || url.startsWith("https://") || url === BLANK_PAGE$1;
13
15
  }
16
+ async function removeForeignExtensionEmbeds(tabId) {
17
+ const tab = await chrome.tabs.get(tabId);
18
+ if (!tab.url || !tab.url.startsWith("http://") && !tab.url.startsWith("https://")) {
19
+ return { removed: 0 };
20
+ }
21
+ if (!chrome.scripting?.executeScript) return { removed: 0 };
22
+ try {
23
+ const [result] = await chrome.scripting.executeScript({
24
+ target: { tabId },
25
+ args: [`${FOREIGN_EXTENSION_URL_PREFIX}${chrome.runtime.id}/`],
26
+ func: (ownExtensionPrefix) => {
27
+ const extensionPrefix = "chrome-extension://";
28
+ const selectors = ["iframe", "frame", "embed", "object"];
29
+ const visitedRoots = /* @__PURE__ */ new Set();
30
+ const roots = [document];
31
+ let removed = 0;
32
+ while (roots.length > 0) {
33
+ const root = roots.pop();
34
+ if (!root || visitedRoots.has(root)) continue;
35
+ visitedRoots.add(root);
36
+ for (const selector of selectors) {
37
+ const nodes = root.querySelectorAll(selector);
38
+ for (const node of nodes) {
39
+ const src = node.getAttribute("src") || node.getAttribute("data") || "";
40
+ if (!src.startsWith(extensionPrefix) || src.startsWith(ownExtensionPrefix)) continue;
41
+ node.remove();
42
+ removed++;
43
+ }
44
+ }
45
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
46
+ let current = walker.nextNode();
47
+ while (current) {
48
+ const element = current;
49
+ if (element.shadowRoot) roots.push(element.shadowRoot);
50
+ current = walker.nextNode();
51
+ }
52
+ }
53
+ return { removed };
54
+ }
55
+ });
56
+ return result?.result ?? { removed: 0 };
57
+ } catch {
58
+ return { removed: 0 };
59
+ }
60
+ }
61
+ function delay(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+ async function tryAttach(tabId) {
65
+ await chrome.debugger.attach({ tabId }, "1.3");
66
+ }
14
67
  async function ensureAttached(tabId) {
15
68
  try {
16
69
  const tab = await chrome.tabs.get(tabId);
@@ -35,17 +88,28 @@ async function ensureAttached(tabId) {
35
88
  }
36
89
  }
37
90
  try {
38
- await chrome.debugger.attach({ tabId }, "1.3");
91
+ await tryAttach(tabId);
39
92
  } catch (e) {
40
93
  const msg = e instanceof Error ? e.message : String(e);
41
94
  const hint = msg.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : "";
42
- if (msg.includes("Another debugger is already attached")) {
95
+ if (msg.includes("chrome-extension://")) {
96
+ const recoveryCleanup = await removeForeignExtensionEmbeds(tabId);
97
+ if (recoveryCleanup.removed > 0) {
98
+ console.warn(`[opencli] Removed ${recoveryCleanup.removed} foreign extension frame(s) after attach failure on tab ${tabId}`);
99
+ }
100
+ await delay(ATTACH_RECOVERY_DELAY_MS);
101
+ try {
102
+ await tryAttach(tabId);
103
+ } catch {
104
+ throw new Error(`attach failed: ${msg}${hint}`);
105
+ }
106
+ } else if (msg.includes("Another debugger is already attached")) {
43
107
  try {
44
108
  await chrome.debugger.detach({ tabId });
45
109
  } catch {
46
110
  }
47
111
  try {
48
- await chrome.debugger.attach({ tabId }, "1.3");
112
+ await tryAttach(tabId);
49
113
  } catch {
50
114
  throw new Error(`attach failed: ${msg}${hint}`);
51
115
  }
@@ -58,6 +122,11 @@ async function ensureAttached(tabId) {
58
122
  await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
59
123
  } catch {
60
124
  }
125
+ try {
126
+ await chrome.debugger.sendCommand({ tabId }, "Debugger.enable");
127
+ await chrome.debugger.sendCommand({ tabId }, "Debugger.setBreakpointsActive", { active: false });
128
+ } catch {
129
+ }
61
130
  }
62
131
  async function evaluate(tabId, expression) {
63
132
  await ensureAttached(tabId);
@@ -102,6 +171,23 @@ async function screenshot(tabId, options = {}) {
102
171
  }
103
172
  }
104
173
  }
174
+ async function setFileInputFiles(tabId, files, selector) {
175
+ await ensureAttached(tabId);
176
+ await chrome.debugger.sendCommand({ tabId }, "DOM.enable");
177
+ const doc = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument");
178
+ const query = selector || 'input[type="file"]';
179
+ const result = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", {
180
+ nodeId: doc.root.nodeId,
181
+ selector: query
182
+ });
183
+ if (!result.nodeId) {
184
+ throw new Error(`No element found matching selector: ${query}`);
185
+ }
186
+ await chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", {
187
+ files,
188
+ nodeId: result.nodeId
189
+ });
190
+ }
105
191
  async function detach(tabId) {
106
192
  if (!attached.has(tabId)) return;
107
193
  attached.delete(tabId);
@@ -215,6 +301,11 @@ function resetWindowIdleTimer(workspace) {
215
301
  session.idleTimer = setTimeout(async () => {
216
302
  const current = automationSessions.get(workspace);
217
303
  if (!current) return;
304
+ if (!current.owned) {
305
+ console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`);
306
+ automationSessions.delete(workspace);
307
+ return;
308
+ }
218
309
  try {
219
310
  await chrome.windows.remove(current.windowId);
220
311
  console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
@@ -243,7 +334,9 @@ async function getAutomationWindow(workspace) {
243
334
  const session = {
244
335
  windowId: win.id,
245
336
  idleTimer: null,
246
- idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
337
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
338
+ owned: true,
339
+ preferredTabId: null
247
340
  };
248
341
  automationSessions.set(workspace, session);
249
342
  console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
@@ -306,6 +399,10 @@ async function handleCommand(cmd) {
306
399
  return await handleCloseWindow(cmd, workspace);
307
400
  case "sessions":
308
401
  return await handleSessions(cmd);
402
+ case "set-file-input":
403
+ return await handleSetFileInput(cmd, workspace);
404
+ case "bind-current":
405
+ return await handleBindCurrent(cmd, workspace);
309
406
  default:
310
407
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
311
408
  }
@@ -341,14 +438,91 @@ function normalizeUrlForComparison(url) {
341
438
  function isTargetUrl(currentUrl, targetUrl) {
342
439
  return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
343
440
  }
441
+ function matchesDomain(url, domain) {
442
+ if (!url) return false;
443
+ try {
444
+ const parsed = new URL(url);
445
+ return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
446
+ } catch {
447
+ return false;
448
+ }
449
+ }
450
+ function matchesBindCriteria(tab, cmd) {
451
+ if (!tab.id || !isDebuggableUrl(tab.url)) return false;
452
+ if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false;
453
+ if (cmd.matchPathPrefix) {
454
+ try {
455
+ const parsed = new URL(tab.url);
456
+ if (!parsed.pathname.startsWith(cmd.matchPathPrefix)) return false;
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+ return true;
462
+ }
463
+ function isNotebooklmWorkspace(workspace) {
464
+ return workspace === "site:notebooklm";
465
+ }
466
+ function classifyNotebooklmUrl(url) {
467
+ if (!url) return "other";
468
+ try {
469
+ const parsed = new URL(url);
470
+ if (parsed.hostname !== "notebooklm.google.com") return "other";
471
+ return parsed.pathname.startsWith("/notebook/") ? "notebook" : "home";
472
+ } catch {
473
+ return "other";
474
+ }
475
+ }
476
+ function scoreWorkspaceTab(workspace, tab) {
477
+ if (!tab.id || !isDebuggableUrl(tab.url)) return -1;
478
+ if (isNotebooklmWorkspace(workspace)) {
479
+ const kind = classifyNotebooklmUrl(tab.url);
480
+ if (kind === "other") return -1;
481
+ if (kind === "notebook") return tab.active ? 400 : 300;
482
+ return tab.active ? 200 : 100;
483
+ }
484
+ return -1;
485
+ }
486
+ function setWorkspaceSession(workspace, session) {
487
+ const existing = automationSessions.get(workspace);
488
+ if (existing?.idleTimer) clearTimeout(existing.idleTimer);
489
+ automationSessions.set(workspace, {
490
+ ...session,
491
+ idleTimer: null,
492
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
493
+ });
494
+ }
495
+ async function maybeBindWorkspaceToExistingTab(workspace) {
496
+ if (!isNotebooklmWorkspace(workspace)) return null;
497
+ const tabs = await chrome.tabs.query({});
498
+ let bestTab = null;
499
+ let bestScore = -1;
500
+ for (const tab of tabs) {
501
+ const score = scoreWorkspaceTab(workspace, tab);
502
+ if (score > bestScore) {
503
+ bestScore = score;
504
+ bestTab = tab;
505
+ }
506
+ }
507
+ if (!bestTab?.id || bestScore < 0) return null;
508
+ setWorkspaceSession(workspace, {
509
+ windowId: bestTab.windowId,
510
+ owned: false,
511
+ preferredTabId: bestTab.id
512
+ });
513
+ console.log(`[opencli] Workspace ${workspace} bound to existing tab ${bestTab.id} in window ${bestTab.windowId}`);
514
+ resetWindowIdleTimer(workspace);
515
+ return bestTab.id;
516
+ }
344
517
  async function resolveTabId(tabId, workspace) {
345
518
  if (tabId !== void 0) {
346
519
  try {
347
520
  const tab = await chrome.tabs.get(tabId);
348
521
  const session = automationSessions.get(workspace);
349
- if (isDebuggableUrl(tab.url) && session && tab.windowId === session.windowId) return tabId;
350
- if (session && tab.windowId !== session.windowId) {
351
- console.warn(`[opencli] Tab ${tabId} belongs to window ${tab.windowId}, not automation window ${session.windowId}, re-resolving`);
522
+ const matchesSession = session ? session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId : false;
523
+ if (isDebuggableUrl(tab.url) && matchesSession) return tabId;
524
+ if (session && !matchesSession) {
525
+ console.warn(`[opencli] Tab ${tabId} is not bound to workspace ${workspace}, re-resolving`);
352
526
  } else if (!isDebuggableUrl(tab.url)) {
353
527
  console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
354
528
  }
@@ -356,6 +530,18 @@ async function resolveTabId(tabId, workspace) {
356
530
  console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
357
531
  }
358
532
  }
533
+ const adoptedTabId = await maybeBindWorkspaceToExistingTab(workspace);
534
+ if (adoptedTabId !== null) return adoptedTabId;
535
+ const existingSession = automationSessions.get(workspace);
536
+ if (existingSession && existingSession.preferredTabId !== null) {
537
+ try {
538
+ const preferredTabId = existingSession.preferredTabId;
539
+ const preferredTab = await chrome.tabs.get(preferredTabId);
540
+ if (isDebuggableUrl(preferredTab.url)) return preferredTab.id;
541
+ } catch {
542
+ automationSessions.delete(workspace);
543
+ }
544
+ }
359
545
  const windowId = await getAutomationWindow(workspace);
360
546
  const tabs = await chrome.tabs.query({ windowId });
361
547
  const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
@@ -378,6 +564,14 @@ async function resolveTabId(tabId, workspace) {
378
564
  async function listAutomationTabs(workspace) {
379
565
  const session = automationSessions.get(workspace);
380
566
  if (!session) return [];
567
+ if (session.preferredTabId !== null) {
568
+ try {
569
+ return [await chrome.tabs.get(session.preferredTabId)];
570
+ } catch {
571
+ automationSessions.delete(workspace);
572
+ return [];
573
+ }
574
+ }
381
575
  try {
382
576
  return await chrome.tabs.query({ windowId: session.windowId });
383
577
  } catch {
@@ -559,15 +753,29 @@ async function handleScreenshot(cmd, workspace) {
559
753
  async function handleCloseWindow(cmd, workspace) {
560
754
  const session = automationSessions.get(workspace);
561
755
  if (session) {
562
- try {
563
- await chrome.windows.remove(session.windowId);
564
- } catch {
756
+ if (session.owned) {
757
+ try {
758
+ await chrome.windows.remove(session.windowId);
759
+ } catch {
760
+ }
565
761
  }
566
762
  if (session.idleTimer) clearTimeout(session.idleTimer);
567
763
  automationSessions.delete(workspace);
568
764
  }
569
765
  return { id: cmd.id, ok: true, data: { closed: true } };
570
766
  }
767
+ async function handleSetFileInput(cmd, workspace) {
768
+ if (!cmd.files || !Array.isArray(cmd.files) || cmd.files.length === 0) {
769
+ return { id: cmd.id, ok: false, error: "Missing or empty files array" };
770
+ }
771
+ const tabId = await resolveTabId(cmd.tabId, workspace);
772
+ try {
773
+ await setFileInputFiles(tabId, cmd.files, cmd.selector);
774
+ return { id: cmd.id, ok: true, data: { count: cmd.files.length } };
775
+ } catch (err) {
776
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
777
+ }
778
+ }
571
779
  async function handleSessions(cmd) {
572
780
  const now = Date.now();
573
781
  const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
@@ -578,3 +786,34 @@ async function handleSessions(cmd) {
578
786
  })));
579
787
  return { id: cmd.id, ok: true, data };
580
788
  }
789
+ async function handleBindCurrent(cmd, workspace) {
790
+ const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
791
+ const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
792
+ const allTabs = await chrome.tabs.query({});
793
+ const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd));
794
+ if (!boundTab?.id) {
795
+ return {
796
+ id: cmd.id,
797
+ ok: false,
798
+ error: cmd.matchDomain || cmd.matchPathPrefix ? `No visible tab matching ${cmd.matchDomain ?? "domain"}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ""}` : "No active debuggable tab found"
799
+ };
800
+ }
801
+ setWorkspaceSession(workspace, {
802
+ windowId: boundTab.windowId,
803
+ owned: false,
804
+ preferredTabId: boundTab.id
805
+ });
806
+ resetWindowIdleTimer(workspace);
807
+ console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
808
+ return {
809
+ id: cmd.id,
810
+ ok: true,
811
+ data: {
812
+ tabId: boundTab.id,
813
+ windowId: boundTab.windowId,
814
+ url: boundTab.url,
815
+ title: boundTab.title,
816
+ workspace
817
+ }
818
+ };
819
+ }
@@ -5,6 +5,7 @@
5
5
  "description": "Browser automation bridge for the OpenCLI CLI tool. Executes commands in isolated Chrome windows via a local daemon.",
6
6
  "permissions": [
7
7
  "debugger",
8
+ "scripting",
8
9
  "tabs",
9
10
  "cookies",
10
11
  "activeTab",
@@ -35,4 +36,4 @@
35
36
  "extension_pages": "script-src 'self'; object-src 'self'"
36
37
  },
37
38
  "homepage_url": "https://github.com/jackwener/opencli"
38
- }
39
+ }