chrome-devtools-mcp-for-extension 0.22.6 → 0.23.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/build/src/McpContext.js +11 -5
- package/build/src/index.js +0 -0
- package/build/src/main.js +7 -12
- package/build/src/tools/chatgpt-web.js +7 -19
- package/build/src/tools/gemini-web.js +2 -0
- package/package.json +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Linkifier.js +0 -34
- package/build/node_modules/chrome-devtools-frontend/front_end/core/common/QueryParamHandler.js +0 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Worker.js +0 -44
- package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/DOMUtilities.js +0 -122
- package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/NodeURL.js +0 -31
- package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeTreeCache.js +0 -32
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/lantern/testing/MetricTestUtils.js +0 -46
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/lantern/testing/testing.js +0 -4
- package/build/src/profile-manager.js +0 -150
- package/build/src/tools/dom-utils.js +0 -182
package/build/src/McpContext.js
CHANGED
|
@@ -275,12 +275,18 @@ export class McpContext {
|
|
|
275
275
|
this.#dialog = dialog;
|
|
276
276
|
};
|
|
277
277
|
setSelectedPageIdx(idx) {
|
|
278
|
-
|
|
279
|
-
oldPage
|
|
278
|
+
// Remove dialog handler from old page if exists
|
|
279
|
+
const oldPage = this.#pages[this.#selectedPageIdx];
|
|
280
|
+
if (oldPage && !oldPage.isClosed()) {
|
|
281
|
+
oldPage.off('dialog', this.#dialogHandler);
|
|
282
|
+
}
|
|
280
283
|
this.#selectedPageIdx = idx;
|
|
281
|
-
|
|
282
|
-
newPage
|
|
283
|
-
|
|
284
|
+
// Add dialog handler to new page if exists
|
|
285
|
+
const newPage = this.#pages[idx];
|
|
286
|
+
if (newPage && !newPage.isClosed()) {
|
|
287
|
+
newPage.on('dialog', this.#dialogHandler);
|
|
288
|
+
this.#updateSelectedPageTimeouts();
|
|
289
|
+
}
|
|
284
290
|
}
|
|
285
291
|
#updateSelectedPageTimeouts() {
|
|
286
292
|
const page = this.getSelectedPage();
|
package/build/src/index.js
CHANGED
|
File without changes
|
package/build/src/main.js
CHANGED
|
@@ -46,17 +46,13 @@ import { Mutex } from './Mutex.js';
|
|
|
46
46
|
import { resolveRoots } from './roots-manager.js';
|
|
47
47
|
import { setProjectRoot } from './project-root-state.js';
|
|
48
48
|
import { setupGraceful } from './graceful.js';
|
|
49
|
-
import * as bookmarkTools from './tools/bookmarks.js';
|
|
50
49
|
import * as chatgptWebTools from './tools/chatgpt-web.js';
|
|
51
|
-
import * as deepResearchChatGPTTools from './tools/deep_research_chatgpt.js';
|
|
52
50
|
import * as consoleTools from './tools/console.js';
|
|
53
|
-
import * as diagnoseUiTools from './tools/diagnose-ui.js';
|
|
54
51
|
import * as emulationTools from './tools/emulation.js';
|
|
55
|
-
import * as extensionTools from './tools/extensions.js';
|
|
56
52
|
import * as geminiWebTools from './tools/gemini-web.js';
|
|
57
|
-
import
|
|
53
|
+
import { click, fill, fillForm } from './tools/input.js';
|
|
58
54
|
import * as networkTools from './tools/network.js';
|
|
59
|
-
import
|
|
55
|
+
import { pages, navigate } from './tools/pages.js';
|
|
60
56
|
import * as performanceTools from './tools/performance.js';
|
|
61
57
|
import * as screenshotTools from './tools/screenshot.js';
|
|
62
58
|
import * as scriptTools from './tools/script.js';
|
|
@@ -242,17 +238,16 @@ function registerTool(tool) {
|
|
|
242
238
|
});
|
|
243
239
|
}
|
|
244
240
|
const tools = [
|
|
245
|
-
...Object.values(bookmarkTools),
|
|
246
241
|
...Object.values(chatgptWebTools),
|
|
247
|
-
...Object.values(deepResearchChatGPTTools),
|
|
248
242
|
...Object.values(geminiWebTools),
|
|
249
243
|
...Object.values(consoleTools),
|
|
250
|
-
...Object.values(diagnoseUiTools),
|
|
251
244
|
...Object.values(emulationTools),
|
|
252
|
-
|
|
253
|
-
|
|
245
|
+
click,
|
|
246
|
+
fill,
|
|
247
|
+
fillForm,
|
|
254
248
|
...Object.values(networkTools),
|
|
255
|
-
|
|
249
|
+
pages,
|
|
250
|
+
navigate,
|
|
256
251
|
...Object.values(performanceTools),
|
|
257
252
|
...Object.values(screenshotTools),
|
|
258
253
|
...Object.values(scriptTools),
|
|
@@ -454,15 +454,9 @@ export const askChatGPTWeb = defineTool({
|
|
|
454
454
|
const progressText = progressElements
|
|
455
455
|
.map((el) => el.textContent)
|
|
456
456
|
.join(' ');
|
|
457
|
-
// Check if DeepResearch is still running
|
|
458
|
-
const
|
|
459
|
-
const isRunning =
|
|
460
|
-
const text = btn.textContent || '';
|
|
461
|
-
const aria = btn.getAttribute('aria-label') || '';
|
|
462
|
-
return (text.includes('停止') ||
|
|
463
|
-
text.includes('リサーチを停止') ||
|
|
464
|
-
aria.includes('停止'));
|
|
465
|
-
});
|
|
457
|
+
// Check if DeepResearch is still running - use stop-button data-testid
|
|
458
|
+
const stopButton = document.querySelector('button[data-testid="stop-button"]');
|
|
459
|
+
const isRunning = !!stopButton;
|
|
466
460
|
if (!isRunning) {
|
|
467
461
|
// Research completed - get the report
|
|
468
462
|
const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
@@ -482,16 +476,10 @@ export const askChatGPTWeb = defineTool({
|
|
|
482
476
|
currentText: progressText.substring(0, 200),
|
|
483
477
|
};
|
|
484
478
|
}
|
|
485
|
-
// Normal streaming detection
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
const aria = btn.getAttribute('aria-label') || '';
|
|
490
|
-
return (text.includes('ストリーミングの停止') ||
|
|
491
|
-
text.includes('停止') ||
|
|
492
|
-
aria.includes('ストリーミングの停止') ||
|
|
493
|
-
aria.includes('停止'));
|
|
494
|
-
});
|
|
479
|
+
// Normal streaming detection - check for stop button by data-testid
|
|
480
|
+
// When ChatGPT is generating, send-button becomes stop-button
|
|
481
|
+
const stopButton = document.querySelector('button[data-testid="stop-button"]');
|
|
482
|
+
const isStreaming = !!stopButton;
|
|
495
483
|
if (!isStreaming) {
|
|
496
484
|
// Get final response
|
|
497
485
|
const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
@@ -209,6 +209,8 @@ export const askGeminiWeb = defineTool({
|
|
|
209
209
|
// Navigate directly to target URL (skip intermediate navigation)
|
|
210
210
|
response.appendResponseLine('Geminiに接続中...');
|
|
211
211
|
await navigateWithRetry(page, targetUrl, { waitUntil: 'networkidle2' });
|
|
212
|
+
// Wait for Gemini SPA to fully render (networkidle2 is not enough for SPAs)
|
|
213
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
212
214
|
// Check login only once after navigation
|
|
213
215
|
const needsLogin = await isLoginRequired(page);
|
|
214
216
|
if (needsLogin) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./scripts/cli.mjs",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Copyright 2019 The Chromium Authors
|
|
2
|
-
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
-
// found in the LICENSE file.
|
|
4
|
-
export class Linkifier {
|
|
5
|
-
static async linkify(object, options) {
|
|
6
|
-
if (!object) {
|
|
7
|
-
throw new Error('Can\'t linkify ' + object);
|
|
8
|
-
}
|
|
9
|
-
const linkifierRegistration = getApplicableRegisteredlinkifiers(object)[0];
|
|
10
|
-
if (!linkifierRegistration) {
|
|
11
|
-
throw new Error('No linkifiers registered for object ' + object);
|
|
12
|
-
}
|
|
13
|
-
const linkifier = await linkifierRegistration.loadLinkifier();
|
|
14
|
-
return linkifier.linkify(object, options);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const registeredLinkifiers = [];
|
|
18
|
-
export function registerLinkifier(registration) {
|
|
19
|
-
registeredLinkifiers.push(registration);
|
|
20
|
-
}
|
|
21
|
-
export function getApplicableRegisteredlinkifiers(object) {
|
|
22
|
-
return registeredLinkifiers.filter(isLinkifierApplicableToContextTypes);
|
|
23
|
-
function isLinkifierApplicableToContextTypes(linkifierRegistration) {
|
|
24
|
-
if (!linkifierRegistration.contextTypes) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
for (const contextType of linkifierRegistration.contextTypes()) {
|
|
28
|
-
if (object instanceof contextType) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Copyright 2014 The Chromium Authors
|
|
2
|
-
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
-
// found in the LICENSE file.
|
|
4
|
-
export class WorkerWrapper {
|
|
5
|
-
#workerPromise;
|
|
6
|
-
#disposed;
|
|
7
|
-
constructor(workerLocation) {
|
|
8
|
-
this.#workerPromise = new Promise(fulfill => {
|
|
9
|
-
const worker = new Worker(workerLocation, { type: 'module' });
|
|
10
|
-
worker.onmessage = (event) => {
|
|
11
|
-
console.assert(event.data === 'workerReady');
|
|
12
|
-
worker.onmessage = null;
|
|
13
|
-
fulfill(worker);
|
|
14
|
-
};
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
static fromURL(url) {
|
|
18
|
-
return new WorkerWrapper(url);
|
|
19
|
-
}
|
|
20
|
-
postMessage(message, transfer) {
|
|
21
|
-
void this.#workerPromise.then(worker => {
|
|
22
|
-
if (!this.#disposed) {
|
|
23
|
-
worker.postMessage(message, transfer ?? []);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
dispose() {
|
|
28
|
-
this.#disposed = true;
|
|
29
|
-
void this.#workerPromise.then(worker => worker.terminate());
|
|
30
|
-
}
|
|
31
|
-
terminate() {
|
|
32
|
-
this.dispose();
|
|
33
|
-
}
|
|
34
|
-
set onmessage(listener) {
|
|
35
|
-
void this.#workerPromise.then(worker => {
|
|
36
|
-
worker.onmessage = listener;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
set onerror(listener) {
|
|
40
|
-
void this.#workerPromise.then(worker => {
|
|
41
|
-
worker.onerror = listener;
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// Copyright 2022 The Chromium Authors
|
|
2
|
-
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
-
// found in the LICENSE file.
|
|
4
|
-
/**
|
|
5
|
-
* `document.activeElement` will not enter shadow roots to find the element
|
|
6
|
-
* that has focus; use this method if you need to traverse through any shadow
|
|
7
|
-
* roots to find the actual, specific focused element.
|
|
8
|
-
*/
|
|
9
|
-
export function deepActiveElement(doc) {
|
|
10
|
-
let activeElement = doc.activeElement;
|
|
11
|
-
while (activeElement?.shadowRoot?.activeElement) {
|
|
12
|
-
activeElement = activeElement.shadowRoot.activeElement;
|
|
13
|
-
}
|
|
14
|
-
return activeElement;
|
|
15
|
-
}
|
|
16
|
-
export function getEnclosingShadowRootForNode(node) {
|
|
17
|
-
let parentNode = node.parentNodeOrShadowHost();
|
|
18
|
-
while (parentNode) {
|
|
19
|
-
if (parentNode instanceof ShadowRoot) {
|
|
20
|
-
return parentNode;
|
|
21
|
-
}
|
|
22
|
-
parentNode = parentNode.parentNodeOrShadowHost();
|
|
23
|
-
}
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
export function rangeOfWord(rootNode, offset, stopCharacters, stayWithinNode, direction) {
|
|
27
|
-
let startNode;
|
|
28
|
-
let startOffset = 0;
|
|
29
|
-
let endNode;
|
|
30
|
-
let endOffset = 0;
|
|
31
|
-
if (!stayWithinNode) {
|
|
32
|
-
stayWithinNode = rootNode;
|
|
33
|
-
}
|
|
34
|
-
if (!direction || direction === 'backward' || direction === 'both') {
|
|
35
|
-
let node = rootNode;
|
|
36
|
-
while (node) {
|
|
37
|
-
if (node === stayWithinNode) {
|
|
38
|
-
if (!startNode) {
|
|
39
|
-
startNode = stayWithinNode;
|
|
40
|
-
}
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
if (node.nodeType === Node.TEXT_NODE && node.nodeValue !== null) {
|
|
44
|
-
const start = (node === rootNode ? (offset - 1) : (node.nodeValue.length - 1));
|
|
45
|
-
for (let i = start; i >= 0; --i) {
|
|
46
|
-
if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
|
|
47
|
-
startNode = node;
|
|
48
|
-
startOffset = i + 1;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (startNode) {
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
node = node.traversePreviousNode(stayWithinNode);
|
|
57
|
-
}
|
|
58
|
-
if (!startNode) {
|
|
59
|
-
startNode = stayWithinNode;
|
|
60
|
-
startOffset = 0;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
startNode = rootNode;
|
|
65
|
-
startOffset = offset;
|
|
66
|
-
}
|
|
67
|
-
if (!direction || direction === 'forward' || direction === 'both') {
|
|
68
|
-
let node = rootNode;
|
|
69
|
-
while (node) {
|
|
70
|
-
if (node === stayWithinNode) {
|
|
71
|
-
if (!endNode) {
|
|
72
|
-
endNode = stayWithinNode;
|
|
73
|
-
}
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
if (node.nodeType === Node.TEXT_NODE && node.nodeValue !== null) {
|
|
77
|
-
const start = (node === rootNode ? offset : 0);
|
|
78
|
-
for (let i = start; i < node.nodeValue.length; ++i) {
|
|
79
|
-
if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
|
|
80
|
-
endNode = node;
|
|
81
|
-
endOffset = i;
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (endNode) {
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
node = node.traverseNextNode(stayWithinNode);
|
|
90
|
-
}
|
|
91
|
-
if (!endNode) {
|
|
92
|
-
endNode = stayWithinNode;
|
|
93
|
-
endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue?.length || 0 :
|
|
94
|
-
stayWithinNode.childNodes.length;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
endNode = rootNode;
|
|
99
|
-
endOffset = offset;
|
|
100
|
-
}
|
|
101
|
-
if (!rootNode.ownerDocument) {
|
|
102
|
-
throw new Error('No `ownerDocument` found for rootNode');
|
|
103
|
-
}
|
|
104
|
-
const result = rootNode.ownerDocument.createRange();
|
|
105
|
-
result.setStart(startNode, startOffset);
|
|
106
|
-
result.setEnd(endNode, endOffset);
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Appends the list of `styles` as individual `<style>` elements to the
|
|
111
|
-
* given `node`.
|
|
112
|
-
*
|
|
113
|
-
* @param node the `Node` to append the `<style>` elements to.
|
|
114
|
-
* @param styles an optional list of styles to append to the `node`.
|
|
115
|
-
*/
|
|
116
|
-
export function appendStyle(node, ...styles) {
|
|
117
|
-
for (const cssText of styles) {
|
|
118
|
-
const style = (node.ownerDocument ?? document).createElement('style');
|
|
119
|
-
style.textContent = cssText;
|
|
120
|
-
node.appendChild(style);
|
|
121
|
-
}
|
|
122
|
-
}
|
package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/NodeURL.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Copyright 2018 The Chromium Authors
|
|
2
|
-
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
-
// found in the LICENSE file.
|
|
4
|
-
import * as Common from '../common/common.js';
|
|
5
|
-
import * as Host from '../host/host.js';
|
|
6
|
-
export class NodeURL {
|
|
7
|
-
static patch(object) {
|
|
8
|
-
process(object, '');
|
|
9
|
-
function process(object, path) {
|
|
10
|
-
if (object.url && NodeURL.isPlatformPath(object.url, Host.Platform.isWin())) {
|
|
11
|
-
// object.url can be of both types: RawPathString and UrlString
|
|
12
|
-
object.url = Common.ParsedURL.ParsedURL.rawPathToUrlString(object.url);
|
|
13
|
-
}
|
|
14
|
-
for (const entry of Object.entries(object)) {
|
|
15
|
-
const key = entry[0];
|
|
16
|
-
const value = entry[1];
|
|
17
|
-
const entryPath = path + '.' + key;
|
|
18
|
-
if (entryPath !== '.result.result.value' && value !== null && typeof value === 'object') {
|
|
19
|
-
process(value, entryPath);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
static isPlatformPath(fileSystemPath, isWindows) {
|
|
25
|
-
if (isWindows) {
|
|
26
|
-
const re = /^([a-z]:[\/\\]|\\\\)/i;
|
|
27
|
-
return re.test(fileSystemPath);
|
|
28
|
-
}
|
|
29
|
-
return fileSystemPath.length ? fileSystemPath[0] === '/' : false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// Copyright 2023 The Chromium Authors
|
|
2
|
-
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
-
// found in the LICENSE file.
|
|
4
|
-
import * as Formatter from '../formatter/formatter.js';
|
|
5
|
-
import * as TextUtils from '../text_utils/text_utils.js';
|
|
6
|
-
/** If a script failed to parse, we stash null in order to prevent unnecessary re-parsing */
|
|
7
|
-
const scopeTrees = new WeakMap();
|
|
8
|
-
/**
|
|
9
|
-
* Computes and caches the scope tree for `script`.
|
|
10
|
-
*
|
|
11
|
-
* We use {@link SDK.Script.Script} as a key to uniquely identify scripts.
|
|
12
|
-
* {@link SDK.Script.Script} boils down to "target" + "script ID". This
|
|
13
|
-
* duplicates work in case of identitical script running on multiple targets
|
|
14
|
-
* (e.g. workers).
|
|
15
|
-
*/
|
|
16
|
-
export function scopeTreeForScript(script) {
|
|
17
|
-
let promise = scopeTrees.get(script);
|
|
18
|
-
if (promise === undefined) {
|
|
19
|
-
promise = script.requestContentData().then(content => {
|
|
20
|
-
if (TextUtils.ContentData.ContentData.isError(content)) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
const sourceType = script.isModule ? 'module' : 'script';
|
|
24
|
-
return Formatter.FormatterWorkerPool.formatterWorkerPool()
|
|
25
|
-
.javaScriptScopeTree(content.text, sourceType)
|
|
26
|
-
.catch(() => null);
|
|
27
|
-
});
|
|
28
|
-
scopeTrees.set(script, promise);
|
|
29
|
-
}
|
|
30
|
-
// We intentionally return `null` here if the script already failed to parse once.
|
|
31
|
-
return promise;
|
|
32
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// Copyright 2024 The Chromium Authors
|
|
2
|
-
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
-
// found in the LICENSE file.
|
|
4
|
-
// Unsure why this lint is failing, given `lantern/metrics/SpeedIndex.test.ts` does the same
|
|
5
|
-
// and is fine. Maybe `*.test.*` files are excluded from this rule?
|
|
6
|
-
// eslint-disable-next-line rulesdir/es-modules-import
|
|
7
|
-
import * as TraceLoader from '../../../../testing/TraceLoader.js';
|
|
8
|
-
import * as Trace from '../../trace.js';
|
|
9
|
-
import * as Lantern from '../lantern.js';
|
|
10
|
-
function toLanternTrace(traceEvents) {
|
|
11
|
-
return {
|
|
12
|
-
traceEvents: traceEvents,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
async function runTraceProcessor(context, trace) {
|
|
16
|
-
TraceLoader.TraceLoader.setTestTimeout(context);
|
|
17
|
-
const processor = Trace.Processor.TraceProcessor.createWithAllHandlers();
|
|
18
|
-
await processor.parse(trace.traceEvents, { isCPUProfile: false, isFreshRecording: true });
|
|
19
|
-
if (!processor.data) {
|
|
20
|
-
throw new Error('No data');
|
|
21
|
-
}
|
|
22
|
-
return processor.data;
|
|
23
|
-
}
|
|
24
|
-
async function getComputationDataFromFixture(context, { trace, settings, url }) {
|
|
25
|
-
settings = settings ?? {};
|
|
26
|
-
if (!settings.throttlingMethod) {
|
|
27
|
-
settings.throttlingMethod = 'simulate';
|
|
28
|
-
}
|
|
29
|
-
const data = await runTraceProcessor(context, trace);
|
|
30
|
-
const requests = Trace.LanternComputationData.createNetworkRequests(trace, data);
|
|
31
|
-
const networkAnalysis = Lantern.Core.NetworkAnalyzer.analyze(requests);
|
|
32
|
-
if (!networkAnalysis) {
|
|
33
|
-
throw new Error('no networkAnalysis');
|
|
34
|
-
}
|
|
35
|
-
const frameId = data.Meta.mainFrameId;
|
|
36
|
-
const navigationId = data.Meta.mainFrameNavigations[0].args.data?.navigationId;
|
|
37
|
-
if (!navigationId) {
|
|
38
|
-
throw new Error('no navigation id found');
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
simulator: Lantern.Simulation.Simulator.createSimulator({ ...settings, networkAnalysis }),
|
|
42
|
-
graph: Trace.LanternComputationData.createGraph(requests, trace, data, url),
|
|
43
|
-
processedNavigation: Trace.LanternComputationData.createProcessedNavigation(data, frameId, navigationId),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export { getComputationDataFromFixture, runTraceProcessor as runTrace, toLanternTrace, };
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Profile Manager for Chrome DevTools MCP
|
|
3
|
-
*
|
|
4
|
-
* This module manages dedicated Chrome profiles that use symlinks to share
|
|
5
|
-
* Extensions and Bookmarks from the system Chrome profile while maintaining
|
|
6
|
-
* isolated Cookies, Login Data, and Preferences.
|
|
7
|
-
*/
|
|
8
|
-
import fs from 'fs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import os from 'os';
|
|
11
|
-
/**
|
|
12
|
-
* Detect system Chrome profile for a specific channel
|
|
13
|
-
*/
|
|
14
|
-
function detectSystemChromeProfile(channel) {
|
|
15
|
-
const home = os.homedir();
|
|
16
|
-
const chromePaths = {
|
|
17
|
-
stable: path.join(home, 'Library/Application Support/Google/Chrome'),
|
|
18
|
-
beta: path.join(home, 'Library/Application Support/Google/Chrome Beta'),
|
|
19
|
-
canary: path.join(home, 'Library/Application Support/Google/Chrome Canary'),
|
|
20
|
-
dev: path.join(home, 'Library/Application Support/Google/Chrome Dev'),
|
|
21
|
-
};
|
|
22
|
-
const targetChannel = channel || 'stable';
|
|
23
|
-
const chromePath = chromePaths[targetChannel];
|
|
24
|
-
if (chromePath && fs.existsSync(chromePath)) {
|
|
25
|
-
return { path: chromePath, channel: targetChannel };
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Detect any available system Chrome profile
|
|
31
|
-
*/
|
|
32
|
-
function detectAnySystemChromeProfile() {
|
|
33
|
-
const channels = ['stable', 'beta', 'dev', 'canary'];
|
|
34
|
-
for (const channel of channels) {
|
|
35
|
-
const profile = detectSystemChromeProfile(channel);
|
|
36
|
-
if (profile) {
|
|
37
|
-
return profile;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Get the last used profile directory name from Local State
|
|
44
|
-
*/
|
|
45
|
-
function getLastUsedProfile(userDataDir) {
|
|
46
|
-
const localStatePath = path.join(userDataDir, 'Local State');
|
|
47
|
-
try {
|
|
48
|
-
const localStateContent = fs.readFileSync(localStatePath, 'utf-8');
|
|
49
|
-
const localState = JSON.parse(localStateContent);
|
|
50
|
-
const lastUsed = localState?.profile?.last_used;
|
|
51
|
-
if (lastUsed && typeof lastUsed === 'string') {
|
|
52
|
-
return lastUsed;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
// Ignore errors, will use default
|
|
57
|
-
}
|
|
58
|
-
return 'Default';
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Create or update a symlink safely
|
|
62
|
-
*
|
|
63
|
-
* @param target - The target path that the symlink should point to
|
|
64
|
-
* @param linkPath - The path where the symlink should be created
|
|
65
|
-
*/
|
|
66
|
-
function createSymlinkSafe(target, linkPath) {
|
|
67
|
-
// Check if target exists
|
|
68
|
-
if (!fs.existsSync(target)) {
|
|
69
|
-
console.error(`⚠️ Symlink target does not exist: ${target}`);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
// If link already exists and points to the correct target, skip
|
|
73
|
-
if (fs.existsSync(linkPath)) {
|
|
74
|
-
try {
|
|
75
|
-
const currentTarget = fs.readlinkSync(linkPath);
|
|
76
|
-
if (currentTarget === target) {
|
|
77
|
-
// Already correctly linked
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
// Remove old symlink
|
|
81
|
-
fs.unlinkSync(linkPath);
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
// Not a symlink, remove the file/directory
|
|
85
|
-
if (fs.lstatSync(linkPath).isDirectory()) {
|
|
86
|
-
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
fs.unlinkSync(linkPath);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Create new symlink
|
|
94
|
-
fs.symlinkSync(target, linkPath, 'dir');
|
|
95
|
-
console.error(`🔗 Created symlink: ${path.basename(linkPath)} -> ${target}`);
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Setup a dedicated Chrome profile with symlinks to system profile
|
|
99
|
-
*
|
|
100
|
-
* This function:
|
|
101
|
-
* 1. Detects the system Chrome profile
|
|
102
|
-
* 2. Creates a dedicated profile directory
|
|
103
|
-
* 3. Creates symlinks for Extensions and Bookmarks
|
|
104
|
-
* 4. Returns the dedicated profile information
|
|
105
|
-
*
|
|
106
|
-
* @param channel - Chrome channel to use (stable, beta, canary, dev)
|
|
107
|
-
* @returns Dedicated profile information
|
|
108
|
-
*/
|
|
109
|
-
export async function setupDedicatedProfile(channel) {
|
|
110
|
-
// Detect system Chrome profile
|
|
111
|
-
const systemProfile = detectSystemChromeProfile(channel) || detectAnySystemChromeProfile();
|
|
112
|
-
if (!systemProfile) {
|
|
113
|
-
throw new Error('No system Chrome profile found. Please install Chrome and run it at least once.');
|
|
114
|
-
}
|
|
115
|
-
// Get the last used profile directory
|
|
116
|
-
const profileDirectory = getLastUsedProfile(systemProfile.path);
|
|
117
|
-
const systemProfileDir = path.join(systemProfile.path, profileDirectory);
|
|
118
|
-
// Create dedicated profile directory
|
|
119
|
-
const dedicatedUserDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', 'chrome-profile-dedicated');
|
|
120
|
-
await fs.promises.mkdir(dedicatedUserDataDir, { recursive: true });
|
|
121
|
-
const dedicatedProfileDir = path.join(dedicatedUserDataDir, profileDirectory);
|
|
122
|
-
await fs.promises.mkdir(dedicatedProfileDir, { recursive: true });
|
|
123
|
-
console.error('📁 Dedicated Profile Setup:');
|
|
124
|
-
console.error(` System Profile: ${systemProfileDir}`);
|
|
125
|
-
console.error(` Dedicated Profile: ${dedicatedProfileDir}`);
|
|
126
|
-
console.error(` Profile Directory: ${profileDirectory}`);
|
|
127
|
-
// Create symlinks for shared resources
|
|
128
|
-
const symlinkTargets = [
|
|
129
|
-
{ name: 'Extensions', required: false },
|
|
130
|
-
{ name: 'Bookmarks', required: false },
|
|
131
|
-
];
|
|
132
|
-
for (const { name, required } of symlinkTargets) {
|
|
133
|
-
const targetPath = path.join(systemProfileDir, name);
|
|
134
|
-
const linkPath = path.join(dedicatedProfileDir, name);
|
|
135
|
-
if (fs.existsSync(targetPath)) {
|
|
136
|
-
createSymlinkSafe(targetPath, linkPath);
|
|
137
|
-
}
|
|
138
|
-
else if (required) {
|
|
139
|
-
console.error(`⚠️ Required item not found: ${name}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
console.error('✅ Dedicated profile setup complete');
|
|
143
|
-
console.error(' First launch will require Google login (login state is not shared)');
|
|
144
|
-
return {
|
|
145
|
-
userDataDir: dedicatedUserDataDir,
|
|
146
|
-
profileDirectory,
|
|
147
|
-
systemProfilePath: systemProfileDir,
|
|
148
|
-
channel: systemProfile.channel,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Wait for DOM changes to settle using MutationObserver.
|
|
8
|
-
* This is more reliable than polling for streaming AI responses.
|
|
9
|
-
*
|
|
10
|
-
* Based on Gemini's recommendation:
|
|
11
|
-
* - Use MutationObserver to detect when DOM stops changing
|
|
12
|
-
* - Consider generation complete when no changes for `silenceDuration` ms
|
|
13
|
-
* - Combine with additional checks (e.g., stop button disappearance)
|
|
14
|
-
*/
|
|
15
|
-
export async function waitForDomSilence(page, options = {}) {
|
|
16
|
-
const { silenceDuration = 2000, timeout = 300000, observeSelector = 'body', additionalCheck, } = options;
|
|
17
|
-
return page.evaluate(({ silenceDuration, timeout, observeSelector, additionalCheck }) => {
|
|
18
|
-
return new Promise((resolve) => {
|
|
19
|
-
const target = document.querySelector(observeSelector);
|
|
20
|
-
if (!target) {
|
|
21
|
-
resolve({ completed: false });
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
let silenceTimeout;
|
|
25
|
-
let overallTimeout;
|
|
26
|
-
const observer = new MutationObserver(() => {
|
|
27
|
-
// Reset silence timer on any DOM change
|
|
28
|
-
clearTimeout(silenceTimeout);
|
|
29
|
-
silenceTimeout = setTimeout(() => {
|
|
30
|
-
// DOM has been silent for silenceDuration
|
|
31
|
-
// Run additional check if provided
|
|
32
|
-
if (additionalCheck) {
|
|
33
|
-
try {
|
|
34
|
-
const checkFn = new Function('return (' + additionalCheck + ')()');
|
|
35
|
-
if (!checkFn()) {
|
|
36
|
-
// Additional check failed, keep waiting
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// Check failed, consider complete anyway
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
cleanup();
|
|
45
|
-
resolve({ completed: true });
|
|
46
|
-
}, silenceDuration);
|
|
47
|
-
});
|
|
48
|
-
const cleanup = () => {
|
|
49
|
-
clearTimeout(silenceTimeout);
|
|
50
|
-
clearTimeout(overallTimeout);
|
|
51
|
-
observer.disconnect();
|
|
52
|
-
};
|
|
53
|
-
// Start overall timeout
|
|
54
|
-
overallTimeout = setTimeout(() => {
|
|
55
|
-
cleanup();
|
|
56
|
-
resolve({ completed: false, timedOut: true });
|
|
57
|
-
}, timeout);
|
|
58
|
-
// Start observing
|
|
59
|
-
observer.observe(target, {
|
|
60
|
-
childList: true,
|
|
61
|
-
subtree: true,
|
|
62
|
-
characterData: true,
|
|
63
|
-
attributes: true,
|
|
64
|
-
});
|
|
65
|
-
// Initial silence timer
|
|
66
|
-
silenceTimeout = setTimeout(() => {
|
|
67
|
-
if (additionalCheck) {
|
|
68
|
-
try {
|
|
69
|
-
const checkFn = new Function('return (' + additionalCheck + ')()');
|
|
70
|
-
if (!checkFn()) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// Check failed
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
cleanup();
|
|
79
|
-
resolve({ completed: true });
|
|
80
|
-
}, silenceDuration);
|
|
81
|
-
});
|
|
82
|
-
}, { silenceDuration, timeout, observeSelector, additionalCheck });
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Multi-fallback selector strategy.
|
|
86
|
-
* Try multiple selectors in order of preference until one matches.
|
|
87
|
-
*/
|
|
88
|
-
export async function findElementWithFallback(page, selectors, options = {}) {
|
|
89
|
-
const { timeout = 5000 } = options;
|
|
90
|
-
for (const selector of selectors) {
|
|
91
|
-
try {
|
|
92
|
-
const element = await page.waitForSelector(selector, {
|
|
93
|
-
timeout: Math.min(1000, timeout / selectors.length),
|
|
94
|
-
visible: options.visible,
|
|
95
|
-
});
|
|
96
|
-
if (element) {
|
|
97
|
-
return { found: true, selector };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Continue to next selector
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return { found: false };
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Combined completion detection for ChatGPT/Gemini.
|
|
108
|
-
* Uses MutationObserver + stop button check for robust detection.
|
|
109
|
-
*/
|
|
110
|
-
export async function waitForAIResponseComplete(page, options = {}) {
|
|
111
|
-
const { stopSelectors = [], completeSelectors = [], responseSelector = 'body', silenceDuration = 2000, timeout = 300000, } = options;
|
|
112
|
-
const startTime = Date.now();
|
|
113
|
-
// Build additional check function as string (to be evaluated in browser)
|
|
114
|
-
const additionalCheckFn = `
|
|
115
|
-
function() {
|
|
116
|
-
// Check if still generating (stop button visible)
|
|
117
|
-
const stopSelectors = ${JSON.stringify(stopSelectors)};
|
|
118
|
-
for (const sel of stopSelectors) {
|
|
119
|
-
if (document.querySelector(sel)) {
|
|
120
|
-
return false; // Still generating
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check for completion indicators
|
|
125
|
-
const completeSelectors = ${JSON.stringify(completeSelectors)};
|
|
126
|
-
if (completeSelectors.length > 0) {
|
|
127
|
-
for (const sel of completeSelectors) {
|
|
128
|
-
if (document.querySelector(sel)) {
|
|
129
|
-
return true; // Explicitly complete
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return true; // No stop button, consider complete
|
|
135
|
-
}
|
|
136
|
-
`;
|
|
137
|
-
const result = await waitForDomSilence(page, {
|
|
138
|
-
silenceDuration,
|
|
139
|
-
timeout,
|
|
140
|
-
observeSelector: responseSelector,
|
|
141
|
-
additionalCheck: additionalCheckFn,
|
|
142
|
-
});
|
|
143
|
-
// Get final response text
|
|
144
|
-
let responseText = '';
|
|
145
|
-
if (result.completed) {
|
|
146
|
-
responseText = await page.evaluate((selector) => {
|
|
147
|
-
const el = document.querySelector(selector);
|
|
148
|
-
return el?.textContent || '';
|
|
149
|
-
}, responseSelector);
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
completed: result.completed,
|
|
153
|
-
timedOut: result.timedOut,
|
|
154
|
-
responseText,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Type text using clipboard injection for faster input.
|
|
159
|
-
* Much faster than page.type() for long text.
|
|
160
|
-
*/
|
|
161
|
-
export async function typeViaClipboard(page, text, targetSelector) {
|
|
162
|
-
try {
|
|
163
|
-
// Focus the target element
|
|
164
|
-
await page.click(targetSelector);
|
|
165
|
-
// Use clipboard to paste text (faster than typing)
|
|
166
|
-
await page.evaluate(async (text) => {
|
|
167
|
-
// Write to clipboard
|
|
168
|
-
await navigator.clipboard.writeText(text);
|
|
169
|
-
}, text);
|
|
170
|
-
// Paste using keyboard shortcut
|
|
171
|
-
const isMac = (await page.evaluate(() => navigator.platform.includes('Mac'))) || false;
|
|
172
|
-
const modifier = isMac ? 'Meta' : 'Control';
|
|
173
|
-
await page.keyboard.down(modifier);
|
|
174
|
-
await page.keyboard.press('KeyV');
|
|
175
|
-
await page.keyboard.up(modifier);
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
// Fallback to regular typing if clipboard fails
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
}
|