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