chrome-devtools-mcp-for-extension 0.9.21 → 0.9.23

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/main.js CHANGED
@@ -17,6 +17,7 @@ import { McpResponse } from './McpResponse.js';
17
17
  import { Mutex } from './Mutex.js';
18
18
  import * as bookmarkTools from './tools/bookmarks.js';
19
19
  import * as chatgptWebTools from './tools/chatgpt-web.js';
20
+ import * as deepResearchChatGPTTools from './tools/deep_research_chatgpt.js';
20
21
  import * as consoleTools from './tools/console.js';
21
22
  import * as emulationTools from './tools/emulation.js';
22
23
  import * as extensionTools from './tools/extensions.js';
@@ -135,6 +136,7 @@ function registerTool(tool) {
135
136
  const tools = [
136
137
  ...Object.values(bookmarkTools),
137
138
  ...Object.values(chatgptWebTools),
139
+ ...Object.values(deepResearchChatGPTTools),
138
140
  ...Object.values(consoleTools),
139
141
  ...Object.values(emulationTools),
140
142
  ...Object.values(extensionTools),
@@ -114,9 +114,15 @@ export const askChatGPTWeb = defineTool({
114
114
  .boolean()
115
115
  .optional()
116
116
  .describe('Force creation of a new chat instead of reusing existing project chat. Default: false'),
117
+ useDeepResearch: z
118
+ .boolean()
119
+ .optional()
120
+ .describe('Enable DeepResearch mode for complex research tasks requiring comprehensive analysis. ' +
121
+ 'Use when the question involves market research, comparative analysis, trend analysis, ' +
122
+ 'or requires gathering information from multiple sources. Default: false'),
117
123
  },
118
124
  handler: async (request, response, context) => {
119
- const { question, projectName, createNewChat = false } = request.params;
125
+ const { question, projectName, createNewChat = false, useDeepResearch = false } = request.params;
120
126
  // Sanitize question
121
127
  const sanitizedQuestion = sanitizeQuestion(question);
122
128
  // Determine project name
@@ -184,6 +190,45 @@ export const askChatGPTWeb = defineTool({
184
190
  await new Promise((resolve) => setTimeout(resolve, 500));
185
191
  }
186
192
  }
193
+ // Step 3.5: Enable DeepResearch mode if requested
194
+ if (useDeepResearch) {
195
+ response.appendResponseLine('DeepResearchモードを有効化中...');
196
+ // Click the "+" button to open tools menu
197
+ const menuOpened = await page.evaluate(() => {
198
+ const buttons = Array.from(document.querySelectorAll('button'));
199
+ const plusButton = buttons.find((btn) => {
200
+ const aria = btn.getAttribute('aria-label') || '';
201
+ const desc = btn.getAttribute('description') || '';
202
+ return (aria.includes('ファイルの追加') ||
203
+ desc.includes('ファイルの追加'));
204
+ });
205
+ if (plusButton) {
206
+ plusButton.click();
207
+ return true;
208
+ }
209
+ return false;
210
+ });
211
+ if (menuOpened) {
212
+ await new Promise((resolve) => setTimeout(resolve, 500));
213
+ // Click "Deep Research" menu item
214
+ const deepResearchEnabled = await page.evaluate(() => {
215
+ const menuItems = Array.from(document.querySelectorAll('[role="menuitemradio"]'));
216
+ const deepResearchItem = menuItems.find((item) => item.textContent?.includes('Deep Research'));
217
+ if (deepResearchItem) {
218
+ deepResearchItem.click();
219
+ return true;
220
+ }
221
+ return false;
222
+ });
223
+ if (deepResearchEnabled) {
224
+ response.appendResponseLine('✅ DeepResearchモード有効化完了');
225
+ await new Promise((resolve) => setTimeout(resolve, 500));
226
+ }
227
+ else {
228
+ response.appendResponseLine('⚠️ DeepResearchオプションが見つかりませんでした');
229
+ }
230
+ }
231
+ }
187
232
  // Step 4: Send question
188
233
  response.appendResponseLine('質問を送信中...');
189
234
  const questionSent = await page.evaluate((questionText) => {
@@ -221,14 +266,55 @@ export const askChatGPTWeb = defineTool({
221
266
  return messages.length > 0;
222
267
  }, { timeout: 10000 });
223
268
  response.appendResponseLine('✅ 質問送信完了');
224
- // Step 5: Monitor streaming with progress updates
225
- response.appendResponseLine('ChatGPTの回答を待機中... (10秒ごとに進捗を表示)');
269
+ // Step 5: Monitor streaming/research with progress updates
270
+ if (useDeepResearch) {
271
+ response.appendResponseLine('DeepResearchを実行中... (10秒ごとに進捗を表示)');
272
+ }
273
+ else {
274
+ response.appendResponseLine('ChatGPTの回答を待機中... (10秒ごとに進捗を表示)');
275
+ }
226
276
  const startTime = Date.now();
227
277
  let lastText = '';
278
+ let lastProgress = '';
228
279
  while (true) {
229
280
  await new Promise((resolve) => setTimeout(resolve, 2000));
230
- const status = await page.evaluate(() => {
231
- // Check if streaming - check both textContent and aria-label
281
+ const status = await page.evaluate((isDeepResearch) => {
282
+ // DeepResearch progress detection
283
+ if (isDeepResearch) {
284
+ // Look for research progress indicators
285
+ const progressElements = Array.from(document.querySelectorAll('[role="status"], [aria-live="polite"]'));
286
+ const progressText = progressElements
287
+ .map((el) => el.textContent)
288
+ .join(' ');
289
+ // Check if DeepResearch is still running
290
+ const buttons = Array.from(document.querySelectorAll('button'));
291
+ const isRunning = buttons.some((btn) => {
292
+ const text = btn.textContent || '';
293
+ const aria = btn.getAttribute('aria-label') || '';
294
+ return (text.includes('停止') ||
295
+ text.includes('リサーチを停止') ||
296
+ aria.includes('停止'));
297
+ });
298
+ if (!isRunning) {
299
+ // Research completed - get the report
300
+ const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
301
+ if (assistantMessages.length === 0)
302
+ return { completed: false, progress: progressText };
303
+ const latestMessage = assistantMessages[assistantMessages.length - 1];
304
+ return {
305
+ completed: true,
306
+ text: latestMessage.textContent || '',
307
+ isDeepResearch: true,
308
+ };
309
+ }
310
+ return {
311
+ completed: false,
312
+ streaming: true,
313
+ progress: progressText,
314
+ currentText: progressText.substring(0, 200),
315
+ };
316
+ }
317
+ // Normal streaming detection
232
318
  const buttons = Array.from(document.querySelectorAll('button'));
233
319
  const isStreaming = buttons.some((btn) => {
234
320
  const text = btn.textContent || '';
@@ -265,9 +351,12 @@ export const askChatGPTWeb = defineTool({
265
351
  streaming: true,
266
352
  currentText,
267
353
  };
268
- });
354
+ }, useDeepResearch);
269
355
  if (status.completed) {
270
- response.appendResponseLine(`\n✅ 回答完了 (所要時間: ${Math.floor((Date.now() - startTime) / 1000)}秒)`);
356
+ const completionMessage = useDeepResearch
357
+ ? `\n✅ DeepResearch完了 (所要時間: ${Math.floor((Date.now() - startTime) / 1000)}秒)`
358
+ : `\n✅ 回答完了 (所要時間: ${Math.floor((Date.now() - startTime) / 1000)}秒)`;
359
+ response.appendResponseLine(completionMessage);
271
360
  if (status.thinkingTime) {
272
361
  response.appendResponseLine(`🤔 思考時間: ${status.thinkingTime}秒`);
273
362
  }
@@ -306,10 +395,13 @@ export const askChatGPTWeb = defineTool({
306
395
  }
307
396
  // Save conversation log
308
397
  const chatUrl = page.url();
398
+ const modelName = useDeepResearch
399
+ ? 'ChatGPT DeepResearch'
400
+ : 'ChatGPT 5 Thinking';
309
401
  const logPath = await saveConversationLog(project, sanitizedQuestion, status.text || '', {
310
402
  thinkingTime: status.thinkingTime,
311
403
  chatUrl,
312
- model: 'ChatGPT 5 Thinking',
404
+ model: modelName,
313
405
  });
314
406
  response.appendResponseLine(`📝 会話ログ保存: ${logPath}`);
315
407
  response.appendResponseLine(`🔗 チャットURL: ${chatUrl}`);
@@ -320,9 +412,15 @@ export const askChatGPTWeb = defineTool({
320
412
  }
321
413
  // Show progress every 10 seconds
322
414
  const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
323
- if (elapsedSeconds % 10 === 0 && status.currentText !== lastText) {
324
- lastText = status.currentText || '';
325
- response.appendResponseLine(`⏱️ ${elapsedSeconds}秒経過 - 現在のテキスト: ${lastText.substring(0, 100)}...`);
415
+ if (elapsedSeconds % 10 === 0) {
416
+ if (useDeepResearch && status.progress !== lastProgress) {
417
+ lastProgress = status.progress || '';
418
+ response.appendResponseLine(`⏱️ ${elapsedSeconds}秒経過 - 進捗: ${lastProgress.substring(0, 100)}...`);
419
+ }
420
+ else if (status.currentText !== lastText) {
421
+ lastText = status.currentText || '';
422
+ response.appendResponseLine(`⏱️ ${elapsedSeconds}秒経過 - 現在のテキスト: ${lastText.substring(0, 100)}...`);
423
+ }
326
424
  }
327
425
  }
328
426
  }
@@ -0,0 +1,601 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import z from 'zod';
9
+ import { ToolCategories } from './categories.js';
10
+ import { defineTool } from './ToolDefinition.js';
11
+ /**
12
+ * Path to store chat session data
13
+ */
14
+ const CHAT_SESSIONS_FILE = path.join(process.cwd(), 'docs/ask/chatgpt/.chat-sessions.json');
15
+ /**
16
+ * Load chat sessions from JSON file
17
+ */
18
+ async function loadChatSessions() {
19
+ try {
20
+ const data = await fs.promises.readFile(CHAT_SESSIONS_FILE, 'utf-8');
21
+ return JSON.parse(data);
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ }
27
+ /**
28
+ * Save a chat session for a project
29
+ */
30
+ async function saveChatSession(projectName, session) {
31
+ const sessions = await loadChatSessions();
32
+ sessions[projectName] = session;
33
+ const dir = path.dirname(CHAT_SESSIONS_FILE);
34
+ await fs.promises.mkdir(dir, { recursive: true });
35
+ await fs.promises.writeFile(CHAT_SESSIONS_FILE, JSON.stringify(sessions, null, 2), 'utf-8');
36
+ }
37
+ /**
38
+ * Sanitize question to remove sensitive information
39
+ */
40
+ function sanitizeQuestion(text) {
41
+ const passwordPatterns = [
42
+ /password\s*[:=]\s*\S+/gi,
43
+ /パスワード\s*[::=]\s*\S+/gi,
44
+ /pwd\s*[:=]\s*\S+/gi,
45
+ /secret\s*[:=]\s*\S+/gi,
46
+ ];
47
+ let sanitized = text;
48
+ for (const pattern of passwordPatterns) {
49
+ sanitized = sanitized.replace(pattern, '[パスワードは除外されました]');
50
+ }
51
+ return sanitized;
52
+ }
53
+ /**
54
+ * Save conversation log to docs/ask/chatgpt/
55
+ */
56
+ async function saveConversationLog(projectName, question, response, metadata) {
57
+ const now = new Date();
58
+ const timestamp = [
59
+ String(now.getFullYear()).slice(2).padStart(2, '0'),
60
+ String(now.getMonth() + 1).padStart(2, '0'),
61
+ String(now.getDate()).padStart(2, '0'),
62
+ '_',
63
+ String(now.getHours()).padStart(2, '0'),
64
+ String(now.getMinutes()).padStart(2, '0'),
65
+ String(now.getSeconds()).padStart(2, '0'),
66
+ ].join('');
67
+ const topicSlug = question
68
+ .substring(0, 50)
69
+ .replace(/[^a-z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]+/gi, '-')
70
+ .toLowerCase()
71
+ .slice(0, 30);
72
+ const filename = `${timestamp}-${projectName}-deepresearch-${topicSlug}.md`;
73
+ const logDir = 'docs/ask/chatgpt';
74
+ const logPath = path.join(process.cwd(), logDir, filename);
75
+ await fs.promises.mkdir(path.dirname(logPath), { recursive: true });
76
+ const content = `# ${topicSlug}
77
+
78
+ ## 📅 メタ情報
79
+ - **日時**: ${now.toLocaleString('ja-JP')}
80
+ - **プロジェクト**: ${projectName}
81
+ - **AIモデル**: ${metadata.model || 'ChatGPT DeepResearch'}
82
+ ${metadata.researchTime ? `- **リサーチ時間**: ${metadata.researchTime}秒\n` : ''}${metadata.chatUrl ? `- **チャットURL**: ${metadata.chatUrl}\n` : ''}
83
+ ## ❓ リサーチテーマ
84
+
85
+ ${question}
86
+
87
+ ## 🔍 DeepResearch 結果
88
+
89
+ ${response}
90
+ `;
91
+ await fs.promises.writeFile(logPath, content, 'utf-8');
92
+ return path.relative(process.cwd(), logPath);
93
+ }
94
+ /**
95
+ * Detect if question is code-related
96
+ */
97
+ function isCodeRelatedQuestion(question) {
98
+ const codeKeywords = [
99
+ 'code',
100
+ 'コード',
101
+ 'programming',
102
+ 'プログラミング',
103
+ 'github',
104
+ 'repository',
105
+ 'リポジトリ',
106
+ 'api',
107
+ 'library',
108
+ 'ライブラリ',
109
+ 'framework',
110
+ 'フレームワーク',
111
+ 'typescript',
112
+ 'javascript',
113
+ 'python',
114
+ 'implementation',
115
+ '実装',
116
+ 'algorithm',
117
+ 'アルゴリズム',
118
+ 'database',
119
+ 'データベース',
120
+ 'function',
121
+ '関数',
122
+ ];
123
+ const lowerQuestion = question.toLowerCase();
124
+ return codeKeywords.some((keyword) => lowerQuestion.includes(keyword));
125
+ }
126
+ /**
127
+ * Detect if currently in DeepResearch mode
128
+ */
129
+ async function detectDeepResearchMode(page) {
130
+ return await page.evaluate(() => {
131
+ // Check for DeepResearch indicator in the UI
132
+ const allElements = Array.from(document.querySelectorAll('div, span, button'));
133
+ // Look for DeepResearch badge/text near input area
134
+ const deepResearchIndicator = allElements.find((el) => {
135
+ const text = el.textContent || '';
136
+ const ariaLabel = el.getAttribute('aria-label') || '';
137
+ return (text.includes('Deep Research') ||
138
+ text.includes('ディープリサーチ') ||
139
+ ariaLabel.includes('Deep Research') ||
140
+ ariaLabel.includes('ディープリサーチ'));
141
+ });
142
+ if (deepResearchIndicator) {
143
+ return {
144
+ isEnabled: true,
145
+ indicator: deepResearchIndicator.textContent?.substring(0, 50) ||
146
+ 'DeepResearch',
147
+ };
148
+ }
149
+ // Check for the "+" button state (if DeepResearch is selected)
150
+ const menuItems = Array.from(document.querySelectorAll('[role="menuitemradio"]'));
151
+ const deepResearchSelected = menuItems.some((item) => {
152
+ const text = item.textContent || '';
153
+ const isChecked = item.getAttribute('aria-checked') === 'true';
154
+ return (isChecked &&
155
+ (text.includes('Deep Research') || text.includes('ディープリサーチ')));
156
+ });
157
+ return {
158
+ isEnabled: deepResearchSelected,
159
+ indicator: deepResearchSelected
160
+ ? 'DeepResearch (selected in menu)'
161
+ : undefined,
162
+ };
163
+ });
164
+ }
165
+ /**
166
+ * Enable DeepResearch mode by clicking + button and selecting option
167
+ */
168
+ async function enableDeepResearchMode(page, response) {
169
+ try {
170
+ response.appendResponseLine('DeepResearchモードを有効化中...');
171
+ // Step 1: Click "+" button
172
+ const plusClicked = await page.evaluate(() => {
173
+ const buttons = Array.from(document.querySelectorAll('button'));
174
+ const plusButton = buttons.find((btn) => {
175
+ const aria = btn.getAttribute('aria-label') || '';
176
+ const desc = btn.getAttribute('description') || '';
177
+ return aria.includes('ファイルの追加') || desc.includes('ファイルの追加');
178
+ });
179
+ if (!plusButton)
180
+ return { success: false, error: '+ボタンが見つかりません' };
181
+ plusButton.click();
182
+ return { success: true };
183
+ });
184
+ if (!plusClicked.success) {
185
+ return { success: false, error: plusClicked.error };
186
+ }
187
+ await new Promise((resolve) => setTimeout(resolve, 800));
188
+ // Step 2: Select DeepResearch option
189
+ const deepResearchSelected = await page.evaluate(() => {
190
+ const menuItems = Array.from(document.querySelectorAll('[role="menuitemradio"]'));
191
+ const deepResearchItem = menuItems.find((item) => item.textContent?.includes('Deep Research') ||
192
+ item.textContent?.includes('ディープリサーチ'));
193
+ if (!deepResearchItem) {
194
+ return {
195
+ success: false,
196
+ error: 'DeepResearchオプションが見つかりません',
197
+ };
198
+ }
199
+ deepResearchItem.click();
200
+ return { success: true };
201
+ });
202
+ if (!deepResearchSelected.success) {
203
+ return { success: false, error: deepResearchSelected.error };
204
+ }
205
+ response.appendResponseLine('✅ DeepResearchモード有効化');
206
+ await new Promise((resolve) => setTimeout(resolve, 500));
207
+ return { success: true };
208
+ }
209
+ catch (error) {
210
+ return {
211
+ success: false,
212
+ error: error instanceof Error ? error.message : String(error),
213
+ };
214
+ }
215
+ }
216
+ /**
217
+ * Configure information sources (enable GitHub if needed)
218
+ */
219
+ async function configureSources(page, response, enableGitHub) {
220
+ if (!enableGitHub) {
221
+ response.appendResponseLine('📚 情報源設定: Web (デフォルト)');
222
+ return;
223
+ }
224
+ response.appendResponseLine('📚 情報源設定: Web + GitHub (コード関連質問)');
225
+ const sourcesConfigured = await page.evaluate(() => {
226
+ const buttons = Array.from(document.querySelectorAll('button'));
227
+ const sourcesButton = buttons.find((btn) => btn.textContent?.includes('情報源'));
228
+ if (!sourcesButton) {
229
+ return { success: false, error: '情報源ボタンが見つかりません' };
230
+ }
231
+ sourcesButton.click();
232
+ return { success: true };
233
+ });
234
+ if (!sourcesConfigured.success) {
235
+ response.appendResponseLine(`⚠️ ${sourcesConfigured.error}`);
236
+ return;
237
+ }
238
+ await new Promise((resolve) => setTimeout(resolve, 500));
239
+ // Enable GitHub source
240
+ const githubEnabled = await page.evaluate(() => {
241
+ const checkboxes = Array.from(document.querySelectorAll('[role="menuitemcheckbox"]'));
242
+ const githubCheckbox = checkboxes.find((cb) => cb.textContent?.includes('GitHub'));
243
+ if (!githubCheckbox) {
244
+ return { success: false, error: 'GitHubオプションが見つかりません' };
245
+ }
246
+ const isChecked = githubCheckbox.getAttribute('aria-checked') === 'true';
247
+ if (!isChecked) {
248
+ githubCheckbox.click();
249
+ }
250
+ return { success: true, wasAlreadyEnabled: isChecked };
251
+ });
252
+ if (githubEnabled.success) {
253
+ response.appendResponseLine(githubEnabled.wasAlreadyEnabled
254
+ ? '✅ GitHub情報源は既に有効です'
255
+ : '✅ GitHub情報源を有効化');
256
+ }
257
+ else {
258
+ response.appendResponseLine(`⚠️ ${githubEnabled.error}`);
259
+ }
260
+ // Close menu
261
+ await page.keyboard.press('Escape');
262
+ await new Promise((resolve) => setTimeout(resolve, 300));
263
+ }
264
+ /**
265
+ * Send question text and click send button
266
+ */
267
+ async function sendQuestion(page, response, question) {
268
+ response.appendResponseLine('リサーチテーマを送信中...');
269
+ const questionSent = await page.evaluate((questionText) => {
270
+ const prosemirror = document.querySelector('.ProseMirror[contenteditable="true"]');
271
+ if (!prosemirror)
272
+ return false;
273
+ prosemirror.innerHTML = '';
274
+ const p = document.createElement('p');
275
+ p.textContent = questionText;
276
+ prosemirror.appendChild(p);
277
+ prosemirror.dispatchEvent(new Event('input', { bubbles: true }));
278
+ return true;
279
+ }, question);
280
+ if (!questionSent) {
281
+ return { success: false, error: 'エディタが見つかりません' };
282
+ }
283
+ await new Promise((resolve) => setTimeout(resolve, 500));
284
+ // Click send button
285
+ const sent = await page.evaluate(() => {
286
+ const sendButton = document.querySelector('button[data-testid="send-button"]');
287
+ if (sendButton && !sendButton.disabled) {
288
+ sendButton.click();
289
+ return true;
290
+ }
291
+ return false;
292
+ });
293
+ if (!sent) {
294
+ return { success: false, error: '送信ボタンが見つかりません' };
295
+ }
296
+ response.appendResponseLine('✅ リサーチテーマ送信完了');
297
+ return { success: true };
298
+ }
299
+ /**
300
+ * Handle conversation continuation until research starts
301
+ */
302
+ async function handleConversationLoop(page, response, maxTurns = 5) {
303
+ response.appendResponseLine('💬 ChatGPTとの対話を開始(リサーチ開始まで継続)...');
304
+ let conversationTurns = 0;
305
+ while (conversationTurns < maxTurns) {
306
+ await new Promise((resolve) => setTimeout(resolve, 3000));
307
+ const status = await page.evaluate(() => {
308
+ // Check for research progress indicator
309
+ const progressIndicators = Array.from(document.querySelectorAll('div, span'));
310
+ const isResearching = progressIndicators.some((el) => el.textContent?.includes('リサーチ中') ||
311
+ el.textContent?.includes('Researching') ||
312
+ el.textContent?.includes('情報を収集中'));
313
+ if (isResearching) {
314
+ return { phase: 'researching' };
315
+ }
316
+ // Check if ChatGPT is asking a clarifying question
317
+ const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
318
+ if (assistantMessages.length === 0) {
319
+ return { phase: 'waiting' };
320
+ }
321
+ const latestMessage = assistantMessages[assistantMessages.length - 1];
322
+ const messageText = latestMessage.textContent || '';
323
+ // Check if it's still streaming
324
+ const buttons = Array.from(document.querySelectorAll('button'));
325
+ const isStreaming = buttons.some((btn) => {
326
+ const text = btn.textContent || '';
327
+ const aria = btn.getAttribute('aria-label') || '';
328
+ return (text.includes('ストリーミングの停止') ||
329
+ text.includes('停止') ||
330
+ aria.includes('ストリーミングの停止') ||
331
+ aria.includes('停止'));
332
+ });
333
+ if (isStreaming) {
334
+ return { phase: 'streaming' };
335
+ }
336
+ // ChatGPT has asked a question
337
+ return {
338
+ phase: 'clarification',
339
+ question: messageText.substring(0, 200),
340
+ };
341
+ });
342
+ if (status.phase === 'researching') {
343
+ response.appendResponseLine('\n🔍 リサーチが開始されました!監視を開始...');
344
+ return { researchStarted: true };
345
+ }
346
+ if (status.phase === 'clarification') {
347
+ conversationTurns++;
348
+ response.appendResponseLine(`\n💬 ChatGPTの質問 (${conversationTurns}/${maxTurns}):`);
349
+ response.appendResponseLine(`"${status.question}..."`);
350
+ // Auto-respond to continue
351
+ response.appendResponseLine('自動応答: その内容で実施してください');
352
+ const responded = await page.evaluate(() => {
353
+ const prosemirror = document.querySelector('.ProseMirror[contenteditable="true"]');
354
+ if (!prosemirror)
355
+ return false;
356
+ prosemirror.innerHTML = '';
357
+ const p = document.createElement('p');
358
+ p.textContent = 'その内容で実施してください';
359
+ prosemirror.appendChild(p);
360
+ prosemirror.dispatchEvent(new Event('input', { bubbles: true }));
361
+ return true;
362
+ });
363
+ if (responded) {
364
+ await new Promise((resolve) => setTimeout(resolve, 500));
365
+ await page.evaluate(() => {
366
+ const sendButton = document.querySelector('button[data-testid="send-button"]');
367
+ if (sendButton && !sendButton.disabled) {
368
+ sendButton.click();
369
+ }
370
+ });
371
+ response.appendResponseLine('✅ 応答を送信');
372
+ }
373
+ continue;
374
+ }
375
+ if (status.phase === 'streaming' || status.phase === 'waiting') {
376
+ // Still processing, wait
377
+ continue;
378
+ }
379
+ }
380
+ return {
381
+ researchStarted: false,
382
+ error: '会話ターン数が上限に達しました。リサーチが開始されませんでした。',
383
+ };
384
+ }
385
+ /**
386
+ * Monitor research progress until completion
387
+ */
388
+ async function monitorResearch(page, response, startTime) {
389
+ response.appendResponseLine('⏳ DeepResearchを実行中... (数分かかる場合があります)');
390
+ const MAX_WAIT_TIME = 15 * 60 * 1000; // 15 minutes max
391
+ let progressCounter = 0;
392
+ while (Date.now() - startTime < MAX_WAIT_TIME) {
393
+ await new Promise((resolve) => setTimeout(resolve, 5000));
394
+ const researchStatus = await page.evaluate(() => {
395
+ // Check if research completed
396
+ const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
397
+ if (assistantMessages.length === 0) {
398
+ return { completed: false, stillResearching: true };
399
+ }
400
+ const latestMessage = assistantMessages[assistantMessages.length - 1];
401
+ // Check if still researching
402
+ const progressIndicators = Array.from(document.querySelectorAll('div, span'));
403
+ const isResearching = progressIndicators.some((el) => el.textContent?.includes('リサーチ中') ||
404
+ el.textContent?.includes('Researching') ||
405
+ el.textContent?.includes('情報を収集中'));
406
+ if (isResearching) {
407
+ return { completed: false, stillResearching: true };
408
+ }
409
+ // Check if streaming
410
+ const buttons = Array.from(document.querySelectorAll('button'));
411
+ const isStreaming = buttons.some((btn) => {
412
+ const text = btn.textContent || '';
413
+ const aria = btn.getAttribute('aria-label') || '';
414
+ return (text.includes('ストリーミングの停止') ||
415
+ aria.includes('ストリーミングの停止'));
416
+ });
417
+ if (isStreaming) {
418
+ return { completed: false, stillResearching: true };
419
+ }
420
+ // Research completed
421
+ return {
422
+ completed: true,
423
+ result: latestMessage.textContent || '',
424
+ };
425
+ });
426
+ if (researchStatus.completed) {
427
+ const elapsedMinutes = Math.floor((Date.now() - startTime) / 60000);
428
+ response.appendResponseLine(`\n✅ DeepResearch完了 (所要時間: ${elapsedMinutes}分)`);
429
+ return {
430
+ completed: true,
431
+ result: researchStatus.result || '',
432
+ };
433
+ }
434
+ // Show progress every 30 seconds
435
+ progressCounter++;
436
+ if (progressCounter % 6 === 0) {
437
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
438
+ response.appendResponseLine(`⏱️ ${elapsedSeconds}秒経過 - リサーチ継続中...`);
439
+ }
440
+ }
441
+ return {
442
+ completed: false,
443
+ error: 'リサーチがタイムアウトしました(15分経過)',
444
+ };
445
+ }
446
+ export const deepResearchChatGPT = defineTool({
447
+ name: 'deep_research_chatgpt',
448
+ description: `Perform deep research using ChatGPT's DeepResearch mode. This tool automatically handles mode detection, source selection, conversation continuation, and result retrieval. Use this when thorough research is needed.`,
449
+ annotations: {
450
+ category: ToolCategories.NAVIGATION_AUTOMATION,
451
+ readOnlyHint: false,
452
+ },
453
+ schema: {
454
+ question: z
455
+ .string()
456
+ .describe('The research question or topic. Should be detailed and well-formed.'),
457
+ projectName: z
458
+ .string()
459
+ .optional()
460
+ .describe('Project name for organizing research sessions. Defaults to current working directory name.'),
461
+ enableGitHub: z
462
+ .boolean()
463
+ .optional()
464
+ .describe('Enable GitHub as information source. Auto-detected if question is code-related.'),
465
+ reuseSession: z
466
+ .boolean()
467
+ .optional()
468
+ .describe('Reuse existing project chat session instead of creating new chat. Default: false'),
469
+ },
470
+ handler: async (request, response, context) => {
471
+ const { question, projectName, enableGitHub, reuseSession = false } = request.params;
472
+ const sanitizedQuestion = sanitizeQuestion(question);
473
+ const project = projectName || path.basename(process.cwd()) || 'unknown-project';
474
+ // Auto-detect if GitHub should be enabled
475
+ const shouldEnableGitHub = enableGitHub !== undefined
476
+ ? enableGitHub
477
+ : isCodeRelatedQuestion(question);
478
+ const page = context.getSelectedPage();
479
+ try {
480
+ // Phase 1: Navigate to ChatGPT
481
+ response.appendResponseLine('🔍 DeepResearchモードを開始...');
482
+ let needsNewChat = true;
483
+ if (reuseSession) {
484
+ // Try to load existing session
485
+ const sessions = await loadChatSessions();
486
+ const existingSession = sessions[project];
487
+ if (existingSession) {
488
+ response.appendResponseLine(`既存のプロジェクトチャットを使用: ${existingSession.url}`);
489
+ await page.goto(existingSession.url, { waitUntil: 'networkidle2' });
490
+ needsNewChat = false;
491
+ await new Promise((resolve) => setTimeout(resolve, 1000));
492
+ }
493
+ else {
494
+ response.appendResponseLine('既存チャットが見つかりませんでした。新規作成します。');
495
+ }
496
+ }
497
+ if (needsNewChat) {
498
+ await page.goto('https://chatgpt.com/', { waitUntil: 'networkidle2' });
499
+ }
500
+ // Check if logged in
501
+ const currentUrl = page.url();
502
+ if (currentUrl.includes('auth') || currentUrl.includes('login')) {
503
+ response.appendResponseLine('❌ ChatGPTにログインが必要です。ブラウザで手動ログインしてください。');
504
+ response.appendResponseLine(`ログインURL: ${currentUrl}`);
505
+ return;
506
+ }
507
+ response.appendResponseLine('✅ ログイン確認完了');
508
+ // Phase 2: Create new chat if needed
509
+ if (needsNewChat) {
510
+ response.appendResponseLine('新規チャットを作成中...');
511
+ await page.evaluate(() => {
512
+ const newChatLink = document.querySelector('a[href="/"]');
513
+ if (newChatLink) {
514
+ newChatLink.click();
515
+ }
516
+ });
517
+ await new Promise((resolve) => setTimeout(resolve, 1000));
518
+ // Turn off temporary chat
519
+ const tempChatDisabled = await page.evaluate(() => {
520
+ const buttons = Array.from(document.querySelectorAll('button'));
521
+ const btn = buttons.find((b) => {
522
+ const label = b.getAttribute('aria-label') || '';
523
+ return label.includes('一時チャットをオフにする');
524
+ });
525
+ if (btn) {
526
+ btn.click();
527
+ return true;
528
+ }
529
+ return false;
530
+ });
531
+ if (tempChatDisabled) {
532
+ response.appendResponseLine('✅ 一時チャット無効化');
533
+ await new Promise((resolve) => setTimeout(resolve, 500));
534
+ }
535
+ }
536
+ // Phase 3: Detect and enable DeepResearch mode if needed
537
+ const modeStatus = await detectDeepResearchMode(page);
538
+ if (modeStatus.isEnabled) {
539
+ response.appendResponseLine(`✅ DeepResearchモード既に有効 (${modeStatus.indicator})`);
540
+ }
541
+ else {
542
+ response.appendResponseLine('DeepResearchモードが無効です。有効化します...');
543
+ const enableResult = await enableDeepResearchMode(page, response);
544
+ if (!enableResult.success) {
545
+ response.appendResponseLine(`❌ ${enableResult.error}`);
546
+ return;
547
+ }
548
+ }
549
+ // Phase 4: Configure information sources
550
+ await configureSources(page, response, shouldEnableGitHub);
551
+ // Phase 5: Send research question
552
+ const sendResult = await sendQuestion(page, response, sanitizedQuestion);
553
+ if (!sendResult.success) {
554
+ response.appendResponseLine(`❌ ${sendResult.error}`);
555
+ return;
556
+ }
557
+ // Phase 6: Conversation continuation loop
558
+ const startTime = Date.now();
559
+ const loopResult = await handleConversationLoop(page, response);
560
+ if (!loopResult.researchStarted) {
561
+ response.appendResponseLine(`⚠️ ${loopResult.error}`);
562
+ return;
563
+ }
564
+ // Phase 7: Monitor research progress
565
+ const monitorResult = await monitorResearch(page, response, startTime);
566
+ if (!monitorResult.completed) {
567
+ response.appendResponseLine(`❌ ${monitorResult.error}`);
568
+ return;
569
+ }
570
+ // Phase 8: Save results
571
+ const chatUrl = page.url();
572
+ const chatIdMatch = chatUrl.match(/\/c\/([a-f0-9-]+)/);
573
+ if (chatIdMatch) {
574
+ const chatId = chatIdMatch[1];
575
+ await saveChatSession(project, {
576
+ chatId,
577
+ url: chatUrl,
578
+ lastUsed: new Date().toISOString(),
579
+ title: `[DeepResearch: ${project}]`,
580
+ });
581
+ response.appendResponseLine(`💾 チャットセッション保存: ${chatId}`);
582
+ }
583
+ // Save conversation log
584
+ const logPath = await saveConversationLog(project, sanitizedQuestion, monitorResult.result || '', {
585
+ researchTime: Math.floor((Date.now() - startTime) / 1000),
586
+ chatUrl,
587
+ model: 'ChatGPT DeepResearch',
588
+ });
589
+ response.appendResponseLine(`📝 リサーチログ保存: ${logPath}`);
590
+ response.appendResponseLine(`🔗 チャットURL: ${chatUrl}`);
591
+ response.appendResponseLine('\n' + '='.repeat(60));
592
+ response.appendResponseLine('DeepResearch結果:\n');
593
+ response.appendResponseLine(monitorResult.result || '');
594
+ }
595
+ catch (error) {
596
+ const errorMessage = error instanceof Error ? error.message : String(error);
597
+ response.appendResponseLine(`❌ エラー: ${errorMessage}`);
598
+ throw error;
599
+ }
600
+ },
601
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
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": "./build/src/index.js",