Introduce token-based author authentication (register/verify API), skill forking with EditGate protection, tag metadata on skills, and download/push stats. Enhanced push scripts with token auth and per-skill filtering. Updated UI with stats, tags, and author info on skill cards.
110 lines
3.4 KiB
JavaScript
110 lines
3.4 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import matter from 'gray-matter';
|
|
|
|
const SKILLS_DIR = path.resolve(
|
|
process.env.SKILLS_DIR || "data/skills"
|
|
);
|
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
const MAX_SLUG_LENGTH = 64;
|
|
function isValidSlug(slug) {
|
|
return slug.length >= 2 && slug.length <= MAX_SLUG_LENGTH && SLUG_RE.test(slug);
|
|
}
|
|
function skillPath(slug) {
|
|
return path.join(SKILLS_DIR, `${slug}.md`);
|
|
}
|
|
function parseTools(val) {
|
|
if (Array.isArray(val)) return val.map(String);
|
|
if (typeof val === "string") return val.split(",").map((t) => t.trim()).filter(Boolean);
|
|
return [];
|
|
}
|
|
function parseSkill(slug, raw) {
|
|
const { data, content } = matter(raw);
|
|
return {
|
|
slug,
|
|
name: data.name || slug,
|
|
description: data.description || "",
|
|
"allowed-tools": parseTools(data["allowed-tools"] ?? data.allowedTools),
|
|
"argument-hint": data["argument-hint"] || "",
|
|
model: data.model || "",
|
|
"user-invocable": data["user-invocable"] !== false,
|
|
"disable-model-invocation": Boolean(data["disable-model-invocation"]),
|
|
context: data.context || "",
|
|
agent: data.agent || "",
|
|
author: data.author || "",
|
|
"author-email": data["author-email"] || "",
|
|
"fork-of": data["fork-of"] || "",
|
|
tags: parseTools(data.tags),
|
|
hooks: typeof data.hooks === "object" && data.hooks !== null ? data.hooks : null,
|
|
content: content.trim(),
|
|
raw
|
|
};
|
|
}
|
|
async function listSkills() {
|
|
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
|
const files = await fs.readdir(SKILLS_DIR);
|
|
const skills = [];
|
|
for (const file of files) {
|
|
if (!file.endsWith(".md")) continue;
|
|
const slug = file.replace(/\.md$/, "");
|
|
const raw = await fs.readFile(path.join(SKILLS_DIR, file), "utf-8");
|
|
const { content: _, raw: __, ...meta } = parseSkill(slug, raw);
|
|
skills.push(meta);
|
|
}
|
|
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
async function getAllTags() {
|
|
const all = await listSkills();
|
|
return [...new Set(all.flatMap((s) => s.tags))].sort();
|
|
}
|
|
async function getForksOf(slug) {
|
|
const all = await listSkills();
|
|
return all.filter((s) => s["fork-of"] === slug);
|
|
}
|
|
async function getSkill(slug) {
|
|
try {
|
|
const raw = await fs.readFile(skillPath(slug), "utf-8");
|
|
return parseSkill(slug, raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
async function createSkill(slug, content) {
|
|
if (!isValidSlug(slug)) {
|
|
throw new Error(`Invalid slug: ${slug}`);
|
|
}
|
|
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
|
const dest = skillPath(slug);
|
|
try {
|
|
await fs.access(dest);
|
|
throw new Error(`Skill already exists: ${slug}`);
|
|
} catch (err) {
|
|
if (err instanceof Error && err.message.startsWith("Skill already exists")) {
|
|
throw err;
|
|
}
|
|
}
|
|
await fs.writeFile(dest, content, "utf-8");
|
|
return parseSkill(slug, content);
|
|
}
|
|
async function updateSkill(slug, content) {
|
|
const dest = skillPath(slug);
|
|
try {
|
|
await fs.access(dest);
|
|
} catch {
|
|
throw new Error(`Skill not found: ${slug}`);
|
|
}
|
|
await fs.writeFile(dest, content, "utf-8");
|
|
return parseSkill(slug, content);
|
|
}
|
|
async function deleteSkill(slug) {
|
|
const dest = skillPath(slug);
|
|
try {
|
|
await fs.access(dest);
|
|
} catch {
|
|
throw new Error(`Skill not found: ${slug}`);
|
|
}
|
|
await fs.unlink(dest);
|
|
}
|
|
|
|
export { getAllTags as a, getForksOf as b, createSkill as c, deleteSkill as d, getSkill as g, isValidSlug as i, listSkills as l, updateSkill as u };
|