@jackwener/opencli 1.0.1 → 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 (253) hide show
  1. package/.github/workflows/build-extension.yml +80 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/.github/workflows/docs.yml +52 -0
  4. package/.github/workflows/e2e-headed.yml +2 -2
  5. package/.github/workflows/pkg-pr-new.yml +2 -2
  6. package/.github/workflows/release.yml +2 -5
  7. package/.github/workflows/security.yml +2 -2
  8. package/CDP.md +1 -1
  9. package/CDP.zh-CN.md +1 -1
  10. package/README.md +42 -34
  11. package/README.zh-CN.md +42 -34
  12. package/SKILL.md +3 -5
  13. package/dist/browser/cdp.d.ts +42 -0
  14. package/dist/browser/cdp.js +339 -0
  15. package/dist/browser/daemon-client.d.ts +3 -1
  16. package/dist/browser/daemon-client.js +4 -0
  17. package/dist/browser/dom-helpers.d.ts +20 -0
  18. package/dist/browser/dom-helpers.js +109 -0
  19. package/dist/browser/index.d.ts +3 -0
  20. package/dist/browser/index.js +4 -0
  21. package/dist/browser/mcp.d.ts +1 -0
  22. package/dist/browser/mcp.js +10 -5
  23. package/dist/browser/page.d.ts +7 -0
  24. package/dist/browser/page.js +39 -123
  25. package/dist/browser/utils.d.ts +10 -0
  26. package/dist/browser/utils.js +27 -0
  27. package/dist/browser.test.js +49 -1
  28. package/dist/build-manifest.js +3 -1
  29. package/dist/build-manifest.test.js +34 -0
  30. package/dist/capabilityRouting.d.ts +2 -0
  31. package/dist/capabilityRouting.js +30 -0
  32. package/dist/capabilityRouting.test.d.ts +1 -0
  33. package/dist/capabilityRouting.test.js +42 -0
  34. package/dist/chaoxing.d.ts +58 -0
  35. package/dist/chaoxing.js +225 -0
  36. package/dist/chaoxing.test.d.ts +1 -0
  37. package/dist/chaoxing.test.js +45 -0
  38. package/dist/cli-manifest.json +885 -48
  39. package/dist/cli.d.ts +1 -0
  40. package/dist/cli.js +234 -0
  41. package/dist/clis/antigravity/serve.d.ts +14 -0
  42. package/dist/clis/antigravity/serve.js +263 -0
  43. package/dist/clis/bilibili/download.js +4 -14
  44. package/dist/clis/boss/chatlist.d.ts +1 -0
  45. package/dist/clis/boss/chatlist.js +50 -0
  46. package/dist/clis/boss/chatmsg.d.ts +1 -0
  47. package/dist/clis/boss/chatmsg.js +73 -0
  48. package/dist/clis/boss/resume.d.ts +1 -0
  49. package/dist/clis/boss/resume.js +249 -0
  50. package/dist/clis/boss/send.d.ts +1 -0
  51. package/dist/clis/boss/send.js +176 -0
  52. package/dist/clis/chaoxing/assignments.d.ts +1 -0
  53. package/dist/clis/chaoxing/assignments.js +74 -0
  54. package/dist/clis/chaoxing/exams.d.ts +1 -0
  55. package/dist/clis/chaoxing/exams.js +74 -0
  56. package/dist/clis/chatgpt/ask.js +15 -14
  57. package/dist/clis/chatgpt/ax.d.ts +1 -0
  58. package/dist/clis/chatgpt/ax.js +78 -0
  59. package/dist/clis/chatgpt/read.js +5 -6
  60. package/dist/clis/hf/top.d.ts +1 -0
  61. package/dist/clis/hf/top.js +119 -0
  62. package/dist/clis/jike/comment.d.ts +1 -0
  63. package/dist/clis/jike/comment.js +107 -0
  64. package/dist/clis/jike/create.d.ts +1 -0
  65. package/dist/clis/jike/create.js +106 -0
  66. package/dist/clis/jike/feed.d.ts +1 -0
  67. package/dist/clis/jike/feed.js +67 -0
  68. package/dist/clis/jike/like.d.ts +1 -0
  69. package/dist/clis/jike/like.js +61 -0
  70. package/dist/clis/jike/notifications.d.ts +1 -0
  71. package/dist/clis/jike/notifications.js +169 -0
  72. package/dist/clis/jike/post.yaml +58 -0
  73. package/dist/clis/jike/repost.d.ts +1 -0
  74. package/dist/clis/jike/repost.js +103 -0
  75. package/dist/clis/jike/search.d.ts +1 -0
  76. package/dist/clis/jike/search.js +67 -0
  77. package/dist/clis/jike/shared.d.ts +19 -0
  78. package/dist/clis/jike/shared.js +25 -0
  79. package/dist/clis/jike/topic.yaml +52 -0
  80. package/dist/clis/jike/user.yaml +51 -0
  81. package/dist/clis/smzdm/search.js +28 -39
  82. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  83. package/dist/clis/stackoverflow/hot.yaml +28 -0
  84. package/dist/clis/stackoverflow/search.yaml +32 -0
  85. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  86. package/dist/clis/twitter/download.js +6 -16
  87. package/dist/clis/twitter/post.js +9 -2
  88. package/dist/clis/twitter/search.js +14 -33
  89. package/dist/clis/xiaohongshu/download.d.ts +1 -1
  90. package/dist/clis/xiaohongshu/download.js +4 -4
  91. package/dist/clis/zhihu/download.js +3 -3
  92. package/dist/doctor.d.ts +7 -0
  93. package/dist/doctor.js +16 -0
  94. package/dist/download/index.d.ts +12 -8
  95. package/dist/download/index.js +11 -3
  96. package/dist/download/index.test.d.ts +1 -0
  97. package/dist/download/index.test.js +14 -0
  98. package/dist/engine.js +25 -14
  99. package/dist/explore.d.ts +1 -0
  100. package/dist/explore.js +48 -103
  101. package/dist/generate.js +1 -0
  102. package/dist/interceptor.js +3 -2
  103. package/dist/main.js +4 -193
  104. package/dist/output.d.ts +2 -1
  105. package/dist/output.js +3 -1
  106. package/dist/pipeline/executor.test.js +1 -0
  107. package/dist/pipeline/steps/download.js +14 -18
  108. package/dist/registry.d.ts +4 -3
  109. package/dist/registry.js +5 -2
  110. package/dist/runtime.d.ts +4 -1
  111. package/dist/runtime.js +2 -2
  112. package/dist/scripts/framework.d.ts +4 -0
  113. package/dist/scripts/framework.js +21 -0
  114. package/dist/scripts/interact.d.ts +4 -0
  115. package/dist/scripts/interact.js +20 -0
  116. package/dist/scripts/store.d.ts +9 -0
  117. package/dist/scripts/store.js +44 -0
  118. package/dist/synthesize.js +1 -1
  119. package/dist/types.d.ts +12 -0
  120. package/dist/verify.d.ts +6 -1
  121. package/dist/verify.js +54 -2
  122. package/docs/.vitepress/config.mts +193 -0
  123. package/docs/adapters/browser/apple-podcasts.md +28 -0
  124. package/docs/adapters/browser/bbc.md +26 -0
  125. package/docs/adapters/browser/bilibili.md +38 -0
  126. package/docs/adapters/browser/boss.md +28 -0
  127. package/docs/adapters/browser/coupang.md +28 -0
  128. package/docs/adapters/browser/ctrip.md +27 -0
  129. package/docs/adapters/browser/github.md +26 -0
  130. package/docs/adapters/browser/hackernews.md +26 -0
  131. package/docs/adapters/browser/linkedin.md +27 -0
  132. package/docs/adapters/browser/reddit.md +41 -0
  133. package/docs/adapters/browser/reuters.md +27 -0
  134. package/docs/adapters/browser/smzdm.md +27 -0
  135. package/docs/adapters/browser/twitter.md +47 -0
  136. package/docs/adapters/browser/v2ex.md +32 -0
  137. package/docs/adapters/browser/weibo.md +27 -0
  138. package/docs/adapters/browser/xiaohongshu.md +32 -0
  139. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  140. package/docs/adapters/browser/xueqiu.md +32 -0
  141. package/docs/adapters/browser/yahoo-finance.md +26 -0
  142. package/docs/adapters/browser/youtube.md +29 -0
  143. package/docs/adapters/browser/zhihu.md +30 -0
  144. package/docs/adapters/desktop/antigravity.md +46 -0
  145. package/docs/adapters/desktop/chatgpt.md +43 -0
  146. package/docs/adapters/desktop/chatwise.md +38 -0
  147. package/docs/adapters/desktop/codex.md +32 -0
  148. package/docs/adapters/desktop/cursor.md +33 -0
  149. package/docs/adapters/desktop/discord.md +28 -0
  150. package/docs/adapters/desktop/feishu.md +20 -0
  151. package/docs/adapters/desktop/neteasemusic.md +31 -0
  152. package/docs/adapters/desktop/notion.md +29 -0
  153. package/docs/adapters/desktop/wechat.md +28 -0
  154. package/docs/adapters/index.md +49 -0
  155. package/docs/advanced/cdp.md +103 -0
  156. package/docs/advanced/download.md +63 -0
  157. package/docs/advanced/electron.md +125 -0
  158. package/docs/advanced/remote-chrome.md +72 -0
  159. package/docs/developer/ai-workflow.md +66 -0
  160. package/docs/developer/architecture.md +90 -0
  161. package/docs/developer/contributing.md +136 -0
  162. package/docs/developer/testing.md +237 -0
  163. package/docs/developer/ts-adapter.md +87 -0
  164. package/docs/developer/yaml-adapter.md +108 -0
  165. package/docs/guide/browser-bridge.md +38 -0
  166. package/docs/guide/getting-started.md +56 -0
  167. package/docs/guide/installation.md +37 -0
  168. package/docs/guide/troubleshooting.md +56 -0
  169. package/docs/index.md +35 -0
  170. package/docs/zh/adapters/index.md +5 -0
  171. package/docs/zh/advanced/cdp.md +3 -0
  172. package/docs/zh/developer/contributing.md +24 -0
  173. package/docs/zh/guide/browser-bridge.md +25 -0
  174. package/docs/zh/guide/getting-started.md +40 -0
  175. package/docs/zh/guide/installation.md +37 -0
  176. package/docs/zh/index.md +29 -0
  177. package/extension/dist/background.js +386 -438
  178. package/extension/manifest.json +2 -2
  179. package/extension/package-lock.json +1156 -0
  180. package/extension/src/background.test.ts +151 -0
  181. package/extension/src/background.ts +124 -53
  182. package/extension/src/protocol.ts +3 -1
  183. package/package.json +7 -3
  184. package/src/browser/cdp.ts +367 -0
  185. package/src/browser/daemon-client.ts +7 -1
  186. package/src/browser/dom-helpers.ts +116 -0
  187. package/src/browser/index.ts +4 -0
  188. package/src/browser/mcp.ts +14 -6
  189. package/src/browser/page.ts +47 -124
  190. package/src/browser/utils.ts +27 -0
  191. package/src/browser.test.ts +56 -0
  192. package/src/build-manifest.test.ts +36 -0
  193. package/src/build-manifest.ts +2 -1
  194. package/src/capabilityRouting.test.ts +47 -0
  195. package/src/capabilityRouting.ts +28 -0
  196. package/src/chaoxing.test.ts +53 -0
  197. package/src/chaoxing.ts +268 -0
  198. package/src/cli.ts +205 -0
  199. package/src/clis/antigravity/SKILL.md +5 -0
  200. package/src/clis/antigravity/serve.ts +329 -0
  201. package/src/clis/bilibili/download.ts +4 -15
  202. package/src/clis/boss/chatlist.ts +50 -0
  203. package/src/clis/boss/chatmsg.ts +70 -0
  204. package/src/clis/boss/resume.ts +262 -0
  205. package/src/clis/boss/send.ts +193 -0
  206. package/src/clis/chaoxing/README.md +36 -0
  207. package/src/clis/chaoxing/README.zh-CN.md +35 -0
  208. package/src/clis/chaoxing/assignments.ts +88 -0
  209. package/src/clis/chaoxing/exams.ts +88 -0
  210. package/src/clis/chatgpt/ask.ts +14 -15
  211. package/src/clis/chatgpt/ax.ts +81 -0
  212. package/src/clis/chatgpt/read.ts +5 -7
  213. package/src/clis/hf/top.ts +141 -0
  214. package/src/clis/jike/comment.ts +113 -0
  215. package/src/clis/jike/create.ts +113 -0
  216. package/src/clis/jike/feed.ts +74 -0
  217. package/src/clis/jike/like.ts +65 -0
  218. package/src/clis/jike/notifications.ts +185 -0
  219. package/src/clis/jike/post.yaml +58 -0
  220. package/src/clis/jike/repost.ts +114 -0
  221. package/src/clis/jike/search.ts +74 -0
  222. package/src/clis/jike/shared.ts +36 -0
  223. package/src/clis/jike/topic.yaml +52 -0
  224. package/src/clis/jike/user.yaml +51 -0
  225. package/src/clis/smzdm/search.ts +30 -39
  226. package/src/clis/stackoverflow/bounties.yaml +29 -0
  227. package/src/clis/stackoverflow/hot.yaml +28 -0
  228. package/src/clis/stackoverflow/search.yaml +32 -0
  229. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  230. package/src/clis/twitter/download.ts +6 -17
  231. package/src/clis/twitter/post.ts +9 -2
  232. package/src/clis/twitter/search.ts +15 -33
  233. package/src/clis/xiaohongshu/download.ts +4 -4
  234. package/src/clis/zhihu/download.ts +3 -3
  235. package/src/doctor.ts +18 -2
  236. package/src/download/index.test.ts +16 -0
  237. package/src/download/index.ts +22 -4
  238. package/src/engine.ts +20 -13
  239. package/src/explore.ts +54 -103
  240. package/src/generate.ts +1 -0
  241. package/src/interceptor.ts +3 -2
  242. package/src/main.ts +4 -180
  243. package/src/output.ts +15 -13
  244. package/src/pipeline/executor.test.ts +1 -0
  245. package/src/pipeline/steps/download.ts +14 -17
  246. package/src/registry.ts +9 -5
  247. package/src/runtime.ts +3 -2
  248. package/src/scripts/framework.ts +20 -0
  249. package/src/scripts/interact.ts +22 -0
  250. package/src/scripts/store.ts +40 -0
  251. package/src/synthesize.ts +1 -1
  252. package/src/types.ts +9 -0
  253. package/src/verify.ts +64 -3
