@jackwener/opencli 0.9.8 → 1.0.1

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 (165) hide show
  1. package/CDP.md +1 -1
  2. package/CDP.zh-CN.md +1 -1
  3. package/CLI-ELECTRON.md +2 -2
  4. package/CLI-EXPLORER.md +4 -4
  5. package/README.md +35 -58
  6. package/README.zh-CN.md +36 -60
  7. package/SKILL.md +10 -8
  8. package/TESTING.md +7 -7
  9. package/dist/browser/daemon-client.d.ts +37 -0
  10. package/dist/browser/daemon-client.js +82 -0
  11. package/dist/browser/discover.d.ts +11 -34
  12. package/dist/browser/discover.js +15 -205
  13. package/dist/browser/errors.d.ts +6 -20
  14. package/dist/browser/errors.js +24 -63
  15. package/dist/browser/index.d.ts +2 -12
  16. package/dist/browser/index.js +2 -12
  17. package/dist/browser/mcp.d.ts +9 -21
  18. package/dist/browser/mcp.js +70 -285
  19. package/dist/browser/page.d.ts +36 -7
  20. package/dist/browser/page.js +212 -81
  21. package/dist/browser.test.js +10 -231
  22. package/dist/cli-manifest.json +561 -14
  23. package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
  24. package/dist/clis/apple-podcasts/episodes.js +28 -0
  25. package/dist/clis/apple-podcasts/search.d.ts +1 -0
  26. package/dist/clis/apple-podcasts/search.js +29 -0
  27. package/dist/clis/apple-podcasts/top.d.ts +1 -0
  28. package/dist/clis/apple-podcasts/top.js +34 -0
  29. package/dist/clis/apple-podcasts/utils.d.ts +11 -0
  30. package/dist/clis/apple-podcasts/utils.js +30 -0
  31. package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
  32. package/dist/clis/apple-podcasts/utils.test.js +57 -0
  33. package/dist/clis/chatwise/history.js +18 -1
  34. package/dist/clis/discord-app/channels.js +33 -21
  35. package/dist/clis/neteasemusic/like.d.ts +1 -0
  36. package/dist/clis/neteasemusic/like.js +25 -0
  37. package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
  38. package/dist/clis/neteasemusic/lyrics.js +47 -0
  39. package/dist/clis/neteasemusic/next.d.ts +1 -0
  40. package/dist/clis/neteasemusic/next.js +26 -0
  41. package/dist/clis/neteasemusic/play.d.ts +1 -0
  42. package/dist/clis/neteasemusic/play.js +26 -0
  43. package/dist/clis/neteasemusic/playing.d.ts +1 -0
  44. package/dist/clis/neteasemusic/playing.js +59 -0
  45. package/dist/clis/neteasemusic/playlist.d.ts +1 -0
  46. package/dist/clis/neteasemusic/playlist.js +46 -0
  47. package/dist/clis/neteasemusic/prev.d.ts +1 -0
  48. package/dist/clis/neteasemusic/prev.js +25 -0
  49. package/dist/clis/neteasemusic/search.d.ts +1 -0
  50. package/dist/clis/neteasemusic/search.js +52 -0
  51. package/dist/clis/neteasemusic/status.d.ts +1 -0
  52. package/dist/clis/neteasemusic/status.js +16 -0
  53. package/dist/clis/neteasemusic/volume.d.ts +1 -0
  54. package/dist/clis/neteasemusic/volume.js +54 -0
  55. package/dist/clis/twitter/accept.d.ts +1 -0
  56. package/dist/clis/twitter/accept.js +202 -0
  57. package/dist/clis/twitter/followers.js +30 -22
  58. package/dist/clis/twitter/following.js +19 -14
  59. package/dist/clis/twitter/notifications.js +29 -22
  60. package/dist/clis/twitter/reply-dm.d.ts +1 -0
  61. package/dist/clis/twitter/reply-dm.js +181 -0
  62. package/dist/clis/twitter/search.js +50 -12
  63. package/dist/clis/weread/book.d.ts +1 -0
  64. package/dist/clis/weread/book.js +26 -0
  65. package/dist/clis/weread/highlights.d.ts +1 -0
  66. package/dist/clis/weread/highlights.js +23 -0
  67. package/dist/clis/weread/notebooks.d.ts +1 -0
  68. package/dist/clis/weread/notebooks.js +21 -0
  69. package/dist/clis/weread/notes.d.ts +1 -0
  70. package/dist/clis/weread/notes.js +29 -0
  71. package/dist/clis/weread/ranking.d.ts +1 -0
  72. package/dist/clis/weread/ranking.js +28 -0
  73. package/dist/clis/weread/search.d.ts +1 -0
  74. package/dist/clis/weread/search.js +25 -0
  75. package/dist/clis/weread/shelf.d.ts +1 -0
  76. package/dist/clis/weread/shelf.js +24 -0
  77. package/dist/clis/weread/utils.d.ts +20 -0
  78. package/dist/clis/weread/utils.js +72 -0
  79. package/dist/clis/weread/utils.test.d.ts +1 -0
  80. package/dist/clis/weread/utils.test.js +85 -0
  81. package/dist/daemon.d.ts +13 -0
  82. package/dist/daemon.js +187 -0
  83. package/dist/doctor.d.ts +10 -65
  84. package/dist/doctor.js +49 -602
  85. package/dist/doctor.test.js +30 -170
  86. package/dist/main.js +12 -41
  87. package/dist/pipeline/executor.test.js +1 -0
  88. package/dist/pipeline/steps/browser.js +2 -2
  89. package/dist/pipeline/steps/intercept.js +1 -2
  90. package/dist/runtime.d.ts +1 -4
  91. package/dist/runtime.js +1 -4
  92. package/dist/setup.d.ts +6 -0
  93. package/dist/setup.js +46 -160
  94. package/dist/types.d.ts +6 -0
  95. package/extension/dist/background.js +484 -0
  96. package/extension/icons/icon-128.png +0 -0
  97. package/extension/icons/icon-16.png +0 -0
  98. package/extension/icons/icon-32.png +0 -0
  99. package/extension/icons/icon-48.png +0 -0
  100. package/extension/manifest.json +31 -0
  101. package/extension/package.json +16 -0
  102. package/extension/src/background.ts +370 -0
  103. package/extension/src/cdp.ts +125 -0
  104. package/extension/src/protocol.ts +57 -0
  105. package/extension/store-assets/screenshot-1280x800.png +0 -0
  106. package/extension/tsconfig.json +15 -0
  107. package/extension/vite.config.ts +18 -0
  108. package/package.json +5 -5
  109. package/src/browser/daemon-client.ts +113 -0
  110. package/src/browser/discover.ts +18 -232
  111. package/src/browser/errors.ts +30 -100
  112. package/src/browser/index.ts +2 -13
  113. package/src/browser/mcp.ts +81 -282
  114. package/src/browser/page.ts +223 -83
  115. package/src/browser.test.ts +9 -239
  116. package/src/clis/apple-podcasts/episodes.ts +28 -0
  117. package/src/clis/apple-podcasts/search.ts +29 -0
  118. package/src/clis/apple-podcasts/top.ts +34 -0
  119. package/src/clis/apple-podcasts/utils.test.ts +72 -0
  120. package/src/clis/apple-podcasts/utils.ts +37 -0
  121. package/src/clis/chatgpt/README.md +1 -1
  122. package/src/clis/chatgpt/README.zh-CN.md +1 -1
  123. package/src/clis/chatwise/history.ts +15 -1
  124. package/src/clis/discord-app/channels.ts +33 -21
  125. package/src/clis/neteasemusic/README.md +31 -0
  126. package/src/clis/neteasemusic/README.zh-CN.md +31 -0
  127. package/src/clis/neteasemusic/like.ts +28 -0
  128. package/src/clis/neteasemusic/lyrics.ts +53 -0
  129. package/src/clis/neteasemusic/next.ts +30 -0
  130. package/src/clis/neteasemusic/play.ts +30 -0
  131. package/src/clis/neteasemusic/playing.ts +62 -0
  132. package/src/clis/neteasemusic/playlist.ts +51 -0
  133. package/src/clis/neteasemusic/prev.ts +29 -0
  134. package/src/clis/neteasemusic/search.ts +58 -0
  135. package/src/clis/neteasemusic/status.ts +18 -0
  136. package/src/clis/neteasemusic/volume.ts +61 -0
  137. package/src/clis/twitter/accept.ts +213 -0
  138. package/src/clis/twitter/followers.ts +36 -29
  139. package/src/clis/twitter/following.ts +25 -20
  140. package/src/clis/twitter/notifications.ts +34 -27
  141. package/src/clis/twitter/reply-dm.ts +193 -0
  142. package/src/clis/twitter/search.ts +53 -13
  143. package/src/clis/weread/book.ts +28 -0
  144. package/src/clis/weread/highlights.ts +25 -0
  145. package/src/clis/weread/notebooks.ts +23 -0
  146. package/src/clis/weread/notes.ts +31 -0
  147. package/src/clis/weread/ranking.ts +29 -0
  148. package/src/clis/weread/search.ts +26 -0
  149. package/src/clis/weread/shelf.ts +26 -0
  150. package/src/clis/weread/utils.test.ts +104 -0
  151. package/src/clis/weread/utils.ts +74 -0
  152. package/src/daemon.ts +217 -0
  153. package/src/doctor.test.ts +32 -193
  154. package/src/doctor.ts +58 -669
  155. package/src/main.ts +11 -34
  156. package/src/pipeline/executor.test.ts +1 -0
  157. package/src/pipeline/steps/browser.ts +2 -2
  158. package/src/pipeline/steps/intercept.ts +1 -2
  159. package/src/runtime.ts +2 -6
  160. package/src/setup.ts +47 -183
  161. package/src/types.ts +1 -0
  162. package/tests/e2e/public-commands.test.ts +68 -1
  163. package/dist/clis/grok/debug.d.ts +0 -1
  164. package/dist/clis/grok/debug.js +0 -45
  165. package/src/clis/grok/debug.ts +0 -49
