@jackwener/opencli 1.0.3 → 1.0.4

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 (190) hide show
  1. package/.github/workflows/build-extension.yml +21 -3
  2. package/.github/workflows/docs.yml +52 -0
  3. package/README.md +28 -28
  4. package/README.zh-CN.md +28 -28
  5. package/dist/browser/cdp.d.ts +16 -1
  6. package/dist/browser/cdp.js +124 -80
  7. package/dist/browser/daemon-client.d.ts +3 -1
  8. package/dist/browser/daemon-client.js +4 -0
  9. package/dist/browser/dom-helpers.d.ts +20 -0
  10. package/dist/browser/dom-helpers.js +109 -0
  11. package/dist/browser/mcp.d.ts +1 -0
  12. package/dist/browser/mcp.js +10 -5
  13. package/dist/browser/page.d.ts +7 -0
  14. package/dist/browser/page.js +37 -100
  15. package/dist/browser.test.js +7 -0
  16. package/dist/build-manifest.js +3 -1
  17. package/dist/build-manifest.test.js +34 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +30 -0
  20. package/dist/capabilityRouting.test.d.ts +1 -0
  21. package/dist/capabilityRouting.test.js +42 -0
  22. package/dist/chaoxing.test.js +11 -4
  23. package/dist/cli-manifest.json +635 -1
  24. package/dist/cli.js +45 -8
  25. package/dist/clis/antigravity/serve.d.ts +14 -0
  26. package/dist/clis/antigravity/serve.js +263 -0
  27. package/dist/clis/bilibili/download.js +4 -14
  28. package/dist/clis/boss/resume.d.ts +1 -0
  29. package/dist/clis/boss/resume.js +249 -0
  30. package/dist/clis/hf/top.d.ts +1 -0
  31. package/dist/clis/hf/top.js +119 -0
  32. package/dist/clis/jike/comment.d.ts +1 -0
  33. package/dist/clis/jike/comment.js +107 -0
  34. package/dist/clis/jike/create.d.ts +1 -0
  35. package/dist/clis/jike/create.js +106 -0
  36. package/dist/clis/jike/feed.d.ts +1 -0
  37. package/dist/clis/jike/feed.js +67 -0
  38. package/dist/clis/jike/like.d.ts +1 -0
  39. package/dist/clis/jike/like.js +61 -0
  40. package/dist/clis/jike/notifications.d.ts +1 -0
  41. package/dist/clis/jike/notifications.js +169 -0
  42. package/dist/clis/jike/post.yaml +58 -0
  43. package/dist/clis/jike/repost.d.ts +1 -0
  44. package/dist/clis/jike/repost.js +103 -0
  45. package/dist/clis/jike/search.d.ts +1 -0
  46. package/dist/clis/jike/search.js +67 -0
  47. package/dist/clis/jike/shared.d.ts +19 -0
  48. package/dist/clis/jike/shared.js +25 -0
  49. package/dist/clis/jike/topic.yaml +52 -0
  50. package/dist/clis/jike/user.yaml +51 -0
  51. package/dist/clis/smzdm/search.js +28 -39
  52. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  53. package/dist/clis/stackoverflow/hot.yaml +28 -0
  54. package/dist/clis/stackoverflow/search.yaml +32 -0
  55. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  56. package/dist/clis/twitter/download.js +6 -16
  57. package/dist/clis/xiaohongshu/download.js +3 -3
  58. package/dist/clis/zhihu/download.js +3 -3
  59. package/dist/doctor.d.ts +7 -0
  60. package/dist/doctor.js +16 -0
  61. package/dist/download/index.d.ts +12 -8
  62. package/dist/download/index.js +11 -3
  63. package/dist/download/index.test.d.ts +1 -0
  64. package/dist/download/index.test.js +14 -0
  65. package/dist/engine.js +5 -5
  66. package/dist/explore.d.ts +1 -0
  67. package/dist/explore.js +3 -3
  68. package/dist/generate.js +1 -0
  69. package/dist/interceptor.js +3 -2
  70. package/dist/output.d.ts +1 -0
  71. package/dist/output.js +3 -1
  72. package/dist/pipeline/executor.test.js +1 -0
  73. package/dist/pipeline/steps/download.js +14 -18
  74. package/dist/registry.d.ts +1 -0
  75. package/dist/registry.js +5 -2
  76. package/dist/runtime.d.ts +4 -1
  77. package/dist/runtime.js +2 -2
  78. package/dist/types.d.ts +12 -0
  79. package/dist/verify.d.ts +6 -1
  80. package/dist/verify.js +54 -2
  81. package/docs/.vitepress/config.mts +193 -0
  82. package/docs/adapters/browser/apple-podcasts.md +28 -0
  83. package/docs/adapters/browser/bbc.md +26 -0
  84. package/docs/adapters/browser/bilibili.md +38 -0
  85. package/docs/adapters/browser/boss.md +28 -0
  86. package/docs/adapters/browser/coupang.md +28 -0
  87. package/docs/adapters/browser/ctrip.md +27 -0
  88. package/docs/adapters/browser/github.md +26 -0
  89. package/docs/adapters/browser/hackernews.md +26 -0
  90. package/docs/adapters/browser/linkedin.md +27 -0
  91. package/docs/adapters/browser/reddit.md +41 -0
  92. package/docs/adapters/browser/reuters.md +27 -0
  93. package/docs/adapters/browser/smzdm.md +27 -0
  94. package/docs/adapters/browser/twitter.md +47 -0
  95. package/docs/adapters/browser/v2ex.md +32 -0
  96. package/docs/adapters/browser/weibo.md +27 -0
  97. package/docs/adapters/browser/xiaohongshu.md +32 -0
  98. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  99. package/docs/adapters/browser/xueqiu.md +32 -0
  100. package/docs/adapters/browser/yahoo-finance.md +26 -0
  101. package/docs/adapters/browser/youtube.md +29 -0
  102. package/docs/adapters/browser/zhihu.md +30 -0
  103. package/docs/adapters/desktop/antigravity.md +46 -0
  104. package/docs/adapters/desktop/chatgpt.md +43 -0
  105. package/docs/adapters/desktop/chatwise.md +38 -0
  106. package/docs/adapters/desktop/codex.md +32 -0
  107. package/docs/adapters/desktop/cursor.md +33 -0
  108. package/docs/adapters/desktop/discord.md +28 -0
  109. package/docs/adapters/desktop/feishu.md +20 -0
  110. package/docs/adapters/desktop/neteasemusic.md +31 -0
  111. package/docs/adapters/desktop/notion.md +29 -0
  112. package/docs/adapters/desktop/wechat.md +28 -0
  113. package/docs/adapters/index.md +49 -0
  114. package/docs/advanced/cdp.md +103 -0
  115. package/docs/advanced/download.md +63 -0
  116. package/docs/advanced/electron.md +125 -0
  117. package/docs/advanced/remote-chrome.md +72 -0
  118. package/docs/developer/ai-workflow.md +66 -0
  119. package/docs/developer/architecture.md +90 -0
  120. package/docs/developer/contributing.md +136 -0
  121. package/docs/developer/testing.md +237 -0
  122. package/docs/developer/ts-adapter.md +87 -0
  123. package/docs/developer/yaml-adapter.md +108 -0
  124. package/docs/guide/browser-bridge.md +38 -0
  125. package/docs/guide/getting-started.md +56 -0
  126. package/docs/guide/installation.md +37 -0
  127. package/docs/guide/troubleshooting.md +56 -0
  128. package/docs/index.md +35 -0
  129. package/docs/zh/adapters/index.md +5 -0
  130. package/docs/zh/advanced/cdp.md +3 -0
  131. package/docs/zh/developer/contributing.md +24 -0
  132. package/docs/zh/guide/browser-bridge.md +25 -0
  133. package/docs/zh/guide/getting-started.md +40 -0
  134. package/docs/zh/guide/installation.md +37 -0
  135. package/docs/zh/index.md +29 -0
  136. package/extension/dist/background.js +92 -52
  137. package/extension/package-lock.json +1156 -0
  138. package/extension/src/background.test.ts +151 -0
  139. package/extension/src/background.ts +122 -51
  140. package/extension/src/protocol.ts +3 -1
  141. package/package.json +7 -3
  142. package/src/browser/cdp.ts +154 -82
  143. package/src/browser/daemon-client.ts +7 -1
  144. package/src/browser/dom-helpers.ts +116 -0
  145. package/src/browser/mcp.ts +14 -6
  146. package/src/browser/page.ts +45 -100
  147. package/src/browser.test.ts +10 -0
  148. package/src/build-manifest.test.ts +36 -0
  149. package/src/build-manifest.ts +2 -1
  150. package/src/capabilityRouting.test.ts +47 -0
  151. package/src/capabilityRouting.ts +28 -0
  152. package/src/chaoxing.test.ts +12 -4
  153. package/src/cli.ts +28 -8
  154. package/src/clis/antigravity/serve.ts +329 -0
  155. package/src/clis/bilibili/download.ts +4 -15
  156. package/src/clis/boss/resume.ts +262 -0
  157. package/src/clis/hf/top.ts +141 -0
  158. package/src/clis/jike/comment.ts +113 -0
  159. package/src/clis/jike/create.ts +113 -0
  160. package/src/clis/jike/feed.ts +74 -0
  161. package/src/clis/jike/like.ts +65 -0
  162. package/src/clis/jike/notifications.ts +185 -0
  163. package/src/clis/jike/post.yaml +58 -0
  164. package/src/clis/jike/repost.ts +114 -0
  165. package/src/clis/jike/search.ts +74 -0
  166. package/src/clis/jike/shared.ts +36 -0
  167. package/src/clis/jike/topic.yaml +52 -0
  168. package/src/clis/jike/user.yaml +51 -0
  169. package/src/clis/smzdm/search.ts +30 -39
  170. package/src/clis/stackoverflow/bounties.yaml +29 -0
  171. package/src/clis/stackoverflow/hot.yaml +28 -0
  172. package/src/clis/stackoverflow/search.yaml +32 -0
  173. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  174. package/src/clis/twitter/download.ts +6 -17
  175. package/src/clis/xiaohongshu/download.ts +3 -3
  176. package/src/clis/zhihu/download.ts +3 -3
  177. package/src/doctor.ts +18 -2
  178. package/src/download/index.test.ts +16 -0
  179. package/src/download/index.ts +22 -4
  180. package/src/engine.ts +4 -4
  181. package/src/explore.ts +4 -4
  182. package/src/generate.ts +1 -0
  183. package/src/interceptor.ts +3 -2
  184. package/src/output.ts +3 -1
  185. package/src/pipeline/executor.test.ts +1 -0
  186. package/src/pipeline/steps/download.ts +14 -17
  187. package/src/registry.ts +6 -2
  188. package/src/runtime.ts +3 -2
  189. package/src/types.ts +9 -0
  190. package/src/verify.ts +64 -3
