chrome-ai-bridge 1.0.6 → 1.0.7

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.
@@ -86,24 +86,53 @@ async function cropWatermark(inputPath, outputPath, margin = DEFAULT_CROP_MARGIN
86
86
  }
87
87
  /**
88
88
  * Wait for download to complete and return the file path
89
+ * Looks for new image files (png, jpg, jpeg) in the download directory
89
90
  */
90
91
  async function waitForDownload(downloadDir, timeoutMs = 60000) {
91
92
  const startTime = Date.now();
92
- const checkInterval = 500;
93
- // Get initial files
94
- const initialFiles = new Set(await fs.promises.readdir(downloadDir));
93
+ const checkInterval = 1000; // Check every second
94
+ // Get initial files with their mtimes
95
+ const initialFiles = new Map();
96
+ try {
97
+ const files = await fs.promises.readdir(downloadDir);
98
+ for (const f of files) {
99
+ if (/\.(png|jpg|jpeg)$/i.test(f)) {
100
+ const stat = await fs.promises.stat(path.join(downloadDir, f));
101
+ initialFiles.set(f, stat.mtime.getTime());
102
+ }
103
+ }
104
+ }
105
+ catch {
106
+ // Directory might not exist, continue
107
+ }
95
108
  while (Date.now() - startTime < timeoutMs) {
96
109
  await new Promise(resolve => setTimeout(resolve, checkInterval));
97
- const currentFiles = await fs.promises.readdir(downloadDir);
98
- const newFiles = currentFiles.filter(f => !initialFiles.has(f) &&
99
- !f.endsWith('.crdownload') &&
100
- !f.endsWith('.tmp'));
101
- if (newFiles.length > 0) {
102
- // Return the newest file (by modification time)
103
- const filePaths = newFiles.map(f => path.join(downloadDir, f));
104
- const stats = await Promise.all(filePaths.map(async (p) => ({ path: p, mtime: (await fs.promises.stat(p)).mtime })));
105
- stats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
106
- return stats[0].path;
110
+ try {
111
+ const currentFiles = await fs.promises.readdir(downloadDir);
112
+ for (const f of currentFiles) {
113
+ // Only check image files
114
+ if (!/\.(png|jpg|jpeg)$/i.test(f))
115
+ continue;
116
+ // Skip incomplete downloads
117
+ if (f.endsWith('.crdownload') || f.endsWith('.tmp'))
118
+ continue;
119
+ const filePath = path.join(downloadDir, f);
120
+ const stat = await fs.promises.stat(filePath);
121
+ const mtime = stat.mtime.getTime();
122
+ // Check if this is a new file or modified after we started
123
+ const initialMtime = initialFiles.get(f);
124
+ if (!initialMtime || mtime > initialMtime) {
125
+ // Verify file is complete (size > 0 and not growing)
126
+ await new Promise(resolve => setTimeout(resolve, 500));
127
+ const stat2 = await fs.promises.stat(filePath);
128
+ if (stat2.size > 0 && stat2.size === stat.size) {
129
+ return filePath;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ catch {
135
+ // Continue on error
107
136
  }
108
137
  }
109
138
  throw new Error(`Download timeout after ${timeoutMs}ms`);
@@ -140,15 +169,6 @@ export const askGeminiImage = defineTool({
140
169
  handler: async (request, response, context) => {
141
170
  const { prompt, outputPath, cropMargin = DEFAULT_CROP_MARGIN, skipCrop = false, } = request.params;
142
171
  const page = await getOrCreateGeminiPage(context);
143
- // Set up download directory
144
- const downloadDir = path.join(os.tmpdir(), 'gemini-image-downloads');
145
- await fs.promises.mkdir(downloadDir, { recursive: true });
146
- // Configure download behavior
147
- const client = await page.createCDPSession();
148
- await client.send('Page.setDownloadBehavior', {
149
- behavior: 'allow',
150
- downloadPath: downloadDir,
151
- });
152
172
  try {
153
173
  response.appendResponseLine('Geminiに接続中...');
154
174
  // Navigate to Gemini
@@ -262,49 +282,47 @@ export const askGeminiImage = defineTool({
262
282
  }
263
283
  // Try to download the image
264
284
  response.appendResponseLine('📥 画像をダウンロード中...');
265
- // Method 1: Click download button
285
+ // Click download button - Gemini uses "フルサイズの画像をダウンロード" button
266
286
  const downloadClicked = await page.evaluate(() => {
267
- // Look for three-dot menu or download button on the image
268
- const buttons = Array.from(document.querySelectorAll('button, [role="menuitem"]'));
269
- // First try direct download button
270
- let downloadBtn = buttons.find(b => b.textContent?.includes('ダウンロード') ||
271
- b.textContent?.includes('Download'));
287
+ const buttons = Array.from(document.querySelectorAll('button'));
288
+ // Look for "フルサイズの画像をダウンロード" or "フルサイズでダウンロード" button
289
+ const downloadBtn = buttons.find(b => {
290
+ const text = b.textContent || '';
291
+ const ariaLabel = b.getAttribute('aria-label') || '';
292
+ const description = b.getAttribute('aria-describedby')
293
+ ? document.getElementById(b.getAttribute('aria-describedby'))?.textContent || ''
294
+ : '';
295
+ return (text.includes('フルサイズ') ||
296
+ text.includes('ダウンロード') ||
297
+ ariaLabel.includes('ダウンロード') ||
298
+ ariaLabel.includes('download') ||
299
+ description.includes('フルサイズ') ||
300
+ description.includes('ダウンロード'));
301
+ });
272
302
  if (downloadBtn) {
273
303
  downloadBtn.click();
274
- return 'direct';
275
- }
276
- // Try three-dot menu on generated images
277
- const moreButtons = buttons.filter(b => b.getAttribute('aria-label')?.includes('more') ||
278
- b.getAttribute('aria-label')?.includes('その他') ||
279
- b.querySelector('mat-icon[data-mat-icon-name="more_vert"]'));
280
- if (moreButtons.length > 0) {
281
- // Click the last one (likely the image menu)
282
- moreButtons[moreButtons.length - 1].click();
283
- return 'menu-opened';
304
+ return true;
284
305
  }
285
- return null;
306
+ return false;
286
307
  });
287
- if (downloadClicked === 'menu-opened') {
288
- // Wait for menu to open and click download
289
- await new Promise(resolve => setTimeout(resolve, 500));
290
- await page.evaluate(() => {
291
- const menuItems = Array.from(document.querySelectorAll('[role="menuitem"], button'));
292
- const downloadItem = menuItems.find(item => item.textContent?.includes('ダウンロード') ||
293
- item.textContent?.includes('Download'));
294
- if (downloadItem) {
295
- downloadItem.click();
296
- }
297
- });
308
+ if (!downloadClicked) {
309
+ response.appendResponseLine('⚠️ ダウンロードボタンが見つかりません');
310
+ response.appendResponseLine('ヒント: ブラウザで画像を右クリックして保存してください');
311
+ return;
298
312
  }
299
- // Wait for download to complete
313
+ // Wait for download to start (Gemini shows progress bar)
314
+ response.appendResponseLine('⏳ ダウンロード処理を待機中...');
315
+ await new Promise(resolve => setTimeout(resolve, 3000));
316
+ // Wait for download to complete - check user's Downloads folder
317
+ const userDownloadsDir = path.join(os.homedir(), 'Downloads');
300
318
  let downloadedPath;
301
319
  try {
302
- downloadedPath = await waitForDownload(downloadDir, 30000);
320
+ downloadedPath = await waitForDownload(userDownloadsDir, 60000); // 60 seconds
303
321
  response.appendResponseLine(`✅ ダウンロード完了: ${path.basename(downloadedPath)}`);
304
322
  }
305
323
  catch (error) {
306
- response.appendResponseLine('❌ ダウンロード待機タイムアウト');
307
- response.appendResponseLine('ヒント: ブラウザで手動でダウンロードしてください');
324
+ response.appendResponseLine('❌ ダウンロード待機タイムアウト (60秒)');
325
+ response.appendResponseLine('ヒント: ブラウザで画像を右クリックして「画像を保存」してください');
308
326
  return;
309
327
  }
310
328
  // Ensure output directory exists
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "MCP server bridging Chrome browser and AI assistants (ChatGPT, Gemini). Browser automation + AI consultation.",
5
5
  "type": "module",
6
6
  "bin": "./scripts/cli.mjs",