Files
awesome-chatgpt-prompts-pro…/scripts/seed-skills.ts
2026-01-15 16:46:37 +03:00

271 lines
7.5 KiB
TypeScript

/**
* Seed script to import skills from Anthropic's skills repository
*
* Usage:
* npx tsx scripts/seed-skills.ts [skill-name]
*
* Examples:
* npx tsx scripts/seed-skills.ts pdf
* npx tsx scripts/seed-skills.ts --all
*/
import { PrismaClient } from "@prisma/client";
import * as fs from "fs";
import * as path from "path";
const prisma = new PrismaClient();
// File separator uses ASCII control characters (injection-proof):
// \x1F (Unit Separator, ASCII 31) marks start
// \x1E (Record Separator, ASCII 30) marks end
const FILE_SEPARATOR = (filename: string) => `\x1FFILE:${filename}\x1E`;
interface SkillMetadata {
name: string;
description: string;
license?: string;
}
/**
* Parse YAML frontmatter from SKILL.md
*/
function parseFrontmatter(content: string): { metadata: SkillMetadata; body: string } {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) {
return {
metadata: { name: "Unknown", description: "" },
body: content,
};
}
const [, frontmatter, body] = frontmatterMatch;
const metadata: SkillMetadata = { name: "Unknown", description: "" };
// Simple YAML parsing for frontmatter
frontmatter.split("\n").forEach((line) => {
const match = line.match(/^(\w+):\s*(.+)$/);
if (match) {
const [, key, value] = match;
if (key === "name") metadata.name = value.trim();
if (key === "description") metadata.description = value.trim();
if (key === "license") metadata.license = value.trim();
}
});
return { metadata, body };
}
/**
* Recursively read all files from a directory
*/
function readSkillFiles(skillDir: string, basePath: string = ""): Array<{ path: string; content: string }> {
const files: Array<{ path: string; content: string }> = [];
const entries = fs.readdirSync(skillDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(skillDir, entry.name);
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
files.push(...readSkillFiles(fullPath, relativePath));
} else if (entry.isFile()) {
// Skip binary files and hidden files
if (entry.name.startsWith(".")) continue;
try {
const content = fs.readFileSync(fullPath, "utf-8");
files.push({ path: relativePath, content });
} catch (e) {
console.warn(` Skipping binary file: ${relativePath}`);
}
}
}
return files;
}
/**
* Serialize skill files into multi-file format
*/
function serializeSkillFiles(files: Array<{ path: string; content: string }>): string {
// SKILL.md should be first
const skillMd = files.find((f) => f.path === "SKILL.md");
const otherFiles = files.filter((f) => f.path !== "SKILL.md");
if (!skillMd) {
throw new Error("SKILL.md not found");
}
let result = skillMd.content;
for (const file of otherFiles) {
result += `\n${FILE_SEPARATOR(file.path)}\n${file.content}`;
}
return result;
}
/**
* Import a single skill into the database
*/
async function importSkill(skillDir: string, authorId: string): Promise<void> {
const skillName = path.basename(skillDir);
console.log(`\nImporting skill: ${skillName}`);
// Read all files
const files = readSkillFiles(skillDir);
console.log(` Found ${files.length} files`);
// Find and parse SKILL.md
const skillMdFile = files.find((f) => f.path === "SKILL.md");
if (!skillMdFile) {
console.error(` ERROR: SKILL.md not found in ${skillDir}`);
return;
}
const { metadata } = parseFrontmatter(skillMdFile.content);
console.log(` Name: ${metadata.name}`);
console.log(` Description: ${metadata.description.substring(0, 80)}...`);
// Serialize all files
const content = serializeSkillFiles(files);
// Check if skill already exists
const existing = await prisma.prompt.findFirst({
where: {
title: metadata.name,
type: "SKILL",
authorId,
},
});
if (existing) {
console.log(` Skill "${metadata.name}" already exists, updating...`);
await prisma.prompt.update({
where: { id: existing.id },
data: {
content,
description: metadata.description,
},
});
} else {
// Create new skill
await prisma.prompt.create({
data: {
title: metadata.name,
description: metadata.description,
content,
type: "SKILL",
authorId,
isPrivate: false,
},
});
console.log(` Created skill: ${metadata.name}`);
}
}
/**
* Main function
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("Usage:");
console.log(" npx tsx scripts/seed-skills.ts <skill-name> - Import a specific skill");
console.log(" npx tsx scripts/seed-skills.ts --all - Import all skills");
console.log(" npx tsx scripts/seed-skills.ts --list - List available skills");
console.log("\nAvailable skills:");
const skillsDir = "/tmp/anthropic-skills/skills";
if (fs.existsSync(skillsDir)) {
const skills = fs.readdirSync(skillsDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
skills.forEach((s) => console.log(` - ${s}`));
} else {
console.log(" (Skills repo not found. Clone it first with: git clone https://github.com/anthropics/skills.git /tmp/anthropic-skills)");
}
return;
}
// Find or create admin user for importing
let author = await prisma.user.findFirst({
where: { role: "ADMIN" },
});
if (!author) {
console.log("No admin user found. Creating system user...");
author = await prisma.user.create({
data: {
email: "system@prompts.chat",
username: "system",
name: "System",
role: "ADMIN",
},
});
}
console.log(`Using author: ${author.username} (${author.id})`);
const skillsBaseDir = "/tmp/anthropic-skills/skills";
if (!fs.existsSync(skillsBaseDir)) {
console.error("Skills directory not found. Please clone the repo first:");
console.error(" git clone https://github.com/anthropics/skills.git /tmp/anthropic-skills");
return;
}
if (args[0] === "--all") {
// Import all skills
const skillDirs = fs.readdirSync(skillsBaseDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => path.join(skillsBaseDir, d.name));
console.log(`Found ${skillDirs.length} skills to import`);
for (const skillDir of skillDirs) {
try {
await importSkill(skillDir, author.id);
} catch (e) {
console.error(` ERROR importing ${path.basename(skillDir)}:`, e);
}
}
} else if (args[0] === "--list") {
// List available skills
const skills = fs.readdirSync(skillsBaseDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
console.log("Available skills:");
skills.forEach((s) => console.log(` - ${s}`));
} else {
// Import specific skill
const skillDir = path.join(skillsBaseDir, args[0]);
if (!fs.existsSync(skillDir)) {
console.error(`Skill not found: ${args[0]}`);
console.log("\nAvailable skills:");
const skills = fs.readdirSync(skillsBaseDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
skills.forEach((s) => console.log(` - ${s}`));
return;
}
await importSkill(skillDir, author.id);
}
console.log("\nDone!");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});