Spaces:
Running
Running
File size: 7,831 Bytes
b30b7c5 73063cc b30b7c5 73063cc b30b7c5 73063cc b30b7c5 73063cc b30b7c5 | 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 | // 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)
// `loaded` distingue «no hay skills» de «no se pudo leer»: si la lectura
// falla, cache queda vacío pero loaded=false, y install() se niega a persistir
// (antes, un fallo transitorio de IndexedDB borraba TODAS las skills al
// guardar la siguiente).
let loaded = false;
export async function initSkills() {
try {
const stored = await db.get('kv', KEY);
cache = Array.isArray(stored) ? stored : [];
loaded = true;
} catch (e) {
cache = [];
loaded = false;
console.warn('[elffuss] no se pudieron leer las skills; no las sobrescribiré', e);
}
return cache;
}
export function skillsLoaded() { return loaded; }
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) {
// Si la carga inicial falló, cache no representa lo instalado: reintentar
// leer antes de escribir. Sin esto, guardar aquí borraría las skills reales.
if (!loaded) {
await initSkills();
if (!loaded) throw new Error('No pude leer tus skills guardadas; no instalo para no perderlas. Recarga la página e inténtalo otra vez.');
}
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')}`;
}
|