@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
@@ -0,0 +1,295 @@
1
+ /**
2
+ * CDP client — implements IPage by connecting directly to a Chrome/Electron CDP WebSocket.
3
+ */
4
+ import { WebSocket } from 'ws';
5
+ import { wrapForEval } from './utils.js';
6
+ export class CDPBridge {
7
+ _ws = null;
8
+ _idCounter = 0;
9
+ _pending = new Map();
10
+ async connect(opts) {
11
+ const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
12
+ if (!endpoint)
13
+ throw new Error('OPENCLI_CDP_ENDPOINT is not set');
14
+ // If it's a direct ws:// URL, use it. Otherwise, fetch the /json endpoint to find a page.
15
+ let wsUrl = endpoint;
16
+ if (endpoint.startsWith('http')) {
17
+ const res = await fetch(`${endpoint.replace(/\/$/, '')}/json`);
18
+ if (!res.ok)
19
+ throw new Error(`Failed to fetch CDP targets: ${res.statusText}`);
20
+ const targets = await res.json();
21
+ const target = selectCDPTarget(targets);
22
+ if (!target || !target.webSocketDebuggerUrl) {
23
+ throw new Error('No inspectable targets found at CDP endpoint');
24
+ }
25
+ wsUrl = target.webSocketDebuggerUrl;
26
+ }
27
+ return new Promise((resolve, reject) => {
28
+ const ws = new WebSocket(wsUrl);
29
+ const timeout = setTimeout(() => reject(new Error('CDP connect timeout')), opts?.timeout ?? 10000);
30
+ ws.on('open', () => {
31
+ clearTimeout(timeout);
32
+ this._ws = ws;
33
+ resolve(new CDPPage(this));
34
+ });
35
+ ws.on('error', (err) => {
36
+ clearTimeout(timeout);
37
+ reject(err);
38
+ });
39
+ ws.on('message', (data) => {
40
+ try {
41
+ const msg = JSON.parse(data.toString());
42
+ if (msg.id && this._pending.has(msg.id)) {
43
+ const { resolve, reject } = this._pending.get(msg.id);
44
+ this._pending.delete(msg.id);
45
+ if (msg.error) {
46
+ reject(new Error(msg.error.message));
47
+ }
48
+ else {
49
+ resolve(msg.result);
50
+ }
51
+ }
52
+ }
53
+ catch (e) {
54
+ // ignore parsing errors
55
+ }
56
+ });
57
+ });
58
+ }
59
+ async close() {
60
+ if (this._ws) {
61
+ this._ws.close();
62
+ this._ws = null;
63
+ }
64
+ for (const p of this._pending.values()) {
65
+ p.reject(new Error('CDP connection closed'));
66
+ }
67
+ this._pending.clear();
68
+ }
69
+ async send(method, params = {}) {
70
+ if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
71
+ throw new Error('CDP connection is not open');
72
+ }
73
+ const id = ++this._idCounter;
74
+ return new Promise((resolve, reject) => {
75
+ this._pending.set(id, { resolve, reject });
76
+ this._ws.send(JSON.stringify({ id, method, params }));
77
+ });
78
+ }
79
+ }
80
+ class CDPPage {
81
+ bridge;
82
+ constructor(bridge) {
83
+ this.bridge = bridge;
84
+ }
85
+ async goto(url) {
86
+ await this.bridge.send('Page.navigate', { url });
87
+ await new Promise(r => setTimeout(r, 1000));
88
+ }
89
+ async evaluate(js) {
90
+ const expression = wrapForEval(js);
91
+ const result = await this.bridge.send('Runtime.evaluate', {
92
+ expression,
93
+ returnByValue: true,
94
+ awaitPromise: true
95
+ });
96
+ if (result.exceptionDetails) {
97
+ throw new Error('Evaluate error: ' + (result.exceptionDetails.exception?.description || 'Unknown exception'));
98
+ }
99
+ return result.result?.value;
100
+ }
101
+ async snapshot(opts) {
102
+ throw new Error('Method not implemented.');
103
+ }
104
+ async click(ref) {
105
+ const safeRef = JSON.stringify(ref);
106
+ const code = `
107
+ (() => {
108
+ const ref = ${safeRef};
109
+ const el = document.querySelector('[data-ref="' + ref + '"]')
110
+ || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
111
+ if (!el) throw new Error('Element not found: ' + ref);
112
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
113
+ el.click();
114
+ return 'clicked';
115
+ })()
116
+ `;
117
+ await this.evaluate(code);
118
+ }
119
+ async typeText(ref, text) {
120
+ const safeRef = JSON.stringify(ref);
121
+ const safeText = JSON.stringify(text);
122
+ const code = `
123
+ (() => {
124
+ const ref = ${safeRef};
125
+ const el = document.querySelector('[data-ref="' + ref + '"]')
126
+ || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
127
+ if (!el) throw new Error('Element not found: ' + ref);
128
+ el.focus();
129
+ el.value = ${safeText};
130
+ el.dispatchEvent(new Event('input', { bubbles: true }));
131
+ el.dispatchEvent(new Event('change', { bubbles: true }));
132
+ return 'typed';
133
+ })()
134
+ `;
135
+ await this.evaluate(code);
136
+ }
137
+ async pressKey(key) {
138
+ const code = `
139
+ (() => {
140
+ const el = document.activeElement || document.body;
141
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
142
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
143
+ return 'pressed';
144
+ })()
145
+ `;
146
+ await this.evaluate(code);
147
+ }
148
+ async wait(options) {
149
+ if (typeof options === 'number') {
150
+ await new Promise(resolve => setTimeout(resolve, options * 1000));
151
+ return;
152
+ }
153
+ if (options.time) {
154
+ await new Promise(resolve => setTimeout(resolve, options.time * 1000));
155
+ return;
156
+ }
157
+ if (options.text) {
158
+ const timeout = (options.timeout ?? 30) * 1000;
159
+ const code = `
160
+ new Promise((resolve, reject) => {
161
+ const deadline = Date.now() + ${timeout};
162
+ const check = () => {
163
+ if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
164
+ if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
165
+ setTimeout(check, 200);
166
+ };
167
+ check();
168
+ })
169
+ `;
170
+ await this.evaluate(code);
171
+ }
172
+ }
173
+ async tabs() {
174
+ throw new Error('Method not implemented.');
175
+ }
176
+ async closeTab(index) {
177
+ throw new Error('Method not implemented.');
178
+ }
179
+ async newTab() {
180
+ throw new Error('Method not implemented.');
181
+ }
182
+ async selectTab(index) {
183
+ throw new Error('Method not implemented.');
184
+ }
185
+ async networkRequests(includeStatic) {
186
+ throw new Error('Method not implemented.');
187
+ }
188
+ async consoleMessages(level) {
189
+ throw new Error('Method not implemented.');
190
+ }
191
+ async scroll(direction, amount) {
192
+ throw new Error('Method not implemented.');
193
+ }
194
+ async autoScroll(options) {
195
+ throw new Error('Method not implemented.');
196
+ }
197
+ async installInterceptor(pattern) {
198
+ throw new Error('Method not implemented.');
199
+ }
200
+ async getInterceptedRequests() {
201
+ throw new Error('Method not implemented.');
202
+ }
203
+ async screenshot(options) {
204
+ throw new Error('Method not implemented.');
205
+ }
206
+ }
207
+ function selectCDPTarget(targets) {
208
+ const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET);
209
+ const ranked = targets
210
+ .map((target, index) => ({ target, index, score: scoreCDPTarget(target, preferredPattern) }))
211
+ .filter(({ score }) => Number.isFinite(score))
212
+ .sort((a, b) => {
213
+ if (b.score !== a.score)
214
+ return b.score - a.score;
215
+ return a.index - b.index;
216
+ });
217
+ return ranked[0]?.target;
218
+ }
219
+ function scoreCDPTarget(target, preferredPattern) {
220
+ if (!target.webSocketDebuggerUrl)
221
+ return Number.NEGATIVE_INFINITY;
222
+ const type = (target.type ?? '').toLowerCase();
223
+ const url = (target.url ?? '').toLowerCase();
224
+ const title = (target.title ?? '').toLowerCase();
225
+ const haystack = `${title} ${url}`;
226
+ if (!haystack.trim() && !type)
227
+ return Number.NEGATIVE_INFINITY;
228
+ if (haystack.includes('devtools'))
229
+ return Number.NEGATIVE_INFINITY;
230
+ let score = 0;
231
+ if (preferredPattern && preferredPattern.test(haystack))
232
+ score += 1000;
233
+ if (type === 'app')
234
+ score += 120;
235
+ else if (type === 'webview')
236
+ score += 100;
237
+ else if (type === 'page')
238
+ score += 80;
239
+ else if (type === 'iframe')
240
+ score += 20;
241
+ if (url.startsWith('http://localhost') || url.startsWith('https://localhost'))
242
+ score += 90;
243
+ if (url.startsWith('file://'))
244
+ score += 60;
245
+ if (url.startsWith('http://127.0.0.1') || url.startsWith('https://127.0.0.1'))
246
+ score += 50;
247
+ if (url.startsWith('about:blank'))
248
+ score -= 120;
249
+ if (url === '' || url === 'about:blank')
250
+ score -= 40;
251
+ if (title && title !== 'devtools')
252
+ score += 25;
253
+ if (title.includes('antigravity'))
254
+ score += 120;
255
+ if (title.includes('codex'))
256
+ score += 120;
257
+ if (title.includes('cursor'))
258
+ score += 120;
259
+ if (title.includes('chatwise'))
260
+ score += 120;
261
+ if (title.includes('notion'))
262
+ score += 120;
263
+ if (title.includes('discord'))
264
+ score += 120;
265
+ if (title.includes('netease'))
266
+ score += 120;
267
+ if (url.includes('antigravity'))
268
+ score += 100;
269
+ if (url.includes('codex'))
270
+ score += 100;
271
+ if (url.includes('cursor'))
272
+ score += 100;
273
+ if (url.includes('chatwise'))
274
+ score += 100;
275
+ if (url.includes('notion'))
276
+ score += 100;
277
+ if (url.includes('discord'))
278
+ score += 100;
279
+ if (url.includes('netease'))
280
+ score += 100;
281
+ return score;
282
+ }
283
+ function compilePreferredPattern(raw) {
284
+ const value = raw?.trim();
285
+ if (!value)
286
+ return undefined;
287
+ return new RegExp(escapeRegExp(value.toLowerCase()));
288
+ }
289
+ function escapeRegExp(value) {
290
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
291
+ }
292
+ export const __test__ = {
293
+ selectCDPTarget,
294
+ scoreCDPTarget,
295
+ };
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export interface DaemonCommand {
7
7
  id: string;
8
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot';
8
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
9
9
  tabId?: number;
10
10
  code?: string;
11
11
  url?: string;
@@ -5,9 +5,9 @@
5
5
  * External code should import from './browser/index.js' (or './browser.js' via Node resolution).
6
6
  */
7
7
  export { Page } from './page.js';
8
- export { PlaywrightMCP } from './mcp.js';
8
+ export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
9
+ export { CDPBridge } from './cdp.js';
9
10
  export { isDaemonRunning } from './daemon-client.js';
10
- export declare function getTokenFingerprint(_token: string | undefined): string | null;
11
11
  import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
12
12
  import { withTimeoutMs } from '../runtime.js';
13
13
  export declare const __test__: {
@@ -15,4 +15,6 @@ export declare const __test__: {
15
15
  diffTabIndexes: typeof diffTabIndexes;
16
16
  appendLimited: typeof appendLimited;
17
17
  withTimeoutMs: typeof withTimeoutMs;
18
+ selectCDPTarget: (targets: import("./cdp.js").CDPTarget[]) => import("./cdp.js").CDPTarget | undefined;
19
+ scoreCDPTarget: (target: import("./cdp.js").CDPTarget, preferredPattern?: RegExp) => number;
18
20
  };
@@ -5,17 +5,17 @@
5
5
  * External code should import from './browser/index.js' (or './browser.js' via Node resolution).
6
6
  */
7
7
  export { Page } from './page.js';
8
- export { PlaywrightMCP } from './mcp.js';
8
+ export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
9
+ export { CDPBridge } from './cdp.js';
9
10
  export { isDaemonRunning } from './daemon-client.js';
10
- // Backward compatibility: getTokenFingerprint is no longer needed but kept as no-op export
11
- export function getTokenFingerprint(_token) {
12
- return null;
13
- }
14
11
  import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
12
+ import { __test__ as cdpTest } from './cdp.js';
15
13
  import { withTimeoutMs } from '../runtime.js';
16
14
  export const __test__ = {
17
15
  extractTabEntries,
18
16
  diffTabIndexes,
19
17
  appendLimited,
20
18
  withTimeoutMs,
19
+ selectCDPTarget: cdpTest.selectCDPTarget,
20
+ scoreCDPTarget: cdpTest.scoreCDPTarget,
21
21
  };
@@ -1,24 +1,21 @@
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
  import type { IPage } from '../types.js';
8
- export type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
5
+ export type BrowserBridgeState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
9
6
  /**
10
7
  * Browser factory: manages daemon lifecycle and provides IPage instances.
11
- *
12
- * Kept as `PlaywrightMCP` class name for backward compatibility.
13
8
  */
14
- export declare class PlaywrightMCP {
9
+ export declare class BrowserBridge {
15
10
  private _state;
16
11
  private _page;
17
12
  private _daemonProc;
18
- get state(): PlaywrightMCPState;
13
+ get state(): BrowserBridgeState;
19
14
  connect(opts?: {
20
15
  timeout?: number;
21
16
  }): Promise<IPage>;
22
17
  close(): Promise<void>;
23
18
  private _ensureDaemon;
24
19
  }
20
+ /** @deprecated Use BrowserBridge instead */
21
+ export declare const PlaywrightMCP: typeof BrowserBridge;
@@ -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
  import { spawn } from 'node:child_process';
8
5
  import { fileURLToPath } from 'node:url';
@@ -13,10 +10,8 @@ import { isDaemonRunning, isExtensionConnected } from './daemon-client.js';
13
10
  const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension
14
11
  /**
15
12
  * Browser factory: manages daemon lifecycle and provides IPage instances.
16
- *
17
- * Kept as `PlaywrightMCP` class name for backward compatibility.
18
13
  */
19
- export class PlaywrightMCP {
14
+ export class BrowserBridge {
20
15
  _state = 'idle';
21
16
  _page = null;
22
17
  _daemonProc = null;
@@ -68,10 +63,12 @@ export class PlaywrightMCP {
68
63
  if (process.env.OPENCLI_VERBOSE) {
69
64
  console.error(`[opencli] Starting daemon (${isTs ? 'ts' : 'js'})...`);
70
65
  }
71
- // Use the current runtime to spawn daemon avoids slow npx resolution.
72
- // If already running under tsx (dev), process.execPath is tsx's node.
73
- // If running compiled (node dist/), process.execPath is node.
74
- this._daemonProc = spawn(process.execPath, [daemonPath], {
66
+ // For compiled .js, use the current node binary directly (fast).
67
+ // For .ts dev mode, node can't run .ts files — use tsx via --import.
68
+ const spawnArgs = isTs
69
+ ? [process.execPath, '--import', 'tsx/esm', daemonPath]
70
+ : [process.execPath, daemonPath];
71
+ this._daemonProc = spawn(spawnArgs[0], spawnArgs.slice(1), {
75
72
  detached: true,
76
73
  stdio: 'ignore',
77
74
  env: { ...process.env },
@@ -94,3 +91,5 @@ export class PlaywrightMCP {
94
91
  'Make sure port 19825 is available.');
95
92
  }
96
93
  }
94
+ /** @deprecated Use BrowserBridge instead */
95
+ export const PlaywrightMCP = BrowserBridge;
@@ -19,6 +19,8 @@ export declare class Page implements IPage {
19
19
  /** Helper: spread tabId into command params if we have one */
20
20
  private _tabOpt;
21
21
  goto(url: string): Promise<void>;
22
+ /** Close the automation window in the extension */
23
+ closeWindow(): Promise<void>;
22
24
  evaluate(js: string): Promise<any>;
23
25
  snapshot(opts?: {
24
26
  interactive?: boolean;
@@ -39,7 +41,12 @@ export declare class Page implements IPage {
39
41
  newTab(): Promise<void>;
40
42
  selectTab(index: number): Promise<void>;
41
43
  networkRequests(includeStatic?: boolean): Promise<any>;
42
- consoleMessages(level?: string): Promise<any>;
44
+ /**
45
+ * Console messages are not available in lightweight daemon mode.
46
+ * Would require CDP Runtime.consoleAPICalled event listener.
47
+ * @returns Always returns empty array.
48
+ */
49
+ consoleMessages(_level?: string): Promise<any>;
43
50
  /**
44
51
  * Capture a screenshot via CDP Page.captureScreenshot.
45
52
  * @param options.format - 'png' (default) or 'jpeg'
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { formatSnapshot } from '../snapshotFormatter.js';
13
13
  import { sendCommand } from './daemon-client.js';
14
+ import { wrapForEval } from './utils.js';
14
15
  /**
15
16
  * Page — implements IPage by talking to the daemon via HTTP.
16
17
  */
@@ -31,6 +32,15 @@ export class Page {
31
32
  this._tabId = result.tabId;
32
33
  }
33
34
  }
35
+ /** Close the automation window in the extension */
36
+ async closeWindow() {
37
+ try {
38
+ await sendCommand('close-window', {});
39
+ }
40
+ catch {
41
+ // Window may already be closed or daemon may be down
42
+ }
43
+ }
34
44
  async evaluate(js) {
35
45
  const code = wrapForEval(js);
36
46
  return sendCommand('exec', { code, ...this._tabOpt() });
@@ -168,12 +178,12 @@ export class Page {
168
178
  `;
169
179
  return sendCommand('exec', { code, ...this._tabOpt() });
170
180
  }
171
- async consoleMessages(level = 'info') {
172
- // Console messages can't be retrospectively read via CDP Runtime.evaluate.
173
- // Would need Runtime.consoleAPICalled event listener, which is not yet implemented.
174
- if (process.env.OPENCLI_VERBOSE) {
175
- console.error('[page] consoleMessages() not supported in lightweight mode — returning empty');
176
- }
181
+ /**
182
+ * Console messages are not available in lightweight daemon mode.
183
+ * Would require CDP Runtime.consoleAPICalled event listener.
184
+ * @returns Always returns empty array.
185
+ */
186
+ async consoleMessages(_level = 'info') {
177
187
  return [];
178
188
  }
179
189
  /**
@@ -234,43 +244,18 @@ export class Page {
234
244
  }
235
245
  async installInterceptor(pattern) {
236
246
  const { generateInterceptorJs } = await import('../interceptor.js');
237
- await sendCommand('exec', {
238
- code: generateInterceptorJs(JSON.stringify(pattern), {
239
- arrayName: '__opencli_xhr',
240
- patchGuard: '__opencli_interceptor_patched',
241
- }),
242
- ...this._tabOpt(),
243
- });
247
+ // Must use evaluate() so wrapForEval() converts the arrow function into an IIFE;
248
+ // sendCommand('exec') sends the code as-is, and CDP never executes a bare arrow.
249
+ await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
250
+ arrayName: '__opencli_xhr',
251
+ patchGuard: '__opencli_interceptor_patched',
252
+ }));
244
253
  }
245
254
  async getInterceptedRequests() {
246
255
  const { generateReadInterceptedJs } = await import('../interceptor.js');
247
- const result = await sendCommand('exec', {
248
- code: generateReadInterceptedJs('__opencli_xhr'),
249
- ...this._tabOpt(),
250
- });
256
+ // Same as installInterceptor: must go through evaluate() for IIFE wrapping
257
+ const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
251
258
  return result || [];
252
259
  }
253
260
  }
254
- // ─── Helpers ─────────────────────────────────────────────────────────
255
- /**
256
- * Wrap JS code for CDP Runtime.evaluate:
257
- * - Already an IIFE `(...)()` → send as-is
258
- * - Arrow/function literal → wrap as IIFE `(code)()`
259
- * - `new Promise(...)` or raw expression → send as-is (expression)
260
- */
261
- function wrapForEval(js) {
262
- const code = js.trim();
263
- if (!code)
264
- return 'undefined';
265
- // Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
266
- if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code))
267
- return code;
268
- // Arrow function: `() => ...` or `async () => ...`
269
- if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code))
270
- return `(${code})()`;
271
- // Function declaration: `function ...` or `async function ...`
272
- if (/^(async\s+)?function[\s(]/.test(code))
273
- return `(${code})()`;
274
- // Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
275
- return code;
276
- }
261
+ // (End of file)
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Utility functions for browser operations
3
+ */
4
+ /**
5
+ * Wrap JS code for CDP Runtime.evaluate:
6
+ * - Already an IIFE `(...)()` → send as-is
7
+ * - Arrow/function literal → wrap as IIFE `(code)()`
8
+ * - `new Promise(...)` or raw expression → send as-is (expression)
9
+ */
10
+ export declare function wrapForEval(js: string): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Utility functions for browser operations
3
+ */
4
+ /**
5
+ * Wrap JS code for CDP Runtime.evaluate:
6
+ * - Already an IIFE `(...)()` → send as-is
7
+ * - Arrow/function literal → wrap as IIFE `(code)()`
8
+ * - `new Promise(...)` or raw expression → send as-is (expression)
9
+ */
10
+ export function wrapForEval(js) {
11
+ if (typeof js !== 'string')
12
+ return 'undefined';
13
+ const code = js.trim();
14
+ if (!code)
15
+ return 'undefined';
16
+ // Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
17
+ if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code))
18
+ return code;
19
+ // Arrow function: `() => ...` or `async () => ...`
20
+ if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code))
21
+ return `(${code})()`;
22
+ // Function declaration: `function ...` or `async function ...`
23
+ if (/^(async\s+)?function[\s(]/.test(code))
24
+ return `(${code})()`;
25
+ // Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
26
+ return code;
27
+ }
@@ -1,5 +1,5 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { PlaywrightMCP, __test__ } from './browser/index.js';
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { BrowserBridge, __test__ } from './browser/index.js';
3
3
  describe('browser helpers', () => {
4
4
  it('extracts tab entries from string snapshots', () => {
5
5
  const entries = __test__.extractTabEntries('Tab 0 https://example.com\nTab 1 Chrome Extension');
@@ -30,26 +30,67 @@ describe('browser helpers', () => {
30
30
  it('times out slow promises', async () => {
31
31
  await expect(__test__.withTimeoutMs(new Promise(() => { }), 10, 'timeout')).rejects.toThrow('timeout');
32
32
  });
33
+ it('prefers the real Electron app target over DevTools and blank pages', () => {
34
+ const target = __test__.selectCDPTarget([
35
+ {
36
+ type: 'page',
37
+ title: 'DevTools - localhost:9224',
38
+ url: 'devtools://devtools/bundled/inspector.html',
39
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/devtools',
40
+ },
41
+ {
42
+ type: 'page',
43
+ title: '',
44
+ url: 'about:blank',
45
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/blank',
46
+ },
47
+ {
48
+ type: 'app',
49
+ title: 'Antigravity',
50
+ url: 'http://localhost:3000/',
51
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/app',
52
+ },
53
+ ]);
54
+ expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9224/app');
55
+ });
56
+ it('honors OPENCLI_CDP_TARGET when multiple inspectable targets exist', () => {
57
+ vi.stubEnv('OPENCLI_CDP_TARGET', 'codex');
58
+ const target = __test__.selectCDPTarget([
59
+ {
60
+ type: 'app',
61
+ title: 'Cursor',
62
+ url: 'http://localhost:3000/cursor',
63
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9226/cursor',
64
+ },
65
+ {
66
+ type: 'app',
67
+ title: 'OpenAI Codex',
68
+ url: 'http://localhost:3000/codex',
69
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9226/codex',
70
+ },
71
+ ]);
72
+ expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9226/codex');
73
+ });
33
74
  });
34
- describe('PlaywrightMCP state', () => {
75
+ describe('BrowserBridge state', () => {
35
76
  it('transitions to closed after close()', async () => {
36
- const mcp = new PlaywrightMCP();
77
+ const mcp = new BrowserBridge();
37
78
  expect(mcp.state).toBe('idle');
38
79
  await mcp.close();
39
80
  expect(mcp.state).toBe('closed');
40
81
  });
41
82
  it('rejects connect() after the session has been closed', async () => {
42
- const mcp = new PlaywrightMCP();
83
+ const mcp = new BrowserBridge();
43
84
  await mcp.close();
44
85
  await expect(mcp.connect()).rejects.toThrow('Session is closed');
45
86
  });
46
87
  it('rejects connect() while already connecting', async () => {
47
- const mcp = new PlaywrightMCP();
88
+ const mcp = new BrowserBridge();
48
89
  mcp._state = 'connecting';
49
90
  await expect(mcp.connect()).rejects.toThrow('Already connecting');
50
91
  });
51
92
  it('rejects connect() while closing', async () => {
52
- const mcp = new PlaywrightMCP();
93
+ const mcp = new BrowserBridge();
53
94
  mcp._state = 'closing';
54
95
  await expect(mcp.connect()).rejects.toThrow('Session is closing');
55
96
  });