@@ -1,310 +1,95 @@
1
1
  /**
2
- * Playwright MCP process manager.
3
- * Handles lifecycle management, JSON-RPC communication, and browser session orchestration.
2
+ * Browser session manager — auto-spawns daemon and provides IPage.
4
3
  */
5
4
  import { spawn } from 'node:child_process';
6
- import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
7
- import { PKG_VERSION } from '../version.js';
5
+ import { fileURLToPath } from 'node:url';
6
+ import * as path from 'node:path';
7
+ import * as fs from 'node:fs';
8
8
  import { Page } from './page.js';
9
- import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js';
10
- import { findMcpServerPath, buildMcpLaunchSpec, resolveCdpEndpoint } from './discover.js';
11
- import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
12
- const STDERR_BUFFER_LIMIT = 16 * 1024;
13
- const INITIAL_TABS_TIMEOUT_MS = 1500;
14
- const TAB_CLEANUP_TIMEOUT_MS = 2000;
15
- // JSON-RPC helpers
16
- let _nextId = 1;
17
- export function createJsonRpcRequest(method, params = {}) {
18
- const id = _nextId++;
19
- return {
20
- id,
21
- message: JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n',
22
- };
23
- }
9
+ import { isDaemonRunning, isExtensionConnected } from './daemon-client.js';
10
+ const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension
24
11
  /**
25
- * Playwright MCP process manager.
12
+ * Browser factory: manages daemon lifecycle and provides IPage instances.
26
13
  */
