import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { getAllSkillConfigs, type SkillConfigRecord, } from '@electron/utils/skill-config'; import { getOpenClawConfigDir } from '@electron/utils/paths'; import { parseSkillCapability, type SkillCapability, } from './skill-capability-parser'; type SkillConfigEntry = { enabled?: boolean; apiKey?: string; env?: Record; }; type RegistrySnapshot = { capabilities: SkillCapability[]; loadedAt: string; source: 'disk-sync' | 'skill-config'; }; const OPENCLAW_CONFIG_FILE = 'openclaw.json'; const OPENCLAW_SKILLS_DIR = 'skills'; const SKILL_MANIFEST_FILE = 'SKILL.md'; let registrySnapshot: RegistrySnapshot | null = null; let refreshPromise: Promise | null = null; function uniq(values: string[]): string[] { return Array.from(new Set(values)); } function cloneCapability(capability: SkillCapability): SkillCapability { return { ...capability, allowedTools: [...capability.allowedTools], operationHints: [...capability.operationHints], triggerHints: [...capability.triggerHints], inputExtensions: [...capability.inputExtensions], requiredEnvVars: [...capability.requiredEnvVars], commandExamples: capability.commandExamples ? [...capability.commandExamples] : undefined, renderHints: capability.renderHints ? { ...capability.renderHints, metadata: capability.renderHints.metadata ? { ...capability.renderHints.metadata } : undefined, } : undefined, }; } function sortCapabilities(capabilities: SkillCapability[]): SkillCapability[] { return [...capabilities].sort((left, right) => { if (left.enabled !== right.enabled) { return left.enabled ? -1 : 1; } return left.slug.localeCompare(right.slug); }); } function readManifestContent(manifestPath?: string): string { if (!manifestPath || !existsSync(manifestPath)) { return ''; } try { return readFileSync(manifestPath, 'utf-8'); } catch { return ''; } } function setRegistrySnapshot( capabilities: SkillCapability[], source: RegistrySnapshot['source'], ): SkillCapability[] { registrySnapshot = { capabilities: sortCapabilities(capabilities), loadedAt: new Date().toISOString(), source, }; return registrySnapshot.capabilities.map(cloneCapability); } function buildCapabilityFromConfig( skillKey: string, config: Partial & { enabled?: boolean; apiKey?: string; env?: Record; }, ): SkillCapability { const manifestPath = config.filePath || (config.baseDir ? join(config.baseDir, SKILL_MANIFEST_FILE) : undefined); const manifestContent = readManifestContent(manifestPath); return parseSkillCapability({ skillKey, config: { ...config, filePath: manifestPath, }, manifestContent, }); } function readSkillConfigEntriesSync(): Record { const configPath = join(getOpenClawConfigDir(), OPENCLAW_CONFIG_FILE); if (!existsSync(configPath)) { return {}; } try { const raw = readFileSync(configPath, 'utf-8'); const parsed = JSON.parse(raw) as { skills?: { entries?: Record; }; }; return parsed.skills?.entries || {}; } catch { return {}; } } function readInstalledSkillsSync(): Record> { const skillsDir = join(getOpenClawConfigDir(), OPENCLAW_SKILLS_DIR); if (!existsSync(skillsDir)) { return {}; } const result: Record> = {}; for (const entry of readdirSync(skillsDir, { withFileTypes: true })) { if (!entry.isDirectory()) { continue; } const baseDir = join(skillsDir, entry.name); const manifestPath = join(baseDir, SKILL_MANIFEST_FILE); if (!existsSync(manifestPath)) { continue; } result[entry.name] = { slug: entry.name, name: entry.name, baseDir, filePath: manifestPath, }; } return result; } function buildSynchronousSnapshot(): SkillCapability[] { const installedSkills = readInstalledSkillsSync(); const configEntries = readSkillConfigEntriesSync(); const skillKeys = uniq([ ...Object.keys(installedSkills), ...Object.keys(configEntries), ]); const capabilities = skillKeys.map((skillKey) => { const installedSkill = installedSkills[skillKey] || {}; const configEntry = configEntries[skillKey] || {}; return buildCapabilityFromConfig(skillKey, { ...installedSkill, enabled: configEntry.enabled, apiKey: configEntry.apiKey, env: configEntry.env, }); }); return setRegistrySnapshot(capabilities, 'disk-sync'); } function ensureRegistrySnapshot(): RegistrySnapshot { if (!registrySnapshot) { buildSynchronousSnapshot(); } return registrySnapshot || { capabilities: [], loadedAt: new Date().toISOString(), source: 'disk-sync', }; } export function hydrateSkillCapabilityRegistry( configs: Record, ): SkillCapability[] { const capabilities = Object.entries(configs).map(([skillKey, config]) => buildCapabilityFromConfig(skillKey, config), ); return setRegistrySnapshot(capabilities, 'skill-config'); } export async function refreshSkillCapabilityRegistry(): Promise { if (!refreshPromise) { refreshPromise = getAllSkillConfigs() .then((configs) => hydrateSkillCapabilityRegistry(configs)) .finally(() => { refreshPromise = null; }); } return refreshPromise; } export function listSkillCapabilities(options?: { enabledOnly?: boolean }): SkillCapability[] { const snapshot = ensureRegistrySnapshot(); const capabilities = options?.enabledOnly ? snapshot.capabilities.filter((capability) => capability.enabled) : snapshot.capabilities; return capabilities.map(cloneCapability); } export function getEnabledSkillCapabilities(): SkillCapability[] { return listSkillCapabilities({ enabledOnly: true }); }