UNPKG

@steipete/peekaboo-mcp

Version:

A macOS utility exposed via Node.js MCP server for advanced screen captures, image analysis, and window management

312 lines 11.8 kB
import OpenAI from "openai"; export function parseAIProviders(aiProvidersEnv) { if (!aiProvidersEnv || !aiProvidersEnv.trim()) { return []; } return aiProvidersEnv .split(/[,;]/) // Support both comma and semicolon separators .map((p) => p.trim()) .filter(Boolean) .map((provider) => { const [providerName, model] = provider.split("/"); return { provider: providerName?.trim() || "", model: model?.trim() || "", }; }) .filter((p) => p.provider && p.model); } export async function isProviderAvailable(provider, logger) { const status = await getProviderStatus(provider, logger); return status.available; } export async function getProviderStatus(provider, logger) { try { switch (provider.provider.toLowerCase()) { case "ollama": return await checkOllamaStatus(provider.model, logger); case "openai": return await checkOpenAIStatus(provider.model, logger); case "anthropic": return checkAnthropicStatus(provider.model); default: logger.warn({ provider: provider.provider }, "Unknown AI provider"); return { available: false, error: `Unknown provider: ${provider.provider}`, }; } } catch (error) { logger.error({ error, provider: provider.provider }, "Error checking provider status"); return { available: false, error: error instanceof Error ? error.message : "Unknown error", }; } } async function checkOllamaStatus(model, logger) { try { const baseUrl = process.env.PEEKABOO_OLLAMA_BASE_URL || "http://localhost:11434"; // Check if server is reachable const tagsResponse = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(3000), // 3 second timeout }); if (!tagsResponse.ok) { return { available: false, error: `Ollama server returned ${tagsResponse.status}`, details: { serverReachable: false, }, }; } const tagsData = await tagsResponse.json(); const availableModels = tagsData.models?.map((m) => m.name) || []; // Check if the specific model is available const modelAvailable = availableModels.some((m) => m === model || m.startsWith(model + ":") || model.startsWith(m.split(":")[0])); if (!modelAvailable) { return { available: false, error: `Model '${model}' not found. Available models: ${availableModels.join(", ") || "none"}`, details: { serverReachable: true, modelAvailable: false, modelList: availableModels, }, }; } return { available: true, details: { serverReachable: true, modelAvailable: true, modelList: availableModels, }, }; } catch (error) { logger.debug({ error }, "Ollama not available"); const errorMessage = error instanceof Error ? error.message : "Unknown error"; if (errorMessage.includes("fetch") || errorMessage.includes("timeout")) { return { available: false, error: "Ollama server not reachable (not running or network issue)", details: { serverReachable: false, }, }; } return { available: false, error: errorMessage, details: { serverReachable: false, }, }; } } async function checkOpenAIStatus(model, logger) { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { return { available: false, error: "OpenAI API key not configured (OPENAI_API_KEY environment variable missing)", details: { apiKeyPresent: false, }, }; } try { // Test the API key by making a simple models list request const openai = new OpenAI({ apiKey, timeout: 3000, // 3 second timeout }); const modelsResponse = await openai.models.list(); const availableModels = modelsResponse.data.map(m => m.id); // Check if the specific model is available const modelAvailable = availableModels.includes(model); if (!modelAvailable) { // For OpenAI, we'll be more lenient and just warn if model isn't in the list // since the models list API might not include all available models logger.debug({ model, availableCount: availableModels.length }, "Model not found in OpenAI models list, but this might be normal"); } return { available: true, details: { apiKeyPresent: true, serverReachable: true, modelAvailable: modelAvailable, modelList: availableModels.slice(0, 10), // Limit to first 10 models for brevity }, }; } catch (error) { logger.debug({ error }, "OpenAI API check failed"); const errorMessage = error instanceof Error ? error.message : "Unknown error"; if (errorMessage.includes("401") || errorMessage.includes("Unauthorized")) { return { available: false, error: "Invalid OpenAI API key", details: { apiKeyPresent: true, serverReachable: true, }, }; } if (errorMessage.includes("network") || errorMessage.includes("fetch")) { return { available: false, error: "Cannot reach OpenAI API (network issue)", details: { apiKeyPresent: true, serverReachable: false, }, }; } return { available: false, error: `OpenAI API error: ${errorMessage}`, details: { apiKeyPresent: true, serverReachable: false, }, }; } } function checkAnthropicStatus(_model) { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { return { available: false, error: "Anthropic API key not configured (ANTHROPIC_API_KEY environment variable missing)", details: { apiKeyPresent: false, }, }; } return { available: false, error: "Anthropic support not yet implemented", details: { apiKeyPresent: true, }, }; } export async function analyzeImageWithProvider(provider, imagePath, imageBase64, question, logger) { switch (provider.provider.toLowerCase()) { case "ollama": return await analyzeWithOllama(provider.model, imageBase64, question, logger); case "openai": return await analyzeWithOpenAI(provider.model, imageBase64, question, logger); case "anthropic": throw new Error("Anthropic support not yet implemented"); default: throw new Error(`Unsupported AI provider: ${provider.provider}`); } } async function analyzeWithOllama(model, imageBase64, question, logger) { const baseUrl = process.env.PEEKABOO_OLLAMA_BASE_URL || "http://localhost:11434"; logger.debug({ model, baseUrl }, "Analyzing image with Ollama"); // Default to describing the image if no question is provided const prompt = question.trim() || "Please describe what you see in this image."; const response = await fetch(`${baseUrl}/api/generate`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ model, prompt, images: [imageBase64], stream: false, }), }); if (!response.ok) { const errorText = await response.text(); logger.error({ status: response.status, error: errorText }, "Ollama API error"); throw new Error(`Ollama API error: ${response.status} - ${errorText}`); } const result = await response.json(); return result.response || "No response from Ollama"; } async function analyzeWithOpenAI(model, imageBase64, question, logger) { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OpenAI API key not configured"); } logger.debug({ model }, "Analyzing image with OpenAI"); const openai = new OpenAI({ apiKey }); // Default to describing the image if no question is provided const prompt = question.trim() || "Please describe what you see in this image."; const response = await openai.chat.completions.create({ model: model || "gpt-4o", messages: [ { role: "user", content: [ { type: "text", text: prompt }, { type: "image_url", image_url: { url: `data:image/jpeg;base64,${imageBase64}`, }, }, ], }, ], max_tokens: 1000, }); return response.choices[0]?.message?.content || "No response from OpenAI"; } export function getDefaultModelForProvider(provider) { switch (provider.toLowerCase()) { case "ollama": return "llava:latest"; case "openai": return "gpt-4o"; case "anthropic": return "claude-3-sonnet-20240229"; default: return "unknown"; } } export async function determineProviderAndModel(providerConfig, configuredProviders, logger) { const requestedProviderType = providerConfig?.type || "auto"; const requestedModelName = providerConfig?.model; if (requestedProviderType !== "auto") { // Find specific provider in configuration const configuredProvider = configuredProviders.find((p) => p.provider.toLowerCase() === requestedProviderType.toLowerCase()); if (!configuredProvider) { throw new Error(`Provider '${requestedProviderType}' is not enabled in server's PEEKABOO_AI_PROVIDERS configuration.`); } // Check if provider is available const available = await isProviderAvailable(configuredProvider, logger); if (!available) { throw new Error(`Provider '${requestedProviderType}' is configured but not currently available.`); } const model = requestedModelName || configuredProvider.model || getDefaultModelForProvider(requestedProviderType); return { provider: requestedProviderType, model, }; } // Auto mode - find first available provider for (const configuredProvider of configuredProviders) { const available = await isProviderAvailable(configuredProvider, logger); if (available) { const model = requestedModelName || configuredProvider.model || getDefaultModelForProvider(configuredProvider.provider); return { provider: configuredProvider.provider, model, }; } } return { provider: null, model: "" }; } //# sourceMappingURL=ai-providers.js.map