27
- export class PlaywrightMCP {
28
- static _activeInsts = new Set();
29
- static _cleanupRegistered = false;
30
- static _registerGlobalCleanup() {
31
- if (this._cleanupRegistered)
32
- return;
33
- this._cleanupRegistered = true;
34
- const cleanup = () => {
35
- for (const inst of this._activeInsts) {
36
- if (inst._proc && !inst._proc.killed) {
37
- try {
38
- inst._proc.kill('SIGKILL');
39
- }
40
- catch { }
41
- }
42
- }
43
- };
44
- process.on('exit', cleanup);
45
- process.on('SIGINT', () => { cleanup(); process.exit(130); });
46
- process.on('SIGTERM', () => { cleanup(); process.exit(143); });
47
- }
48
- _proc = null;
49
- _buffer = '';
50
- _pending = new Map();
51
- _initialTabIdentities = [];
52
- _closingPromise = null;
14
+ export class BrowserBridge {
53
15
  _state = 'idle';
54
16
  _page = null;
17
+ _daemonProc = null;
55
18
  get state() {
56
19
  return this._state;
57
20
  }
58
- _sendRequest(method, params = {}) {
59
- return new Promise((resolve, reject) => {
60
- if (!this._proc?.stdin?.writable) {
61
- reject(new Error('Playwright MCP process is not writable'));
62
- return;
63
- }
64
- const { id, message } = createJsonRpcRequest(method, params);
65
- this._pending.set(id, { resolve, reject });
66
- this._proc.stdin.write(message, (err) => {
67
- if (!err)
68
- return;
69
- this._pending.delete(id);
70
- reject(err);
71
- });
72
- });
73
- }
74
- _rejectPendingRequests(error) {
75
- const pending = [...this._pending.values()];
76
- this._pending.clear();
77
- for (const waiter of pending)
78
- waiter.reject(error);
79
- }
80
- _resetAfterFailedConnect() {
81
- const proc = this._proc;
82
- this._page = null;
83
- this._proc = null;
84
- this._buffer = '';
85
- this._initialTabIdentities = [];
86
- this._rejectPendingRequests(new Error('Playwright MCP connect failed'));
87
- PlaywrightMCP._activeInsts.delete(this);
88
- if (proc && !proc.killed) {
89
- try {
90
- proc.kill('SIGKILL');
91
- }
92
- catch { }
93
- }
94
- }
95
21
  async connect(opts = {}) {
96
22
  if (this._state === 'connected' && this._page)
97
23
  return this._page;
98
24
  if (this._state === 'connecting')
99
- throw new Error('Playwright MCP is already connecting');
25
+ throw new Error('Already connecting');
100
26
  if (this._state === 'closing')
101
- throw new Error('Playwright MCP is closing');
27
+ throw new Error('Session is closing');
102
28
  if (this._state === 'closed')
103
- throw new Error('Playwright MCP session is closed');
104
- const mcpPath = findMcpServerPath();
105
- PlaywrightMCP._registerGlobalCleanup();
106
- PlaywrightMCP._activeInsts.add(this);
29
+ throw new Error('Session is closed');
107
30
  this._state = 'connecting';
108
- const timeout = opts.timeout ?? DEFAULT_BROWSER_CONNECT_TIMEOUT;
109
- return new Promise((resolve, reject) => {
110
- const isDebug = process.env.DEBUG?.includes('opencli:mcp');
111
- const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
112
- const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint();
113
- const useExtension = !requestedCdp;
114
- const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
115
- const tokenFingerprint = getTokenFingerprint(extensionToken);
116
- let stderrBuffer = '';
117
- let settled = false;
118
- const settleError = (kind, extra = {}) => {
119
- if (settled)
120
- return;
121
- settled = true;
122
- this._state = 'idle';
123
- clearTimeout(timer);
124
- this._resetAfterFailedConnect();
125
- reject(formatBrowserConnectError({
126
- kind,
127
- timeout,
128
- hasExtensionToken: !!extensionToken,
129
- tokenFingerprint,
130
- stderr: stderrBuffer,
131
- exitCode: extra.exitCode,
132
- rawMessage: extra.rawMessage,
133
- }));
134
- };
135
- const settleSuccess = (pageToResolve) => {
136
- if (settled)
137
- return;
138
- settled = true;
139
- this._state = 'connected';
140
- clearTimeout(timer);
141
- resolve(pageToResolve);
142
- };
143
- const timer = setTimeout(() => {
144
- debugLog('Connection timed out');
145
- settleError(inferConnectFailureKind({
146
- hasExtensionToken: !!extensionToken,
147
- stderr: stderrBuffer,
148
- isCdpMode: requestedCdp,
149
- }));
150
- }, timeout * 1000);
151
- const launchSpec = buildMcpLaunchSpec({
152
- mcpPath,
153
- executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
154
- cdpEndpoint,
155
- });
156
- if (process.env.OPENCLI_VERBOSE) {
157
- console.error(`[opencli] Mode: ${requestedCdp ? 'CDP' : useExtension ? 'extension' : 'standalone'}`);
158
- if (useExtension)
159
- console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
160
- if (launchSpec.usedNpxFallback) {
161
- console.error('[opencli] Playwright MCP not found locally; bootstrapping via npx @playwright/mcp@latest');
162
- }
163
- }
164
- debugLog(`Spawning ${launchSpec.command} ${launchSpec.args.join(' ')}`);
165
- this._proc = spawn(launchSpec.command, launchSpec.args, {
166
- stdio: ['pipe', 'pipe', 'pipe'],
167
- env: { ...process.env },
168
- });
169
- // Increase max listeners to avoid warnings
170
- this._proc.setMaxListeners(20);
171
- if (this._proc.stdout)
172
- this._proc.stdout.setMaxListeners(20);
173
- const page = new Page((method, params = {}) => this._sendRequest(method, params));
174
- this._page = page;
175
- this._proc.stdout?.on('data', (chunk) => {
176
- this._buffer += chunk.toString();
177
- const lines = this._buffer.split('\n');
178
- this._buffer = lines.pop() ?? '';
179
- for (const line of lines) {
180
- if (!line.trim())
181
- continue;
182
- debugLog(`RECV: ${line}`);
183
- try {
184
- const parsed = JSON.parse(line);
185
- if (typeof parsed?.id === 'number') {
186
- const waiter = this._pending.get(parsed.id);
187
- if (waiter) {
188
- this._pending.delete(parsed.id);
189
- waiter.resolve(parsed);
190
- }
191
- }
192
- }
193
- catch (e) {
194
- debugLog(`Parse error: ${e}`);
195
- }
196
- }
197
- });
198
- this._proc.stderr?.on('data', (chunk) => {
199
- const text = chunk.toString();
200
- stderrBuffer = appendLimited(stderrBuffer, text, STDERR_BUFFER_LIMIT);
201
- debugLog(`STDERR: ${text}`);
202
- });
203
- this._proc.on('error', (err) => {
204
- debugLog(`Subprocess error: ${err.message}`);
205
- this._rejectPendingRequests(new Error(`Playwright MCP process error: ${err.message}`));
206
- settleError('process-exit', { rawMessage: err.message });
207
- });
208
- this._proc.on('close', (code) => {
209
- debugLog(`Subprocess closed with code ${code}`);
210
- this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
211
- if (!settled) {
212
- settleError(inferConnectFailureKind({
213
- hasExtensionToken: !!extensionToken,
214
- stderr: stderrBuffer,
215
- exited: true,
216
- isCdpMode: requestedCdp,
217
- }), { exitCode: code });
218
- }
219
- });
220
- // Initialize: send initialize request
221
- debugLog('Waiting for initialize response...');
222
- this._sendRequest('initialize', {
223
- protocolVersion: '2024-11-05',
224
- capabilities: {},
225
- clientInfo: { name: 'opencli', version: PKG_VERSION },
226
- }).then((resp) => {
227
- debugLog('Got initialize response');
228
- if (resp.error) {
229
- settleError(inferConnectFailureKind({
230
- hasExtensionToken: !!extensionToken,
231
- stderr: stderrBuffer,
232
- rawMessage: `MCP init failed: ${resp.error.message}`,
233
- isCdpMode: requestedCdp,
234
- }), { rawMessage: resp.error.message });
235
- return;
236
- }
237
- const initializedMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n';
238
- debugLog(`SEND: ${initializedMsg.trim()}`);
239
- this._proc?.stdin?.write(initializedMsg);
240
- // Use tabs as a readiness probe and for tab cleanup bookkeeping.
241
- debugLog('Fetching initial tabs count...');
242
- withTimeoutMs(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs) => {
243
- debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
244
- this._initialTabIdentities = extractTabIdentities(tabs);
245
- settleSuccess(page);
246
- }).catch((err) => {
247
- debugLog(`Tabs fetch error: ${err.message}`);
248
- settleSuccess(page);
249
- });
250
- }).catch((err) => {
251
- debugLog(`Init promise rejected: ${err.message}`);
252
- settleError('mcp-init', { rawMessage: err.message });
253
- });
254
- });
31
+ try {
32
+ await this._ensureDaemon();
33
+ this._page = new Page();
34
+ this._state = 'connected';
35
+ return this._page;
36
+ }
37
+ catch (err) {
38
+ this._state = 'idle';
39
+ throw err;
40
+ }
255
41
  }
256
42
  async close() {
257
- if (this._closingPromise)
258
- return this._closingPromise;
259
43
  if (this._state === 'closed')
260
44
  return;
261
45
  this._state = 'closing';
262
- this._closingPromise = (async () => {
263
- try {
264
- // Extension mode opens bridge/session tabs that we can clean up best-effort.
265
- if (this._page && this._proc && !this._proc.killed) {
266
- try {
267
- const tabs = await withTimeoutMs(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
268
- const tabEntries = extractTabEntries(tabs);
269
- const tabsToClose = diffTabIndexes(this._initialTabIdentities, tabEntries);
270
- for (const index of tabsToClose) {
271
- try {
272
- await this._page.closeTab(index);
273
- }
274
- catch { }
275
- }
276
- }
277
- catch { }
278
- }
279
- if (this._proc && !this._proc.killed) {
280
- this._proc.kill('SIGTERM');
281
- const exited = await new Promise((res) => {
282
- let done = false;
283
- const finish = (value) => {
284
- if (done)
285
- return;
286
- done = true;
287
- res(value);
288
- };
289
- this._proc?.once('exit', () => finish(true));
290
- setTimeout(() => finish(false), 3000);
291
- });
292
- if (!exited && this._proc && !this._proc.killed) {
293
- try {
294
- this._proc.kill('SIGKILL');
295
- }
296
- catch { }
297
- }
298
- }
299
- }
300
- finally {
301
- this._rejectPendingRequests(new Error('Playwright MCP session closed'));
302
- this._page = null;
303
- this._proc = null;
304
- this._state = 'closed';
305
- PlaywrightMCP._activeInsts.delete(this);
306
- }
307
- })();
308
- return this._closingPromise;
46
+ // We don't kill the daemon — it auto-exits on idle.
47
+ // Just clean up our reference.
48
+ this._page = null;
49
+ this._state = 'closed';
50
+ }
51
+ async _ensureDaemon() {
52
+ if (await isDaemonRunning())
53
+ return;
54
+ // Find daemon relative to this file — works for both:
55
+ // npx tsx src/main.ts → src/browser/mcp.ts → src/daemon.ts
56
+ // node dist/main.js → dist/browser/mcp.js → dist/daemon.js
57
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
58
+ const parentDir = path.resolve(__dirname, '..');
59
+ const daemonTs = path.join(parentDir, 'daemon.ts');
60
+ const daemonJs = path.join(parentDir, 'daemon.js');
61
+ const isTs = fs.existsSync(daemonTs);
62
+ const daemonPath = isTs ? daemonTs : daemonJs;
63
+ if (process.env.OPENCLI_VERBOSE) {
64
+ console.error(`[opencli] Starting daemon (${isTs ? 'ts' : 'js'})...`);
65
+ }
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), {
72
+ detached: true,
73
+ stdio: 'ignore',
74
+ env: { ...process.env },
75
+ });
76
+ this._daemonProc.unref();
77
+ // Wait for daemon to be ready AND extension to connect
78
+ const deadline = Date.now() + DAEMON_SPAWN_TIMEOUT;
79
+ while (Date.now() < deadline) {
80
+ await new Promise(resolve => setTimeout(resolve, 300));
81
+ if (await isExtensionConnected())
82
+ return;
83
+ }
84
+ // Daemon might be up but extension not connected — give a useful error
85
+ if (await isDaemonRunning()) {
86
+ throw new Error('Daemon is running but the Browser Extension is not connected.\n' +
87
+ 'Please install and enable the opencli Browser Bridge extension in Chrome.');
88
+ }
89
+ throw new Error('Failed to start opencli daemon. Try running manually:\n' +
90
+ ` node ${daemonPath}\n` +
91
+ 'Make sure port 19825 is available.');
309
92
  }