@@ -0,0 +1,151 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ type Listener<T extends (...args: any[]) => void> = { addListener: (fn: T) => void };
4
+
5
+ type MockTab = {
6
+ id: number;
7
+ windowId: number;
8
+ url?: string;
9
+ title?: string;
10
+ active?: boolean;
11
+ status?: string;
12
+ };
13
+
14
+ class MockWebSocket {
15
+ static OPEN = 1;
16
+ static CONNECTING = 0;
17
+ readyState = MockWebSocket.CONNECTING;
18
+ onopen: (() => void) | null = null;
19
+ onmessage: ((event: { data: string }) => void) | null = null;
20
+ onclose: (() => void) | null = null;
21
+ onerror: (() => void) | null = null;
22
+
23
+ constructor(_url: string) {}
24
+ send(_data: string): void {}
25
+ close(): void {
26
+ this.onclose?.();
27
+ }
28
+ }
29
+
30
+ function createChromeMock() {
31
+ let nextTabId = 10;
32
+ const tabs: MockTab[] = [
33
+ { id: 1, windowId: 1, url: 'https://automation.example', title: 'automation', active: true, status: 'complete' },
34
+ { id: 2, windowId: 2, url: 'https://user.example', title: 'user', active: true, status: 'complete' },
35
+ { id: 3, windowId: 1, url: 'chrome://extensions', title: 'chrome', active: false, status: 'complete' },
36
+ ];
37
+
38
+ const query = vi.fn(async (queryInfo: { windowId?: number } = {}) => {
39
+ return tabs.filter((tab) => queryInfo.windowId === undefined || tab.windowId === queryInfo.windowId);
40
+ });
41
+ const create = vi.fn(async ({ windowId, url, active }: { windowId?: number; url?: string; active?: boolean }) => {
42
+ const tab: MockTab = {
43
+ id: nextTabId++,
44
+ windowId: windowId ?? 999,
45
+ url,
46
+ title: url ?? 'blank',
47
+ active: !!active,
48
+ status: 'complete',
49
+ };
50
+ tabs.push(tab);
51
+ return tab;
52
+ });
53
+ const update = vi.fn(async (tabId: number, updates: { active?: boolean; url?: string }) => {
54
+ const tab = tabs.find((entry) => entry.id === tabId);
55
+ if (!tab) throw new Error(`Unknown tab ${tabId}`);
56
+ if (updates.active !== undefined) tab.active = updates.active;
57
+ if (updates.url !== undefined) tab.url = updates.url;
58
+ return tab;
59
+ });
60
+
61
+ const chrome = {
62
+ tabs: {
63
+ query,
64
+ create,
65
+ update,
66
+ remove: vi.fn(async (_tabId: number) => {}),
67
+ get: vi.fn(async (tabId: number) => {
68
+ const tab = tabs.find((entry) => entry.id === tabId);
69
+ if (!tab) throw new Error(`Unknown tab ${tabId}`);
70
+ return tab;
71
+ }),
72
+ onUpdated: { addListener: vi.fn(), removeListener: vi.fn() } as Listener<(id: number, info: chrome.tabs.TabChangeInfo) => void>,
73
+ },
74
+ windows: {
75
+ get: vi.fn(async (windowId: number) => ({ id: windowId })),
76
+ create: vi.fn(async ({ url, focused, width, height, type }: any) => ({ id: 1, url, focused, width, height, type })),
77
+ remove: vi.fn(async (_windowId: number) => {}),
78
+ onRemoved: { addListener: vi.fn() } as Listener<(windowId: number) => void>,
79
+ },
80
+ alarms: {
81
+ create: vi.fn(),
82
+ onAlarm: { addListener: vi.fn() } as Listener<(alarm: { name: string }) => void>,
83
+ },
84
+ runtime: {
85
+ onInstalled: { addListener: vi.fn() } as Listener<() => void>,
86
+ onStartup: { addListener: vi.fn() } as Listener<() => void>,
87
+ },
88
+ cookies: {
89
+ getAll: vi.fn(async () => []),
90
+ },
91
+ };
92
+
93
+ return { chrome, tabs, query, create, update };
94
+ }
95
+
96
+ describe('background tab isolation', () => {
97
+ beforeEach(() => {
98
+ vi.resetModules();
99
+ vi.stubGlobal('WebSocket', MockWebSocket);
100
+ });
101
+
102
+ it('lists only automation-window web tabs', async () => {
103
+ const { chrome } = createChromeMock();
104
+ vi.stubGlobal('chrome', chrome);
105
+
106
+ const mod = await import('./background');
107
+ mod.__test__.setAutomationWindowId('site:twitter', 1);
108
+
109
+ const result = await mod.__test__.handleTabs({ id: '1', action: 'tabs', op: 'list', workspace: 'site:twitter' }, 'site:twitter');
110
+
111
+ expect(result.ok).toBe(true);
112
+ expect(result.data).toEqual([
113
+ {
114
+ index: 0,
115
+ tabId: 1,
116
+ url: 'https://automation.example',
117
+ title: 'automation',
118
+ active: true,
119
+ },
120
+ ]);
121
+ });
122
+
123
+ it('creates new tabs inside the automation window', async () => {
124
+ const { chrome, create } = createChromeMock();
125
+ vi.stubGlobal('chrome', chrome);
126
+
127
+ const mod = await import('./background');
128
+ mod.__test__.setAutomationWindowId('site:twitter', 1);
129
+
130
+ const result = await mod.__test__.handleTabs({ id: '2', action: 'tabs', op: 'new', url: 'https://new.example', workspace: 'site:twitter' }, 'site:twitter');
131
+
132
+ expect(result.ok).toBe(true);
133
+ expect(create).toHaveBeenCalledWith({ windowId: 1, url: 'https://new.example', active: true });
134
+ });
135
+
136
+ it('reports sessions per workspace', async () => {
137
+ const { chrome } = createChromeMock();
138
+ vi.stubGlobal('chrome', chrome);
139
+
140
+ const mod = await import('./background');
141
+ mod.__test__.setAutomationWindowId('site:twitter', 1);
142
+ mod.__test__.setAutomationWindowId('site:zhihu', 2);
143
+
144
+ const result = await mod.__test__.handleSessions({ id: '3', action: 'sessions' });
145
+ expect(result.ok).toBe(true);
146
+ expect(result.data).toEqual(expect.arrayContaining([
147
+ expect.objectContaining({ workspace: 'site:twitter', windowId: 1 }),
148
+ expect.objectContaining({ workspace: 'site:zhihu', windowId: 2 }),
149
+ ]));
150
+ });
151
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * opencli Browser Bridge — Service Worker (background script).
2
+ * OpenCLI — Service Worker (background script).
3
3
  *
4
4
  * Connects to the opencli daemon via WebSocket, receives commands,
5
5
  * dispatches them to Chrome APIs (debugger/tabs/cookies), returns results.
@@ -90,36 +90,48 @@ function scheduleReconnect(): void {
90
90
  // user's active browsing session is never touched.
91
91
  // The window auto-closes after 30s of idle (no commands).
92
92
 
93
- let automationWindowId: number | null = null;
94
- let windowIdleTimer: ReturnType<typeof setTimeout> | null = null;
93
+ type AutomationSession = {
94
+ windowId: number;
95
+ idleTimer: ReturnType<typeof setTimeout> | null;
96
+ idleDeadlineAt: number;
97
+ };
98
+
99
+ const automationSessions = new Map<string, AutomationSession>();
95
100
  const WINDOW_IDLE_TIMEOUT = 30000; // 30s
96
101
 
97
- function resetWindowIdleTimer(): void {
98
- if (windowIdleTimer) clearTimeout(windowIdleTimer);
99
- windowIdleTimer = setTimeout(async () => {
100
- if (automationWindowId !== null) {
101
- try {
102
- await chrome.windows.remove(automationWindowId);
103
- console.log(`[opencli] Automation window ${automationWindowId} closed (idle timeout)`);
104
- } catch {
105
- // Already gone
106
- }
107
- automationWindowId = null;
102
+ function getWorkspaceKey(workspace?: string): string {
103
+ return workspace?.trim() || 'default';
104
+ }
105
+
106
+ function resetWindowIdleTimer(workspace: string): void {
107
+ const session = automationSessions.get(workspace);
108
+ if (!session) return;
109
+ if (session.idleTimer) clearTimeout(session.idleTimer);
110
+ session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
111
+ session.idleTimer = setTimeout(async () => {
112
+ const current = automationSessions.get(workspace);
113
+ if (!current) return;
114
+ try {
115
+ await chrome.windows.remove(current.windowId);
116
+ console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
117
+ } catch {
118
+ // Already gone
108
119
  }
109
- windowIdleTimer = null;
120
+ automationSessions.delete(workspace);
110
121
  }, WINDOW_IDLE_TIMEOUT);
111
122
  }
112
123
 
113
124
  /** Get or create the dedicated automation window. */
114
- async function getAutomationWindow(): Promise<number> {
125
+ async function getAutomationWindow(workspace: string): Promise<number> {
115
126
  // Check if our window is still alive
116
- if (automationWindowId !== null) {
127
+ const existing = automationSessions.get(workspace);
128
+ if (existing) {
117
129
  try {
118
- await chrome.windows.get(automationWindowId);
119
- return automationWindowId;
130
+ await chrome.windows.get(existing.windowId);
131
+ return existing.windowId;
120
132
  } catch {
121
133
  // Window was closed by user
122
- automationWindowId = null;
134
+ automationSessions.delete(workspace);
123
135
  }
124
136
  }
125
137
 
@@ -131,17 +143,25 @@ async function getAutomationWindow(): Promise<number> {
131
143
  height: 900,
132
144
  type: 'normal',
133
145
  });
134
- automationWindowId = win.id!;
135
- console.log(`[opencli] Created automation window ${automationWindowId}`);
136
- return automationWindowId;
146
+ const session: AutomationSession = {
147
+ windowId: win.id!,
148
+ idleTimer: null,
149
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
150
+ };
151
+ automationSessions.set(workspace, session);
152
+ console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
153
+ resetWindowIdleTimer(workspace);
154
+ return session.windowId;
137
155
  }
138
156
 
139
157
  // Clean up when the automation window is closed
140
158
  chrome.windows.onRemoved.addListener((windowId) => {
141
- if (windowId === automationWindowId) {
142
- console.log('[opencli] Automation window closed');
143
- automationWindowId = null;
144
- if (windowIdleTimer) { clearTimeout(windowIdleTimer); windowIdleTimer = null; }
159
+ for (const [workspace, session] of automationSessions.entries()) {
160
+ if (session.windowId === windowId) {
161
+ console.log(`[opencli] Automation window closed (${workspace})`);
162
+ if (session.idleTimer) clearTimeout(session.idleTimer);
163
+ automationSessions.delete(workspace);
164
+ }
145
165
  }
146
166
  });
147
167
 
@@ -155,7 +175,7 @@ function initialize(): void {
155
175
  chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds
156
176
  executor.registerListeners();
157
177
  connect();
158
- console.log('[opencli] Browser Bridge extension initialized');
178
+ console.log('[opencli] OpenCLI extension initialized');
159
179
  }
160
180
 
161
181
  chrome.runtime.onInstalled.addListener(() => {
@@ -173,22 +193,25 @@ chrome.alarms.onAlarm.addListener((alarm) => {
173
193
  // ─── Command dispatcher ─────────────────────────────────────────────
174
194
 
175
195
  async function handleCommand(cmd: Command): Promise<Result> {
196
+ const workspace = getWorkspaceKey(cmd.workspace);
176
197
  // Reset idle timer on every command (window stays alive while active)
177
- resetWindowIdleTimer();
198
+ resetWindowIdleTimer(workspace);
178
199
  try {
179
200
  switch (cmd.action) {
180
201
  case 'exec':
181
- return await handleExec(cmd);
202
+ return await handleExec(cmd, workspace);
182
203
  case 'navigate':
183
- return await handleNavigate(cmd);
204
+ return await handleNavigate(cmd, workspace);
184
205
  case 'tabs':
185
- return await handleTabs(cmd);
206
+ return await handleTabs(cmd, workspace);
186
207
  case 'cookies':
187
208
  return await handleCookies(cmd);
188
209
  case 'screenshot':
189
- return await handleScreenshot(cmd);
210
+ return await handleScreenshot(cmd, workspace);
190
211
  case 'close-window':
191
- return await handleCloseWindow(cmd);
212
+ return await handleCloseWindow(cmd, workspace);
213
+ case 'sessions':
214
+ return await handleSessions(cmd);
192
215
  default:
193
216
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
194
217
  }
@@ -214,11 +237,11 @@ function isWebUrl(url?: string): boolean {
214
237
  * If explicit tabId is given, use that directly.
215
238
  * Otherwise, find or create a tab in the dedicated automation window.
216
239
  */
217
- async function resolveTabId(tabId?: number): Promise<number> {
240
+ async function resolveTabId(tabId: number | undefined, workspace: string): Promise<number> {
218
241
  if (tabId !== undefined) return tabId;
219
242
 
220
243
  // Get (or create) the automation window
221
- const windowId = await getAutomationWindow();
244
+ const windowId = await getAutomationWindow(workspace);
222
245
 
223
246
  // Find the active tab in our automation window
224
247
  const tabs = await chrome.tabs.query({ windowId });
@@ -234,9 +257,25 @@ async function resolveTabId(tabId?: number): Promise<number> {
234
257
  return newTab.id;
235
258
  }
236
259
 
237
- async function handleExec(cmd: Command): Promise<Result> {
260
+ async function listAutomationTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
261
+ const session = automationSessions.get(workspace);
262
+ if (!session) return [];
263
+ try {
264
+ return await chrome.tabs.query({ windowId: session.windowId });
265
+ } catch {
266
+ automationSessions.delete(workspace);
267
+ return [];
268
+ }
269
+ }
270
+
271
+ async function listAutomationWebTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
272
+ const tabs = await listAutomationTabs(workspace);
273
+ return tabs.filter((tab) => isWebUrl(tab.url));
274
+ }
275
+
276
+ async function handleExec(cmd: Command, workspace: string): Promise<Result> {
238
277
  if (!cmd.code) return { id: cmd.id, ok: false, error: 'Missing code' };
239
- const tabId = await resolveTabId(cmd.tabId);
278
+ const tabId = await resolveTabId(cmd.tabId, workspace);
240
279
  try {
241
280
  const data = await executor.evaluateAsync(tabId, cmd.code);
242
281
  return { id: cmd.id, ok: true, data };
@@ -245,9 +284,9 @@ async function handleExec(cmd: Command): Promise<Result> {
245
284
  }
246
285
  }
247
286
 
248
- async function handleNavigate(cmd: Command): Promise<Result> {
287
+ async function handleNavigate(cmd: Command, workspace: string): Promise<Result> {
249
288
  if (!cmd.url) return { id: cmd.id, ok: false, error: 'Missing url' };
250
- const tabId = await resolveTabId(cmd.tabId);
289
+ const tabId = await resolveTabId(cmd.tabId, workspace);
251
290
  await chrome.tabs.update(tabId, { url: cmd.url });
252
291
 
253
292
  // Wait for page to finish loading, checking current status first to avoid race
@@ -275,12 +314,11 @@ async function handleNavigate(cmd: Command): Promise<Result> {
275
314
  return { id: cmd.id, ok: true, data: { title: tab.title, url: tab.url, tabId } };
276
315
  }
277
316
 
278
- async function handleTabs(cmd: Command): Promise<Result> {
317
+ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
279
318
  switch (cmd.op) {
280
319
  case 'list': {
281
- const tabs = await chrome.tabs.query({});
320
+ const tabs = await listAutomationWebTabs(workspace);
282
321
  const data = tabs
283
- .filter((t) => isWebUrl(t.url))
284
322
  .map((t, i) => ({
285
323
  index: i,
286
324
  tabId: t.id,
@@ -291,19 +329,20 @@ async function handleTabs(cmd: Command): Promise<Result> {
291
329
  return { id: cmd.id, ok: true, data };
292
330
  }
293
331
  case 'new': {
294
- const tab = await chrome.tabs.create({ url: cmd.url, active: true });
332
+ const windowId = await getAutomationWindow(workspace);
333
+ const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? 'about:blank', active: true });
295
334
  return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } };
296
335
  }
297
336
  case 'close': {
298
337
  if (cmd.index !== undefined) {
299
- const tabs = await chrome.tabs.query({});
338
+ const tabs = await listAutomationWebTabs(workspace);
300
339
  const target = tabs[cmd.index];
301
340
  if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
302
341
  await chrome.tabs.remove(target.id);
303
342
  executor.detach(target.id);
304
343
  return { id: cmd.id, ok: true, data: { closed: target.id } };
305
344
  }
306
- const tabId = await resolveTabId(cmd.tabId);
345
+ const tabId = await resolveTabId(cmd.tabId, workspace);
307
346
  await chrome.tabs.remove(tabId);
308
347
  executor.detach(tabId);
309
348
  return { id: cmd.id, ok: true, data: { closed: tabId } };
@@ -315,7 +354,7 @@ async function handleTabs(cmd: Command): Promise<Result> {
315
354
  await chrome.tabs.update(cmd.tabId, { active: true });
316
355
  return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
317
356
  }
318
- const tabs = await chrome.tabs.query({});
357
+ const tabs = await listAutomationWebTabs(workspace);
319
358
  const target = tabs[cmd.index!];
320
359
  if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
321
360
  await chrome.tabs.update(target.id, { active: true });
@@ -343,8 +382,8 @@ async function handleCookies(cmd: Command): Promise<Result> {
343
382
  return { id: cmd.id, ok: true, data };
344
383
  }
345
384
 
346
- async function handleScreenshot(cmd: Command): Promise<Result> {
347
- const tabId = await resolveTabId(cmd.tabId);
385
+ async function handleScreenshot(cmd: Command, workspace: string): Promise<Result> {
386
+ const tabId = await resolveTabId(cmd.tabId, workspace);
348
387
  try {
349
388
  const data = await executor.screenshot(tabId, {
350
389
  format: cmd.format,
@@ -357,14 +396,46 @@ async function handleScreenshot(cmd: Command): Promise<Result> {
357
396
  }
358
397
  }
359
398
 
360
- async function handleCloseWindow(cmd: Command): Promise<Result> {
361
- if (automationWindowId !== null) {
399
+ async function handleCloseWindow(cmd: Command, workspace: string): Promise<Result> {
400
+ const session = automationSessions.get(workspace);
401
+ if (session) {
362
402
  try {
363
- await chrome.windows.remove(automationWindowId);
403
+ await chrome.windows.remove(session.windowId);
364
404
  } catch {
365
405
  // Window may already be closed
366
406
  }
367
- automationWindowId = null;
407
+ if (session.idleTimer) clearTimeout(session.idleTimer);
408
+ automationSessions.delete(workspace);
368
409
  }
369
410
  return { id: cmd.id, ok: true, data: { closed: true } };
370
411
  }
412
+
413
+ async function handleSessions(cmd: Command): Promise<Result> {
414
+ const now = Date.now();
415
+ const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
416
+ workspace,
417
+ windowId: session.windowId,
418
+ tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isWebUrl(tab.url)).length,
419
+ idleMsRemaining: Math.max(0, session.idleDeadlineAt - now),
420
+ })));
421
+ return { id: cmd.id, ok: true, data };
422
+ }
423
+
424
+ export const __test__ = {
425
+ handleTabs,
426
+ handleSessions,
427
+ getAutomationWindowId: (workspace: string = 'default') => automationSessions.get(workspace)?.windowId ?? null,
428
+ setAutomationWindowId: (workspace: string, windowId: number | null) => {
429
+ if (windowId === null) {
430
+ const session = automationSessions.get(workspace);
431
+ if (session?.idleTimer) clearTimeout(session.idleTimer);
432
+ automationSessions.delete(workspace);
433
+ return;
434
+ }
435
+ automationSessions.set(workspace, {
436
+ windowId,
437
+ idleTimer: null,
438
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
439
+ });
440
+ },
441
+ };
@@ -5,7 +5,7 @@
5
5
  * Everything else is just JS code sent via 'exec'.
6
6
  */
7
7
 
8
- export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
8
+ export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions';
9
9
 
10
10
  export interface Command {
11
11
  /** Unique request ID */
@@ -16,6 +16,8 @@ export interface Command {
16
16
  tabId?: number;
17
17
  /** JS code to evaluate in page context (exec action) */
18
18
  code?: string;
19
+ /** Logical workspace for automation session reuse */
20
+ workspace?: string;
19
21
  /** URL to navigate to (navigate action) */
20
22
  url?: string;
21
23
  /** Sub-operation for tabs: list, new, close, select */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -26,7 +26,10 @@
26
26
  "prepublishOnly": "npm run build",
27
27
  "test": "vitest run",
28
28
  "test:site": "node scripts/test-site.mjs",
29
- "test:watch": "vitest"
29
+ "test:watch": "vitest",
30
+ "docs:dev": "vitepress dev docs",
31
+ "docs:build": "vitepress build docs",
32
+ "docs:preview": "vitepress preview docs"
30
33
  },
31
34
  "keywords": [
32
35
  "cli",
@@ -49,10 +52,11 @@
49
52
  },
50
53
  "devDependencies": {
51
54
  "@types/js-yaml": "^4.0.9",
52
- "@types/ws": "^8.5.13",
53
55
  "@types/node": "^22.13.10",
56
+ "@types/ws": "^8.5.13",
54
57
  "tsx": "^4.19.3",
55
58
  "typescript": "^5.8.2",
59
+ "vitepress": "^1.6.4",
56
60
  "vitest": "^4.1.0"
57
61
  }
58
62
  }