/** * Skill Config Utilities * Direct read/write access to skill configuration in ~/.openclaw/openclaw.json * This bypasses the Gateway RPC for faster and more reliable config updates. * * All file I/O uses async fs/promises to avoid blocking the main thread. */ import { readFile, writeFile, access } from 'fs/promises'; import { constants } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json'); interface SkillEntry { enabled?: boolean; apiKey?: string; env?: Record; } interface OpenClawConfig { skills?: { entries?: Record; [key: string]: unknown; }; [key: string]: unknown; } async function fileExists(p: string): Promise { try { await access(p, constants.F_OK); return true; } catch { return false; } } /** * Read the current OpenClaw config */ async function readConfig(): Promise { if (!(await fileExists(OPENCLAW_CONFIG_PATH))) { return {}; } try { const raw = await readFile(OPENCLAW_CONFIG_PATH, 'utf-8'); return JSON.parse(raw); } catch (err) { console.error('Failed to read openclaw config:', err); return {}; } } /** * Write the OpenClaw config */ async function writeConfig(config: OpenClawConfig): Promise { const json = JSON.stringify(config, null, 2); await writeFile(OPENCLAW_CONFIG_PATH, json, 'utf-8'); } /** * Get skill config */ export async function getSkillConfig(skillKey: string): Promise { const config = await readConfig(); return config.skills?.entries?.[skillKey]; } /** * Update skill config (apiKey and env) */ export async function updateSkillConfig( skillKey: string, updates: { apiKey?: string; env?: Record } ): Promise<{ success: boolean; error?: string }> { try { const config = await readConfig(); // Ensure skills.entries exists if (!config.skills) { config.skills = {}; } if (!config.skills.entries) { config.skills.entries = {}; } // Get or create skill entry const entry = config.skills.entries[skillKey] || {}; // Update apiKey if (updates.apiKey !== undefined) { const trimmed = updates.apiKey.trim(); if (trimmed) { entry.apiKey = trimmed; } else { delete entry.apiKey; } } // Update env if (updates.env !== undefined) { const newEnv: Record = {}; for (const [key, value] of Object.entries(updates.env)) { const trimmedKey = key.trim(); if (!trimmedKey) continue; const trimmedVal = value.trim(); if (trimmedVal) { newEnv[trimmedKey] = trimmedVal; } } if (Object.keys(newEnv).length > 0) { entry.env = newEnv; } else { delete entry.env; } } // Save entry back config.skills.entries[skillKey] = entry; await writeConfig(config); return { success: true }; } catch (err) { console.error('Failed to update skill config:', err); return { success: false, error: String(err) }; } } /** * Get all skill configs (for syncing to frontend) */ export async function getAllSkillConfigs(): Promise> { const config = await readConfig(); return config.skills?.entries || {}; }