import { BrowserWindow } from 'electron'; import { ClawHubService } from '@electron/gateway/clawhub'; import { hydrateSkillCapabilityRegistry, refreshSkillCapabilityRegistry, } from '@electron/gateway/skill-capability-registry'; import { windowManager } from '@electron/service/window-service'; import { SkillInstallService, SkillInstallServiceError, type SkillInstallRequest, } from '@electron/service/skill-install-service'; import { getAllSkillConfigs, updateSkillConfig } from '@electron/utils/skill-config'; import type { GatewayEvent, GatewayRpcParams, GatewayRpcReturns } from '../types'; type GatewayBroadcast = (event: GatewayEvent) => void; function broadcastGatewayEvent(event: GatewayEvent): void { const mainWindow = BrowserWindow.getAllWindows().find( (win) => windowManager.getName(win) === 'main', ) ?? BrowserWindow.getAllWindows().find((win) => !win.isDestroyed()); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:event', event); } } function broadcastSkillsRuntimeChanged(broadcast?: GatewayBroadcast, reason = 'skills:changed'): void { const event: GatewayEvent = { type: 'runtime:changed', topics: ['skills'], reason, syncedAt: new Date().toISOString(), }; if (broadcast) { broadcast(event); return; } broadcastGatewayEvent(event); } export async function handleSkillsStatus(): Promise { const configs = await getAllSkillConfigs(); hydrateSkillCapabilityRegistry(configs); return { skills: Object.entries(configs).map(([skillKey, config]) => ({ skillKey, slug: config.slug || skillKey, name: config.name || skillKey, description: config.description || '', disabled: config.enabled === false, emoji: config.icon, version: config.version || '1.0.0', author: config.author, config: { ...(config.config || {}), ...(config.apiKey ? { apiKey: config.apiKey } : {}), ...(config.env ? { env: config.env } : {}), }, bundled: typeof config.isBundled === 'boolean' ? config.isBundled : config.source === 'openclaw-bundled', always: Boolean(config.isCore), source: config.source, baseDir: config.baseDir, filePath: config.filePath, })), }; } export async function handleSkillsUpdate( params: { skillKey: string; enabled?: boolean }, broadcast?: GatewayBroadcast, ): Promise { const { skillKey, enabled } = params; if (!skillKey || !String(skillKey).trim()) { throw new Error('skillKey is required'); } const result = await updateSkillConfig(String(skillKey).trim(), { enabled }); if (!result.success) { throw new Error(result.error || 'Failed to update skill'); } try { const configs = await getAllSkillConfigs(); hydrateSkillCapabilityRegistry(configs); } catch (error) { console.warn('Failed to refresh skill capability registry after skills.update:', error); } const normalizedSkillKey = String(skillKey).trim(); const action = enabled === false ? 'disabled' : enabled === true ? 'enabled' : 'updated'; broadcastSkillsRuntimeChanged(broadcast, `skills:update:${normalizedSkillKey}:${action}`); return { success: true }; } export async function handleSkillsInstall( params: GatewayRpcParams['skills.install'], broadcast?: GatewayBroadcast, ): Promise { const request = params as SkillInstallRequest; const installService = new SkillInstallService({ clawHubService: new ClawHubService(), }); try { const result = await installService.install(request); try { await refreshSkillCapabilityRegistry(); } catch (error) { console.warn('Failed to refresh skill capability registry after skills.install:', error); } broadcastSkillsRuntimeChanged( broadcast, `skills:install:${result.source}:${result.slug}`, ); return result; } catch (error) { if (error instanceof SkillInstallServiceError) { throw new Error(error.message); } throw error instanceof Error ? error : new Error(String(error)); } }