Spaces:
Running
Running
| // 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')}`; | |
| } | |