@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,857 @@
1
+ # Daemon Lifecycle Redesign Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Replace the daemon's aggressive 5-minute idle timeout with a long-lived model (4h default) that requires both CLI inactivity AND Extension disconnection before exiting, plus add `daemon status/stop/restart` CLI commands.
6
+
7
+ **Architecture:** The daemon keeps its existing HTTP + WebSocket bridge architecture. We change the idle timeout logic to track two independent activity signals (CLI requests and Extension connection), add `/status` and `/shutdown` HTTP endpoints, reduce the Extension reconnect backoff cap, and register new CLI commands via Commander.js.
8
+
9
+ **Tech Stack:** Node.js, TypeScript, Commander.js, ws, Vitest
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | File | Action | Responsibility |
16
+ |------|--------|----------------|
17
+ | `src/constants.ts` | Modify | Add `DEFAULT_DAEMON_IDLE_TIMEOUT` constant |
18
+ | `src/daemon.ts` | Modify | Dual-condition idle timer, `/status` endpoint, `/shutdown` endpoint |
19
+ | `src/daemon.test.ts` | Create | Unit tests for idle timer logic, `/status`, `/shutdown` |
20
+ | `extension/src/protocol.ts` | Modify | Change `WS_RECONNECT_MAX_DELAY` from 60000 to 5000 |
21
+ | `src/cli.ts` | Modify | Register `daemon` subcommand group |
22
+ | `src/commands/daemon.ts` | Create | `status`, `stop`, `restart` subcommand implementations |
23
+ | `src/commands/daemon.test.ts` | Create | Unit tests for daemon commands |
24
+ | `src/browser/mcp.ts` | Modify | Better connection-waiting UX messages, 200ms poll interval |
25
+
26
+ ---
27
+
28
+ ### Task 1: Add `DEFAULT_DAEMON_IDLE_TIMEOUT` constant
29
+
30
+ **Files:**
31
+ - Modify: `src/constants.ts`
32
+
33
+ - [ ] **Step 1: Add the constant**
34
+
35
+ In `src/constants.ts`, add after the `DEFAULT_DAEMON_PORT` line:
36
+
37
+ ```typescript
38
+ /** Default idle timeout before daemon auto-exits (ms). Override via OPENCLI_DAEMON_TIMEOUT env var. */
39
+ export const DEFAULT_DAEMON_IDLE_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
40
+ ```
41
+
42
+ - [ ] **Step 2: Commit**
43
+
44
+ ```bash
45
+ git add src/constants.ts
46
+ git commit -m "feat(daemon): add DEFAULT_DAEMON_IDLE_TIMEOUT constant (4 hours)"
47
+ ```
48
+
49
+ ---
50
+
51
+ ### Task 2: Implement dual-condition idle timer in daemon
52
+
53
+ **Files:**
54
+ - Modify: `src/daemon.ts:27,29-57,116-123,196-198,245-262,265-269`
55
+ - Test: `src/daemon.test.ts` (create)
56
+
57
+ - [ ] **Step 1: Write failing tests for the new idle timer logic**
58
+
59
+ Create `src/daemon.test.ts`:
60
+
61
+ ```typescript
62
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
63
+
64
+ // We test the idle timer logic by extracting it into testable functions.
65
+ // The daemon module has side effects (starts server), so we test the logic unit directly.
66
+
67
+ describe('IdleManager', () => {
68
+ beforeEach(() => {
69
+ vi.useFakeTimers();
70
+ });
71
+
72
+ afterEach(() => {
73
+ vi.useRealTimers();
74
+ });
75
+
76
+ it('does not start timer when extension is connected', async () => {
77
+ const { IdleManager } = await import('./daemon.js');
78
+ const exit = vi.fn();
79
+ const mgr = new IdleManager(300_000, exit); // 5 min for fast test
80
+
81
+ mgr.setExtensionConnected(true);
82
+ mgr.onCliRequest();
83
+
84
+ vi.advanceTimersByTime(300_000 + 1000);
85
+ expect(exit).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('starts timer when extension disconnects and CLI is idle', async () => {
89
+ const { IdleManager } = await import('./daemon.js');
90
+ const exit = vi.fn();
91
+ const mgr = new IdleManager(300_000, exit);
92
+
93
+ mgr.onCliRequest(); // CLI was active
94
+ mgr.setExtensionConnected(true);
95
+ mgr.setExtensionConnected(false); // Extension disconnects
96
+
97
+ // Should not exit immediately — CLI was just active
98
+ expect(exit).not.toHaveBeenCalled();
99
+
100
+ // Advance past timeout
101
+ vi.advanceTimersByTime(300_000 + 1000);
102
+ expect(exit).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ it('exits immediately on extension disconnect if CLI has been idle past timeout', async () => {
106
+ const { IdleManager } = await import('./daemon.js');
107
+ const exit = vi.fn();
108
+ const mgr = new IdleManager(300_000, exit);
109
+
110
+ mgr.onCliRequest(); // Last CLI activity
111
+ vi.advanceTimersByTime(400_000); // 400s elapsed — past 300s timeout
112
+
113
+ mgr.setExtensionConnected(true);
114
+ mgr.setExtensionConnected(false);
115
+
116
+ expect(exit).toHaveBeenCalledTimes(1);
117
+ });
118
+
119
+ it('resets timer on new CLI request', async () => {
120
+ const { IdleManager } = await import('./daemon.js');
121
+ const exit = vi.fn();
122
+ const mgr = new IdleManager(300_000, exit);
123
+
124
+ mgr.onCliRequest();
125
+ vi.advanceTimersByTime(200_000); // 200s elapsed
126
+ mgr.onCliRequest(); // Reset
127
+
128
+ vi.advanceTimersByTime(200_000); // 200s more — only 200s since last request
129
+ expect(exit).not.toHaveBeenCalled();
130
+
131
+ vi.advanceTimersByTime(100_001); // Now 300s+ since last request
132
+ expect(exit).toHaveBeenCalledTimes(1);
133
+ });
134
+
135
+ it('does not exit when timeout is 0 (disabled)', async () => {
136
+ const { IdleManager } = await import('./daemon.js');
137
+ const exit = vi.fn();
138
+ const mgr = new IdleManager(0, exit);
139
+
140
+ mgr.onCliRequest();
141
+ vi.advanceTimersByTime(24 * 60 * 60 * 1000); // 24 hours
142
+ expect(exit).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it('clears timer when extension connects', async () => {
146
+ const { IdleManager } = await import('./daemon.js');
147
+ const exit = vi.fn();
148
+ const mgr = new IdleManager(300_000, exit);
149
+
150
+ mgr.onCliRequest();
151
+ vi.advanceTimersByTime(200_000); // Timer running
152
+
153
+ mgr.setExtensionConnected(true); // Should clear timer
154
+ vi.advanceTimersByTime(200_000); // Would have fired
155
+ expect(exit).not.toHaveBeenCalled();
156
+ });
157
+ });
158
+ ```
159
+
160
+ - [ ] **Step 2: Run tests to verify they fail**
161
+
162
+ ```bash
163
+ npx vitest run src/daemon.test.ts
164
+ ```
165
+
166
+ Expected: FAIL — `IdleManager` is not exported from `./daemon.js`
167
+
168
+ - [ ] **Step 3: Extract IdleManager class and refactor daemon.ts**
169
+
170
+ In `src/daemon.ts`, replace the idle timeout section (lines 27, 29-57) with:
171
+
172
+ Replace the `IDLE_TIMEOUT` constant (line 27):
173
+ ```typescript
174
+ import { DEFAULT_DAEMON_PORT, DEFAULT_DAEMON_IDLE_TIMEOUT } from './constants.js';
175
+
176
+ const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
177
+ const IDLE_TIMEOUT = Number(process.env.OPENCLI_DAEMON_TIMEOUT ?? DEFAULT_DAEMON_IDLE_TIMEOUT);
178
+ ```
179
+
180
+ Replace the idle timer state and `resetIdleTimer` function (lines 37, 49-57) with the `IdleManager` class:
181
+
182
+ ```typescript
183
+ /**
184
+ * Manages daemon idle timeout with dual-condition logic:
185
+ * exits only when BOTH CLI is idle AND Extension is disconnected.
186
+ */
187
+ export class IdleManager {
188
+ private _timer: ReturnType<typeof setTimeout> | null = null;
189
+ private _lastCliRequestTime = Date.now();
190
+ private _extensionConnected = false;
191
+ private _timeoutMs: number;
192
+ private _onExit: () => void;
193
+
194
+ constructor(timeoutMs: number, onExit: () => void) {
195
+ this._timeoutMs = timeoutMs;
196
+ this._onExit = onExit;
197
+ }
198
+
199
+ /** Call when an HTTP request arrives from CLI */
200
+ onCliRequest(): void {
201
+ this._lastCliRequestTime = Date.now();
202
+ this._resetTimer();
203
+ }
204
+
205
+ /** Call when Extension WebSocket connects or disconnects */
206
+ setExtensionConnected(connected: boolean): void {
207
+ this._extensionConnected = connected;
208
+ if (connected) {
209
+ // Extension is alive — clear any pending exit timer
210
+ this._clearTimer();
211
+ } else {
212
+ // Extension gone — check if CLI has also been idle long enough
213
+ this._resetTimer();
214
+ }
215
+ }
216
+
217
+ private _clearTimer(): void {
218
+ if (this._timer) {
219
+ clearTimeout(this._timer);
220
+ this._timer = null;
221
+ }
222
+ }
223
+
224
+ private _resetTimer(): void {
225
+ this._clearTimer();
226
+
227
+ // Timeout disabled
228
+ if (this._timeoutMs <= 0) return;
229
+
230
+ // Extension connected — don't start timer
231
+ if (this._extensionConnected) return;
232
+
233
+ const elapsed = Date.now() - this._lastCliRequestTime;
234
+ if (elapsed >= this._timeoutMs) {
235
+ // CLI has been idle past the timeout already
236
+ this._onExit();
237
+ return;
238
+ }
239
+
240
+ // Start timer for remaining duration
241
+ this._timer = setTimeout(() => {
242
+ this._onExit();
243
+ }, this._timeoutMs - elapsed);
244
+ }
245
+ }
246
+ ```
247
+
248
+ Then create the global `idleManager` instance after the class definition:
249
+
250
+ ```typescript
251
+ const idleManager = new IdleManager(IDLE_TIMEOUT, () => {
252
+ console.error('[daemon] Idle timeout (no CLI requests + no Extension), shutting down');
253
+ process.exit(0);
254
+ });
255
+ ```
256
+
257
+ - [ ] **Step 4: Wire IdleManager into existing daemon code**
258
+
259
+ In the `handleRequest` function, replace `resetIdleTimer()` (line 142) with:
260
+ ```typescript
261
+ idleManager.onCliRequest();
262
+ ```
263
+
264
+ In the `wss.on('connection')` handler (around line 196-198), add after `extensionWs = ws;`:
265
+ ```typescript
266
+ idleManager.setExtensionConnected(true);
267
+ ```
268
+
269
+ In the `ws.on('close')` handler (around line 245-249), add after `extensionWs = null;`:
270
+ ```typescript
271
+ idleManager.setExtensionConnected(false);
272
+ ```
273
+
274
+ In the `ws.on('error')` handler (around line 259-261), add after `extensionWs = null;`:
275
+ ```typescript
276
+ idleManager.setExtensionConnected(false);
277
+ ```
278
+
279
+ In the `httpServer.listen` callback (line 268-269), replace `resetIdleTimer()` with:
280
+ ```typescript
281
+ idleManager.onCliRequest(); // Start initial idle countdown
282
+ ```
283
+
284
+ Remove the old `resetIdleTimer` function and `idleTimer` variable entirely.
285
+
286
+ - [ ] **Step 5: Run tests to verify they pass**
287
+
288
+ ```bash
289
+ npx vitest run src/daemon.test.ts
290
+ ```
291
+
292
+ Expected: All 6 tests PASS
293
+
294
+ - [ ] **Step 6: Commit**
295
+
296
+ ```bash
297
+ git add src/daemon.ts src/daemon.test.ts
298
+ git commit -m "feat(daemon): replace fixed 5min timeout with dual-condition idle manager (4h default)"
299
+ ```
300
+
301
+ ---
302
+
303
+ ### Task 3: Add `/status` and `/shutdown` endpoints to daemon
304
+
305
+ **Files:**
306
+ - Modify: `src/daemon.ts:116-123`
307
+
308
+ - [ ] **Step 1: Add tests for /status and /shutdown endpoints**
309
+
310
+ Append to `src/daemon.test.ts`:
311
+
312
+ ```typescript
313
+ describe('/status endpoint', () => {
314
+ it('returns daemon status with correct fields', async () => {
315
+ // This is an integration test — tested via the daemon command tests.
316
+ // Here we just verify the shape of the status response type.
317
+ expect(true).toBe(true); // Placeholder — real coverage in Task 6
318
+ });
319
+ });
320
+ ```
321
+
322
+ Note: The `/status` and `/shutdown` endpoints run inside the daemon process, which makes them hard to unit test in isolation. They are integration-tested via the `opencli daemon status/stop` commands in Task 6.
323
+
324
+ - [ ] **Step 2: Enhance the existing `/status` endpoint**
325
+
326
+ In `src/daemon.ts`, replace the existing `/status` handler (lines 116-123) with:
327
+
328
+ ```typescript
329
+ if (req.method === 'GET' && pathname === '/status') {
330
+ const uptime = process.uptime();
331
+ const mem = process.memoryUsage();
332
+ jsonResponse(res, 200, {
333
+ ok: true,
334
+ pid: process.pid,
335
+ uptime,
336
+ extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
337
+ pending: pending.size,
338
+ lastCliRequestTime: idleManager.lastCliRequestTime,
339
+ memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
340
+ port: PORT,
341
+ });
342
+ return;
343
+ }
344
+ ```
345
+
346
+ Also add a public getter to `IdleManager`:
347
+
348
+ ```typescript
349
+ get lastCliRequestTime(): number {
350
+ return this._lastCliRequestTime;
351
+ }
352
+ ```
353
+
354
+ - [ ] **Step 3: Add the `/shutdown` endpoint**
355
+
356
+ In `src/daemon.ts`, add before the `POST /command` handler:
357
+
358
+ ```typescript
359
+ if (req.method === 'POST' && pathname === '/shutdown') {
360
+ jsonResponse(res, 200, { ok: true, message: 'Shutting down' });
361
+ // Graceful shutdown after response is sent
362
+ setTimeout(() => shutdown(), 100);
363
+ return;
364
+ }
365
+ ```
366
+
367
+ - [ ] **Step 4: Run all tests**
368
+
369
+ ```bash
370
+ npx vitest run src/daemon.test.ts
371
+ ```
372
+
373
+ Expected: PASS
374
+
375
+ - [ ] **Step 5: Commit**
376
+
377
+ ```bash
378
+ git add src/daemon.ts src/daemon.test.ts
379
+ git commit -m "feat(daemon): enhance /status endpoint, add /shutdown endpoint"
380
+ ```
381
+
382
+ ---
383
+
384
+ ### Task 4: Reduce Extension WebSocket reconnect backoff cap
385
+
386
+ **Files:**
387
+ - Modify: `extension/src/protocol.ts:57`
388
+
389
+ - [ ] **Step 1: Change the constant**
390
+
391
+ In `extension/src/protocol.ts`, change line 57:
392
+
393
+ ```typescript
394
+ /** Max reconnect delay (ms) — kept short since daemon is long-lived */
395
+ export const WS_RECONNECT_MAX_DELAY = 5000;
396
+ ```
397
+
398
+ - [ ] **Step 2: Commit**
399
+
400
+ ```bash
401
+ git add extension/src/protocol.ts
402
+ git commit -m "feat(extension): reduce WS reconnect backoff cap from 60s to 5s"
403
+ ```
404
+
405
+ ---
406
+
407
+ ### Task 5: Implement `daemon status/stop/restart` CLI commands
408
+
409
+ **Files:**
410
+ - Create: `src/commands/daemon.ts`
411
+ - Modify: `src/cli.ts`
412
+
413
+ - [ ] **Step 1: Create daemon command module**
414
+
415
+ Create `src/commands/daemon.ts`:
416
+
417
+ ```typescript
418
+ /**
419
+ * CLI commands for daemon lifecycle management:
420
+ * opencli daemon status — show daemon state
421
+ * opencli daemon stop — graceful shutdown
422
+ * opencli daemon restart — stop + respawn
423
+ */
424
+
425
+ import chalk from 'chalk';
426
+ import { DEFAULT_DAEMON_PORT } from '../constants.js';
427
+
428
+ const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
429
+ const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
430
+
431
+ interface DaemonStatus {
432
+ ok: boolean;
433
+ pid: number;
434
+ uptime: number;
435
+ extensionConnected: boolean;
436
+ pending: number;
437
+ lastCliRequestTime: number;
438
+ memoryMB: number;
439
+ port: number;
440
+ }
441
+
442
+ async function fetchStatus(): Promise<DaemonStatus | null> {
443
+ try {
444
+ const controller = new AbortController();
445
+ const timer = setTimeout(() => controller.abort(), 2000);
446
+ const res = await fetch(`${DAEMON_URL}/status`, {
447
+ headers: { 'X-OpenCLI': '1' },
448
+ signal: controller.signal,
449
+ });
450
+ clearTimeout(timer);
451
+ if (!res.ok) return null;
452
+ return await res.json() as DaemonStatus;
453
+ } catch {
454
+ return null;
455
+ }
456
+ }
457
+
458
+ async function requestShutdown(): Promise<boolean> {
459
+ try {
460
+ const controller = new AbortController();
461
+ const timer = setTimeout(() => controller.abort(), 5000);
462
+ const res = await fetch(`${DAEMON_URL}/shutdown`, {
463
+ method: 'POST',
464
+ headers: { 'X-OpenCLI': '1' },
465
+ signal: controller.signal,
466
+ });
467
+ clearTimeout(timer);
468
+ return res.ok;
469
+ } catch {
470
+ return false;
471
+ }
472
+ }
473
+
474
+ function formatUptime(seconds: number): string {
475
+ const h = Math.floor(seconds / 3600);
476
+ const m = Math.floor((seconds % 3600) / 60);
477
+ if (h > 0) return `${h}h ${m}m`;
478
+ if (m > 0) return `${m}m`;
479
+ return `${Math.floor(seconds)}s`;
480
+ }
481
+
482
+ function formatTimeSince(timestampMs: number): string {
483
+ const seconds = (Date.now() - timestampMs) / 1000;
484
+ if (seconds < 60) return `${Math.floor(seconds)}s ago`;
485
+ const m = Math.floor(seconds / 60);
486
+ if (m < 60) return `${m} min ago`;
487
+ const h = Math.floor(m / 60);
488
+ return `${h}h ${m % 60}m ago`;
489
+ }
490
+
491
+ export async function daemonStatus(): Promise<void> {
492
+ const status = await fetchStatus();
493
+ if (!status) {
494
+ console.log(`Daemon: ${chalk.dim('not running')}`);
495
+ return;
496
+ }
497
+
498
+ console.log(`Daemon: ${chalk.green('running')} (PID ${status.pid})`);
499
+ console.log(`Uptime: ${formatUptime(status.uptime)}`);
500
+ console.log(`Extension: ${status.extensionConnected ? chalk.green('connected') : chalk.yellow('disconnected')}`);
501
+ console.log(`Last CLI request: ${formatTimeSince(status.lastCliRequestTime)}`);
502
+ console.log(`Memory: ${status.memoryMB} MB`);
503
+ console.log(`Port: ${status.port}`);
504
+ }
505
+
506
+ export async function daemonStop(): Promise<void> {
507
+ const status = await fetchStatus();
508
+ if (!status) {
509
+ console.log(chalk.dim('Daemon is not running.'));
510
+ return;
511
+ }
512
+
513
+ const ok = await requestShutdown();
514
+ if (ok) {
515
+ console.log(chalk.green('Daemon stopped.'));
516
+ } else {
517
+ console.error(chalk.red('Failed to stop daemon.'));
518
+ process.exitCode = 1;
519
+ }
520
+ }
521
+
522
+ export async function daemonRestart(): Promise<void> {
523
+ const status = await fetchStatus();
524
+ if (status) {
525
+ const ok = await requestShutdown();
526
+ if (!ok) {
527
+ console.error(chalk.red('Failed to stop daemon.'));
528
+ process.exitCode = 1;
529
+ return;
530
+ }
531
+ // Wait for daemon to exit
532
+ await new Promise(r => setTimeout(r, 500));
533
+ }
534
+
535
+ // Import BrowserBridge to spawn a new daemon
536
+ const { BrowserBridge } = await import('../browser/mcp.js');
537
+ const bridge = new BrowserBridge();
538
+ try {
539
+ console.log('Starting daemon...');
540
+ await bridge.connect({ timeout: 10 });
541
+ console.log(chalk.green('Daemon restarted.'));
542
+ } catch (err) {
543
+ console.error(chalk.red(`Failed to restart daemon: ${err instanceof Error ? err.message : err}`));
544
+ process.exitCode = 1;
545
+ }
546
+ }
547
+ ```
548
+
549
+ - [ ] **Step 2: Register daemon commands in cli.ts**
550
+
551
+ In `src/cli.ts`, add the import at the top:
552
+
553
+ ```typescript
554
+ import { daemonStatus, daemonStop, daemonRestart } from './commands/daemon.js';
555
+ ```
556
+
557
+ Add the daemon subcommand group before the `// ── External CLIs` section (around line 380):
558
+
559
+ ```typescript
560
+ // ── Built-in: daemon ──────────────────────────────────────────────────────
561
+ const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
562
+ daemonCmd
563
+ .command('status')
564
+ .description('Show daemon status')
565
+ .action(async () => { await daemonStatus(); });
566
+ daemonCmd
567
+ .command('stop')
568
+ .description('Stop the daemon')
569
+ .action(async () => { await daemonStop(); });
570
+ daemonCmd
571
+ .command('restart')
572
+ .description('Restart the daemon')
573
+ .action(async () => { await daemonRestart(); });
574
+ ```
575
+
576
+ - [ ] **Step 3: Run linter/type check**
577
+
578
+ ```bash
579
+ npx tsc --noEmit
580
+ ```
581
+
582
+ Expected: No errors
583
+
584
+ - [ ] **Step 4: Commit**
585
+
586
+ ```bash
587
+ git add src/commands/daemon.ts src/cli.ts
588
+ git commit -m "feat(daemon): add opencli daemon status/stop/restart commands"
589
+ ```
590
+
591
+ ---
592
+
593
+ ### Task 6: Write tests for daemon commands
594
+
595
+ **Files:**
596
+ - Create: `src/commands/daemon.test.ts`
597
+
598
+ - [ ] **Step 1: Write tests**
599
+
600
+ Create `src/commands/daemon.test.ts`:
601
+
602
+ ```typescript
603
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
604
+
605
+ // Mock fetch globally for all tests
606
+ const mockFetch = vi.fn();
607
+ vi.stubGlobal('fetch', mockFetch);
608
+
609
+ // Mock chalk to avoid ANSI in assertions
610
+ vi.mock('chalk', () => ({
611
+ default: {
612
+ green: (s: string) => s,
613
+ yellow: (s: string) => s,
614
+ red: (s: string) => s,
615
+ dim: (s: string) => s,
616
+ },
617
+ }));
618
+
619
+ describe('daemonStatus', () => {
620
+ let consoleSpy: ReturnType<typeof vi.spyOn>;
621
+
622
+ beforeEach(() => {
623
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
624
+ });
625
+
626
+ afterEach(() => {
627
+ consoleSpy.mockRestore();
628
+ mockFetch.mockReset();
629
+ });
630
+
631
+ it('shows "not running" when daemon is unreachable', async () => {
632
+ mockFetch.mockRejectedValue(new TypeError('fetch failed'));
633
+
634
+ const { daemonStatus } = await import('./daemon.js');
635
+ await daemonStatus();
636
+
637
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
638
+ });
639
+
640
+ it('shows daemon info when running', async () => {
641
+ mockFetch.mockResolvedValue({
642
+ ok: true,
643
+ json: async () => ({
644
+ ok: true,
645
+ pid: 12345,
646
+ uptime: 7200,
647
+ extensionConnected: true,
648
+ pending: 0,
649
+ lastCliRequestTime: Date.now() - 60_000,
650
+ memoryMB: 12.3,
651
+ port: 19825,
652
+ }),
653
+ });
654
+
655
+ const { daemonStatus } = await import('./daemon.js');
656
+ await daemonStatus();
657
+
658
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
659
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('12345'));
660
+ });
661
+ });
662
+
663
+ describe('daemonStop', () => {
664
+ let consoleSpy: ReturnType<typeof vi.spyOn>;
665
+ let consoleErrSpy: ReturnType<typeof vi.spyOn>;
666
+
667
+ beforeEach(() => {
668
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
669
+ consoleErrSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
670
+ });
671
+
672
+ afterEach(() => {
673
+ consoleSpy.mockRestore();
674
+ consoleErrSpy.mockRestore();
675
+ mockFetch.mockReset();
676
+ });
677
+
678
+ it('reports when daemon is not running', async () => {
679
+ mockFetch.mockRejectedValue(new TypeError('fetch failed'));
680
+
681
+ const { daemonStop } = await import('./daemon.js');
682
+ await daemonStop();
683
+
684
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
685
+ });
686
+
687
+ it('sends shutdown and reports success', async () => {
688
+ // First call: fetchStatus
689
+ // Second call: requestShutdown
690
+ mockFetch
691
+ .mockResolvedValueOnce({
692
+ ok: true,
693
+ json: async () => ({ ok: true, pid: 123, uptime: 100, extensionConnected: false, pending: 0, lastCliRequestTime: Date.now(), memoryMB: 10, port: 19825 }),
694
+ })
695
+ .mockResolvedValueOnce({ ok: true });
696
+
697
+ const { daemonStop } = await import('./daemon.js');
698
+ await daemonStop();
699
+
700
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('stopped'));
701
+ });
702
+ });
703
+ ```
704
+
705
+ - [ ] **Step 2: Run tests**
706
+
707
+ ```bash
708
+ npx vitest run src/commands/daemon.test.ts
709
+ ```
710
+
711
+ Expected: PASS
712
+
713
+ - [ ] **Step 3: Commit**
714
+
715
+ ```bash
716
+ git add src/commands/daemon.test.ts
717
+ git commit -m "test(daemon): add tests for daemon status/stop commands"
718
+ ```
719
+
720
+ ---
721
+
722
+ ### Task 7: Improve CLI connection-waiting UX
723
+
724
+ **Files:**
725
+ - Modify: `src/browser/mcp.ts:58-118`
726
+
727
+ - [ ] **Step 1: Improve error messages and poll interval**
728
+
729
+ In `src/browser/mcp.ts`, replace the `_ensureDaemon` method (lines 58-118) with:
730
+
731
+ ```typescript
732
+ private async _ensureDaemon(timeoutSeconds?: number): Promise<void> {
733
+ const effectiveSeconds = (timeoutSeconds && timeoutSeconds > 0) ? timeoutSeconds : Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000);
734
+ const timeoutMs = effectiveSeconds * 1000;
735
+
736
+ // Fast path: extension already connected
737
+ if (await isExtensionConnected()) return;
738
+
739
+ // Daemon running but no extension — wait for extension with progress
740
+ if (await isDaemonRunning()) {
741
+ if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
742
+ process.stderr.write('⏳ Waiting for Chrome extension to connect...\n');
743
+ process.stderr.write(' Make sure Chrome is open and the OpenCLI extension is enabled.\n');
744
+ }
745
+ const deadline = Date.now() + timeoutMs;
746
+ while (Date.now() < deadline) {
747
+ await new Promise(resolve => setTimeout(resolve, 200));
748
+ if (await isExtensionConnected()) return;
749
+ }
750
+ throw new Error(
751
+ 'Daemon is running but the Browser Extension is not connected.\n' +
752
+ 'Please install and enable the opencli Browser Bridge extension in Chrome.',
753
+ );
754
+ }
755
+
756
+ // No daemon — spawn one
757
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
758
+ const parentDir = path.resolve(__dirname, '..');
759
+ const daemonTs = path.join(parentDir, 'daemon.ts');
760
+ const daemonJs = path.join(parentDir, 'daemon.js');
761
+ const isTs = fs.existsSync(daemonTs);
762
+ const daemonPath = isTs ? daemonTs : daemonJs;
763
+
764
+ if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
765
+ process.stderr.write('⏳ Starting daemon...\n');
766
+ }
767
+
768
+ const spawnArgs = isTs
769
+ ? [process.execPath, '--import', 'tsx/esm', daemonPath]
770
+ : [process.execPath, daemonPath];
771
+
772
+ this._daemonProc = spawn(spawnArgs[0], spawnArgs.slice(1), {
773
+ detached: true,
774
+ stdio: 'ignore',
775
+ env: { ...process.env },
776
+ });
777
+ this._daemonProc.unref();
778
+
779
+ // Wait for daemon + extension with faster polling
780
+ const deadline = Date.now() + timeoutMs;
781
+ while (Date.now() < deadline) {
782
+ await new Promise(resolve => setTimeout(resolve, 200));
783
+ if (await isExtensionConnected()) return;
784
+ }
785
+
786
+ if (await isDaemonRunning()) {
787
+ throw new Error(
788
+ 'Daemon is running but the Browser Extension is not connected.\n' +
789
+ 'Please install and enable the opencli Browser Bridge extension in Chrome.',
790
+ );
791
+ }
792
+
793
+ throw new Error(
794
+ 'Failed to start opencli daemon. Try running manually:\n' +
795
+ ` node ${daemonPath}\n` +
796
+ `Make sure port ${DEFAULT_DAEMON_PORT} is available.`,
797
+ );
798
+ }
799
+ ```
800
+
801
+ - [ ] **Step 2: Run existing browser tests to check for regressions**
802
+
803
+ ```bash
804
+ npx vitest run src/browser.test.ts
805
+ ```
806
+
807
+ Expected: PASS
808
+
809
+ - [ ] **Step 3: Commit**
810
+
811
+ ```bash
812
+ git add src/browser/mcp.ts
813
+ git commit -m "feat(daemon): improve CLI connection-waiting UX with progress messages and 200ms polling"
814
+ ```
815
+
816
+ ---
817
+
818
+ ### Task 8: Run full test suite and verify
819
+
820
+ - [ ] **Step 1: Run type check**
821
+
822
+ ```bash
823
+ npx tsc --noEmit
824
+ ```
825
+
826
+ Expected: No errors
827
+
828
+ - [ ] **Step 2: Run all tests**
829
+
830
+ ```bash
831
+ npx vitest run
832
+ ```
833
+
834
+ Expected: All tests pass, no regressions
835
+
836
+ - [ ] **Step 3: Manual smoke test**
837
+
838
+ ```bash
839
+ # Check daemon status (should be "not running" if daemon isn't started)
840
+ npx tsx src/main.ts daemon status
841
+
842
+ # Start daemon by running any browser command, then check status
843
+ npx tsx src/main.ts daemon status
844
+
845
+ # Stop daemon
846
+ npx tsx src/main.ts daemon stop
847
+
848
+ # Verify stopped
849
+ npx tsx src/main.ts daemon status
850
+ ```
851
+
852
+ - [ ] **Step 4: Final commit if any fixes needed**
853
+
854
+ ```bash
855
+ git add -A
856
+ git commit -m "fix: address issues found during smoke testing"
857
+ ```