// Skills de Elffuss Code: instrucciones en markdown (formato SKILL.md de // Claude Code) que el modelo sigue cuando aplican. Se instalan desde el // catálogo grande OFICIAL (github.com/anthropics/skills), desde los plugins // oficiales, o desde CUALQUIER repo público (marketplaces de la comunidad // tipo OpenClaude/openclaw). Todo transparente: se ve el repo, la lista y el // SKILL.md que se inyecta. Se guardan en IndexedDB (nada sale del navegador). import * as db from './db.js'; const KEY = 'skills'; // skills instaladas const SRC_KEY = 'skills.sources'; // repos personalizados añadidos por el usuario const MAX_SKILL = 12_000; // caracteres del cuerpo que se inyectan al modelo export const CATALOG_REPO = 'anthropics/skills'; export const DEFAULT_SOURCES = [ { repo: 'bbgnsurftech/claude-skills-collection', label: 'Comunidad · Mega-colección (1000+ skills)', official: true }, { repo: 'anthropics/skills', label: 'Anthropic · Agent Skills (oficial de Claude Code)', official: true }, { repo: 'anthropics/claude-plugins-official', label: 'Anthropic · Claude Code Plugins (oficial)', official: true }, ]; let cache = []; // instaladas, en memoria (para que el systemPrompt sea síncrono) export async function initSkills() { cache = (await db.get('kv', KEY).catch(() => null)) || []; return cache; } export async function all() { return cache; } export function installed() { return cache; } export function isInstalled(repo, path) { return cache.some(s => s.repo === repo && s.path === path); } export async function install(skill) { cache = cache.filter(s => !(s.repo === skill.repo && s.path === skill.path) && s.name !== skill.name); const entry = { ...skill, content: (skill.content || '').slice(0, MAX_SKILL) }; cache.push(entry); await db.set('kv', KEY, cache); return entry; // devuelve la skill completa (name, description…) } // Mensaje «cómo usarla» tras instalar: qué hace + un ejemplo de qué pedir. export function usageMessage(skill) { const desc = (skill.description || '').trim(); const ejemplo = firstExample(skill) || `algo relacionado con «${skill.name}»`; return `✳ **Skill «${skill.name}» instalada.**\n\n` + (desc ? desc + '\n\n' : '') + `Ya la sigo en cada conversación. Para usarla, pídeme por ejemplo:\n\n> ${ejemplo}`; } // Intenta sacar un ejemplo de uso del cuerpo del SKILL.md (líneas de ejemplo/uso). function firstExample(skill) { const body = skill.content || ''; const m = body.match(/(?:ejemplo|example|uso|usage|prueba|try)[^\n:]*[::]\s*["“]?([^\n"”]{6,90})/i) || body.match(/^[-*]\s+([A-ZÁÉÍÓÚ][^\n]{10,80})/m); return m ? m[1].trim().replace(/[.*_`]+$/, '') : null; } export async function remove(nameOrPath) { cache = cache.filter(s => s.name !== nameOrPath && s.path !== nameOrPath); await db.set('kv', KEY, cache); } export async function get(name) { return cache.find(s => s.name.toLowerCase() === String(name).toLowerCase()) || null; } // ---- fuentes (repos) ---- export async function sources() { const custom = (await db.get('kv', SRC_KEY).catch(() => null)) || []; return [...DEFAULT_SOURCES, ...custom]; } export async function addSource(repoOrUrl, label) { const repo = repoOrUrl.trim().replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '').replace(/\/$/, ''); if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error('Usa owner/repo (o la URL de GitHub)'); const custom = (await db.get('kv', SRC_KEY).catch(() => null)) || []; if (!custom.some(s => s.repo === repo) && !DEFAULT_SOURCES.some(s => s.repo === repo)) custom.push({ repo, label: label || repo }); await db.set('kv', SRC_KEY, custom); return repo; } export async function removeSource(repo) { const custom = (await db.get('kv', SRC_KEY).catch(() => null)) || []; await db.set('kv', SRC_KEY, custom.filter(s => s.repo !== repo)); } // ---- catálogo desde GitHub ---- // Una sola llamada al árbol git del repo → todos los SKILL.md. export async function listFromRepo(repo) { let tree, branch; for (const b of ['main', 'master']) { const r = await fetch(`https://api.github.com/repos/${repo}/git/trees/${b}?recursive=1`); if (r.ok) { tree = await r.json(); branch = b; break; } if (r.status === 403) throw new Error('GitHub limitó las peticiones (60/h sin login). Reintenta en unos minutos.'); } if (!tree) throw new Error('No pude leer el repo (¿existe y es público?)'); return (tree.tree || []) .filter(n => /(^|\/)SKILL\.md$/i.test(n.path)) .map(n => ({ repo, branch, path: n.path, dir: n.path.replace(/\/SKILL\.md$/i, ''), name: n.path.replace(/\/SKILL\.md$/i, '').split('/').pop() || repo })) .sort((a, b) => a.dir.localeCompare(b.dir)); } // Descarga el SKILL.md y lo instala (frontmatter YAML simple). export async function installFromRepo(entry) { let md = null; for (const b of [entry.branch, 'main', 'master'].filter(Boolean)) { const r = await fetch(`https://raw.githubusercontent.com/${entry.repo}/${b}/${entry.path}`); if (r.ok) { md = await r.text(); break; } } if (md == null) throw new Error('No pude descargar el SKILL.md'); const skill = parseSkill(md, entry.name); return install({ ...skill, repo: entry.repo, path: entry.path }); } // SKILL.md → { name, description, content } export function parseSkill(md, fallbackName = 'skill') { const fm = md.match(/^---\n([\s\S]*?)\n---/); const meta = fm?.[1] || ''; const name = meta.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName; const description = (meta.match(/^description:\s*([\s\S]+?)(?:\n\w+:|$)/m)?.[1] || '').replace(/\s+/g, ' ').trim(); const content = md.replace(/^---\n[\s\S]*?\n---\n?/, '').trim(); return { name, description, content }; } // Creador de skills: Elffuss fabrica una skill propia (SKILL.md) a partir de // lo que el usuario quiere, y la instala al instante. Aparece en la pestaña // Skills y se inyecta en el prompt de los siguientes turnos. export async function createSkill({ name, description, instructions } = {}) { if (!name || !instructions) throw new Error('Faltan name o instructions'); const skill = { name: String(name).slice(0, 48), description: (description || '').slice(0, 240), content: String(instructions).slice(0, MAX_SKILL), repo: 'creada por Elffuss', path: 'local/' + Date.now(), }; await install(skill); return `Skill «${skill.name}» creada e instalada. Ya la sigo en cada conversación (mírala en la pestaña Skills).`; } // Bloque para el systemPrompt (síncrono, desde la caché). export function skillsPromptBlock() { if (!cache.length) return ''; const parts = cache.map(s => `### Skill «${s.name}»${s.repo ? ` (de ${s.repo})` : ''}\n${s.description || ''}\n${(s.content || '').slice(0, MAX_SKILL)}`); return `\n\nSKILLS ACTIVAS (instrucciones especializadas; síguelas cuando la tarea encaje):\n${parts.join('\n\n')}`; }