@jackwener/opencli 1.0.0 → 1.0.3

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 (171) hide show
  1. package/.github/workflows/build-extension.yml +62 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/.github/workflows/e2e-headed.yml +2 -2
  4. package/.github/workflows/pkg-pr-new.yml +2 -2
  5. package/.github/workflows/release.yml +2 -5
  6. package/.github/workflows/security.yml +2 -2
  7. package/CDP.md +1 -1
  8. package/CDP.zh-CN.md +1 -1
  9. package/README.md +35 -8
  10. package/README.zh-CN.md +35 -8
  11. package/SKILL.md +3 -5
  12. package/dist/browser/cdp.d.ts +27 -0
  13. package/dist/browser/cdp.js +295 -0
  14. package/dist/browser/daemon-client.d.ts +1 -1
  15. package/dist/browser/index.d.ts +4 -2
  16. package/dist/browser/index.js +5 -5
  17. package/dist/browser/mcp.d.ts +5 -8
  18. package/dist/browser/mcp.js +9 -10
  19. package/dist/browser/page.d.ts +8 -1
  20. package/dist/browser/page.js +25 -40
  21. package/dist/browser/utils.d.ts +10 -0
  22. package/dist/browser/utils.js +27 -0
  23. package/dist/browser.test.js +48 -7
  24. package/dist/chaoxing.d.ts +58 -0
  25. package/dist/chaoxing.js +225 -0
  26. package/dist/chaoxing.test.d.ts +1 -0
  27. package/dist/chaoxing.test.js +38 -0
  28. package/dist/cli-manifest.json +597 -14
  29. package/dist/cli.d.ts +1 -0
  30. package/dist/cli.js +197 -0
  31. package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
  32. package/dist/clis/apple-podcasts/episodes.js +28 -0
  33. package/dist/clis/apple-podcasts/search.d.ts +1 -0
  34. package/dist/clis/apple-podcasts/search.js +29 -0
  35. package/dist/clis/apple-podcasts/top.d.ts +1 -0
  36. package/dist/clis/apple-podcasts/top.js +34 -0
  37. package/dist/clis/apple-podcasts/utils.d.ts +11 -0
  38. package/dist/clis/apple-podcasts/utils.js +30 -0
  39. package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
  40. package/dist/clis/apple-podcasts/utils.test.js +57 -0
  41. package/dist/clis/boss/chatlist.d.ts +1 -0
  42. package/dist/clis/boss/chatlist.js +50 -0
  43. package/dist/clis/boss/chatmsg.d.ts +1 -0
  44. package/dist/clis/boss/chatmsg.js +73 -0
  45. package/dist/clis/boss/send.d.ts +1 -0
  46. package/dist/clis/boss/send.js +176 -0
  47. package/dist/clis/chaoxing/assignments.d.ts +1 -0
  48. package/dist/clis/chaoxing/assignments.js +74 -0
  49. package/dist/clis/chaoxing/exams.d.ts +1 -0
  50. package/dist/clis/chaoxing/exams.js +74 -0
  51. package/dist/clis/chatgpt/ask.js +15 -14
  52. package/dist/clis/chatgpt/ax.d.ts +1 -0
  53. package/dist/clis/chatgpt/ax.js +78 -0
  54. package/dist/clis/chatgpt/read.js +5 -6
  55. package/dist/clis/chatwise/history.js +18 -1
  56. package/dist/clis/discord-app/channels.js +33 -21
  57. package/dist/clis/twitter/accept.d.ts +1 -0
  58. package/dist/clis/twitter/accept.js +202 -0
  59. package/dist/clis/twitter/followers.js +30 -22
  60. package/dist/clis/twitter/following.js +19 -14
  61. package/dist/clis/twitter/notifications.js +29 -22
  62. package/dist/clis/twitter/post.js +9 -2
  63. package/dist/clis/twitter/reply-dm.d.ts +1 -0
  64. package/dist/clis/twitter/reply-dm.js +181 -0
  65. package/dist/clis/twitter/search.js +30 -11
  66. package/dist/clis/weread/book.d.ts +1 -0
  67. package/dist/clis/weread/book.js +26 -0
  68. package/dist/clis/weread/highlights.d.ts +1 -0
  69. package/dist/clis/weread/highlights.js +23 -0
  70. package/dist/clis/weread/notebooks.d.ts +1 -0
  71. package/dist/clis/weread/notebooks.js +21 -0
  72. package/dist/clis/weread/notes.d.ts +1 -0
  73. package/dist/clis/weread/notes.js +29 -0
  74. package/dist/clis/weread/ranking.d.ts +1 -0
  75. package/dist/clis/weread/ranking.js +28 -0
  76. package/dist/clis/weread/search.d.ts +1 -0
  77. package/dist/clis/weread/search.js +25 -0
  78. package/dist/clis/weread/shelf.d.ts +1 -0
  79. package/dist/clis/weread/shelf.js +24 -0
  80. package/dist/clis/weread/utils.d.ts +20 -0
  81. package/dist/clis/weread/utils.js +72 -0
  82. package/dist/clis/weread/utils.test.d.ts +1 -0
  83. package/dist/clis/weread/utils.test.js +85 -0
  84. package/dist/clis/xiaohongshu/download.d.ts +1 -1
  85. package/dist/clis/xiaohongshu/download.js +1 -1
  86. package/dist/daemon.js +2 -2
  87. package/dist/doctor.d.ts +0 -21
  88. package/dist/doctor.js +2 -24
  89. package/dist/engine.js +24 -13
  90. package/dist/explore.js +46 -101
  91. package/dist/main.js +4 -203
  92. package/dist/output.d.ts +1 -1
  93. package/dist/registry.d.ts +3 -3
  94. package/dist/runtime.d.ts +1 -4
  95. package/dist/runtime.js +1 -4
  96. package/dist/scripts/framework.d.ts +4 -0
  97. package/dist/scripts/framework.js +21 -0
  98. package/dist/scripts/interact.d.ts +4 -0
  99. package/dist/scripts/interact.js +20 -0
  100. package/dist/scripts/store.d.ts +9 -0
  101. package/dist/scripts/store.js +44 -0
  102. package/dist/setup.js +2 -2
  103. package/dist/synthesize.js +1 -1
  104. package/extension/dist/background.js +392 -0
  105. package/extension/manifest.json +3 -3
  106. package/extension/package.json +1 -1
  107. package/extension/src/background.ts +101 -24
  108. package/extension/src/protocol.ts +1 -1
  109. package/package.json +1 -1
  110. package/src/browser/cdp.ts +295 -0
  111. package/src/browser/daemon-client.ts +1 -1
  112. package/src/browser/index.ts +5 -6
  113. package/src/browser/mcp.ts +14 -15
  114. package/src/browser/page.ts +25 -41
  115. package/src/browser/utils.ts +27 -0
  116. package/src/browser.test.ts +52 -6
  117. package/src/chaoxing.test.ts +45 -0
  118. package/src/chaoxing.ts +268 -0
  119. package/src/cli.ts +185 -0
  120. package/src/clis/antigravity/SKILL.md +5 -0
  121. package/src/clis/apple-podcasts/episodes.ts +28 -0
  122. package/src/clis/apple-podcasts/search.ts +29 -0
  123. package/src/clis/apple-podcasts/top.ts +34 -0
  124. package/src/clis/apple-podcasts/utils.test.ts +72 -0
  125. package/src/clis/apple-podcasts/utils.ts +37 -0
  126. package/src/clis/boss/chatlist.ts +50 -0
  127. package/src/clis/boss/chatmsg.ts +70 -0
  128. package/src/clis/boss/send.ts +193 -0
  129. package/src/clis/chaoxing/README.md +36 -0
  130. package/src/clis/chaoxing/README.zh-CN.md +35 -0
  131. package/src/clis/chaoxing/assignments.ts +88 -0
  132. package/src/clis/chaoxing/exams.ts +88 -0
  133. package/src/clis/chatgpt/ask.ts +14 -15
  134. package/src/clis/chatgpt/ax.ts +81 -0
  135. package/src/clis/chatgpt/read.ts +5 -7
  136. package/src/clis/chatwise/history.ts +15 -1
  137. package/src/clis/discord-app/channels.ts +33 -21
  138. package/src/clis/twitter/accept.ts +213 -0
  139. package/src/clis/twitter/followers.ts +36 -29
  140. package/src/clis/twitter/following.ts +25 -20
  141. package/src/clis/twitter/notifications.ts +34 -27
  142. package/src/clis/twitter/post.ts +9 -2
  143. package/src/clis/twitter/reply-dm.ts +193 -0
  144. package/src/clis/twitter/search.ts +34 -12
  145. package/src/clis/weread/book.ts +28 -0
  146. package/src/clis/weread/highlights.ts +25 -0
  147. package/src/clis/weread/notebooks.ts +23 -0
  148. package/src/clis/weread/notes.ts +31 -0
  149. package/src/clis/weread/ranking.ts +29 -0
  150. package/src/clis/weread/search.ts +26 -0
  151. package/src/clis/weread/shelf.ts +26 -0
  152. package/src/clis/weread/utils.test.ts +104 -0
  153. package/src/clis/weread/utils.ts +74 -0
  154. package/src/clis/xiaohongshu/download.ts +1 -1
  155. package/src/daemon.ts +2 -2
  156. package/src/doctor.ts +2 -19
  157. package/src/engine.ts +20 -13
  158. package/src/explore.ts +51 -100
  159. package/src/main.ts +4 -186
  160. package/src/output.ts +12 -12
  161. package/src/registry.ts +3 -3
  162. package/src/runtime.ts +2 -6
  163. package/src/scripts/framework.ts +20 -0
  164. package/src/scripts/interact.ts +22 -0
  165. package/src/scripts/store.ts +40 -0
  166. package/src/setup.ts +2 -2
  167. package/src/synthesize.ts +1 -1
  168. package/tests/e2e/public-commands.test.ts +68 -1
  169. package/dist/clis/grok/debug.d.ts +0 -1
  170. package/dist/clis/grok/debug.js +0 -45
  171. package/src/clis/grok/debug.ts +0 -49
