All files / src/lib skill-adapter.ts

0% Statements 0/118
100% Branches 1/1
100% Functions 1/1
0% Lines 0/118

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216                                                                                                                                                                                                                                                                                                                                                                                                                                               
/**
 * 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<string, string[]> = {
  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<string>();
 
  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<string>();
  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);
}