diff --git a/build_process/process.md b/build_process/process.md index e60e4d9d5..1d6f9d343 100644 --- a/build_process/process.md +++ b/build_process/process.md @@ -8,11 +8,12 @@ ### Completed: * [commit_1] Project skeleton - Electron + React + TypeScript foundation (v0.1.0-alpha) * [commit_2] Gateway refinements - Auto-reconnection, health checks, better state management +* [commit_3] Setup wizard - Multi-step onboarding flow with provider, channel, skill selection ### Plan: 1. ~~Initialize project structure~~ ✅ 2. ~~Add Gateway process management refinements~~ ✅ -3. Implement Setup wizard with actual functionality +3. ~~Implement Setup wizard with actual functionality~~ ✅ 4. Add Provider configuration (API Key management) 5. Implement Channel connection flows 6. Add auto-update functionality diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 8913e0d4d..9d0df4509 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -4,6 +4,21 @@ */ import { ipcMain, BrowserWindow, shell, dialog, app } from 'electron'; import { GatewayManager } from '../gateway/manager'; +import { + storeApiKey, + getApiKey, + deleteApiKey, + hasApiKey, + saveProvider, + getProvider, + getAllProviders, + deleteProvider, + setDefaultProvider, + getDefaultProvider, + getAllProvidersWithKeyInfo, + isEncryptionAvailable, + type ProviderConfig, +} from '../utils/secure-storage'; /** * Register all IPC handlers @@ -15,6 +30,9 @@ export function registerIpcHandlers( // Gateway handlers registerGatewayHandlers(gatewayManager, mainWindow); + // Provider handlers + registerProviderHandlers(); + // Shell handlers registerShellHandlers(); @@ -136,6 +154,136 @@ function registerGatewayHandlers( }); } +/** + * 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); + } + + 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); + 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 test request (simulated for now) + ipcMain.handle('provider:validateKey', async (_, providerId: string, apiKey: string) => { + // In a real implementation, this would make a test API call to the provider + // For now, we'll just do basic format validation + try { + // Basic validation based on provider type + const provider = await getProvider(providerId); + if (!provider) { + return { valid: false, error: 'Provider not found' }; + } + + switch (provider.type) { + case 'anthropic': + if (!apiKey.startsWith('sk-ant-')) { + return { valid: false, error: 'Anthropic keys should start with sk-ant-' }; + } + break; + case 'openai': + if (!apiKey.startsWith('sk-')) { + return { valid: false, error: 'OpenAI keys should start with sk-' }; + } + break; + case 'google': + if (apiKey.length < 20) { + return { valid: false, error: 'Google API key seems too short' }; + } + break; + } + + // Simulate API validation delay + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { valid: true }; + } catch (error) { + return { valid: false, error: String(error) }; + } + }); +} + /** * Shell-related IPC handlers */ diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 7db323bc3..81c7dc577 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -52,6 +52,17 @@ const electronAPI = { 'env:setApiKey', 'env:deleteApiKey', // Provider + 'provider:encryptionAvailable', + 'provider:list', + 'provider:get', + 'provider:save', + 'provider:delete', + 'provider:setApiKey', + 'provider:deleteApiKey', + 'provider:hasApiKey', + 'provider:getApiKey', + 'provider:setDefault', + 'provider:getDefault', 'provider:validateKey', // Cron 'cron:list', diff --git a/electron/utils/secure-storage.ts b/electron/utils/secure-storage.ts new file mode 100644 index 000000000..fbb861d30 --- /dev/null +++ b/electron/utils/secure-storage.ts @@ -0,0 +1,273 @@ +/** + * Secure Storage Utility + * Uses Electron's safeStorage for encrypting sensitive data like API keys + */ +import { safeStorage } from 'electron'; + +// Lazy-load electron-store (ESM module) +let store: any = null; +let providerStore: any = null; + +async function getStore() { + if (!store) { + const Store = (await import('electron-store')).default; + store = new Store({ + name: 'clawx-secure', + defaults: { + encryptedKeys: {}, + }, + }); + } + return store; +} + +async function getProviderStore() { + if (!providerStore) { + const Store = (await import('electron-store')).default; + providerStore = new Store({ + name: 'clawx-providers', + defaults: { + providers: {}, + }, + }); + } + return providerStore; +} + +/** + * Provider configuration + */ +export interface ProviderConfig { + id: string; + name: string; + type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom'; + baseUrl?: string; + model?: string; + enabled: boolean; + createdAt: string; + updatedAt: string; +} + +/** + * Check if encryption is available + */ +export function isEncryptionAvailable(): boolean { + return safeStorage.isEncryptionAvailable(); +} + +/** + * Store an API key securely + */ +export async function storeApiKey(providerId: string, apiKey: string): Promise { + try { + const s = await getStore(); + + if (!safeStorage.isEncryptionAvailable()) { + console.warn('Encryption not available, storing key in plain text'); + // Fallback to plain storage (not recommended for production) + const keys = s.get('encryptedKeys') as Record; + keys[providerId] = Buffer.from(apiKey).toString('base64'); + s.set('encryptedKeys', keys); + return true; + } + + // Encrypt the API key + const encrypted = safeStorage.encryptString(apiKey); + const keys = s.get('encryptedKeys') as Record; + keys[providerId] = encrypted.toString('base64'); + s.set('encryptedKeys', keys); + + return true; + } catch (error) { + console.error('Failed to store API key:', error); + return false; + } +} + +/** + * Retrieve an API key + */ +export async function getApiKey(providerId: string): Promise { + try { + const s = await getStore(); + const keys = s.get('encryptedKeys') as Record; + const encryptedBase64 = keys[providerId]; + + if (!encryptedBase64) { + return null; + } + + if (!safeStorage.isEncryptionAvailable()) { + // Fallback for plain storage + return Buffer.from(encryptedBase64, 'base64').toString('utf-8'); + } + + // Decrypt the API key + const encrypted = Buffer.from(encryptedBase64, 'base64'); + return safeStorage.decryptString(encrypted); + } catch (error) { + console.error('Failed to retrieve API key:', error); + return null; + } +} + +/** + * Delete an API key + */ +export async function deleteApiKey(providerId: string): Promise { + try { + const s = await getStore(); + const keys = s.get('encryptedKeys') as Record; + delete keys[providerId]; + s.set('encryptedKeys', keys); + return true; + } catch (error) { + console.error('Failed to delete API key:', error); + return false; + } +} + +/** + * Check if an API key exists for a provider + */ +export async function hasApiKey(providerId: string): Promise { + const s = await getStore(); + const keys = s.get('encryptedKeys') as Record; + return providerId in keys; +} + +/** + * List all provider IDs that have stored keys + */ +export async function listStoredKeyIds(): Promise { + const s = await getStore(); + const keys = s.get('encryptedKeys') as Record; + return Object.keys(keys); +} + +// ==================== Provider Configuration ==================== + +/** + * Save a provider configuration + */ +export async function saveProvider(config: ProviderConfig): Promise { + const s = await getProviderStore(); + const providers = s.get('providers') as Record; + providers[config.id] = config; + s.set('providers', providers); +} + +/** + * Get a provider configuration + */ +export async function getProvider(providerId: string): Promise { + const s = await getProviderStore(); + const providers = s.get('providers') as Record; + return providers[providerId] || null; +} + +/** + * Get all provider configurations + */ +export async function getAllProviders(): Promise { + const s = await getProviderStore(); + const providers = s.get('providers') as Record; + return Object.values(providers); +} + +/** + * Delete a provider configuration + */ +export async function deleteProvider(providerId: string): Promise { + try { + // Delete the API key first + await deleteApiKey(providerId); + + // Delete the provider config + const s = await getProviderStore(); + const providers = s.get('providers') as Record; + delete providers[providerId]; + s.set('providers', providers); + + // Clear default if this was the default + if (s.get('defaultProvider') === providerId) { + s.delete('defaultProvider'); + } + + return true; + } catch (error) { + console.error('Failed to delete provider:', error); + return false; + } +} + +/** + * Set the default provider + */ +export async function setDefaultProvider(providerId: string): Promise { + const s = await getProviderStore(); + s.set('defaultProvider', providerId); +} + +/** + * Get the default provider + */ +export async function getDefaultProvider(): Promise { + const s = await getProviderStore(); + return s.get('defaultProvider') as string | undefined; +} + +/** + * Get provider with masked key info (for UI display) + */ +export async function getProviderWithKeyInfo(providerId: string): Promise<(ProviderConfig & { hasKey: boolean; keyMasked: string | null }) | null> { + const provider = await getProvider(providerId); + if (!provider) return null; + + const apiKey = await getApiKey(providerId); + let keyMasked: string | null = null; + + if (apiKey) { + // Show first 4 and last 4 characters + if (apiKey.length > 12) { + keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`; + } else { + keyMasked = '*'.repeat(apiKey.length); + } + } + + return { + ...provider, + hasKey: !!apiKey, + keyMasked, + }; +} + +/** + * Get all providers with key info (for UI display) + */ +export async function getAllProvidersWithKeyInfo(): Promise> { + const providers = await getAllProviders(); + const results: Array = []; + + for (const provider of providers) { + const apiKey = await getApiKey(provider.id); + let keyMasked: string | null = null; + + if (apiKey) { + if (apiKey.length > 12) { + keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`; + } else { + keyMasked = '*'.repeat(apiKey.length); + } + } + + results.push({ + ...provider, + hasKey: !!apiKey, + keyMasked, + }); + } + + return results; +} diff --git a/src/components/settings/ProvidersSettings.tsx b/src/components/settings/ProvidersSettings.tsx new file mode 100644 index 000000000..f452fc32d --- /dev/null +++ b/src/components/settings/ProvidersSettings.tsx @@ -0,0 +1,441 @@ +/** + * Providers Settings Component + * Manage AI provider configurations and API keys + */ +import { useState, useEffect } from 'react'; +import { + Plus, + Trash2, + Edit, + Eye, + EyeOff, + Check, + X, + Loader2, + Star, + Key, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { Switch } from '@/components/ui/switch'; +import { useProviderStore, type ProviderWithKeyInfo } from '@/stores/providers'; +import { cn } from '@/lib/utils'; +import { toast } from 'sonner'; + +// Provider type definitions +const providerTypes = [ + { id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...' }, + { id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...' }, + { id: 'google', name: 'Google', icon: '🔷', placeholder: 'AIza...' }, + { id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required' }, + { id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...' }, +]; + +export function ProvidersSettings() { + const { + providers, + defaultProviderId, + loading, + fetchProviders, + addProvider, + updateProvider, + deleteProvider, + setApiKey, + setDefaultProvider, + validateApiKey, + } = useProviderStore(); + + const [showAddDialog, setShowAddDialog] = useState(false); + const [editingProvider, setEditingProvider] = useState(null); + + // Fetch providers on mount + useEffect(() => { + fetchProviders(); + }, [fetchProviders]); + + const handleAddProvider = async (type: string, name: string, apiKey: string) => { + try { + await addProvider({ + id: `${type}-${Date.now()}`, + type: type as 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom', + name, + enabled: true, + }, apiKey || undefined); + + setShowAddDialog(false); + toast.success('Provider added successfully'); + } catch (error) { + toast.error(`Failed to add provider: ${error}`); + } + }; + + const handleDeleteProvider = async (providerId: string) => { + try { + await deleteProvider(providerId); + toast.success('Provider deleted'); + } catch (error) { + toast.error(`Failed to delete provider: ${error}`); + } + }; + + const handleSetDefault = async (providerId: string) => { + try { + await setDefaultProvider(providerId); + toast.success('Default provider updated'); + } catch (error) { + toast.error(`Failed to set default: ${error}`); + } + }; + + const handleToggleEnabled = async (provider: ProviderWithKeyInfo) => { + try { + await updateProvider(provider.id, { enabled: !provider.enabled }); + } catch (error) { + toast.error(`Failed to update provider: ${error}`); + } + }; + + return ( +
+
+
+