@@ -0,0 +1,56 @@
1
+ # Troubleshooting
2
+
3
+ ## Common Issues
4
+
5
+ ### "Extension not connected"
6
+
7
+ - Ensure the opencli Browser Bridge extension is installed and **enabled** in `chrome://extensions`.
8
+ - Run `opencli doctor` to diagnose connectivity.
9
+
10
+ ### Empty data or 'Unauthorized' error
11
+
12
+ - Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page.
13
+ - Some sites have geographic restrictions (e.g., Bilibili, Zhihu from outside China).
14
+
15
+ ### Node API errors
16
+
17
+ - Make sure you are using **Node.js >= 20**. Some dependencies require modern Node APIs.
18
+ - Run `node --version` to verify.
19
+
20
+ ### Daemon issues
21
+
22
+ ```bash
23
+ # Check daemon status
24
+ curl localhost:19825/status
25
+
26
+ # View extension logs
27
+ curl localhost:19825/logs
28
+
29
+ # Kill and restart daemon
30
+ pkill -f opencli-daemon
31
+ opencli doctor
32
+ ```
33
+
34
+ ### Desktop adapter connection issues
35
+
36
+ For Electron/CDP-based adapters (Cursor, Codex, etc.):
37
+
38
+ 1. Make sure the app is launched with `--remote-debugging-port=XXXX`
39
+ 2. Verify the endpoint is set: `echo $OPENCLI_CDP_ENDPOINT`
40
+ 3. Test the endpoint: `curl http://127.0.0.1:XXXX/json/version`
41
+
42
+ ### Build errors
43
+
44
+ ```bash
45
+ # Clean rebuild
46
+ rm -rf dist/
47
+ npm run build
48
+
49
+ # Type check
50
+ npx tsc --noEmit
51
+ ```
52
+
53
+ ## Getting Help
54
+
55
+ - [GitHub Issues](https://github.com/jackwener/opencli/issues) — Bug reports and feature requests
56
+ - Run `opencli doctor --live` for comprehensive diagnostics
package/docs/index.md ADDED
@@ -0,0 +1,35 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: OpenCLI
6
+ text: Make any website or Electron App your CLI
7
+ tagline: Zero risk · Reuse Chrome login · AI-powered discovery · Browser + Desktop automation
8
+ actions:
9
+ - theme: brand
10
+ text: Get Started
11
+ link: /guide/getting-started
12
+ - theme: alt
13
+ text: View on GitHub
14
+ link: https://github.com/jackwener/opencli
15
+
16
+ features:
17
+ - icon: 🖥️
18
+ title: CLI All Electron
19
+ details: Turn ANY Electron application into a CLI tool — Cursor, Codex, Antigravity, ChatGPT, Notion, and more. AI can control itself natively.
20
+ - icon: 🔐
21
+ title: Account Safe
22
+ details: Reuses Chrome's logged-in state. Your credentials never leave the browser — no tokens, no exposed passwords.
23
+ - icon: 🤖
24
+ title: AI Agent Ready
25
+ details: "explore discovers APIs, synthesize generates adapters, cascade finds auth strategies. Built for AI-first workflows."
26
+ - icon: ⚡
27
+ title: Dual-Engine Architecture
28
+ details: Supports both YAML declarative data pipelines and robust browser runtime TypeScript injections for maximum flexibility.
29
+ - icon: 🔧
30
+ title: Self-Healing Setup
31
+ details: "opencli setup verifies Browser Bridge connectivity. opencli doctor diagnoses daemon, extension, and live browser."
32
+ - icon: 📦
33
+ title: Dynamic Loader
34
+ details: Simply drop .ts or .yaml adapters into the clis/ folder for auto-registration. Zero boilerplate.
35
+ ---
@@ -0,0 +1,5 @@
1
+ # 所有适配器
2
+
3
+ 运行 `opencli list` 查看完整命令列表。
4
+
5
+ 详细文档请参考 [英文版本](/adapters/)。
@@ -0,0 +1,3 @@
1
+ # Chrome DevTools Protocol
2
+
3
+ 详细文档请参考 [英文版本](/advanced/cdp)。
@@ -0,0 +1,24 @@
1
+ # 贡献指南
2
+
3
+ 详细贡献指南请参考 [英文版本](/developer/contributing)。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ git clone git@github.com:<your-username>/opencli.git
9
+ cd opencli
10
+ npm install
11
+ npm run build
12
+ npx tsc --noEmit
13
+ npx vitest run src/
14
+ ```
15
+
16
+ ## 提交规范
17
+
18
+ 使用 [Conventional Commits](https://www.conventionalcommits.org/):
19
+
20
+ ```
21
+ feat(twitter): add thread command
22
+ fix(browser): handle CDP timeout gracefully
23
+ docs: update CONTRIBUTING.md
24
+ ```
@@ -0,0 +1,25 @@
1
+ # Browser Bridge 设置
2
+
3
+ > **⚠️ 重要**: 浏览器命令复用你的 Chrome 登录会话。运行命令前必须在 Chrome 中登录目标网站。
4
+
5
+ OpenCLI 通过轻量级 **Browser Bridge** Chrome 扩展 + 微守护进程连接浏览器(零配置,自动启动)。
6
+
7
+ ## 扩展安装
8
+
9
+ ### 方法 1:下载预构建版本(推荐)
10
+
11
+ 1. 前往 GitHub [Releases 页面](https://github.com/jackwener/opencli/releases) 下载最新的 `opencli-extension.zip` 或 `opencli-extension.crx`。
12
+ 2. 打开 `chrome://extensions`,启用**开发者模式**。
13
+ 3. 拖放 `.crx` 文件或解压后的文件夹到扩展页面。
14
+
15
+ ### 方法 2:加载源码(开发者)
16
+
17
+ 1. 打开 `chrome://extensions`,启用**开发者模式**。
18
+ 2. 点击**加载已解压的扩展程序**,选择仓库中的 `extension/` 目录。
19
+
20
+ ## 验证
21
+
22
+ ```bash
23
+ opencli doctor # 检查扩展 + 守护进程连接
24
+ opencli doctor --live # 同时测试实时浏览器命令
25
+ ```
@@ -0,0 +1,40 @@
1
+ # 快速开始
2
+
3
+ > **让任何网站或 Electron 应用成为你的 CLI。**
4
+ > 零风险 · 复用 Chrome 登录态 · AI 驱动发现 · 浏览器 + 桌面自动化
5
+
6
+ OpenCLI 将**任何网站**或 **Electron 应用**变成命令行界面 — Bilibili、知乎、小红书、Twitter/X、Reddit、YouTube、Antigravity 等 — 基于浏览器会话复用和 AI 原生发现。
7
+
8
+ ## 安装
9
+
10
+ ```bash
11
+ npm install -g @jackwener/opencli
12
+ ```
13
+
14
+ ## 基本使用
15
+
16
+ ```bash
17
+ opencli list # 查看所有命令
18
+ opencli hackernews top --limit 5 # 公开 API,无需浏览器
19
+ opencli bilibili hot --limit 5 # 浏览器命令
20
+ opencli zhihu hot -f json # JSON 输出
21
+ ```
22
+
23
+ ## 输出格式
24
+
25
+ 所有命令支持 `--format` / `-f`:
26
+
27
+ ```bash
28
+ opencli bilibili hot -f table # 默认:终端表格
29
+ opencli bilibili hot -f json # JSON
30
+ opencli bilibili hot -f yaml # YAML
31
+ opencli bilibili hot -f md # Markdown
32
+ opencli bilibili hot -f csv # CSV
33
+ ```
34
+
35
+ ## 下一步
36
+
37
+ - [安装详情](/zh/guide/installation)
38
+ - [Browser Bridge 设置](/zh/guide/browser-bridge)
39
+ - [所有适配器](/zh/adapters/)
40
+ - [开发者指南](/zh/developer/contributing)
@@ -0,0 +1,37 @@
1
+ # 安装
2
+
3
+ ## 系统要求
4
+
5
+ - **Node.js**: >= 20.0.0
6
+ - **Chrome** 已运行并登录目标网站(浏览器命令需要)
7
+
8
+ ## 通过 npm 安装(推荐)
9
+
10
+ ```bash
11
+ npm install -g @jackwener/opencli
12
+ ```
13
+
14
+ ## 从源码安装
15
+
16
+ ```bash
17
+ git clone git@github.com:jackwener/opencli.git
18
+ cd opencli
19
+ npm install
20
+ npm run build
21
+ npm link
22
+ opencli list
23
+ ```
24
+
25
+ ## 更新
26
+
27
+ ```bash
28
+ npm install -g @jackwener/opencli@latest
29
+ ```
30
+
31
+ ## 验证安装
32
+
33
+ ```bash
34
+ opencli --version
35
+ opencli list
36
+ opencli doctor
37
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: OpenCLI
6
+ text: 让任何网站或 Electron 应用成为你的 CLI
7
+ tagline: 零风险 · 复用 Chrome 登录态 · AI 驱动发现 · 浏览器 + 桌面自动化
8
+ actions:
9
+ - theme: brand
10
+ text: 快速开始
11
+ link: /zh/guide/getting-started
12
+ - theme: alt
13
+ text: 在 GitHub 查看
14
+ link: https://github.com/jackwener/opencli
15
+
16
+ features:
17
+ - icon: 🖥️
18
+ title: CLI 所有 Electron 应用
19
+ details: 将任何 Electron 应用变成 CLI 工具 — Cursor、Codex、Antigravity、ChatGPT、Notion 等。AI 可以原生控制自身。
20
+ - icon: 🔐
21
+ title: 账号安全
22
+ details: 复用 Chrome 登录态,凭证永远不会离开浏览器 — 无 token,无密码泄露。
23
+ - icon: 🤖
24
+ title: AI Agent 就绪
25
+ details: explore 发现 API,synthesize 生成适配器,cascade 查找认证策略。为 AI 优先工作流而生。
26
+ - icon: ⚡
27
+ title: 双引擎架构
28
+ details: 同时支持 YAML 声明式数据管道和强大的浏览器运行时 TypeScript 注入。
29
+ ---
@@ -160,30 +160,35 @@ function scheduleReconnect() {
160
160
  connect();
161
161
  }, delay);
162
162
  }
163
- let automationWindowId = null;
164
- let windowIdleTimer = null;
163
+ const automationSessions = /* @__PURE__ */ new Map();
165
164
  const WINDOW_IDLE_TIMEOUT = 3e4;
166
- function resetWindowIdleTimer() {
167
- if (windowIdleTimer) clearTimeout(windowIdleTimer);
168
- windowIdleTimer = setTimeout(async () => {
169
- if (automationWindowId !== null) {
170
- try {
171
- await chrome.windows.remove(automationWindowId);
172
- console.log(`[opencli] Automation window ${automationWindowId} closed (idle timeout)`);
173
- } catch {
174
- }
175
- automationWindowId = null;
165
+ function getWorkspaceKey(workspace) {
166
+ return workspace?.trim() || "default";
167
+ }
168
+ function resetWindowIdleTimer(workspace) {
169
+ const session = automationSessions.get(workspace);
170
+ if (!session) return;
171
+ if (session.idleTimer) clearTimeout(session.idleTimer);
172
+ session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
173
+ session.idleTimer = setTimeout(async () => {
174
+ const current = automationSessions.get(workspace);
175
+ if (!current) return;
176
+ try {
177
+ await chrome.windows.remove(current.windowId);
178
+ console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
179
+ } catch {
176
180
  }
177
- windowIdleTimer = null;
181
+ automationSessions.delete(workspace);
178
182
  }, WINDOW_IDLE_TIMEOUT);
179
183
  }
180
- async function getAutomationWindow() {
181
- if (automationWindowId !== null) {
184
+ async function getAutomationWindow(workspace) {
185
+ const existing = automationSessions.get(workspace);
186
+ if (existing) {
182
187
  try {
183
- await chrome.windows.get(automationWindowId);
184
- return automationWindowId;
188
+ await chrome.windows.get(existing.windowId);
189
+ return existing.windowId;
185
190
  } catch {
186
- automationWindowId = null;
191
+ automationSessions.delete(workspace);
187
192
  }
188
193
  }
189
194
  const win = await chrome.windows.create({
@@ -193,17 +198,22 @@ async function getAutomationWindow() {
193
198
  height: 900,
194
199
  type: "normal"
195
200
  });
196
- automationWindowId = win.id;
197
- console.log(`[opencli] Created automation window ${automationWindowId}`);
198
- return automationWindowId;
201
+ const session = {
202
+ windowId: win.id,
203
+ idleTimer: null,
204
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
205
+ };
206
+ automationSessions.set(workspace, session);
207
+ console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
208
+ resetWindowIdleTimer(workspace);
209
+ return session.windowId;
199
210
  }
200
211
  chrome.windows.onRemoved.addListener((windowId) => {
201
- if (windowId === automationWindowId) {
202
- console.log("[opencli] Automation window closed");
203
- automationWindowId = null;
204
- if (windowIdleTimer) {
205
- clearTimeout(windowIdleTimer);
206
- windowIdleTimer = null;
212
+ for (const [workspace, session] of automationSessions.entries()) {
213
+ if (session.windowId === windowId) {
214
+ console.log(`[opencli] Automation window closed (${workspace})`);
215
+ if (session.idleTimer) clearTimeout(session.idleTimer);
216
+ automationSessions.delete(workspace);
207
217
  }
208
218
  }
209
219
  });
@@ -226,21 +236,24 @@ chrome.alarms.onAlarm.addListener((alarm) => {
226
236
  if (alarm.name === "keepalive") connect();
227
237
  });
228
238
  async function handleCommand(cmd) {
229
- resetWindowIdleTimer();
239
+ const workspace = getWorkspaceKey(cmd.workspace);
240
+ resetWindowIdleTimer(workspace);
230
241
  try {
231
242
  switch (cmd.action) {
232
243
  case "exec":
233
- return await handleExec(cmd);
244
+ return await handleExec(cmd, workspace);
234
245
  case "navigate":
235
- return await handleNavigate(cmd);
246
+ return await handleNavigate(cmd, workspace);
236
247
  case "tabs":
237
- return await handleTabs(cmd);
248
+ return await handleTabs(cmd, workspace);
238
249
  case "cookies":
239
250
  return await handleCookies(cmd);
240
251
  case "screenshot":
241
- return await handleScreenshot(cmd);
252
+ return await handleScreenshot(cmd, workspace);
242
253
  case "close-window":
243
- return await handleCloseWindow(cmd);
254
+ return await handleCloseWindow(cmd, workspace);
255
+ case "sessions":
256
+ return await handleSessions(cmd);
244
257
  default:
245
258
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
246
259
  }
@@ -256,9 +269,9 @@ function isWebUrl(url) {
256
269
  if (!url) return false;
257
270
  return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
258
271
  }
259
- async function resolveTabId(tabId) {
272
+ async function resolveTabId(tabId, workspace) {
260
273
  if (tabId !== void 0) return tabId;
261
- const windowId = await getAutomationWindow();
274
+ const windowId = await getAutomationWindow(workspace);
262
275
  const tabs = await chrome.tabs.query({ windowId });
263
276
  const webTab = tabs.find((t) => t.id && isWebUrl(t.url));
264
277
  if (webTab?.id) return webTab.id;
@@ -267,9 +280,23 @@ async function resolveTabId(tabId) {
267
280
  if (!newTab.id) throw new Error("Failed to create tab in automation window");
268
281
  return newTab.id;
269
282
  }
270
- async function handleExec(cmd) {
283
+ async function listAutomationTabs(workspace) {
284
+ const session = automationSessions.get(workspace);
285
+ if (!session) return [];
286
+ try {
287
+ return await chrome.tabs.query({ windowId: session.windowId });
288
+ } catch {
289
+ automationSessions.delete(workspace);
290
+ return [];
291
+ }
292
+ }
293
+ async function listAutomationWebTabs(workspace) {
294
+ const tabs = await listAutomationTabs(workspace);
295
+ return tabs.filter((tab) => isWebUrl(tab.url));
296
+ }
297
+ async function handleExec(cmd, workspace) {
271
298
  if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" };
272
- const tabId = await resolveTabId(cmd.tabId);
299
+ const tabId = await resolveTabId(cmd.tabId, workspace);
273
300
  try {
274
301
  const data = await evaluateAsync(tabId, cmd.code);
275
302
  return { id: cmd.id, ok: true, data };
@@ -277,9 +304,9 @@ async function handleExec(cmd) {
277
304
  return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
278
305
  }
279
306
  }
280
- async function handleNavigate(cmd) {
307
+ async function handleNavigate(cmd, workspace) {
281
308
  if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" };
282
- const tabId = await resolveTabId(cmd.tabId);
309
+ const tabId = await resolveTabId(cmd.tabId, workspace);
283
310
  await chrome.tabs.update(tabId, { url: cmd.url });
284
311
  await new Promise((resolve) => {
285
312
  chrome.tabs.get(tabId).then((tab2) => {
@@ -303,11 +330,11 @@ async function handleNavigate(cmd) {
303
330
  const tab = await chrome.tabs.get(tabId);
304
331
  return { id: cmd.id, ok: true, data: { title: tab.title, url: tab.url, tabId } };
305
332
  }
306
- async function handleTabs(cmd) {
333
+ async function handleTabs(cmd, workspace) {
307
334
  switch (cmd.op) {
308
335
  case "list": {
309
- const tabs = await chrome.tabs.query({});
310
- const data = tabs.filter((t) => isWebUrl(t.url)).map((t, i) => ({
336
+ const tabs = await listAutomationWebTabs(workspace);
337
+ const data = tabs.map((t, i) => ({
311
338
  index: i,
312
339
  tabId: t.id,
313
340
  url: t.url,
@@ -317,19 +344,20 @@ async function handleTabs(cmd) {
317
344
  return { id: cmd.id, ok: true, data };
318
345
  }
319
346
  case "new": {
320
- const tab = await chrome.tabs.create({ url: cmd.url, active: true });
347
+ const windowId = await getAutomationWindow(workspace);
348
+ const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? "about:blank", active: true });
321
349
  return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
322
350
  }
323
351
  case "close": {
324
352
  if (cmd.index !== void 0) {
325
- const tabs = await chrome.tabs.query({});
353
+ const tabs = await listAutomationWebTabs(workspace);
326
354
  const target = tabs[cmd.index];
327
355
  if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
328
356
  await chrome.tabs.remove(target.id);
329
357
  detach(target.id);
330
358
  return { id: cmd.id, ok: true, data: { closed: target.id } };
331
359
  }
332
- const tabId = await resolveTabId(cmd.tabId);
360
+ const tabId = await resolveTabId(cmd.tabId, workspace);
333
361
  await chrome.tabs.remove(tabId);
334
362
  detach(tabId);
335
363
  return { id: cmd.id, ok: true, data: { closed: tabId } };
@@ -341,7 +369,7 @@ async function handleTabs(cmd) {
341
369
  await chrome.tabs.update(cmd.tabId, { active: true });
342
370
  return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
343
371
  }
344
- const tabs = await chrome.tabs.query({});
372
+ const tabs = await listAutomationWebTabs(workspace);
345
373
  const target = tabs[cmd.index];
346
374
  if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
347
375
  await chrome.tabs.update(target.id, { active: true });
@@ -367,8 +395,8 @@ async function handleCookies(cmd) {
367
395
  }));
368
396
  return { id: cmd.id, ok: true, data };
369
397
  }
370
- async function handleScreenshot(cmd) {
371
- const tabId = await resolveTabId(cmd.tabId);
398
+ async function handleScreenshot(cmd, workspace) {
399
+ const tabId = await resolveTabId(cmd.tabId, workspace);
372
400
  try {
373
401
  const data = await screenshot(tabId, {
374
402
  format: cmd.format,
@@ -380,13 +408,25 @@ async function handleScreenshot(cmd) {
380
408
  return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
381
409
  }
382
410
  }
383
- async function handleCloseWindow(cmd) {
384
- if (automationWindowId !== null) {
411
+ async function handleCloseWindow(cmd, workspace) {
412
+ const session = automationSessions.get(workspace);
413
+ if (session) {
385
414
  try {
386
- await chrome.windows.remove(automationWindowId);
415
+ await chrome.windows.remove(session.windowId);
387
416
  } catch {
388
417
  }
389
- automationWindowId = null;
418
+ if (session.idleTimer) clearTimeout(session.idleTimer);
419
+ automationSessions.delete(workspace);
390
420
  }
391
421
  return { id: cmd.id, ok: true, data: { closed: true } };
392
422
  }
423
+ async function handleSessions(cmd) {
424
+ const now = Date.now();
425
+ const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
426
+ workspace,
427
+ windowId: session.windowId,
428
+ tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isWebUrl(tab.url)).length,
429
+ idleMsRemaining: Math.max(0, session.idleDeadlineAt - now)
430
+ })));
431
+ return { id: cmd.id, ok: true, data };
432
+ }