chrome-devtools-mcp-for-extension 0.22.5 → 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.
@@ -275,12 +275,18 @@ export class McpContext {
275
275
  this.#dialog = dialog;
276
276
  };
277
277
  setSelectedPageIdx(idx) {
278
- const oldPage = this.getSelectedPage();
279
- oldPage.off('dialog', this.#dialogHandler);
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
- const newPage = this.getSelectedPage();
282
- newPage.on('dialog', this.#dialogHandler);
283
- this.#updateSelectedPageTimeouts();
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();
@@ -8,7 +8,7 @@ export class WaitForHelper {
8
8
  #navigationTimeout;
9
9
  constructor(page, cpuTimeoutMultiplier, networkTimeoutMultiplier) {
10
10
  this.#stableDomTimeout = 3000 * cpuTimeoutMultiplier;
11
- this.#stableDomFor = 100 * cpuTimeoutMultiplier;
11
+ this.#stableDomFor = 500 * cpuTimeoutMultiplier;
12
12
  this.#expectNavigationIn = 100 * cpuTimeoutMultiplier;
13
13
  this.#navigationTimeout = 3000 * networkTimeoutMultiplier;
14
14
  this.#page = page;
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 * as inputTools from './tools/input.js';
53
+ import { click, fill, fillForm } from './tools/input.js';
58
54
  import * as networkTools from './tools/network.js';
59
- import * as pagesTools from './tools/pages.js';
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
- ...Object.values(extensionTools),
253
- ...Object.values(inputTools),
245
+ click,
246
+ fill,
247
+ fillForm,
254
248
  ...Object.values(networkTools),
255
- ...Object.values(pagesTools),
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 buttons = Array.from(document.querySelectorAll('button'));
459
- const isRunning = buttons.some((btn) => {
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
- const buttons = Array.from(document.querySelectorAll('button'));
487
- const isStreaming = buttons.some((btn) => {
488
- const text = btn.textContent || '';
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.22.5",
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,4 +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 {};
@@ -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
- }
@@ -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,4 +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
- export * from './MetricTestUtils.js';
@@ -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
- }