@@ -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.
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { Command, Result } from './protocol';
9
9
  import { DAEMON_WS_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
10
- import * as cdp from './cdp';
10
+ import * as executor from './cdp';
11
11
 
12
12
  let ws: WebSocket | null = null;
13
13
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
@@ -85,6 +85,66 @@ function scheduleReconnect(): void {
85
85
  }, delay);
86
86
  }
87
87
 
88
+ // ─── Automation window isolation ─────────────────────────────────────
89
+ // All opencli operations happen in a dedicated Chrome window so the
90
+ // user's active browsing session is never touched.
91
+ // The window auto-closes after 30s of idle (no commands).
92
+
93
+ let automationWindowId: number | null = null;
94
+ let windowIdleTimer: ReturnType<typeof setTimeout> | null = null;
95
+ const WINDOW_IDLE_TIMEOUT = 30000; // 30s
96
+
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;
108
+ }
109
+ windowIdleTimer = null;
110
+ }, WINDOW_IDLE_TIMEOUT);
111
+ }
112
+
113
+ /** Get or create the dedicated automation window. */
114
+ async function getAutomationWindow(): Promise<number> {
115
+ // Check if our window is still alive
116
+ if (automationWindowId !== null) {
117
+ try {
118
+ await chrome.windows.get(automationWindowId);
119
+ return automationWindowId;
120
+ } catch {
121
+ // Window was closed by user
122
+ automationWindowId = null;
123
+ }
124
+ }
125
+
126
+ // Create a new window with about:blank (not chrome://newtab which blocks scripting)
127
+ const win = await chrome.windows.create({
128
+ url: 'about:blank',
129
+ focused: false,
130
+ width: 1280,
131
+ height: 900,
132
+ type: 'normal',
133
+ });
134
+ automationWindowId = win.id!;
135
+ console.log(`[opencli] Created automation window ${automationWindowId}`);
136
+ return automationWindowId;
137
+ }
138
+
139
+ // Clean up when the automation window is closed
140
+ 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; }
145
+ }
146
+ });
147
+
88
148
  // ─── Lifecycle events ────────────────────────────────────────────────
