/** * IPC Handlers * Registers all IPC handlers for main-renderer communication */ import { ipcMain, BrowserWindow, shell, dialog, app } from 'electron'; import { GatewayManager } from '../gateway/manager'; import { ClawHubService, ClawHubSearchParams, ClawHubInstallParams, ClawHubUninstallParams } from '../gateway/clawhub'; import { storeApiKey, getApiKey, deleteApiKey, hasApiKey, saveProvider, getProvider, deleteProvider, setDefaultProvider, getDefaultProvider, getAllProvidersWithKeyInfo, isEncryptionAvailable, type ProviderConfig, } from '../utils/secure-storage'; import { getOpenClawStatus, getOpenClawDir } from '../utils/paths'; import { getSetting } from '../utils/store'; import { saveProviderKeyToOpenClaw, setOpenClawDefaultModel } from '../utils/openclaw-auth'; import { logger } from '../utils/logger'; import { saveChannelConfig, getChannelConfig, getChannelFormValues, deleteChannelConfig, listConfiguredChannels, setChannelEnabled, validateChannelConfig, validateChannelCredentials, } from '../utils/channel-config'; import { checkUvInstalled, installUv, setupManagedPython } from '../utils/uv-setup'; import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config'; /** * Register all IPC handlers */ export function registerIpcHandlers( gatewayManager: GatewayManager, clawHubService: ClawHubService, mainWindow: BrowserWindow ): void { // Gateway handlers registerGatewayHandlers(gatewayManager, mainWindow); // ClawHub handlers registerClawHubHandlers(clawHubService); // OpenClaw handlers registerOpenClawHandlers(); // Provider handlers registerProviderHandlers(); // Shell handlers registerShellHandlers(); // Dialog handlers registerDialogHandlers(); // App handlers registerAppHandlers(); // UV handlers registerUvHandlers(); // Log handlers (for UI to read gateway/app logs) registerLogHandlers(); // Skill config handlers (direct file access, no Gateway RPC) registerSkillConfigHandlers(); // Cron task handlers (proxy to Gateway RPC) registerCronHandlers(gatewayManager); // Window control handlers (for custom title bar on Windows/Linux) registerWindowHandlers(mainWindow); } /** * Skill config IPC handlers * Direct read/write to ~/.openclaw/openclaw.json (bypasses Gateway RPC) */ function registerSkillConfigHandlers(): void { // Update skill config (apiKey and env) ipcMain.handle('skill:updateConfig', async (_, params: { skillKey: string; apiKey?: string; env?: Record; }) => { return updateSkillConfig(params.skillKey, { apiKey: params.apiKey, env: params.env, }); }); // Get skill config ipcMain.handle('skill:getConfig', async (_, skillKey: string) => { return getSkillConfig(skillKey); }); // Get all skill configs ipcMain.handle('skill:getAllConfigs', async () => { return getAllSkillConfigs(); }); } /** * Gateway CronJob type (as returned by cron.list RPC) */ interface GatewayCronJob { id: string; name: string; description?: string; enabled: boolean; createdAtMs: number; updatedAtMs: number; schedule: { kind: string; expr?: string; everyMs?: number; at?: string; tz?: string }; payload: { kind: string; message?: string; text?: string }; delivery?: { mode: string; channel?: string; to?: string }; state: { nextRunAtMs?: number; lastRunAtMs?: number; lastStatus?: string; lastError?: string; lastDurationMs?: number; }; } /** * Transform a Gateway CronJob to the frontend CronJob format */ function transformCronJob(job: GatewayCronJob) { // Extract message from payload const message = job.payload?.message || job.payload?.text || ''; // Build target from delivery info const channelType = job.delivery?.channel || 'unknown'; const target = { channelType, channelId: channelType, channelName: channelType, }; // Build lastRun from state const lastRun = job.state?.lastRunAtMs ? { time: new Date(job.state.lastRunAtMs).toISOString(), success: job.state.lastStatus === 'ok', error: job.state.lastError, duration: job.state.lastDurationMs, } : undefined; // Build nextRun from state const nextRun = job.state?.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : undefined; return { id: job.id, name: job.name, message, schedule: job.schedule, // Pass the object through; frontend parseCronSchedule handles it target, enabled: job.enabled, createdAt: new Date(job.createdAtMs).toISOString(), updatedAt: new Date(job.updatedAtMs).toISOString(), lastRun, nextRun, }; } /** * Cron task IPC handlers * Proxies cron operations to the Gateway RPC service. * The frontend works with plain cron expression strings, but the Gateway * expects CronSchedule objects ({ kind: "cron", expr: "..." }). * These handlers bridge the two formats. */ function registerCronHandlers(gatewayManager: GatewayManager): void { // List all cron jobs — transforms Gateway CronJob format to frontend CronJob format ipcMain.handle('cron:list', async () => { try { const result = await gatewayManager.rpc('cron.list', { includeDisabled: true }); const data = result as { jobs?: GatewayCronJob[] }; const jobs = data?.jobs ?? []; // Transform Gateway format to frontend format return jobs.map(transformCronJob); } catch (error) { console.error('Failed to list cron jobs:', error); throw error; } }); // Create a new cron job ipcMain.handle('cron:create', async (_, input: { name: string; message: string; schedule: string; target: { channelType: string; channelId: string; channelName: string }; enabled?: boolean; }) => { try { // Transform frontend input to Gateway cron.add format const gatewayInput = { name: input.name, schedule: { kind: 'cron', expr: input.schedule }, payload: { kind: 'agentTurn', message: input.message }, enabled: input.enabled ?? true, wakeMode: 'next-heartbeat', sessionTarget: 'isolated', delivery: { mode: 'announce', channel: input.target.channelType, }, }; const result = await gatewayManager.rpc('cron.add', gatewayInput); // Transform the returned job to frontend format if (result && typeof result === 'object') { return transformCronJob(result as GatewayCronJob); } return result; } catch (error) { console.error('Failed to create cron job:', error); throw error; } }); // Update an existing cron job ipcMain.handle('cron:update', async (_, id: string, input: Record) => { try { // Transform schedule string to CronSchedule object if present const patch = { ...input }; if (typeof patch.schedule === 'string') { patch.schedule = { kind: 'cron', expr: patch.schedule }; } // Transform message to payload format if present if (typeof patch.message === 'string') { patch.payload = { kind: 'agentTurn', message: patch.message }; delete patch.message; } const result = await gatewayManager.rpc('cron.update', { id, patch }); return result; } catch (error) { console.error('Failed to update cron job:', error); throw error; } }); // Delete a cron job ipcMain.handle('cron:delete', async (_, id: string) => { try { const result = await gatewayManager.rpc('cron.remove', { id }); return result; } catch (error) { console.error('Failed to delete cron job:', error); throw error; } }); // Toggle a cron job enabled/disabled ipcMain.handle('cron:toggle', async (_, id: string, enabled: boolean) => { try { const result = await gatewayManager.rpc('cron.update', { id, patch: { enabled } }); return result; } catch (error) { console.error('Failed to toggle cron job:', error); throw error; } }); // Trigger a cron job manually ipcMain.handle('cron:trigger', async (_, id: string) => { try { const result = await gatewayManager.rpc('cron.run', { id, mode: 'force' }); return result; } catch (error) { console.error('Failed to trigger cron job:', error); throw error; } }); } /** * UV-related IPC handlers */ function registerUvHandlers(): void { // Check if uv is installed ipcMain.handle('uv:check', async () => { return await checkUvInstalled(); }); // Install uv and setup managed Python ipcMain.handle('uv:install-all', async () => { try { const isInstalled = await checkUvInstalled(); if (!isInstalled) { await installUv(); } // Always run python setup to ensure it exists in uv's cache await setupManagedPython(); return { success: true }; } catch (error) { console.error('Failed to setup uv/python:', error); return { success: false, error: String(error) }; } }); } /** * Log-related IPC handlers * Allows the renderer to read application logs for diagnostics */ function registerLogHandlers(): void { // Get recent logs from memory ring buffer ipcMain.handle('log:getRecent', async (_, count?: number) => { return logger.getRecentLogs(count); }); // Read log file content (last N lines) ipcMain.handle('log:readFile', async (_, tailLines?: number) => { return logger.readLogFile(tailLines); }); // Get log file path (so user can open in file explorer) ipcMain.handle('log:getFilePath', async () => { return logger.getLogFilePath(); }); // Get log directory path ipcMain.handle('log:getDir', async () => { return logger.getLogDir(); }); // List all log files ipcMain.handle('log:listFiles', async () => { return logger.listLogFiles(); }); } /** * Gateway-related IPC handlers */ function registerGatewayHandlers( gatewayManager: GatewayManager, mainWindow: BrowserWindow ): void { // Get Gateway status ipcMain.handle('gateway:status', () => { return gatewayManager.getStatus(); }); // Check if Gateway is connected ipcMain.handle('gateway:isConnected', () => { return gatewayManager.isConnected(); }); // Start Gateway ipcMain.handle('gateway:start', async () => { try { await gatewayManager.start(); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Stop Gateway ipcMain.handle('gateway:stop', async () => { try { await gatewayManager.stop(); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Restart Gateway ipcMain.handle('gateway:restart', async () => { try { await gatewayManager.restart(); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Gateway RPC call ipcMain.handle('gateway:rpc', async (_, method: string, params?: unknown, timeoutMs?: number) => { try { const result = await gatewayManager.rpc(method, params, timeoutMs); return { success: true, result }; } catch (error) { return { success: false, error: String(error) }; } }); // Get the Control UI URL with token for embedding ipcMain.handle('gateway:getControlUiUrl', async () => { try { const status = gatewayManager.getStatus(); const token = await getSetting('gatewayToken'); const port = status.port || 18789; // Pass token as query param - Control UI will store it in localStorage const url = `http://127.0.0.1:${port}/?token=${encodeURIComponent(token)}`; return { success: true, url, port, token }; } catch (error) { return { success: false, error: String(error) }; } }); // Health check ipcMain.handle('gateway:health', async () => { try { const health = await gatewayManager.checkHealth(); return { success: true, ...health }; } catch (error) { return { success: false, ok: false, error: String(error) }; } }); // Forward Gateway events to renderer gatewayManager.on('status', (status) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:status-changed', status); } }); gatewayManager.on('message', (message) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:message', message); } }); gatewayManager.on('notification', (notification) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:notification', notification); } }); gatewayManager.on('channel:status', (data) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:channel-status', data); } }); gatewayManager.on('chat:message', (data) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:chat-message', data); } }); gatewayManager.on('exit', (code) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:exit', code); } }); gatewayManager.on('error', (error) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:error', error.message); } }); } /** * OpenClaw-related IPC handlers * For checking package status and channel configuration */ function registerOpenClawHandlers(): void { // Get OpenClaw package status ipcMain.handle('openclaw:status', () => { const status = getOpenClawStatus(); logger.info('openclaw:status IPC called', status); return status; }); // Check if OpenClaw is ready (package present) ipcMain.handle('openclaw:isReady', () => { const status = getOpenClawStatus(); return status.packageExists; }); // Get the resolved OpenClaw directory path (for diagnostics) ipcMain.handle('openclaw:getDir', () => { return getOpenClawDir(); }); // ==================== Channel Configuration Handlers ==================== // Save channel configuration ipcMain.handle('channel:saveConfig', async (_, channelType: string, config: Record) => { try { saveChannelConfig(channelType, config); return { success: true }; } catch (error) { console.error('Failed to save channel config:', error); return { success: false, error: String(error) }; } }); // Get channel configuration ipcMain.handle('channel:getConfig', async (_, channelType: string) => { try { const config = getChannelConfig(channelType); return { success: true, config }; } catch (error) { console.error('Failed to get channel config:', error); return { success: false, error: String(error) }; } }); // Get channel form values (reverse-transformed for UI pre-fill) ipcMain.handle('channel:getFormValues', async (_, channelType: string) => { try { const values = getChannelFormValues(channelType); return { success: true, values }; } catch (error) { console.error('Failed to get channel form values:', error); return { success: false, error: String(error) }; } }); // Delete channel configuration ipcMain.handle('channel:deleteConfig', async (_, channelType: string) => { try { deleteChannelConfig(channelType); return { success: true }; } catch (error) { console.error('Failed to delete channel config:', error); return { success: false, error: String(error) }; } }); // List configured channels ipcMain.handle('channel:listConfigured', async () => { try { const channels = listConfiguredChannels(); return { success: true, channels }; } catch (error) { console.error('Failed to list channels:', error); return { success: false, error: String(error) }; } }); // Enable or disable a channel ipcMain.handle('channel:setEnabled', async (_, channelType: string, enabled: boolean) => { try { setChannelEnabled(channelType, enabled); return { success: true }; } catch (error) { console.error('Failed to set channel enabled:', error); return { success: false, error: String(error) }; } }); // Validate channel configuration ipcMain.handle('channel:validate', async (_, channelType: string) => { try { const result = await validateChannelConfig(channelType); return { success: true, ...result }; } catch (error) { console.error('Failed to validate channel:', error); return { success: false, valid: false, errors: [String(error)], warnings: [] }; } }); // Validate channel credentials by calling actual service APIs (before saving) ipcMain.handle('channel:validateCredentials', async (_, channelType: string, config: Record) => { try { const result = await validateChannelCredentials(channelType, config); return { success: true, ...result }; } catch (error) { console.error('Failed to validate channel credentials:', error); return { success: false, valid: false, errors: [String(error)], warnings: [] }; } }); } /** * Provider-related IPC handlers */ function registerProviderHandlers(): void { // Check if encryption is available ipcMain.handle('provider:encryptionAvailable', () => { return isEncryptionAvailable(); }); // Get all providers with key info ipcMain.handle('provider:list', async () => { return await getAllProvidersWithKeyInfo(); }); // Get a specific provider ipcMain.handle('provider:get', async (_, providerId: string) => { return await getProvider(providerId); }); // Save a provider configuration ipcMain.handle('provider:save', async (_, config: ProviderConfig, apiKey?: string) => { try { // Save the provider config await saveProvider(config); // Store the API key if provided if (apiKey) { await storeApiKey(config.id, apiKey); // Also write to OpenClaw auth-profiles.json so the gateway can use it try { saveProviderKeyToOpenClaw(config.type, apiKey); } catch (err) { console.warn('Failed to save key to OpenClaw auth-profiles:', err); } } // Set the default model in OpenClaw config based on provider type try { setOpenClawDefaultModel(config.type); } catch (err) { console.warn('Failed to set OpenClaw default model:', err); } return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Delete a provider ipcMain.handle('provider:delete', async (_, providerId: string) => { try { await deleteProvider(providerId); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Update API key for a provider ipcMain.handle('provider:setApiKey', async (_, providerId: string, apiKey: string) => { try { await storeApiKey(providerId, apiKey); // Also write to OpenClaw auth-profiles.json // Resolve provider type from stored config, or use providerId as type const provider = await getProvider(providerId); const providerType = provider?.type || providerId; try { saveProviderKeyToOpenClaw(providerType, apiKey); } catch (err) { console.warn('Failed to save key to OpenClaw auth-profiles:', err); } return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Delete API key for a provider ipcMain.handle('provider:deleteApiKey', async (_, providerId: string) => { try { await deleteApiKey(providerId); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Check if a provider has an API key ipcMain.handle('provider:hasApiKey', async (_, providerId: string) => { return await hasApiKey(providerId); }); // Get the actual API key (for internal use only - be careful!) ipcMain.handle('provider:getApiKey', async (_, providerId: string) => { return await getApiKey(providerId); }); // Set default provider ipcMain.handle('provider:setDefault', async (_, providerId: string) => { try { await setDefaultProvider(providerId); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Get default provider ipcMain.handle('provider:getDefault', async () => { return await getDefaultProvider(); }); // Validate API key by making a real test request to the provider // providerId can be either a stored provider ID or a provider type (e.g., 'openrouter', 'anthropic') ipcMain.handle('provider:validateKey', async (_, providerId: string, apiKey: string) => { try { // First try to get existing provider const provider = await getProvider(providerId); // Use provider.type if provider exists, otherwise use providerId as the type // This allows validation during setup when provider hasn't been saved yet const providerType = provider?.type || providerId; console.log(`Validating API key for provider type: ${providerType}`); return await validateApiKeyWithProvider(providerType, apiKey); } catch (error) { console.error('Validation error:', error); return { valid: false, error: String(error) }; } }); } /** * Validate API key by making a real chat completion API call to the provider * This sends a minimal "hi" message to verify the key works */ async function validateApiKeyWithProvider( providerType: string, apiKey: string ): Promise<{ valid: boolean; error?: string }> { const trimmedKey = apiKey.trim(); if (!trimmedKey) { return { valid: false, error: 'API key is required' }; } try { switch (providerType) { case 'anthropic': return await validateAnthropicKey(trimmedKey); case 'openai': return await validateOpenAIKey(trimmedKey); case 'google': return await validateGoogleKey(trimmedKey); case 'openrouter': return await validateOpenRouterKey(trimmedKey); case 'ollama': // Ollama doesn't require API key validation return { valid: true }; default: // For custom providers, just check the key is not empty return { valid: true }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { valid: false, error: errorMessage }; } } /** * Parse error message from API response */ function parseApiError(data: unknown): string { if (!data || typeof data !== 'object') return 'Unknown error'; // Anthropic format: { error: { message: "..." } } // OpenAI format: { error: { message: "..." } } // Google format: { error: { message: "..." } } const obj = data as { error?: { message?: string; type?: string }; message?: string }; if (obj.error?.message) return obj.error.message; if (obj.message) return obj.message; return 'Unknown error'; } /** * Validate Anthropic API key by making a minimal chat completion request */ async function validateAnthropicKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: 'claude-3-haiku-20240307', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }], }), }); const data = await response.json().catch(() => ({})); if (response.ok) { return { valid: true }; } // Authentication error if (response.status === 401) { return { valid: false, error: 'Invalid API key' }; } // Permission error (invalid key format, etc.) if (response.status === 403) { return { valid: false, error: parseApiError(data) }; } // Rate limit or overloaded - key is valid but service is busy if (response.status === 429 || response.status === 529) { return { valid: true }; } // Model not found or bad request but auth passed - key is valid if (response.status === 400 || response.status === 404) { const errorType = (data as { error?: { type?: string } })?.error?.type; if (errorType === 'authentication_error' || errorType === 'invalid_api_key') { return { valid: false, error: 'Invalid API key' }; } // Other errors like invalid_request_error mean the key is valid return { valid: true }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Validate OpenAI API key by making a minimal chat completion request */ async function validateOpenAIKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model: 'gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }], }), }); const data = await response.json().catch(() => ({})); if (response.ok) { return { valid: true }; } // Authentication error if (response.status === 401) { return { valid: false, error: 'Invalid API key' }; } // Rate limit - key is valid if (response.status === 429) { return { valid: true }; } // Model not found or bad request but auth passed - key is valid if (response.status === 400 || response.status === 404) { const errorCode = (data as { error?: { code?: string } })?.error?.code; if (errorCode === 'invalid_api_key') { return { valid: false, error: 'Invalid API key' }; } return { valid: true }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Validate Google (Gemini) API key by making a minimal generate content request */ async function validateGoogleKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ contents: [{ parts: [{ text: 'hi' }] }], generationConfig: { maxOutputTokens: 1 }, }), } ); const data = await response.json().catch(() => ({})); if (response.ok) { return { valid: true }; } // Authentication error if (response.status === 400 || response.status === 401 || response.status === 403) { const errorStatus = (data as { error?: { status?: string } })?.error?.status; if (errorStatus === 'UNAUTHENTICATED' || errorStatus === 'PERMISSION_DENIED') { return { valid: false, error: 'Invalid API key' }; } // Check if it's actually an auth error const errorMessage = parseApiError(data).toLowerCase(); if (errorMessage.includes('api key') || errorMessage.includes('invalid') || errorMessage.includes('unauthorized')) { return { valid: false, error: parseApiError(data) }; } // Other errors mean key is valid return { valid: true }; } // Rate limit - key is valid if (response.status === 429) { return { valid: true }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Validate OpenRouter API key by making a minimal chat completion request */ async function validateOpenRouterKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { // Use a popular free model for validation const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'HTTP-Referer': 'https://clawx.app', 'X-Title': 'ClawX', }, body: JSON.stringify({ model: 'meta-llama/llama-3.2-3b-instruct:free', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }], }), }); const data = await response.json().catch(() => ({})); console.log('OpenRouter validation response:', response.status, JSON.stringify(data)); // Helper to check if error message indicates auth failure const isAuthError = (d: unknown): boolean => { const errorObj = (d as { error?: { message?: string; code?: number | string; type?: string } })?.error; if (!errorObj) return false; const message = (errorObj.message || '').toLowerCase(); const code = errorObj.code; const type = (errorObj.type || '').toLowerCase(); // Check for explicit auth-related errors if (code === 401 || code === '401' || code === 403 || code === '403') return true; if (type.includes('auth') || type.includes('invalid')) return true; if (message.includes('invalid api key') || message.includes('invalid key') || message.includes('unauthorized') || message.includes('authentication') || message.includes('invalid credentials') || message.includes('api key is not valid')) { return true; } return false; }; if (response.ok) { return { valid: true }; } // Always check for auth errors in the response body first if (isAuthError(data)) { // Return user-friendly message instead of raw API errors like "User not found." return { valid: false, error: 'Invalid API key' }; } // Authentication error status codes - always return user-friendly message if (response.status === 401 || response.status === 403) { return { valid: false, error: 'Invalid API key' }; } // Rate limit - key is valid if (response.status === 429) { return { valid: true }; } // Payment required or insufficient credits - key format is valid if (response.status === 402) { return { valid: true }; } // For 400/404, we must be very careful - only consider valid if clearly not an auth issue if (response.status === 400 || response.status === 404) { // If we got here without detecting auth error, it might be a model issue // But be conservative - require explicit success indication const errorObj = (data as { error?: { message?: string; code?: number } })?.error; const message = (errorObj?.message || '').toLowerCase(); // Only consider valid if the error is clearly about the model, not the key if (message.includes('model') && !message.includes('key') && !message.includes('auth')) { return { valid: true }; } // Default to invalid for ambiguous 400/404 errors return { valid: false, error: parseApiError(data) || 'Invalid API key or request' }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Shell-related IPC handlers */ function registerShellHandlers(): void { // Open external URL ipcMain.handle('shell:openExternal', async (_, url: string) => { await shell.openExternal(url); }); // Open path in file explorer ipcMain.handle('shell:showItemInFolder', async (_, path: string) => { shell.showItemInFolder(path); }); // Open path ipcMain.handle('shell:openPath', async (_, path: string) => { return await shell.openPath(path); }); } /** * ClawHub-related IPC handlers */ function registerClawHubHandlers(clawHubService: ClawHubService): void { // Search skills ipcMain.handle('clawhub:search', async (_, params: ClawHubSearchParams) => { try { const results = await clawHubService.search(params); return { success: true, results }; } catch (error) { return { success: false, error: String(error) }; } }); // Install skill ipcMain.handle('clawhub:install', async (_, params: ClawHubInstallParams) => { try { await clawHubService.install(params); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Uninstall skill ipcMain.handle('clawhub:uninstall', async (_, params: ClawHubUninstallParams) => { try { await clawHubService.uninstall(params); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // List installed skills ipcMain.handle('clawhub:list', async () => { try { const results = await clawHubService.listInstalled(); return { success: true, results }; } catch (error) { return { success: false, error: String(error) }; } }); // Open skill readme ipcMain.handle('clawhub:openSkillReadme', async (_, slug: string) => { try { await clawHubService.openSkillReadme(slug); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); } /** * Dialog-related IPC handlers */ function registerDialogHandlers(): void { // Show open dialog ipcMain.handle('dialog:open', async (_, options: Electron.OpenDialogOptions) => { const result = await dialog.showOpenDialog(options); return result; }); // Show save dialog ipcMain.handle('dialog:save', async (_, options: Electron.SaveDialogOptions) => { const result = await dialog.showSaveDialog(options); return result; }); // Show message box ipcMain.handle('dialog:message', async (_, options: Electron.MessageBoxOptions) => { const result = await dialog.showMessageBox(options); return result; }); } /** * App-related IPC handlers */ function registerAppHandlers(): void { // Get app version ipcMain.handle('app:version', () => { return app.getVersion(); }); // Get app name ipcMain.handle('app:name', () => { return app.getName(); }); // Get app path ipcMain.handle('app:getPath', (_, name: Parameters[0]) => { return app.getPath(name); }); // Get platform ipcMain.handle('app:platform', () => { return process.platform; }); // Quit app ipcMain.handle('app:quit', () => { app.quit(); }); // Relaunch app ipcMain.handle('app:relaunch', () => { app.relaunch(); app.quit(); }); } /** * Window control handlers (for custom title bar on Windows/Linux) */ function registerWindowHandlers(mainWindow: BrowserWindow): void { ipcMain.handle('window:minimize', () => { mainWindow.minimize(); }); ipcMain.handle('window:maximize', () => { if (mainWindow.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow.maximize(); } }); ipcMain.handle('window:close', () => { mainWindow.close(); }); ipcMain.handle('window:isMaximized', () => { return mainWindow.isMaximized(); }); }