310
93
  }
94
+ /** @deprecated Use BrowserBridge instead */
95
+ export const PlaywrightMCP = BrowserBridge;
@@ -1,15 +1,26 @@
1
1
  /**
2
- * Page abstraction wrapping JSON-RPC calls to Playwright MCP.
2
+ * Page abstraction implements IPage by sending commands to the daemon.
3
+ *
4
+ * All browser operations are ultimately 'exec' (JS evaluation via CDP)
5
+ * plus a few native Chrome Extension APIs (tabs, cookies, navigate).
6
+ *
7
+ * IMPORTANT: After goto(), we remember the tabId returned by the navigate
8
+ * action and pass it to all subsequent commands. This avoids the issue
9
+ * where resolveTabId() in the extension picks a chrome:// or
10
+ * chrome-extension:// tab that can't be debugged.
3
11
  */
4
12
  import type { IPage } from '../types.js';
5
13
  /**
6
- * Page abstraction wrapping JSON-RPC calls to Playwright MCP.
14
+ * Page implements IPage by talking to the daemon via HTTP.
7
15
  */
8
16
  export declare class Page implements IPage {
9
- private _request;
10
- constructor(_request: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>);
11
- call(method: string, params?: Record<string, unknown>): Promise<any>;
17
+ /** Active tab ID, set after navigate and used in all subsequent commands */
18
+ private _tabId;
19
+ /** Helper: spread tabId into command params if we have one */
20
+ private _tabOpt;
12
21
  goto(url: string): Promise<void>;
22
+ /** Close the automation window in the extension */
23
+ closeWindow(): Promise<void>;
13
24
  evaluate(js: string): Promise<any>;
14
25
  snapshot(opts?: {
15
26
  interactive?: boolean;
@@ -30,8 +41,26 @@ export declare class Page implements IPage {
30
41
  newTab(): Promise<void>;
31
42
  selectTab(index: number): Promise<void>;
32
43
  networkRequests(includeStatic?: boolean): Promise<any>;
33
- consoleMessages(level?: string): Promise<any>;
34
- scroll(direction?: string, _amount?: number): Promise<void>;
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>;
50
+ /**
51
+ * Capture a screenshot via CDP Page.captureScreenshot.
52
+ * @param options.format - 'png' (default) or 'jpeg'
53
+ * @param options.quality - JPEG quality 0-100
54
+ * @param options.fullPage - capture full scrollable page
55
+ * @param options.path - save to file path (returns base64 if omitted)
56
+ */
57
+ screenshot(options?: {
58
+ format?: 'png' | 'jpeg';
59
+ quality?: number;
60
+ fullPage?: boolean;
61
+ path?: string;
62
+ }): Promise<string>;
63
+ scroll(direction?: string, amount?: number): Promise<void>;
35
64
  autoScroll(options?: {
36
65
  times?: number;
37
66
  delayMs?: number;