Files
claw/web/js/skills.js
elffuss-bot f6006681f1 Stand-in import of elffuss/claw
One parentless commit with only the files that the open seed tasks touch and the modules they import.
The full squashed import (SPEC 8.6) is a later step: this tree is not the cutoff tree.

T2T-StandIn-Source-Commit: 70b800e87ef05a9574bf2ae8226cef90015dbe21
2026-10-01 00:00:00 +00:00

192 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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: 'KikoCis/elffuss-claw-skills', label: 'Elffuss · Skills que funcionan en Claw (3D·Excel·web·juegos)', official: true },
{ 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 },
{ repo: 'earthtojake/text-to-cad', label: 'Text-to-CAD · CAD/STEP/DXF (Python, solo referencia)', official: true },
{ repo: 'scottstts/Threejs-Awesome-Graphics-Agent-Skills', label: '3D en el navegador · Three.js (funciona en Claw)', 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).`;
}
// Editar una skill YA instalada: reemplaza sus instrucciones (o las añade con append),
// conservando su origen. Para ajustar el script/argumentos de una skill existente.
export async function editSkill({ name, description, instructions, append } = {}) {
if (!name) throw new Error('Falta name');
const s = cache.find(x => x.name.toLowerCase() === String(name).toLowerCase());
if (!s) throw new Error(`No hay ninguna skill «${name}». Créala primero con skill.create.`);
let content = s.content || '';
if (instructions != null) content = append ? (content + '\n' + instructions) : String(instructions);
const updated = { ...s, description: description != null ? String(description).slice(0, 240) : s.description, content: content.slice(0, MAX_SKILL) };
await install(updated);
return `Skill «${s.name}» actualizada. Ya sigo la nueva versión.`;
}
// Bloque para el systemPrompt (síncrono, desde la caché).
export function skillsPromptBlock() {
if (!cache.length) return '';
const nombres = cache.map(s => `«${s.name}»`).join(', ');
const parts = cache.map(s =>
`### Skill «${s.name}»${s.repo ? ` (de ${s.repo})` : ''}\n${s.description || ''}\n${(s.content || '').slice(0, MAX_SKILL)}`);
// Framing DIRECTIVO: un modelo pequeño con el framing pasivo de antes negaba
// tener la skill aunque estuviera inyectada («I don't have that skill»). Aquí
// se le dice explícitamente que ES una capacidad suya AHORA, que no la niegue,
// que empareje por tema aunque el usuario use otro nombre, y que si la skill
// trae una plantilla de app la cree con app.create copiándola.
return `\n\nSKILLS INSTALADAS — son capacidades que TIENES AHORA MISMO: ${nombres}.\n` +
`REGLAS con las skills:\n` +
`- Si la petición encaja con una skill (aunque el usuario la nombre con otras palabras o un nombre parecido, empareja por el TEMA de su descripción), ÚSALA siguiendo sus instrucciones al pie de la letra.\n` +
`- NUNCA digas que no tienes esa skill o esa capacidad: la tienes, está aquí abajo.\n` +
`- Si la skill incluye una plantilla de app (HTML), créala con la herramienta app.create COPIANDO esa plantilla y adaptando solo lo que la skill indique — no inventes una app genérica distinta.\n\n` +
parts.join('\n\n');
}