@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.
- package/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -1
- package/SKILL.md +879 -0
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.js +30 -27
- package/dist/browser/daemon-client.d.ts +7 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.js +1 -0
- package/dist/browser/dom-helpers.test.js +14 -1
- package/dist/browser/mcp.js +18 -13
- package/dist/browser/page.js +22 -2
- package/dist/browser/page.test.d.ts +1 -0
- package/dist/browser/page.test.js +44 -0
- package/dist/browser/stealth.js +198 -0
- package/dist/browser/stealth.test.d.ts +1 -0
- package/dist/browser/stealth.test.js +134 -0
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +5 -1
- package/dist/build-manifest.test.js +2 -0
- package/dist/cli-manifest.json +544 -137
- package/dist/cli.js +20 -3
- package/dist/clis/antigravity/serve.d.ts +1 -1
- package/dist/clis/antigravity/serve.js +5 -8
- package/dist/clis/bilibili/subtitle.js +4 -0
- package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.test.js +48 -0
- package/dist/clis/chatwise/ask.js +0 -2
- package/dist/clis/chatwise/export.js +0 -2
- package/dist/clis/chatwise/history.js +0 -2
- package/dist/clis/chatwise/model.js +0 -2
- package/dist/clis/chatwise/new.js +1 -2
- package/dist/clis/chatwise/read.js +0 -2
- package/dist/clis/chatwise/screenshot.js +1 -2
- package/dist/clis/chatwise/send.js +0 -2
- package/dist/clis/chatwise/status.js +1 -2
- package/dist/clis/ctrip/search.d.ts +13 -0
- package/dist/clis/ctrip/search.js +73 -48
- package/dist/clis/ctrip/search.test.d.ts +1 -0
- package/dist/clis/ctrip/search.test.js +64 -0
- package/dist/clis/douyin/_shared/sts2.js +8 -2
- package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/sts2.test.js +27 -0
- package/dist/clis/douyin/activities.js +4 -2
- package/dist/clis/douyin/activities.test.js +34 -1
- package/dist/clis/douyin/collections.js +1 -1
- package/dist/clis/douyin/collections.test.js +24 -2
- package/dist/clis/douyin/draft.d.ts +8 -11
- package/dist/clis/douyin/draft.js +302 -185
- package/dist/clis/douyin/draft.test.d.ts +1 -1
- package/dist/clis/douyin/draft.test.js +357 -2
- package/dist/clis/douyin/hashtag.js +9 -2
- package/dist/clis/douyin/hashtag.test.js +35 -2
- package/dist/clis/douyin/profile.js +1 -1
- package/dist/clis/douyin/profile.test.js +36 -1
- package/dist/clis/douyin/videos.js +22 -5
- package/dist/clis/douyin/videos.test.js +45 -2
- package/dist/clis/facebook/search.test.d.ts +5 -0
- package/dist/clis/facebook/search.test.js +60 -0
- package/dist/clis/facebook/search.yaml +4 -3
- package/dist/clis/instagram/download.d.ts +16 -0
- package/dist/clis/instagram/download.js +225 -0
- package/dist/clis/instagram/download.test.d.ts +1 -0
- package/dist/clis/instagram/download.test.js +118 -0
- package/dist/clis/notebooklm/bind-current.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.js +29 -0
- package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.test.js +35 -0
- package/dist/clis/notebooklm/binding.test.d.ts +1 -0
- package/dist/clis/notebooklm/binding.test.js +44 -0
- package/dist/clis/notebooklm/compat.test.d.ts +3 -0
- package/dist/clis/notebooklm/compat.test.js +16 -0
- package/dist/clis/notebooklm/current.d.ts +1 -0
- package/dist/clis/notebooklm/current.js +28 -0
- package/dist/clis/notebooklm/get.d.ts +1 -0
- package/dist/clis/notebooklm/get.js +37 -0
- package/dist/clis/notebooklm/history.d.ts +1 -0
- package/dist/clis/notebooklm/history.js +25 -0
- package/dist/clis/notebooklm/history.test.d.ts +1 -0
- package/dist/clis/notebooklm/history.test.js +58 -0
- package/dist/clis/notebooklm/list.d.ts +1 -0
- package/dist/clis/notebooklm/list.js +35 -0
- package/dist/clis/notebooklm/note-list.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.js +28 -0
- package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.test.js +56 -0
- package/dist/clis/notebooklm/notes-get.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.js +47 -0
- package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.test.js +72 -0
- package/dist/clis/notebooklm/rpc.d.ts +36 -0
- package/dist/clis/notebooklm/rpc.js +189 -0
- package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
- package/dist/clis/notebooklm/rpc.test.js +105 -0
- package/dist/clis/notebooklm/shared.d.ts +87 -0
- package/dist/clis/notebooklm/shared.js +3 -0
- package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.js +44 -0
- package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
- package/dist/clis/notebooklm/source-get.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.js +40 -0
- package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.test.js +84 -0
- package/dist/clis/notebooklm/source-guide.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.js +44 -0
- package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.test.js +104 -0
- package/dist/clis/notebooklm/source-list.d.ts +1 -0
- package/dist/clis/notebooklm/source-list.js +30 -0
- package/dist/clis/notebooklm/status.d.ts +1 -0
- package/dist/clis/notebooklm/status.js +31 -0
- package/dist/clis/notebooklm/summary.d.ts +1 -0
- package/dist/clis/notebooklm/summary.js +30 -0
- package/dist/clis/notebooklm/summary.test.d.ts +1 -0
- package/dist/clis/notebooklm/summary.test.js +78 -0
- package/dist/clis/notebooklm/utils.d.ts +37 -0
- package/dist/clis/notebooklm/utils.js +739 -0
- package/dist/clis/notebooklm/utils.test.d.ts +1 -0
- package/dist/clis/notebooklm/utils.test.js +390 -0
- package/dist/clis/substack/utils.d.ts +4 -0
- package/dist/clis/substack/utils.js +8 -2
- package/dist/clis/substack/utils.test.d.ts +1 -0
- package/dist/clis/substack/utils.test.js +46 -0
- package/dist/clis/v2ex/hot.yaml +4 -1
- package/dist/clis/v2ex/latest.yaml +4 -1
- package/dist/clis/v2ex/topic.yaml +6 -1
- package/dist/clis/weixin/download.d.ts +9 -0
- package/dist/clis/weixin/download.js +76 -6
- package/dist/clis/weread/book.js +108 -2
- package/dist/clis/weread/commands.test.js +262 -152
- package/dist/clis/weread/utils.d.ts +10 -0
- package/dist/clis/weread/utils.js +27 -7
- package/dist/clis/xiaohongshu/comments.d.ts +3 -0
- package/dist/clis/xiaohongshu/comments.js +76 -17
- package/dist/clis/xiaohongshu/comments.test.js +70 -9
- package/dist/clis/xiaohongshu/download.d.ts +4 -1
- package/dist/clis/xiaohongshu/download.js +83 -22
- package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/download.test.js +75 -0
- package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
- package/dist/clis/xiaohongshu/note-helpers.js +23 -0
- package/dist/clis/xiaohongshu/note.d.ts +7 -0
- package/dist/clis/xiaohongshu/note.js +76 -0
- package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/note.test.js +136 -0
- package/dist/clis/xiaohongshu/search.js +9 -0
- package/dist/clis/xiaohongshu/search.test.js +10 -4
- package/dist/clis/youtube/search.js +57 -17
- package/dist/clis/zhihu/question.js +19 -17
- package/dist/clis/zhihu/question.test.d.ts +1 -0
- package/dist/clis/zhihu/question.test.js +54 -0
- package/dist/commanderAdapter.js +9 -0
- package/dist/commanderAdapter.test.js +25 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +124 -0
- package/dist/commands/daemon.test.d.ts +1 -0
- package/dist/commands/daemon.test.js +185 -0
- package/dist/completion.js +3 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +25 -14
- package/dist/daemon.test.d.ts +1 -0
- package/dist/daemon.test.js +65 -0
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +47 -2
- package/dist/electron-apps.d.ts +29 -0
- package/dist/electron-apps.js +65 -0
- package/dist/electron-apps.test.d.ts +1 -0
- package/dist/electron-apps.test.js +43 -0
- package/dist/engine.test.js +41 -9
- package/dist/execution.js +20 -16
- package/dist/idle-manager.d.ts +19 -0
- package/dist/idle-manager.js +54 -0
- package/dist/launcher.d.ts +36 -0
- package/dist/launcher.js +152 -0
- package/dist/launcher.test.d.ts +1 -0
- package/dist/launcher.test.js +57 -0
- package/dist/main.js +3 -3
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +31 -3
- package/dist/registry.test.js +13 -0
- package/dist/runtime.d.ts +5 -3
- package/dist/runtime.js +12 -5
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +3 -0
- package/dist/serialization.test.js +17 -1
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +52 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +19 -0
- package/dist/weixin-download.test.js +14 -0
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/notebooklm.md +69 -0
- package/docs/adapters/browser/xiaohongshu.md +19 -10
- package/docs/adapters/index.md +67 -66
- package/docs/guide/browser-bridge.md +12 -0
- package/docs/guide/troubleshooting.md +9 -4
- package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
- package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
- package/docs/zh/guide/browser-bridge.md +12 -0
- package/extension/dist/background.js +794 -513
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +174 -10
- package/extension/src/cdp.ts +12 -0
- package/extension/src/protocol.ts +7 -5
- package/package.json +1 -1
- package/src/browser/cdp.ts +24 -17
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.test.ts +15 -1
- package/src/browser/dom-helpers.ts +1 -0
- package/src/browser/mcp.ts +18 -13
- package/src/browser/page.test.ts +58 -0
- package/src/browser/page.ts +18 -2
- package/src/browser/stealth.test.ts +153 -0
- package/src/browser/stealth.ts +198 -0
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.test.ts +2 -0
- package/src/build-manifest.ts +6 -1
- package/src/cli.ts +21 -3
- package/src/clis/antigravity/SKILL.md +3 -12
- package/src/clis/antigravity/serve.ts +5 -10
- package/src/clis/bilibili/subtitle.test.ts +60 -0
- package/src/clis/bilibili/subtitle.ts +4 -0
- package/src/clis/chatwise/ask.ts +0 -2
- package/src/clis/chatwise/export.ts +0 -2
- package/src/clis/chatwise/history.ts +0 -2
- package/src/clis/chatwise/model.ts +0 -2
- package/src/clis/chatwise/new.ts +1 -2
- package/src/clis/chatwise/read.ts +0 -2
- package/src/clis/chatwise/screenshot.ts +1 -2
- package/src/clis/chatwise/send.ts +0 -2
- package/src/clis/chatwise/status.ts +1 -2
- package/src/clis/ctrip/search.test.ts +73 -0
- package/src/clis/ctrip/search.ts +97 -47
- package/src/clis/douyin/_shared/sts2.test.ts +31 -0
- package/src/clis/douyin/_shared/sts2.ts +11 -3
- package/src/clis/douyin/activities.test.ts +41 -1
- package/src/clis/douyin/activities.ts +12 -3
- package/src/clis/douyin/collections.test.ts +35 -2
- package/src/clis/douyin/collections.ts +1 -1
- package/src/clis/douyin/draft.test.ts +444 -2
- package/src/clis/douyin/draft.ts +382 -218
- package/src/clis/douyin/hashtag.test.ts +42 -2
- package/src/clis/douyin/hashtag.ts +11 -3
- package/src/clis/douyin/profile.test.ts +43 -1
- package/src/clis/douyin/profile.ts +9 -2
- package/src/clis/douyin/videos.test.ts +52 -2
- package/src/clis/douyin/videos.ts +49 -15
- package/src/clis/facebook/search.test.ts +70 -0
- package/src/clis/facebook/search.yaml +4 -3
- package/src/clis/instagram/download.test.ts +159 -0
- package/src/clis/instagram/download.ts +286 -0
- package/src/clis/notebooklm/bind-current.test.ts +43 -0
- package/src/clis/notebooklm/bind-current.ts +36 -0
- package/src/clis/notebooklm/binding.test.ts +53 -0
- package/src/clis/notebooklm/compat.test.ts +19 -0
- package/src/clis/notebooklm/current.ts +38 -0
- package/src/clis/notebooklm/get.ts +53 -0
- package/src/clis/notebooklm/history.test.ts +70 -0
- package/src/clis/notebooklm/history.ts +36 -0
- package/src/clis/notebooklm/list.ts +40 -0
- package/src/clis/notebooklm/note-list.test.ts +64 -0
- package/src/clis/notebooklm/note-list.ts +42 -0
- package/src/clis/notebooklm/notes-get.test.ts +88 -0
- package/src/clis/notebooklm/notes-get.ts +67 -0
- package/src/clis/notebooklm/rpc.test.ts +126 -0
- package/src/clis/notebooklm/rpc.ts +286 -0
- package/src/clis/notebooklm/shared.ts +98 -0
- package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
- package/src/clis/notebooklm/source-fulltext.ts +69 -0
- package/src/clis/notebooklm/source-get.test.ts +100 -0
- package/src/clis/notebooklm/source-get.ts +60 -0
- package/src/clis/notebooklm/source-guide.test.ts +121 -0
- package/src/clis/notebooklm/source-guide.ts +69 -0
- package/src/clis/notebooklm/source-list.ts +45 -0
- package/src/clis/notebooklm/status.ts +34 -0
- package/src/clis/notebooklm/summary.test.ts +94 -0
- package/src/clis/notebooklm/summary.ts +45 -0
- package/src/clis/notebooklm/utils.test.ts +446 -0
- package/src/clis/notebooklm/utils.ts +893 -0
- package/src/clis/substack/utils.test.ts +54 -0
- package/src/clis/substack/utils.ts +10 -2
- package/src/clis/v2ex/hot.yaml +4 -1
- package/src/clis/v2ex/latest.yaml +4 -1
- package/src/clis/v2ex/topic.yaml +6 -1
- package/src/clis/weixin/download.ts +95 -6
- package/src/clis/weread/book.ts +142 -2
- package/src/clis/weread/commands.test.ts +314 -154
- package/src/clis/weread/utils.ts +33 -4
- package/src/clis/xiaohongshu/comments.test.ts +85 -9
- package/src/clis/xiaohongshu/comments.ts +76 -17
- package/src/clis/xiaohongshu/download.test.ts +96 -0
- package/src/clis/xiaohongshu/download.ts +83 -22
- package/src/clis/xiaohongshu/note-helpers.ts +25 -0
- package/src/clis/xiaohongshu/note.test.ts +164 -0
- package/src/clis/xiaohongshu/note.ts +86 -0
- package/src/clis/xiaohongshu/search.test.ts +11 -4
- package/src/clis/xiaohongshu/search.ts +13 -0
- package/src/clis/youtube/search.ts +57 -17
- package/src/clis/zhihu/question.test.ts +71 -0
- package/src/clis/zhihu/question.ts +27 -15
- package/src/commanderAdapter.test.ts +30 -0
- package/src/commanderAdapter.ts +7 -0
- package/src/commands/daemon.test.ts +238 -0
- package/src/commands/daemon.ts +135 -0
- package/src/completion.ts +2 -1
- package/src/constants.ts +3 -0
- package/src/daemon.test.ts +88 -0
- package/src/daemon.ts +26 -14
- package/src/discovery.ts +52 -2
- package/src/electron-apps.test.ts +50 -0
- package/src/electron-apps.ts +89 -0
- package/src/engine.test.ts +45 -9
- package/src/execution.ts +24 -19
- package/src/idle-manager.ts +60 -0
- package/src/launcher.test.ts +67 -0
- package/src/launcher.ts +185 -0
- package/src/main.ts +3 -2
- package/src/registry.test.ts +15 -0
- package/src/registry.ts +32 -3
- package/src/runtime.ts +13 -7
- package/src/serialization.test.ts +19 -1
- package/src/serialization.ts +2 -0
- package/src/tui.test.ts +23 -0
- package/src/tui.ts +65 -0
- package/src/weixin-download.test.ts +27 -0
- package/tests/e2e/browser-public-extended.test.ts +6 -2
- package/chatwise-opencli.ps1 +0 -82
- package/dist/clis/chatwise/shared.d.ts +0 -2
- package/dist/clis/chatwise/shared.js +0 -6
- 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
|
+
```
|