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