rechrome 1.11.1 → 1.12.0
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/extension/connect.html +30 -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/lib/background.mjs +368 -0
- package/extension/lib/ui/authToken.css +1366 -0
- package/extension/lib/ui/authToken.js +7171 -0
- package/extension/lib/ui/connect.js +181 -0
- package/extension/lib/ui/icon-16.png +0 -0
- package/extension/lib/ui/icon-32.png +0 -0
- package/extension/lib/ui/status.js +71 -0
- package/extension/manifest.json +34 -0
- package/extension/status.html +14 -0
- package/package.json +2 -1
- package/rech.js +48 -14
- package/rech.ts +48 -14
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
-->
|
|
16
|
+
<!DOCTYPE html>
|
|
17
|
+
<html>
|
|
18
|
+
<head>
|
|
19
|
+
<title>Playwright MCP extension</title>
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
21
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/lib/ui/icon-32.png">
|
|
22
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/lib/ui/icon-16.png">
|
|
23
|
+
<script type="module" crossorigin src="/lib/ui/connect.js"></script>
|
|
24
|
+
<link rel="modulepreload" crossorigin href="/lib/ui/authToken.js">
|
|
25
|
+
<link rel="stylesheet" crossorigin href="/lib/ui/authToken.css">
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div id="root"></div>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
function debugLog(...args) {
|
|
2
|
+
{
|
|
3
|
+
console.log("[Extension]", ...args);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
class RelayConnection {
|
|
7
|
+
_debuggee;
|
|
8
|
+
_ws;
|
|
9
|
+
_eventListener;
|
|
10
|
+
_detachListener;
|
|
11
|
+
_tabPromise;
|
|
12
|
+
_tabPromiseResolve;
|
|
13
|
+
_closed = false;
|
|
14
|
+
_playwrightTabIds = /* @__PURE__ */ new Set();
|
|
15
|
+
onclose;
|
|
16
|
+
onPlaywrightTabCreated;
|
|
17
|
+
onPlaywrightTabRemoved;
|
|
18
|
+
constructor(ws) {
|
|
19
|
+
this._debuggee = {};
|
|
20
|
+
this._tabPromise = new Promise((resolve) => this._tabPromiseResolve = resolve);
|
|
21
|
+
this._ws = ws;
|
|
22
|
+
this._ws.onmessage = this._onMessage.bind(this);
|
|
23
|
+
this._ws.onclose = () => this._onClose();
|
|
24
|
+
this._eventListener = this._onDebuggerEvent.bind(this);
|
|
25
|
+
this._detachListener = this._onDebuggerDetach.bind(this);
|
|
26
|
+
chrome.debugger.onEvent.addListener(this._eventListener);
|
|
27
|
+
chrome.debugger.onDetach.addListener(this._detachListener);
|
|
28
|
+
}
|
|
29
|
+
// Either setTabId or close is called after creating the connection.
|
|
30
|
+
setTabId(tabId) {
|
|
31
|
+
this._debuggee = { tabId };
|
|
32
|
+
this._tabPromiseResolve();
|
|
33
|
+
}
|
|
34
|
+
close(message) {
|
|
35
|
+
this._ws.close(1e3, message);
|
|
36
|
+
this._onClose();
|
|
37
|
+
}
|
|
38
|
+
_onClose() {
|
|
39
|
+
if (this._closed)
|
|
40
|
+
return;
|
|
41
|
+
this._closed = true;
|
|
42
|
+
chrome.debugger.onEvent.removeListener(this._eventListener);
|
|
43
|
+
chrome.debugger.onDetach.removeListener(this._detachListener);
|
|
44
|
+
chrome.debugger.detach(this._debuggee).catch(() => {
|
|
45
|
+
});
|
|
46
|
+
for (const tabId of this._playwrightTabIds)
|
|
47
|
+
chrome.debugger.detach({ tabId }).catch(() => {
|
|
48
|
+
});
|
|
49
|
+
this._playwrightTabIds.clear();
|
|
50
|
+
this.onclose?.();
|
|
51
|
+
}
|
|
52
|
+
_onDebuggerEvent(source, method, params) {
|
|
53
|
+
const isInitialTab = source.tabId === this._debuggee.tabId;
|
|
54
|
+
const isPlaywrightTab = source.tabId !== void 0 && this._playwrightTabIds.has(source.tabId);
|
|
55
|
+
if (!isInitialTab && !isPlaywrightTab)
|
|
56
|
+
return;
|
|
57
|
+
debugLog("Forwarding CDP event:", method, params);
|
|
58
|
+
const sessionId = source.sessionId;
|
|
59
|
+
const tabId = isPlaywrightTab ? source.tabId : void 0;
|
|
60
|
+
this._sendMessage({
|
|
61
|
+
method: "forwardCDPEvent",
|
|
62
|
+
params: {
|
|
63
|
+
sessionId,
|
|
64
|
+
method,
|
|
65
|
+
params,
|
|
66
|
+
tabId
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
_onDebuggerDetach(source, reason) {
|
|
71
|
+
if (source.tabId !== void 0 && this._playwrightTabIds.has(source.tabId)) {
|
|
72
|
+
debugLog("Playwright tab detached:", source.tabId, reason);
|
|
73
|
+
this._playwrightTabIds.delete(source.tabId);
|
|
74
|
+
this.onPlaywrightTabRemoved?.(source.tabId);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (source.tabId !== this._debuggee.tabId)
|
|
78
|
+
return;
|
|
79
|
+
this.close(`Debugger detached: ${reason}`);
|
|
80
|
+
this._debuggee = {};
|
|
81
|
+
}
|
|
82
|
+
_onMessage(event) {
|
|
83
|
+
this._onMessageAsync(event).catch((e) => debugLog("Error handling message:", e));
|
|
84
|
+
}
|
|
85
|
+
async _onMessageAsync(event) {
|
|
86
|
+
let message;
|
|
87
|
+
try {
|
|
88
|
+
message = JSON.parse(event.data);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
debugLog("Error parsing message:", error);
|
|
91
|
+
this._sendError(-32700, `Error parsing message: ${error.message}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
debugLog("Received message:", message);
|
|
95
|
+
const response = {
|
|
96
|
+
id: message.id
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
response.result = await this._handleCommand(message);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
debugLog("Error handling command:", error);
|
|
102
|
+
response.error = error.message;
|
|
103
|
+
}
|
|
104
|
+
debugLog("Sending response:", response);
|
|
105
|
+
this._sendMessage(response);
|
|
106
|
+
}
|
|
107
|
+
async _handleCommand(message) {
|
|
108
|
+
if (message.method === "attachToTab") {
|
|
109
|
+
await this._tabPromise;
|
|
110
|
+
debugLog("Attaching debugger to tab:", this._debuggee);
|
|
111
|
+
await chrome.debugger.attach(this._debuggee, "1.3");
|
|
112
|
+
const result = await chrome.debugger.sendCommand(this._debuggee, "Target.getTargetInfo");
|
|
113
|
+
return {
|
|
114
|
+
targetInfo: result?.targetInfo,
|
|
115
|
+
tabId: this._debuggee.tabId
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (message.method === "createTab") {
|
|
119
|
+
const url = message.params?.url ?? "about:blank";
|
|
120
|
+
debugLog("Creating new tab:", url);
|
|
121
|
+
const tab = await chrome.tabs.create({ url, active: true });
|
|
122
|
+
const tabId = tab.id;
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
124
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
125
|
+
const result = await chrome.debugger.sendCommand({ tabId }, "Target.getTargetInfo");
|
|
126
|
+
const targetInfo = result?.targetInfo || {
|
|
127
|
+
targetId: String(tabId),
|
|
128
|
+
type: "page",
|
|
129
|
+
title: "",
|
|
130
|
+
url: tab.url || url,
|
|
131
|
+
attached: false,
|
|
132
|
+
canAccessOpener: false
|
|
133
|
+
};
|
|
134
|
+
this._playwrightTabIds.add(tabId);
|
|
135
|
+
this.onPlaywrightTabCreated?.(tabId);
|
|
136
|
+
debugLog("Created playwright tab:", tabId, targetInfo);
|
|
137
|
+
return { tabId, targetInfo };
|
|
138
|
+
}
|
|
139
|
+
if (!this._debuggee.tabId)
|
|
140
|
+
throw new Error("No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.");
|
|
141
|
+
if (message.method === "forwardCDPCommand") {
|
|
142
|
+
const { sessionId, method, params, tabId } = message.params;
|
|
143
|
+
debugLog("CDP command:", method, params, "tabId:", tabId);
|
|
144
|
+
const debuggee = tabId !== void 0 ? { tabId, sessionId } : { ...this._debuggee, sessionId };
|
|
145
|
+
return await chrome.debugger.sendCommand(debuggee, method, params);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
_sendError(code, message) {
|
|
149
|
+
this._sendMessage({
|
|
150
|
+
error: {
|
|
151
|
+
code,
|
|
152
|
+
message
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
_sendMessage(message) {
|
|
157
|
+
if (this._ws.readyState === WebSocket.OPEN)
|
|
158
|
+
this._ws.send(JSON.stringify(message));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
class TabShareExtension {
|
|
162
|
+
_connections = /* @__PURE__ */ new Map();
|
|
163
|
+
_pendingTabSelection = /* @__PURE__ */ new Map();
|
|
164
|
+
constructor() {
|
|
165
|
+
chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this));
|
|
166
|
+
chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this));
|
|
167
|
+
chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this));
|
|
168
|
+
chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
|
|
169
|
+
chrome.action.onClicked.addListener(this._onActionClicked.bind(this));
|
|
170
|
+
}
|
|
171
|
+
// Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031
|
|
172
|
+
_onMessage(message, sender, sendResponse) {
|
|
173
|
+
switch (message.type) {
|
|
174
|
+
case "connectToMCPRelay":
|
|
175
|
+
this._connectToRelay(sender.tab.id, message.mcpRelayUrl).then(
|
|
176
|
+
() => sendResponse({ success: true }),
|
|
177
|
+
(error) => sendResponse({ success: false, error: error.message })
|
|
178
|
+
);
|
|
179
|
+
return true;
|
|
180
|
+
case "getTabs":
|
|
181
|
+
this._getTabs().then(
|
|
182
|
+
(tabs) => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }),
|
|
183
|
+
(error) => sendResponse({ success: false, error: error.message })
|
|
184
|
+
);
|
|
185
|
+
return true;
|
|
186
|
+
case "connectToTab":
|
|
187
|
+
const tabId = message.tabId || sender.tab?.id;
|
|
188
|
+
const windowId = message.windowId || sender.tab?.windowId;
|
|
189
|
+
this._connectTab(sender.tab.id, tabId, windowId, message.mcpRelayUrl).then(
|
|
190
|
+
() => sendResponse({ success: true }),
|
|
191
|
+
(error) => sendResponse({ success: false, error: error.message })
|
|
192
|
+
);
|
|
193
|
+
return true;
|
|
194
|
+
// Return true to indicate that the response will be sent asynchronously
|
|
195
|
+
case "getConnectionStatus":
|
|
196
|
+
sendResponse({
|
|
197
|
+
connections: [...this._connections.values()].map((s) => ({
|
|
198
|
+
mcpRelayUrl: s.mcpRelayUrl,
|
|
199
|
+
connectedTabId: s.connectedTabId,
|
|
200
|
+
playwrightTabIds: [...s.playwrightTabIds]
|
|
201
|
+
})),
|
|
202
|
+
// Legacy fields for backward compat: first connection's tabId
|
|
203
|
+
connectedTabId: [...this._connections.values()][0]?.connectedTabId ?? null,
|
|
204
|
+
playwrightTabIds: [...this._connections.values()].flatMap((s) => [...s.playwrightTabIds])
|
|
205
|
+
});
|
|
206
|
+
return false;
|
|
207
|
+
case "disconnect":
|
|
208
|
+
this._disconnect(message.mcpRelayUrl).then(
|
|
209
|
+
() => sendResponse({ success: true }),
|
|
210
|
+
(error) => sendResponse({ success: false, error: error.message })
|
|
211
|
+
);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
async _connectToRelay(selectorTabId, mcpRelayUrl) {
|
|
217
|
+
try {
|
|
218
|
+
debugLog(`Connecting to relay at ${mcpRelayUrl}`);
|
|
219
|
+
const socket = new WebSocket(mcpRelayUrl);
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
socket.onopen = () => resolve();
|
|
222
|
+
socket.onerror = () => reject(new Error("WebSocket error"));
|
|
223
|
+
setTimeout(() => reject(new Error("Connection timeout")), 5e3);
|
|
224
|
+
});
|
|
225
|
+
const connection = new RelayConnection(socket);
|
|
226
|
+
connection.onclose = () => {
|
|
227
|
+
debugLog("Connection closed");
|
|
228
|
+
this._pendingTabSelection.delete(selectorTabId);
|
|
229
|
+
};
|
|
230
|
+
this._pendingTabSelection.set(selectorTabId, { connection, mcpRelayUrl });
|
|
231
|
+
debugLog(`Connected to MCP relay`);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = `Failed to connect to MCP relay: ${error.message}`;
|
|
234
|
+
debugLog(message);
|
|
235
|
+
throw new Error(message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async _connectTab(selectorTabId, tabId, windowId, mcpRelayUrl) {
|
|
239
|
+
try {
|
|
240
|
+
debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`);
|
|
241
|
+
const pending = this._pendingTabSelection.get(selectorTabId);
|
|
242
|
+
if (!pending)
|
|
243
|
+
throw new Error("No active MCP relay connection");
|
|
244
|
+
this._pendingTabSelection.delete(selectorTabId);
|
|
245
|
+
const connection = pending.connection;
|
|
246
|
+
const relayUrl = pending.mcpRelayUrl;
|
|
247
|
+
const existing = this._connections.get(relayUrl);
|
|
248
|
+
if (existing) {
|
|
249
|
+
existing.connection.close("Another connection is requested");
|
|
250
|
+
this._connections.delete(relayUrl);
|
|
251
|
+
}
|
|
252
|
+
const state = {
|
|
253
|
+
connection,
|
|
254
|
+
connectedTabId: tabId,
|
|
255
|
+
playwrightTabIds: /* @__PURE__ */ new Set(),
|
|
256
|
+
mcpRelayUrl: relayUrl
|
|
257
|
+
};
|
|
258
|
+
this._connections.set(relayUrl, state);
|
|
259
|
+
connection.setTabId(tabId);
|
|
260
|
+
connection.onclose = () => {
|
|
261
|
+
debugLog("MCP connection closed");
|
|
262
|
+
if (this._connections.get(relayUrl)?.connection === connection)
|
|
263
|
+
this._connections.delete(relayUrl);
|
|
264
|
+
void this._updateBadge(state.connectedTabId, { text: "" });
|
|
265
|
+
for (const pwTabId of state.playwrightTabIds)
|
|
266
|
+
void this._updateBadge(pwTabId, { text: "" });
|
|
267
|
+
state.playwrightTabIds.clear();
|
|
268
|
+
};
|
|
269
|
+
connection.onPlaywrightTabCreated = (pwTabId) => {
|
|
270
|
+
state.playwrightTabIds.add(pwTabId);
|
|
271
|
+
void this._updateBadge(pwTabId, { text: "✓", color: "#1976D2", title: "Playwright managed tab" });
|
|
272
|
+
};
|
|
273
|
+
connection.onPlaywrightTabRemoved = (pwTabId) => {
|
|
274
|
+
state.playwrightTabIds.delete(pwTabId);
|
|
275
|
+
void this._updateBadge(pwTabId, { text: "" });
|
|
276
|
+
};
|
|
277
|
+
await Promise.all([
|
|
278
|
+
this._updateBadge(tabId, { text: "✓", color: "#4CAF50", title: "Connected to MCP client" }),
|
|
279
|
+
chrome.tabs.update(tabId, { active: true }),
|
|
280
|
+
chrome.windows.update(windowId, { focused: true })
|
|
281
|
+
]);
|
|
282
|
+
debugLog(`Connected to MCP bridge`);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
debugLog(`Failed to connect tab ${tabId}:`, error.message);
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async _updateBadge(tabId, { text, color, title }) {
|
|
289
|
+
try {
|
|
290
|
+
await chrome.action.setBadgeText({ tabId, text });
|
|
291
|
+
await chrome.action.setTitle({ tabId, title: title || "" });
|
|
292
|
+
if (color)
|
|
293
|
+
await chrome.action.setBadgeBackgroundColor({ tabId, color });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async _onTabRemoved(tabId) {
|
|
298
|
+
const pendingConnection = [...this._pendingTabSelection.entries()].find(([k]) => k === tabId)?.[1];
|
|
299
|
+
if (pendingConnection) {
|
|
300
|
+
this._pendingTabSelection.delete(tabId);
|
|
301
|
+
pendingConnection.connection.close("Browser tab closed");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
for (const [relayUrl, state] of this._connections) {
|
|
305
|
+
if (state.playwrightTabIds.has(tabId)) {
|
|
306
|
+
state.playwrightTabIds.delete(tabId);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (state.connectedTabId === tabId) {
|
|
310
|
+
state.connection.close("Browser tab closed");
|
|
311
|
+
this._connections.delete(relayUrl);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
_onTabActivated(activeInfo) {
|
|
317
|
+
for (const [tabId, pending] of this._pendingTabSelection) {
|
|
318
|
+
if (tabId === activeInfo.tabId) {
|
|
319
|
+
if (pending.timerId) {
|
|
320
|
+
clearTimeout(pending.timerId);
|
|
321
|
+
pending.timerId = void 0;
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (!pending.timerId) {
|
|
326
|
+
pending.timerId = setTimeout(() => {
|
|
327
|
+
const existed = this._pendingTabSelection.delete(tabId);
|
|
328
|
+
if (existed) {
|
|
329
|
+
pending.connection.close("Tab has been inactive for 5 seconds");
|
|
330
|
+
chrome.tabs.sendMessage(tabId, { type: "connectionTimeout" });
|
|
331
|
+
}
|
|
332
|
+
}, 5e3);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
_onTabUpdated(tabId, changeInfo, tab) {
|
|
337
|
+
for (const state of this._connections.values()) {
|
|
338
|
+
if (state.connectedTabId === tabId)
|
|
339
|
+
void this._updateBadge(tabId, { text: "✓", color: "#4CAF50", title: "Connected to MCP client" });
|
|
340
|
+
if (state.playwrightTabIds.has(tabId))
|
|
341
|
+
void this._updateBadge(tabId, { text: "✓", color: "#1976D2", title: "Playwright managed tab" });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async _getTabs() {
|
|
345
|
+
const tabs = await chrome.tabs.query({});
|
|
346
|
+
return tabs.filter((tab) => tab.url && !["chrome:", "edge:", "devtools:"].some((scheme) => tab.url.startsWith(scheme)));
|
|
347
|
+
}
|
|
348
|
+
async _onActionClicked() {
|
|
349
|
+
await chrome.tabs.create({
|
|
350
|
+
url: chrome.runtime.getURL("status.html"),
|
|
351
|
+
active: true
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async _disconnect(mcpRelayUrl) {
|
|
355
|
+
if (mcpRelayUrl) {
|
|
356
|
+
const state = this._connections.get(mcpRelayUrl);
|
|
357
|
+
if (state) {
|
|
358
|
+
state.connection.close("User disconnected");
|
|
359
|
+
this._connections.delete(mcpRelayUrl);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
for (const state of this._connections.values())
|
|
363
|
+
state.connection.close("User disconnected");
|
|
364
|
+
this._connections.clear();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
new TabShareExtension();
|