AI Providers

+

+ Configure your AI model providers and API keys +

+
+ +
+ + {loading ? ( +
+ +
+ ) : providers.length === 0 ? ( + + + +

No providers configured

+

+ Add an AI provider to start using ClawX +

+ +
+
+ ) : ( +
+ {providers.map((provider) => ( + setEditingProvider(provider.id)} + onCancelEdit={() => setEditingProvider(null)} + onDelete={() => handleDeleteProvider(provider.id)} + onSetDefault={() => handleSetDefault(provider.id)} + onToggleEnabled={() => handleToggleEnabled(provider)} + onUpdateKey={async (key) => { + await setApiKey(provider.id, key); + setEditingProvider(null); + }} + onValidateKey={(key) => validateApiKey(provider.id, key)} + /> + ))} +
+ )} + + {/* Add Provider Dialog */} + {showAddDialog && ( + setShowAddDialog(false)} + onAdd={handleAddProvider} + /> + )} +
+ ); +} + +interface ProviderCardProps { + provider: ProviderWithKeyInfo; + isDefault: boolean; + isEditing: boolean; + onEdit: () => void; + onCancelEdit: () => void; + onDelete: () => void; + onSetDefault: () => void; + onToggleEnabled: () => void; + onUpdateKey: (key: string) => Promise; + onValidateKey: (key: string) => Promise<{ valid: boolean; error?: string }>; +} + +function ProviderCard({ + provider, + isDefault, + isEditing, + onEdit, + onCancelEdit, + onDelete, + onSetDefault, + onToggleEnabled, + onUpdateKey, + onValidateKey, +}: ProviderCardProps) { + const [newKey, setNewKey] = useState(''); + const [showKey, setShowKey] = useState(false); + const [validating, setValidating] = useState(false); + const [saving, setSaving] = useState(false); + + const typeInfo = providerTypes.find((t) => t.id === provider.type); + + const handleSaveKey = async () => { + if (!newKey) return; + + setValidating(true); + const result = await onValidateKey(newKey); + setValidating(false); + + if (!result.valid) { + toast.error(result.error || 'Invalid API key'); + return; + } + + setSaving(true); + try { + await onUpdateKey(newKey); + setNewKey(''); + toast.success('API key updated'); + } catch (error) { + toast.error(`Failed to save key: ${error}`); + } finally { + setSaving(false); + } + }; + + return ( + + +
+
+ {typeInfo?.icon || '⚙️'} +
+
+ {provider.name} + {isDefault && ( + Default + )} +
+ {provider.type} +
+
+
+ +
+
+
+ + {isEditing ? ( +
+
+ +
+
+ setNewKey(e.target.value)} + className="pr-10" + /> + +
+ + +
+
+
+ ) : ( +
+
+ + + {provider.hasKey ? provider.keyMasked : 'No API key set'} + + {provider.hasKey && ( + Configured + )} +
+
+ {!isDefault && ( + + )} + + +
+
+ )} +
+
+ ); +} + +interface AddProviderDialogProps { + onClose: () => void; + onAdd: (type: string, name: string, apiKey: string) => Promise; +} + +function AddProviderDialog({ onClose, onAdd }: AddProviderDialogProps) { + const [selectedType, setSelectedType] = useState(null); + const [name, setName] = useState(''); + const [apiKey, setApiKey] = useState(''); + const [showKey, setShowKey] = useState(false); + const [saving, setSaving] = useState(false); + + const typeInfo = providerTypes.find((t) => t.id === selectedType); + + const handleAdd = async () => { + if (!selectedType) return; + + setSaving(true); + try { + await onAdd(selectedType, name || typeInfo?.name || selectedType, apiKey); + } finally { + setSaving(false); + } + }; + + return ( +
+ + + Add AI Provider + + Configure a new AI model provider + + + + {!selectedType ? ( +
+ {providerTypes.map((type) => ( + + ))} +
+ ) : ( +
+
+ {typeInfo?.icon} +
+

{typeInfo?.name}

+ +
+
+ +
+ + setName(e.target.value)} + /> +
+ +
+ +
+ setApiKey(e.target.value)} + className="pr-10" + /> + +
+

+ Your API key will be securely encrypted and stored locally. +

+
+
+ )} + + + +
+ + +
+
+
+
+ ); +} diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index 1fe926608..0c8dd5421 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -11,6 +11,7 @@ import { Loader2, Terminal, ExternalLink, + Key, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -20,6 +21,7 @@ import { Separator } from '@/components/ui/separator'; import { Badge } from '@/components/ui/badge'; import { useSettingsStore } from '@/stores/settings'; import { useGatewayStore } from '@/stores/gateway'; +import { ProvidersSettings } from '@/components/settings/ProvidersSettings'; export function Settings() { const { @@ -109,6 +111,20 @@ export function Settings() { + {/* AI Providers */} + + + + + AI Providers + + Configure your AI model providers and API keys + + + + + + {/* Gateway */} diff --git a/src/stores/providers.ts b/src/stores/providers.ts new file mode 100644 index 000000000..69bb4edb2 --- /dev/null +++ b/src/stores/providers.ts @@ -0,0 +1,198 @@ +/** + * Provider State Store + * Manages AI provider configurations + */ +import { create } from 'zustand'; + +/** + * Provider configuration + */ +export interface ProviderConfig { + id: string; + name: string; + type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom'; + baseUrl?: string; + model?: string; + enabled: boolean; + createdAt: string; + updatedAt: string; +} + +/** + * Provider with key info (for display) + */ +export interface ProviderWithKeyInfo extends ProviderConfig { + hasKey: boolean; + keyMasked: string | null; +} + +interface ProviderState { + providers: ProviderWithKeyInfo[]; + defaultProviderId: string | null; + loading: boolean; + error: string | null; + + // Actions + fetchProviders: () => Promise; + addProvider: (config: Omit, apiKey?: string) => Promise; + updateProvider: (providerId: string, updates: Partial, apiKey?: string) => Promise; + deleteProvider: (providerId: string) => Promise; + setApiKey: (providerId: string, apiKey: string) => Promise; + deleteApiKey: (providerId: string) => Promise; + setDefaultProvider: (providerId: string) => Promise; + validateApiKey: (providerId: string, apiKey: string) => Promise<{ valid: boolean; error?: string }>; + getApiKey: (providerId: string) => Promise; +} + +export const useProviderStore = create((set, get) => ({ + providers: [], + defaultProviderId: null, + loading: false, + error: null, + + fetchProviders: async () => { + set({ loading: true, error: null }); + + try { + const providers = await window.electron.ipcRenderer.invoke('provider:list') as ProviderWithKeyInfo[]; + const defaultId = await window.electron.ipcRenderer.invoke('provider:getDefault') as string | null; + + set({ + providers, + defaultProviderId: defaultId, + loading: false + }); + } catch (error) { + set({ error: String(error), loading: false }); + } + }, + + addProvider: async (config, apiKey) => { + try { + const fullConfig: ProviderConfig = { + ...config, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + const result = await window.electron.ipcRenderer.invoke('provider:save', fullConfig, apiKey) as { success: boolean; error?: string }; + + if (!result.success) { + throw new Error(result.error || 'Failed to save provider'); + } + + // Refresh the list + await get().fetchProviders(); + } catch (error) { + console.error('Failed to add provider:', error); + throw error; + } + }, + + updateProvider: async (providerId, updates, apiKey) => { + try { + const existing = get().providers.find((p) => p.id === providerId); + if (!existing) { + throw new Error('Provider not found'); + } + + const updatedConfig: ProviderConfig = { + ...existing, + ...updates, + updatedAt: new Date().toISOString(), + }; + + const result = await window.electron.ipcRenderer.invoke('provider:save', updatedConfig, apiKey) as { success: boolean; error?: string }; + + if (!result.success) { + throw new Error(result.error || 'Failed to update provider'); + } + + // Refresh the list + await get().fetchProviders(); + } catch (error) { + console.error('Failed to update provider:', error); + throw error; + } + }, + + deleteProvider: async (providerId) => { + try { + const result = await window.electron.ipcRenderer.invoke('provider:delete', providerId) as { success: boolean; error?: string }; + + if (!result.success) { + throw new Error(result.error || 'Failed to delete provider'); + } + + // Refresh the list + await get().fetchProviders(); + } catch (error) { + console.error('Failed to delete provider:', error); + throw error; + } + }, + + setApiKey: async (providerId, apiKey) => { + try { + const result = await window.electron.ipcRenderer.invoke('provider:setApiKey', providerId, apiKey) as { success: boolean; error?: string }; + + if (!result.success) { + throw new Error(result.error || 'Failed to set API key'); + } + + // Refresh the list + await get().fetchProviders(); + } catch (error) { + console.error('Failed to set API key:', error); + throw error; + } + }, + + deleteApiKey: async (providerId) => { + try { + const result = await window.electron.ipcRenderer.invoke('provider:deleteApiKey', providerId) as { success: boolean; error?: string }; + + if (!result.success) { + throw new Error(result.error || 'Failed to delete API key'); + } + + // Refresh the list + await get().fetchProviders(); + } catch (error) { + console.error('Failed to delete API key:', error); + throw error; + } + }, + + setDefaultProvider: async (providerId) => { + try { + const result = await window.electron.ipcRenderer.invoke('provider:setDefault', providerId) as { success: boolean; error?: string }; + + if (!result.success) { + throw new Error(result.error || 'Failed to set default provider'); + } + + set({ defaultProviderId: providerId }); + } catch (error) { + console.error('Failed to set default provider:', error); + throw error; + } + }, + + validateApiKey: async (providerId, apiKey) => { + try { + const result = await window.electron.ipcRenderer.invoke('provider:validateKey', providerId, apiKey) as { valid: boolean; error?: string }; + return result; + } catch (error) { + return { valid: false, error: String(error) }; + } + }, + + getApiKey: async (providerId) => { + try { + return await window.electron.ipcRenderer.invoke('provider:getApiKey', providerId) as string | null; + } catch { + return null; + } + }, +}));