import { AgentSkill } from '@rigstate/rules-engine'; import axios from 'axios'; import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; /** * Provisions Agent Skills to the local project. * * Flow: * 1. Fetch skills from API (database) * 2. Merge with core skills from rules-engine * 3. Write to .agent/skills//SKILL.md * 4. Return list of provisioned skills for .cursorrules injection */ export async function provisionSkills( apiUrl: string, apiKey: string, projectId: string, rootDir: string ): Promise { const skills: AgentSkill[] = []; // 1. Fetch skills from database (user + global + core) try { const response = await axios.get(`${apiUrl}/api/v1/skills`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` } }); if (response.data.success && response.data.data) { for (const dbSkill of response.data.data) { skills.push({ name: dbSkill.name, description: dbSkill.description, specialist: dbSkill.specialist || 'General', version: dbSkill.version || '1.0.0', governance: dbSkill.governance || 'OPEN', content: dbSkill.content }); } } } catch (e: any) { // API might not have skills endpoint yet - fall through to core skills const msg = e.response?.data?.error || e.message; console.log(chalk.dim(` (Skills API not available: ${msg}, using core library)`)); } // 2. If no skills from DB, use core library from rules-engine if (skills.length === 0) { const { getRigstateStandardSkills } = await import('@rigstate/rules-engine'); const coreSkills = getRigstateStandardSkills(); skills.push(...coreSkills); } // 3. Write skills to .agent/skills/ const skillsDir = path.join(rootDir, '.agent', 'skills'); await fs.mkdir(skillsDir, { recursive: true }); for (const skill of skills) { const skillDir = path.join(skillsDir, skill.name); await fs.mkdir(skillDir, { recursive: true }); const skillContent = `--- name: ${skill.name} description: ${skill.description} version: "${skill.version}" specialist: ${skill.specialist} governance: ${skill.governance} --- ${skill.content} --- *Provisioned by Rigstate CLI. Do not modify manually.*`; const skillPath = path.join(skillDir, 'SKILL.md'); await fs.writeFile(skillPath, skillContent, 'utf-8'); } console.log(chalk.green(` ✅ Provisioned ${skills.length} skill(s) to .agent/skills/`)); return skills; } /** * Generate the XML block for .cursorrules */ export function generateSkillsDiscoveryBlock(skills: AgentSkill[]): string { if (skills.length === 0) return ''; const skillBlocks = skills.map(skill => ` ${skill.name} ${skill.description} .agent/skills/${skill.name}/SKILL.md `).join('\n'); return ` ${skillBlocks} `; } /** * Just-In-Time provisioning of a specific skill. * Checks if the skill is already in .cursorrules and injects it if not. */ export async function jitProvisionSkill( skillId: string, apiUrl: string, apiKey: string, projectId: string, rootDir: string ): Promise { const rulesPath = path.join(rootDir, '.cursorrules'); let rulesContent = ''; try { rulesContent = await fs.readFile(rulesPath, 'utf-8'); } catch (e) { return false; } const isProvisioned = rulesContent.includes(`${skillId}`) || rulesContent.includes(`.agent/skills/${skillId}`); if (isProvisioned) return false; console.log(chalk.yellow(` ⚡ JIT PROVISIONING: Injecting ${skillId}...`)); try { const skills = await provisionSkills(apiUrl, apiKey, projectId, rootDir); const skillsBlock = generateSkillsDiscoveryBlock(skills); if (rulesContent.includes('')) { rulesContent = rulesContent.replace( /[\s\S]*?<\/available_skills>/, skillsBlock ); } else if (rulesContent.includes('## 🧠 PROJECT CONTEXT')) { const insertPoint = rulesContent.indexOf('---', rulesContent.indexOf('## 🧠 PROJECT CONTEXT')); if (insertPoint !== -1) { rulesContent = rulesContent.slice(0, insertPoint + 3) + '\n\n' + skillsBlock + '\n' + rulesContent.slice(insertPoint + 3); } } await fs.writeFile(rulesPath, rulesContent, 'utf-8'); return true; } catch (e: any) { console.log(chalk.red(` Failed to provision skill: ${e.message}`)); return false; } }