/** * 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, cp, mkdir } from 'fs/promises'; import { existsSync } from 'fs'; import { constants } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { getOpenClawDir } from './paths'; import { logger } from './logger'; 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 || {}; } /** * Built-in skills bundled with ClawX that should be pre-deployed to * ~/.openclaw/skills/ on first launch. These come from the openclaw package's * extensions directory and are available in both dev and packaged builds. */ const BUILTIN_SKILLS = [ { slug: 'feishu-doc', sourceExtension: 'feishu' }, { slug: 'feishu-drive', sourceExtension: 'feishu' }, { slug: 'feishu-perm', sourceExtension: 'feishu' }, { slug: 'feishu-wiki', sourceExtension: 'feishu' }, ] as const; /** * Ensure built-in skills are deployed to ~/.openclaw/skills//. * Skips any skill that already has a SKILL.md present (idempotent). * Runs at app startup; all errors are logged and swallowed so they never * block the normal startup flow. */ export async function ensureBuiltinSkillsInstalled(): Promise { const skillsRoot = join(homedir(), '.openclaw', 'skills'); for (const { slug, sourceExtension } of BUILTIN_SKILLS) { const targetDir = join(skillsRoot, slug); const targetManifest = join(targetDir, 'SKILL.md'); if (existsSync(targetManifest)) { continue; // already installed } const openclawDir = getOpenClawDir(); const sourceDir = join(openclawDir, 'extensions', sourceExtension, 'skills', slug); if (!existsSync(join(sourceDir, 'SKILL.md'))) { logger.warn(`Built-in skill source not found, skipping: ${sourceDir}`); continue; } try { await mkdir(targetDir, { recursive: true }); await cp(sourceDir, targetDir, { recursive: true }); logger.info(`Installed built-in skill: ${slug} -> ${targetDir}`); } catch (error) { logger.warn(`Failed to install built-in skill ${slug}:`, error); } } }