import { Stagehand } from '@browserbasehq/stagehand'; import { existsSync, cpSync, mkdirSync, readFileSync } from 'fs'; import { platform } from 'os'; import { join } from 'path'; import { execSync } from 'child_process'; // Retrieve Claude Code API key from system keychain export function getClaudeCodeApiKey(): string | null { try { if (platform() === 'darwin') { const result = execSync( 'security find-generic-password -s "Claude Code" -w 2>/dev/null', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } ).trim(); if (result && result.startsWith('sk-ant-')) { return result; } } else if (platform() === 'win32') { try { const psCommand = `$cred = Get-StoredCredential -Target "Claude Code" -ErrorAction SilentlyContinue; if ($cred) { $cred.GetNetworkCredential().Password }`; const result = execSync(`powershell -Command "${psCommand}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); if (result && result.startsWith('sk-ant-')) { return result; } } catch {} } else { // Linux const configPaths = [ join(process.env.HOME || '', '.claude', 'credentials'), join(process.env.HOME || '', '.config', 'claude-code', 'credentials'), join(process.env.XDG_CONFIG_HOME || join(process.env.HOME || '', '.config'), 'claude-code', 'credentials'), ]; for (const configPath of configPaths) { if (existsSync(configPath)) { try { const content = readFileSync(configPath, 'utf-8').trim(); if (content.startsWith('sk-ant-')) { return content; } const parsed = JSON.parse(content); if (parsed.apiKey && parsed.apiKey.startsWith('sk-ant-')) { return parsed.apiKey; } } catch {} } } try { const result = execSync( 'secret-tool lookup service "Claude Code" 2>/dev/null', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } ).trim(); if (result && result.startsWith('sk-ant-')) { return result; } } catch {} } } catch {} return null; } // Get API key from env or Claude Code keychain export function getAnthropicApiKey(): { apiKey: string; source: 'env' | 'claude-code' } | null { if (process.env.ANTHROPIC_API_KEY) { return { apiKey: process.env.ANTHROPIC_API_KEY, source: 'env' }; } const claudeCodeKey = getClaudeCodeApiKey(); if (claudeCodeKey) { return { apiKey: claudeCodeKey, source: 'claude-code' }; } return null; } /** * Finds the local Chrome installation path based on the operating system * @returns The path to the Chrome executable, or undefined if not found */ export function findLocalChrome(): string | undefined { const systemPlatform = platform(); const chromePaths: string[] = []; if (systemPlatform === 'darwin') { // macOS paths chromePaths.push( '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', `${process.env.HOME}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, `${process.env.HOME}/Applications/Chromium.app/Contents/MacOS/Chromium` ); } else if (systemPlatform === 'win32') { // Windows paths chromePaths.push( 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', `${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`, `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`, `${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`, 'C:\\Program Files\\Chromium\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Chromium\\Application\\chrome.exe' ); } else { // Linux paths chromePaths.push( '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium', '/usr/local/bin/google-chrome', '/usr/local/bin/chromium', '/opt/google/chrome/chrome', '/opt/google/chrome/google-chrome' ); } // Find the first existing Chrome installation for (const path of chromePaths) { if (path && existsSync(path)) { return path; } } return undefined; } /** * Gets the Chrome user data directory path based on the operating system * @returns The path to Chrome's user data directory, or undefined if not found */ export function getChromeUserDataDir(): string | undefined { const systemPlatform = platform(); if (systemPlatform === 'darwin') { return `${process.env.HOME}/Library/Application Support/Google/Chrome`; } else if (systemPlatform === 'win32') { return `${process.env.LOCALAPPDATA}\\Google\\Chrome\\User Data`; } else { // Linux return `${process.env.HOME}/.config/google-chrome`; } } /** * Prepares the Chrome profile by copying it to .chrome-profile directory (first run only) * This should be called before initializing Stagehand to avoid timeouts * @param pluginRoot The root directory of the plugin */ export function prepareChromeProfile(pluginRoot: string) { const sourceUserDataDir = getChromeUserDataDir(); const tempUserDataDir = join(pluginRoot, '.chrome-profile'); // Only copy if the temp directory doesn't exist yet if (!existsSync(tempUserDataDir)) { const dim = '\x1b[2m'; const reset = '\x1b[0m'; // Show copying message console.log(`${dim}Copying Chrome profile to .chrome-profile/ (this may take a minute)...${reset}`); mkdirSync(tempUserDataDir, { recursive: true }); // Copy the Default profile directory (contains cookies, local storage, etc.) const sourceDefaultProfile = join(sourceUserDataDir!, 'Default'); const destDefaultProfile = join(tempUserDataDir, 'Default'); if (existsSync(sourceDefaultProfile)) { cpSync(sourceDefaultProfile, destDefaultProfile, { recursive: true }); console.log(`${dim}✓ Profile copied successfully${reset}\n`); } else { console.log(`${dim}No existing profile found, using fresh profile${reset}\n`); } } } // Use CDP to take screenshot directly export async function takeScreenshot(stagehand: Stagehand, pluginRoot: string) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const screenshotDir = join(pluginRoot, 'agent/browser_screenshots'); const screenshotPath = join(screenshotDir, `screenshot-${timestamp}.png`); // Create directory if it doesn't exist if (!existsSync(screenshotDir)) { mkdirSync(screenshotDir, { recursive: true }); } const page = stagehand.context.pages()[0]; const screenshotResult = await page.screenshot({ type: 'png', }); // Save the base64 screenshot data to file with resizing if needed const fs = await import('fs'); const sharp = (await import('sharp')).default; // Check image dimensions const image = sharp(screenshotResult); const metadata = await image.metadata(); const { width, height } = metadata; let finalBuffer: Buffer = screenshotResult; // Only resize if image exceeds 2000x2000 if (width && height && (width > 2000 || height > 2000)) { finalBuffer = await sharp(screenshotResult) .resize(2000, 2000, { fit: 'inside', withoutEnlargement: true }) .png() .toBuffer(); } fs.writeFileSync(screenshotPath, finalBuffer); return screenshotPath; }