- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
218 lines
7.4 KiB
TypeScript
218 lines
7.4 KiB
TypeScript
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;
|
|
}
|