89
149
 
90
150
  let initialized = false;
@@ -93,9 +153,9 @@ function initialize(): void {
93
153
  if (initialized) return;
94
154
  initialized = true;
95
155
  chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds
96
- cdp.registerListeners();
156
+ executor.registerListeners();
97
157
  connect();
98
- console.log('[opencli] Browser Bridge extension initialized');
158
+ console.log('[opencli] OpenCLI extension initialized');
99
159
  }
100
160
 
101
161
  chrome.runtime.onInstalled.addListener(() => {
@@ -113,6 +173,8 @@ chrome.alarms.onAlarm.addListener((alarm) => {
113
173
  // ─── Command dispatcher ─────────────────────────────────────────────
114
174
 
115
175
  async function handleCommand(cmd: Command): Promise<Result> {
176
+ // Reset idle timer on every command (window stays alive while active)
177
+ resetWindowIdleTimer();
116
178
  try {
117
179
  switch (cmd.action) {
118
180
  case 'exec':
@@ -125,6 +187,8 @@ async function handleCommand(cmd: Command): Promise<Result> {
125
187
  return await handleCookies(cmd);
126
188
  case 'screenshot':
127
189
  return await handleScreenshot(cmd);
190
+ case 'close-window':
191
+ return await handleCloseWindow(cmd);
128
192
  default:
129
193
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
130
194
  }
@@ -145,27 +209,28 @@ function isWebUrl(url?: string): boolean {
145
209
  return !url.startsWith('chrome://') && !url.startsWith('chrome-extension://');
146
210
  }
147
211
 
148
- /** Resolve target tab: use specified tabId or fall back to active web page tab */
212
+ /**
213
+ * Resolve target tab in the automation window.
214
+ * If explicit tabId is given, use that directly.
215
+ * Otherwise, find or create a tab in the dedicated automation window.
216
+ */
149
217
  async function resolveTabId(tabId?: number): Promise<number> {
150
218
  if (tabId !== undefined) return tabId;
151
219
 
152
- // Try the active tab first
153
- const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
154
- if (activeTab?.id && isWebUrl(activeTab.url)) {
155
- return activeTab.id;
156
- }
220
+ // Get (or create) the automation window
221
+ const windowId = await getAutomationWindow();
157
222
 
158
- // Active tab is not debuggable try to find any open web page tab
159
- const allTabs = await chrome.tabs.query({ currentWindow: true });
160
- const webTab = allTabs.find(t => t.id && isWebUrl(t.url));
161
- if (webTab?.id) {
162
- await chrome.tabs.update(webTab.id, { active: true });
163
- return webTab.id;
164
- }
223
+ // Find the active tab in our automation window
224
+ const tabs = await chrome.tabs.query({ windowId });
225
+ const webTab = tabs.find(t => t.id && isWebUrl(t.url));
226
+ if (webTab?.id) return webTab.id;
165
227
 
166
- // No web tabs at all create one
167
- const newTab = await chrome.tabs.create({ url: 'about:blank', active: true });
168
- if (!newTab.id) throw new Error('Failed to create new tab');
228
+ // Use the first tab if it's a blank/new tab page
229
+ if (tabs.length > 0 && tabs[0]?.id) return tabs[0].id;
230
+
231
+ // No suitable tab — create one
232
+ const newTab = await chrome.tabs.create({ windowId, url: 'about:blank', active: true });
233
+ if (!newTab.id) throw new Error('Failed to create tab in automation window');
169
234
  return newTab.id;
170
235
  }
171
236
 
@@ -173,7 +238,7 @@ async function handleExec(cmd: Command): Promise<Result> {
173
238
  if (!cmd.code) return { id: cmd.id, ok: false, error: 'Missing code' };
174
239
  const tabId = await resolveTabId(cmd.tabId);
175
240
  try {
176
- const data = await cdp.evaluateAsync(tabId, cmd.code);
241
+ const data = await executor.evaluateAsync(tabId, cmd.code);
177
242
  return { id: cmd.id, ok: true, data };
178
243
  } catch (err) {
179
244
  return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -235,12 +300,12 @@ async function handleTabs(cmd: Command): Promise<Result> {
235
300
  const target = tabs[cmd.index];
236
301
  if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
237
302
  await chrome.tabs.remove(target.id);
238
- cdp.detach(target.id);
303
+ executor.detach(target.id);
239
304
  return { id: cmd.id, ok: true, data: { closed: target.id } };
240
305
  }
241
306
  const tabId = await resolveTabId(cmd.tabId);
242
307
  await chrome.tabs.remove(tabId);
243
- cdp.detach(tabId);
308
+ executor.detach(tabId);
244
309
  return { id: cmd.id, ok: true, data: { closed: tabId } };
245
310
  }
246
311
  case 'select': {
@@ -281,7 +346,7 @@ async function handleCookies(cmd: Command): Promise<Result> {
281
346
  async function handleScreenshot(cmd: Command): Promise<Result> {
282
347
  const tabId = await resolveTabId(cmd.tabId);
283
348
  try {
284
- const data = await cdp.screenshot(tabId, {
349
+ const data = await executor.screenshot(tabId, {
285
350
  format: cmd.format,
286
351
  quality: cmd.quality,
287
352
  fullPage: cmd.fullPage,
@@ -291,3 +356,15 @@ async function handleScreenshot(cmd: Command): Promise<Result> {
291
356
  return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
292
357
  }
293
358
  }
359
+
360
+ async function handleCloseWindow(cmd: Command): Promise<Result> {
361
+ if (automationWindowId !== null) {
362
+ try {
363
+ await chrome.windows.remove(automationWindowId);
364
+ } catch {
365
+ // Window may already be closed
366
+ }
367
+ automationWindowId = null;
368
+ }
369
+ return { id: cmd.id, ok: true, data: { closed: true } };
370
+ }
@@ -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';
8
+ export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
9
9
 
10
10
  export interface Command {
11
11
  /** Unique request ID */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,295 @@
1
+ /**
2
+ * CDP client — implements IPage by connecting directly to a Chrome/Electron CDP WebSocket.
3
+ */
4
+
5
+ import { WebSocket } from 'ws';
6
+ import type { IPage } from '../types.js';
7
+ import { wrapForEval } from './utils.js';
8
+
9
+ export interface CDPTarget {
10
+ type?: string;
11
+ url?: string;
12
+ title?: string;
13
+ webSocketDebuggerUrl?: string;
14
+ }
15
+
16
+ export class CDPBridge {
17
+ private _ws: WebSocket | null = null;
18
+ private _idCounter = 0;
19
+ private _pending = new Map<number, { resolve: (val: any) => void; reject: (err: Error) => void }>();
20
+
21
+ async connect(opts?: { timeout?: number }): Promise<IPage> {
22
+ const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
23
+ if (!endpoint) throw new Error('OPENCLI_CDP_ENDPOINT is not set');
24
+
25
+ // If it's a direct ws:// URL, use it. Otherwise, fetch the /json endpoint to find a page.
26
+ let wsUrl = endpoint;
27
+ if (endpoint.startsWith('http')) {
28
+ const res = await fetch(`${endpoint.replace(/\/$/, '')}/json`);
29
+ if (!res.ok) throw new Error(`Failed to fetch CDP targets: ${res.statusText}`);
30
+ const targets = await res.json() as CDPTarget[];
31
+ const target = selectCDPTarget(targets);
32
+ if (!target || !target.webSocketDebuggerUrl) {
33
+ throw new Error('No inspectable targets found at CDP endpoint');
34
+ }
35
+ wsUrl = target.webSocketDebuggerUrl;
36
+ }
37
+
38
+ return new Promise((resolve, reject) => {
39
+ const ws = new WebSocket(wsUrl);
40
+ const timeout = setTimeout(() => reject(new Error('CDP connect timeout')), opts?.timeout ?? 10000);
41
+
42
+ ws.on('open', () => {
43
+ clearTimeout(timeout);
44
+ this._ws = ws;
45
+ resolve(new CDPPage(this));
46
+ });
47
+
48
+ ws.on('error', (err) => {
49
+ clearTimeout(timeout);
50
+ reject(err);
51
+ });
52
+
53
+ ws.on('message', (data) => {
54
+ try {
55
+ const msg = JSON.parse(data.toString());
56
+ if (msg.id && this._pending.has(msg.id)) {
57
+ const { resolve, reject } = this._pending.get(msg.id)!;
58
+ this._pending.delete(msg.id);
59
+ if (msg.error) {
60
+ reject(new Error(msg.error.message));
61
+ } else {
62
+ resolve(msg.result);
63
+ }
64
+ }
65
+ } catch (e) {
66
+ // ignore parsing errors
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ async close(): Promise<void> {
73
+ if (this._ws) {
74
+ this._ws.close();
75
+ this._ws = null;
76
+ }
77
+ for (const p of this._pending.values()) {
78
+ p.reject(new Error('CDP connection closed'));
79
+ }
80
+ this._pending.clear();
81
+ }
82
+
83
+ async send(method: string, params: any = {}): Promise<any> {
84
+ if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
85
+ throw new Error('CDP connection is not open');
86
+ }
87
+ const id = ++this._idCounter;
88
+ return new Promise((resolve, reject) => {
89
+ this._pending.set(id, { resolve, reject });
90
+ this._ws!.send(JSON.stringify({ id, method, params }));
91
+ });
92
+ }
93
+ }
94
+
95
+ class CDPPage implements IPage {
96
+ constructor(private bridge: CDPBridge) {}
97
+
98
+ async goto(url: string): Promise<void> {
99
+ await this.bridge.send('Page.navigate', { url });
100
+ await new Promise(r => setTimeout(r, 1000));
101
+ }
102
+
103
+ async evaluate(js: string): Promise<any> {
104
+ const expression = wrapForEval(js);
105
+ const result = await this.bridge.send('Runtime.evaluate', {
106
+ expression,
107
+ returnByValue: true,
108
+ awaitPromise: true
109
+ });
110
+ if (result.exceptionDetails) {
111
+ throw new Error('Evaluate error: ' + (result.exceptionDetails.exception?.description || 'Unknown exception'));
112
+ }
113
+ return result.result?.value;
114
+ }
115
+
116
+ async snapshot(opts?: any): Promise<any> {
117
+ throw new Error('Method not implemented.');
118
+ }
119
+ async click(ref: string): Promise<void> {
120
+ const safeRef = JSON.stringify(ref);
121
+ const code = `
122
+ (() => {
123
+ const ref = ${safeRef};
124
+ const el = document.querySelector('[data-ref="' + ref + '"]')
125
+ || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
126
+ if (!el) throw new Error('Element not found: ' + ref);
127
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
128
+ el.click();
129
+ return 'clicked';
130
+ })()
131
+ `;
132
+ await this.evaluate(code);
133
+ }
134
+ async typeText(ref: string, text: string): Promise<void> {
135
+ const safeRef = JSON.stringify(ref);
136
+ const safeText = JSON.stringify(text);
137
+ const code = `
138
+ (() => {
139
+ const ref = ${safeRef};
140
+ const el = document.querySelector('[data-ref="' + ref + '"]')
141
+ || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
142
+ if (!el) throw new Error('Element not found: ' + ref);
143
+ el.focus();
144
+ el.value = ${safeText};
145
+ el.dispatchEvent(new Event('input', { bubbles: true }));
146
+ el.dispatchEvent(new Event('change', { bubbles: true }));
147
+ return 'typed';
148
+ })()
149
+ `;
150
+ await this.evaluate(code);
151
+ }
152
+ async pressKey(key: string): Promise<void> {
153
+ const code = `
154
+ (() => {
155
+ const el = document.activeElement || document.body;
156
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
157
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
158
+ return 'pressed';
159
+ })()
160
+ `;
161
+ await this.evaluate(code);
162
+ }
163
+ async wait(options: any): Promise<void> {
164
+ if (typeof options === 'number') {
165
+ await new Promise(resolve => setTimeout(resolve, options * 1000));
166
+ return;
167
+ }
168
+ if (options.time) {
169
+ await new Promise(resolve => setTimeout(resolve, options.time * 1000));
170
+ return;
171
+ }
172
+ if (options.text) {
173
+ const timeout = (options.timeout ?? 30) * 1000;
174
+ const code = `
175
+ new Promise((resolve, reject) => {
176
+ const deadline = Date.now() + ${timeout};
177
+ const check = () => {
178
+ if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
179
+ if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
180
+ setTimeout(check, 200);
181
+ };
182
+ check();
183
+ })
184
+ `;
185
+ await this.evaluate(code);
186
+ }
187
+ }
188
+ async tabs(): Promise<any> {
189
+ throw new Error('Method not implemented.');
190
+ }
191
+ async closeTab(index?: number): Promise<void> {
192
+ throw new Error('Method not implemented.');
193
+ }
194
+ async newTab(): Promise<void> {
195
+ throw new Error('Method not implemented.');
196
+ }
197
+ async selectTab(index: number): Promise<void> {
198
+ throw new Error('Method not implemented.');
199
+ }
200
+ async networkRequests(includeStatic?: boolean): Promise<any> {
201
+ throw new Error('Method not implemented.');
202
+ }
203
+ async consoleMessages(level?: string): Promise<any> {
204
+ throw new Error('Method not implemented.');
205
+ }
206
+ async scroll(direction?: string, amount?: number): Promise<void> {
207
+ throw new Error('Method not implemented.');
208
+ }
209
+ async autoScroll(options?: any): Promise<void> {
210
+ throw new Error('Method not implemented.');
211
+ }
212
+ async installInterceptor(pattern: string): Promise<void> {
213
+ throw new Error('Method not implemented.');
214
+ }
215
+ async getInterceptedRequests(): Promise<any[]> {
216
+ throw new Error('Method not implemented.');
217
+ }
218
+ async screenshot(options?: any): Promise<string> {
219
+ throw new Error('Method not implemented.');
220
+ }
221
+ }
222
+ function selectCDPTarget(targets: CDPTarget[]): CDPTarget | undefined {
223
+ const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET);
224
+
225
+ const ranked = targets
226
+ .map((target, index) => ({ target, index, score: scoreCDPTarget(target, preferredPattern) }))
227
+ .filter(({ score }) => Number.isFinite(score))
228
+ .sort((a, b) => {
229
+ if (b.score !== a.score) return b.score - a.score;
230
+ return a.index - b.index;
231
+ });
232
+
233
+ return ranked[0]?.target;
234
+ }
235
+
236
+ function scoreCDPTarget(target: CDPTarget, preferredPattern?: RegExp): number {
237
+ if (!target.webSocketDebuggerUrl) return Number.NEGATIVE_INFINITY;
238
+
239
+ const type = (target.type ?? '').toLowerCase();
240
+ const url = (target.url ?? '').toLowerCase();
241
+ const title = (target.title ?? '').toLowerCase();
242
+ const haystack = `${title} ${url}`;
243
+
244
+ if (!haystack.trim() && !type) return Number.NEGATIVE_INFINITY;
245
+ if (haystack.includes('devtools')) return Number.NEGATIVE_INFINITY;
246
+
247
+ let score = 0;
248
+
249
+ if (preferredPattern && preferredPattern.test(haystack)) score += 1000;
250
+
251
+ if (type === 'app') score += 120;
252
+ else if (type === 'webview') score += 100;
253
+ else if (type === 'page') score += 80;
254
+ else if (type === 'iframe') score += 20;
255
+
256
+ if (url.startsWith('http://localhost') || url.startsWith('https://localhost')) score += 90;
257
+ if (url.startsWith('file://')) score += 60;
258
+ if (url.startsWith('http://127.0.0.1') || url.startsWith('https://127.0.0.1')) score += 50;
259
+ if (url.startsWith('about:blank')) score -= 120;
260
+ if (url === '' || url === 'about:blank') score -= 40;
261
+
262
+ if (title && title !== 'devtools') score += 25;
263
+ if (title.includes('antigravity')) score += 120;
264
+ if (title.includes('codex')) score += 120;
265
+ if (title.includes('cursor')) score += 120;
266
+ if (title.includes('chatwise')) score += 120;
267
+ if (title.includes('notion')) score += 120;
268
+ if (title.includes('discord')) score += 120;
269
+ if (title.includes('netease')) score += 120;
270
+
271
+ if (url.includes('antigravity')) score += 100;
272
+ if (url.includes('codex')) score += 100;
273
+ if (url.includes('cursor')) score += 100;
274
+ if (url.includes('chatwise')) score += 100;
275
+ if (url.includes('notion')) score += 100;
276
+ if (url.includes('discord')) score += 100;
277
+ if (url.includes('netease')) score += 100;
278
+
279
+ return score;
280
+ }
281
+
282
+ function compilePreferredPattern(raw: string | undefined): RegExp | undefined {
283
+ const value = raw?.trim();
284
+ if (!value) return undefined;
285
+ return new RegExp(escapeRegExp(value.toLowerCase()));
286
+ }
287
+
288
+ function escapeRegExp(value: string): string {
289
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
290
+ }
291
+
292
+ export const __test__ = {
293
+ selectCDPTarget,
294
+ scoreCDPTarget,
295
+ };
@@ -15,7 +15,7 @@ function generateId(): string {
15
15
 
16
16
  export interface DaemonCommand {
17
17
  id: string;
18
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot';
18
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
19
19
  tabId?: number;
20
20
  code?: string;
21
21
  url?: string;
@@ -6,15 +6,12 @@
6
6
  */
7
7
 
8
8
  export { Page } from './page.js';
9
- export { PlaywrightMCP } from './mcp.js';
9
+ export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
10
+ export { CDPBridge } from './cdp.js';
10
11
  export { isDaemonRunning } from './daemon-client.js';
11
12
 
12
- // Backward compatibility: getTokenFingerprint is no longer needed but kept as no-op export
13
- export function getTokenFingerprint(_token: string | undefined): string | null {
14
- return null;
15
- }
16
-
17
13
  import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
14
+ import { __test__ as cdpTest } from './cdp.js';
18
15
  import { withTimeoutMs } from '../runtime.js';
19
16
 
20
17
  export const __test__ = {
@@ -22,4 +19,6 @@ export const __test__ = {
22
19
  diffTabIndexes,
23
20
  appendLimited,
24
21
  withTimeoutMs,
22
+ selectCDPTarget: cdpTest.selectCDPTarget,
23
+ scoreCDPTarget: cdpTest.scoreCDPTarget,
25
24
  };
@@ -1,8 +1,5 @@
1
1
  /**
2
2
  * Browser session manager — auto-spawns daemon and provides IPage.
3
- *
4
- * Replaces the old PlaywrightMCP class. Still exports as PlaywrightMCP
5
- * for backward compatibility with main.ts and other consumers.
6
3
  */
7
4
 
8
5
  import { spawn, type ChildProcess } from 'node:child_process';
@@ -15,21 +12,17 @@ import { isDaemonRunning, isExtensionConnected } from './daemon-client.js';
15
12
 
16
13
  const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension
17
14
 
18
- export type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
19
-
20
-
15
+ export type BrowserBridgeState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
21
16
 
22
17
  /**
23
18
  * Browser factory: manages daemon lifecycle and provides IPage instances.
24
- *
25
- * Kept as `PlaywrightMCP` class name for backward compatibility.
26
19
  */
27
- export class PlaywrightMCP {
28
- private _state: PlaywrightMCPState = 'idle';
20
+ export class BrowserBridge {
21
+ private _state: BrowserBridgeState = 'idle';
29
22
  private _page: Page | null = null;
30
23
  private _daemonProc: ChildProcess | null = null;
31
24
 
32
- get state(): PlaywrightMCPState {
25
+ get state(): BrowserBridgeState {
33
26
  return this._state;
34
27
  }
35
28
 
@@ -78,10 +71,13 @@ export class PlaywrightMCP {
78
71
  console.error(`[opencli] Starting daemon (${isTs ? 'ts' : 'js'})...`);
79
72
  }
80
73
 
81
- // Use the current runtime to spawn daemon avoids slow npx resolution.
82
- // If already running under tsx (dev), process.execPath is tsx's node.
83
- // If running compiled (node dist/), process.execPath is node.
84
- this._daemonProc = spawn(process.execPath, [daemonPath], {
74
+ // For compiled .js, use the current node binary directly (fast).
75
+ // For .ts dev mode, node can't run .ts files — use tsx via --import.
76
+ const spawnArgs = isTs
77
+ ? [process.execPath, '--import', 'tsx/esm', daemonPath]
78
+ : [process.execPath, daemonPath];
79
+
80
+ this._daemonProc = spawn(spawnArgs[0], spawnArgs.slice(1), {
85
81
  detached: true,
86
82
  stdio: 'ignore',
87
83
  env: { ...process.env },
@@ -110,3 +106,6 @@ export class PlaywrightMCP {
110
106
  );
111
107
  }
112
108
  }
109
+
110
+ /** @deprecated Use BrowserBridge instead */
111
+ export const PlaywrightMCP = BrowserBridge;