/** * Skill Adapter - Converts between configStore and UI skill formats * * Bridges the gap between: * - configStore.SkillInfo (backend/Gateway format) * - SkillMarket UI format (based on skill-discovery types) * * Part of Phase 1: Skill Market Store Unification */ import type { SkillInfo as ConfigSkillInfo } from '../store/configStore'; // === UI Skill Types (aligned with SkillMarket expectations) === export interface UISkillInfo { id: string; name: string; description: string; triggers: string[]; capabilities: string[]; toolDeps: string[]; installed: boolean; category?: string; path?: string; source?: 'builtin' | 'extra'; } // Category mapping based on skill keywords const CATEGORY_KEYWORDS: Record = { development: ['code', 'git', 'frontend', 'backend', 'react', 'vue', 'api', 'typescript', 'javascript'], security: ['security', 'audit', 'vulnerability', 'pentest', 'auth'], analytics: ['data', 'analysis', 'analytics', 'visualization', 'report'], content: ['writing', 'content', 'article', 'copy', 'chinese'], ops: ['devops', 'docker', 'k8s', 'deploy', 'ci', 'cd', 'automation'], management: ['pm', 'project', 'requirement', 'planning', 'prd'], testing: ['test', 'api test', 'e2e', 'unit'], business: ['finance', 'budget', 'expense', 'accounting'], marketing: ['social', 'media', 'marketing', 'campaign', 'operation'], }; /** * Infer category from skill name and description */ function inferCategory(skill: ConfigSkillInfo): string | undefined { const text = `${skill.name} ${skill.description || ''}`.toLowerCase(); for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) { if (keywords.some(keyword => text.includes(keyword))) { return category; } } return undefined; } /** * Extract trigger patterns from config format */ function extractTriggers(triggers?: ConfigSkillInfo['triggers']): string[] { if (!triggers) return []; return triggers .map(t => t.pattern || t.type) .filter((p): p is string => Boolean(p)); } /** * Extract capabilities from actions or capabilities field */ function extractCapabilities(skill: ConfigSkillInfo): string[] { // Prefer explicit capabilities field if available if (skill.capabilities && skill.capabilities.length > 0) { return skill.capabilities; } // Fall back to extracting from actions if (skill.actions) { return skill.actions .map(a => a.type) .filter((t): t is string => Boolean(t)); } return []; } /** * Extract tool dependencies from actions params */ function extractToolDeps(actions?: ConfigSkillInfo['actions']): string[] { if (!actions) return []; const deps = new Set(); for (const action of actions) { if (action.params?.tools && Array.isArray(action.params.tools)) { for (const tool of action.params.tools) { if (typeof tool === 'string') { deps.add(tool); } } } if (action.params?.toolDeps && Array.isArray(action.params.toolDeps)) { for (const dep of action.params.toolDeps) { if (typeof dep === 'string') { deps.add(dep); } } } } return Array.from(deps); } /** * Adapt a single skill from configStore format to UI format */ export function adaptSkillInfo(skill: ConfigSkillInfo): UISkillInfo { return { id: skill.id, name: skill.name, description: skill.description || '', triggers: extractTriggers(skill.triggers), capabilities: extractCapabilities(skill), toolDeps: extractToolDeps(skill.actions), installed: skill.enabled ?? false, category: inferCategory(skill), path: skill.path, source: skill.source, }; } /** * Adapt an array of skills from configStore format to UI format */ export function adaptSkills(skills: ConfigSkillInfo[]): UISkillInfo[] { return skills.map(adaptSkillInfo); } /** * Search skills by query string */ export function searchSkills(skills: UISkillInfo[], query: string): UISkillInfo[] { const q = query.toLowerCase().trim(); if (!q) return skills; const tokens = q.split(/[\s,;.!?.,;!?]+/).filter(t => t.length > 0); const scored = skills.map(skill => { let score = 0; // Name match (highest weight) if (skill.name.toLowerCase().includes(q)) score += 10; // Description match if (skill.description.toLowerCase().includes(q)) score += 5; // Trigger match for (const trigger of skill.triggers) { const tLower = trigger.toLowerCase(); if (tLower === q) { score += 15; break; } if (tLower.includes(q) || q.includes(tLower)) score += 8; } // Capability match for (const cap of skill.capabilities) { if (cap.toLowerCase().includes(q)) score += 4; } // Token-level matching for (const token of tokens) { if (skill.name.toLowerCase().includes(token)) score += 2; if (skill.description.toLowerCase().includes(token)) score += 1; for (const trigger of skill.triggers) { if (trigger.toLowerCase().includes(token)) score += 3; } } // Category match if (skill.category && skill.category.toLowerCase().includes(q)) score += 3; return { skill, score }; }); return scored .filter(s => s.score > 0) .sort((a, b) => b.score - a.score) .map(s => s.skill); } /** * Get unique categories from skills */ export function getCategories(skills: UISkillInfo[]): string[] { const categories = new Set(); for (const skill of skills) { if (skill.category) { categories.add(skill.category); } } return Array.from(categories); } // === Aliases for backward compatibility === /** * Alias for UISkillInfo for backward compatibility */ export type SkillDisplay = UISkillInfo; /** * Alias for adaptSkills for catalog adaptation */ export function adaptSkillsCatalog(skills: ConfigSkillInfo[]): UISkillInfo[] { return adaptSkills(skills); }