mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-23 03:45:13 +00:00
New awesome agent primitive
This commit is contained in:
@@ -77,6 +77,29 @@ Custom agents for GitHub Copilot, making it easy for users and organizations to
|
||||
- Access installed agents through the VS Code Chat interface, assign them in CCA, or through Copilot CLI (coming soon)
|
||||
- Agents will have access to tools from configured MCP servers
|
||||
- Follow agent-specific instructions for optimal usage`,
|
||||
|
||||
skillsSection: `## 🎯 Agent Skills
|
||||
|
||||
Agent Skills are self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks. Based on the [Agent Skills specification](https://github.com/anthropics/skills), each skill contains a \`SKILL.md\` file with detailed instructions that agents load on-demand.
|
||||
|
||||
Skills differ from other primitives by supporting bundled assets (scripts, code samples, reference data) that agents can utilize when performing specialized tasks.`,
|
||||
|
||||
skillsUsage: `### How to Use Agent Skills
|
||||
|
||||
**What's Included:**
|
||||
- Each skill is a folder containing a \`SKILL.md\` instruction file
|
||||
- Skills may include helper scripts, code templates, or reference data
|
||||
- Skills follow the Agent Skills specification for maximum compatibility
|
||||
|
||||
**When to Use:**
|
||||
- Skills are ideal for complex, repeatable workflows that benefit from bundled resources
|
||||
- Use skills when you need code templates, helper utilities, or reference data alongside instructions
|
||||
- Skills provide progressive disclosure - loaded only when needed for specific tasks
|
||||
|
||||
**Usage:**
|
||||
- Browse the skills table below to find relevant capabilities
|
||||
- Copy the skill folder to your local skills directory
|
||||
- Reference skills in your prompts or let the agent discover them automatically`,
|
||||
};
|
||||
|
||||
const vscodeInstallImage =
|
||||
@@ -98,9 +121,16 @@ const ROOT_FOLDER = path.join(__dirname, "..");
|
||||
const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
|
||||
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
|
||||
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
||||
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
|
||||
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
|
||||
const MAX_COLLECTION_ITEMS = 50;
|
||||
|
||||
// Agent Skills validation constants
|
||||
const SKILL_NAME_MIN_LENGTH = 1;
|
||||
const SKILL_NAME_MAX_LENGTH = 64;
|
||||
const SKILL_DESCRIPTION_MIN_LENGTH = 10;
|
||||
const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
|
||||
|
||||
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
|
||||
|
||||
export {
|
||||
@@ -113,8 +143,13 @@ export {
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
AGENTS_DIR,
|
||||
SKILLS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
SKILL_NAME_MIN_LENGTH,
|
||||
SKILL_NAME_MAX_LENGTH,
|
||||
SKILL_DESCRIPTION_MIN_LENGTH,
|
||||
SKILL_DESCRIPTION_MAX_LENGTH,
|
||||
DOCS_DIR,
|
||||
};
|
||||
|
||||
|
||||
219
eng/create-skill.mjs
Normal file
219
eng/create-skill.mjs
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import readline from "readline";
|
||||
import { SKILLS_DIR } from "./constants.mjs";
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
function prompt(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const out = { name: undefined, description: undefined };
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === "--name" || a === "-n") {
|
||||
out.name = args[i + 1];
|
||||
i++;
|
||||
} else if (a.startsWith("--name=")) {
|
||||
out.name = a.split("=")[1];
|
||||
} else if (a === "--description" || a === "-d") {
|
||||
out.description = args[i + 1];
|
||||
i++;
|
||||
} else if (a.startsWith("--description=")) {
|
||||
out.description = a.split("=")[1];
|
||||
} else if (!a.startsWith("-") && !out.name) {
|
||||
out.name = a;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async function createSkillTemplate() {
|
||||
try {
|
||||
console.log("🎯 Agent Skills Creator");
|
||||
console.log(
|
||||
"This tool will help you create a new skill following the Agent Skills specification.\n"
|
||||
);
|
||||
|
||||
const parsed = parseArgs();
|
||||
|
||||
// Get skill name
|
||||
let skillName = parsed.name;
|
||||
if (!skillName) {
|
||||
skillName = await prompt("Skill name (lowercase, hyphens only): ");
|
||||
}
|
||||
|
||||
// Validate skill name format
|
||||
if (!skillName) {
|
||||
console.error("❌ Skill name is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9-]+$/.test(skillName)) {
|
||||
console.error(
|
||||
"❌ Skill name must contain only lowercase letters, numbers, and hyphens"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const skillFolder = path.join(SKILLS_DIR, skillName);
|
||||
|
||||
// Check if folder already exists
|
||||
if (fs.existsSync(skillFolder)) {
|
||||
console.log(`⚠️ Skill folder ${skillName} already exists at ${skillFolder}`);
|
||||
console.log("💡 Please choose a different name or edit the existing skill.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get description
|
||||
let description = parsed.description;
|
||||
if (!description) {
|
||||
description = await prompt(
|
||||
"Description (what this skill does and when to use it): "
|
||||
);
|
||||
}
|
||||
|
||||
if (!description || description.trim().length < 10) {
|
||||
console.error(
|
||||
"❌ Description is required and must be at least 10 characters (max 1024)"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get skill title (display name)
|
||||
const defaultTitle = skillName
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
|
||||
let skillTitle = await prompt(`Skill title (default: ${defaultTitle}): `);
|
||||
if (!skillTitle.trim()) {
|
||||
skillTitle = defaultTitle;
|
||||
}
|
||||
|
||||
// Create skill folder
|
||||
fs.mkdirSync(skillFolder, { recursive: true });
|
||||
|
||||
// Create SKILL.md template
|
||||
const skillMdContent = `---
|
||||
name: ${skillName}
|
||||
description: ${description}
|
||||
---
|
||||
|
||||
# ${skillTitle}
|
||||
|
||||
This skill provides [brief overview of what this skill does].
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- [Primary use case]
|
||||
- [Secondary use case]
|
||||
- [Additional use case]
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Required tool/environment]
|
||||
- [Optional dependency]
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### 1. [Capability Name]
|
||||
[Description of what this capability does]
|
||||
|
||||
### 2. [Capability Name]
|
||||
[Description of what this capability does]
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: [Use Case]
|
||||
\`\`\`[language]
|
||||
// Example code or instructions
|
||||
\`\`\`
|
||||
|
||||
### Example 2: [Use Case]
|
||||
\`\`\`[language]
|
||||
// Example code or instructions
|
||||
\`\`\`
|
||||
|
||||
## Guidelines
|
||||
|
||||
1. **[Guideline 1]** - [Explanation]
|
||||
2. **[Guideline 2]** - [Explanation]
|
||||
3. **[Guideline 3]** - [Explanation]
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: [Pattern Name]
|
||||
\`\`\`[language]
|
||||
// Example pattern
|
||||
\`\`\`
|
||||
|
||||
### Pattern: [Pattern Name]
|
||||
\`\`\`[language]
|
||||
// Example pattern
|
||||
\`\`\`
|
||||
|
||||
## Limitations
|
||||
|
||||
- [Limitation 1]
|
||||
- [Limitation 2]
|
||||
- [Limitation 3]
|
||||
`;
|
||||
|
||||
const skillFilePath = path.join(skillFolder, "SKILL.md");
|
||||
fs.writeFileSync(skillFilePath, skillMdContent);
|
||||
|
||||
console.log(`\n✅ Created skill folder: ${skillFolder}`);
|
||||
console.log(`✅ Created SKILL.md: ${skillFilePath}`);
|
||||
|
||||
// Ask if they want to add bundled assets
|
||||
const addAssets = await prompt(
|
||||
"\nWould you like to add bundled assets? (helper scripts, templates, etc.) [y/N]: "
|
||||
);
|
||||
|
||||
if (addAssets.toLowerCase() === "y" || addAssets.toLowerCase() === "yes") {
|
||||
console.log(
|
||||
"\n📁 You can now add files to the skill folder manually or using your editor."
|
||||
);
|
||||
console.log(
|
||||
" Common bundled assets: helper scripts, code templates, reference data"
|
||||
);
|
||||
console.log(` Skill folder location: ${skillFolder}`);
|
||||
}
|
||||
|
||||
console.log("\n📝 Next steps:");
|
||||
console.log("1. Edit SKILL.md to complete the skill instructions");
|
||||
console.log("2. Add any bundled assets (scripts, templates, data) to the skill folder");
|
||||
console.log("3. Run 'npm run skill:validate' to validate the skill");
|
||||
console.log("4. Run 'npm run build' to generate documentation");
|
||||
|
||||
console.log("\n📖 Resources:");
|
||||
console.log(
|
||||
" - Anthropic Skills Spec: https://github.com/anthropics/skills/blob/main/spec/skill-client-integration.md"
|
||||
);
|
||||
console.log(
|
||||
" - Project Documentation: AGENTS.md (section on Agent Skills)"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error creating skill template: ${error.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the interactive creation process
|
||||
createSkillTemplate();
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
extractMcpServers,
|
||||
extractMcpServerConfigs,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
} from "./yaml-parser.mjs";
|
||||
import {
|
||||
TEMPLATES,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
ROOT_FOLDER,
|
||||
PROMPTS_DIR,
|
||||
AGENTS_DIR,
|
||||
SKILLS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
DOCS_DIR,
|
||||
@@ -51,34 +53,34 @@ let MCP_REGISTRY_SET = null;
|
||||
*/
|
||||
async function loadMcpRegistryNames() {
|
||||
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
|
||||
|
||||
|
||||
try {
|
||||
console.log('Fetching MCP registry from API...');
|
||||
const allServers = [];
|
||||
let cursor = null;
|
||||
const apiUrl = 'https://api.mcp.github.com/v0.1/servers/';
|
||||
|
||||
|
||||
// Fetch all pages using cursor-based pagination
|
||||
do {
|
||||
const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl;
|
||||
const response = await fetch(url);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API returned status ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const json = await response.json();
|
||||
const servers = json?.servers || [];
|
||||
|
||||
|
||||
// Extract server names and displayNames from the response
|
||||
for (const entry of servers) {
|
||||
const serverName = entry?.server?.name;
|
||||
if (serverName) {
|
||||
// Try to get displayName from GitHub metadata, fall back to server name
|
||||
const displayName =
|
||||
entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName ||
|
||||
const displayName =
|
||||
entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName ||
|
||||
serverName;
|
||||
|
||||
|
||||
allServers.push({
|
||||
name: serverName,
|
||||
displayName: displayName.toLowerCase(),
|
||||
@@ -87,18 +89,18 @@ async function loadMcpRegistryNames() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get next cursor for pagination
|
||||
cursor = json?.metadata?.nextCursor || null;
|
||||
} while (cursor);
|
||||
|
||||
|
||||
console.log(`Loaded ${allServers.length} servers from MCP registry`);
|
||||
MCP_REGISTRY_SET = allServers;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load MCP registry from API: ${e.message}`);
|
||||
MCP_REGISTRY_SET = [];
|
||||
}
|
||||
|
||||
|
||||
return MCP_REGISTRY_SET;
|
||||
}
|
||||
|
||||
@@ -435,7 +437,7 @@ function generateMcpServerLinks(servers, registryNames) {
|
||||
if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check if the serverName matches a part of the full name after a slash
|
||||
// e.g., "apify" matches "com.apify/apify-mcp-server"
|
||||
const nameParts = entry.fullName.split('/');
|
||||
@@ -446,7 +448,7 @@ function generateMcpServerLinks(servers, registryNames) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if serverName matches the displayName ignoring case
|
||||
return entry.displayName === serverNameLower;
|
||||
}
|
||||
@@ -477,6 +479,64 @@ function generateAgentsSection(agentsDir, registryNames = []) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the skills section with a table of all skills
|
||||
*/
|
||||
function generateSkillsSection(skillsDir) {
|
||||
if (!fs.existsSync(skillsDir)) {
|
||||
console.log(`Skills directory does not exist: ${skillsDir}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get all skill folders (directories)
|
||||
const skillFolders = fs
|
||||
.readdirSync(skillsDir)
|
||||
.filter((file) => {
|
||||
const filePath = path.join(skillsDir, file);
|
||||
return fs.statSync(filePath).isDirectory();
|
||||
});
|
||||
|
||||
// Parse each skill folder
|
||||
const skillEntries = skillFolders
|
||||
.map((folder) => {
|
||||
const skillPath = path.join(skillsDir, folder);
|
||||
const metadata = parseSkillMetadata(skillPath);
|
||||
if (!metadata) return null;
|
||||
|
||||
return {
|
||||
folder,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
assets: metadata.assets,
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry !== null)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log(`Found ${skillEntries.length} skill(s)`);
|
||||
|
||||
if (skillEntries.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create table header
|
||||
let content =
|
||||
"| Name | Description | Bundled Assets |\n| ---- | ----------- | -------------- |\n";
|
||||
|
||||
// Generate table rows for each skill
|
||||
for (const skill of skillEntries) {
|
||||
const link = `../skills/${skill.folder}/SKILL.md`;
|
||||
const assetsList =
|
||||
skill.assets.length > 0
|
||||
? skill.assets.map((a) => `\`${a}\``).join("<br />")
|
||||
: "None";
|
||||
|
||||
content += `| [${skill.name}](${link}) | ${skill.description} | ${assetsList} |\n`;
|
||||
}
|
||||
|
||||
return `${TEMPLATES.skillsSection}\n${TEMPLATES.skillsUsage}\n\n${content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified generator for chat modes & agents (future consolidation)
|
||||
* @param {Object} cfg
|
||||
@@ -886,6 +946,7 @@ async function main() {
|
||||
);
|
||||
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
|
||||
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
|
||||
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
|
||||
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
||||
/^##\s/m,
|
||||
"# "
|
||||
@@ -914,6 +975,15 @@ async function main() {
|
||||
registryNames
|
||||
);
|
||||
|
||||
// Generate skills README
|
||||
const skillsReadme = buildCategoryReadme(
|
||||
generateSkillsSection,
|
||||
SKILLS_DIR,
|
||||
skillsHeader,
|
||||
TEMPLATES.skillsUsage,
|
||||
registryNames
|
||||
);
|
||||
|
||||
// Generate collections README
|
||||
const collectionsReadme = buildCategoryReadme(
|
||||
generateCollectionsSection,
|
||||
@@ -935,6 +1005,7 @@ async function main() {
|
||||
);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
|
||||
writeFileIfChanged(
|
||||
path.join(DOCS_DIR, "README.collections.md"),
|
||||
collectionsReadme
|
||||
|
||||
172
eng/validate-skills.mjs
Normal file
172
eng/validate-skills.mjs
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { parseSkillMetadata } from "./yaml-parser.mjs";
|
||||
import {
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
SKILL_NAME_MIN_LENGTH,
|
||||
SKILL_NAME_MAX_LENGTH,
|
||||
SKILL_DESCRIPTION_MIN_LENGTH,
|
||||
SKILL_DESCRIPTION_MAX_LENGTH,
|
||||
} from "./constants.mjs";
|
||||
|
||||
// Validation functions
|
||||
function validateSkillName(name) {
|
||||
if (!name || typeof name !== "string") {
|
||||
return "name is required and must be a string";
|
||||
}
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
return "name must contain only lowercase letters, numbers, and hyphens";
|
||||
}
|
||||
if (name.length < SKILL_NAME_MIN_LENGTH || name.length > SKILL_NAME_MAX_LENGTH) {
|
||||
return `name must be between ${SKILL_NAME_MIN_LENGTH} and ${SKILL_NAME_MAX_LENGTH} characters`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateSkillDescription(description) {
|
||||
if (!description || typeof description !== "string") {
|
||||
return "description is required and must be a string";
|
||||
}
|
||||
if (description.length < SKILL_DESCRIPTION_MIN_LENGTH) {
|
||||
return `description must be at least ${SKILL_DESCRIPTION_MIN_LENGTH} characters`;
|
||||
}
|
||||
if (description.length > SKILL_DESCRIPTION_MAX_LENGTH) {
|
||||
return `description must not exceed ${SKILL_DESCRIPTION_MAX_LENGTH} characters`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateSkillFolder(folderPath, folderName) {
|
||||
const errors = [];
|
||||
|
||||
// Check if SKILL.md exists
|
||||
const skillFile = path.join(folderPath, "SKILL.md");
|
||||
if (!fs.existsSync(skillFile)) {
|
||||
errors.push("Missing SKILL.md file");
|
||||
return errors; // Cannot proceed without SKILL.md
|
||||
}
|
||||
|
||||
// Parse and validate frontmatter
|
||||
const metadata = parseSkillMetadata(folderPath);
|
||||
if (!metadata) {
|
||||
errors.push("Failed to parse SKILL.md frontmatter");
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Validate name field
|
||||
const nameError = validateSkillName(metadata.name);
|
||||
if (nameError) {
|
||||
errors.push(`name: ${nameError}`);
|
||||
} else {
|
||||
// Validate that folder name matches skill name
|
||||
if (metadata.name !== folderName) {
|
||||
errors.push(
|
||||
`Folder name "${folderName}" does not match skill name "${metadata.name}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate description field
|
||||
const descError = validateSkillDescription(metadata.description);
|
||||
if (descError) {
|
||||
errors.push(`description: ${descError}`);
|
||||
}
|
||||
|
||||
// Check for reasonable file sizes in bundled assets
|
||||
const MAX_ASSET_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
for (const asset of metadata.assets) {
|
||||
const assetPath = path.join(folderPath, asset);
|
||||
try {
|
||||
const stats = fs.statSync(assetPath);
|
||||
if (stats.size > MAX_ASSET_SIZE) {
|
||||
errors.push(
|
||||
`Bundled asset "${asset}" exceeds maximum size of 5MB (${(
|
||||
stats.size /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)}MB)`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Cannot access bundled asset "${asset}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Main validation function
|
||||
function validateSkills() {
|
||||
if (!fs.existsSync(SKILLS_DIR)) {
|
||||
console.log("No skills directory found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
const skillFolders = fs
|
||||
.readdirSync(SKILLS_DIR)
|
||||
.filter((file) => {
|
||||
const filePath = path.join(SKILLS_DIR, file);
|
||||
return fs.statSync(filePath).isDirectory();
|
||||
});
|
||||
|
||||
if (skillFolders.length === 0) {
|
||||
console.log("No skill folders found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Validating ${skillFolders.length} skill folder(s)...`);
|
||||
|
||||
let hasErrors = false;
|
||||
const usedNames = new Set();
|
||||
|
||||
for (const folder of skillFolders) {
|
||||
const folderPath = path.join(SKILLS_DIR, folder);
|
||||
console.log(`\nValidating ${folder}...`);
|
||||
|
||||
const errors = validateSkillFolder(folderPath, folder);
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`❌ Validation errors in ${folder}:`);
|
||||
errors.forEach((error) => console.error(` - ${error}`));
|
||||
hasErrors = true;
|
||||
} else {
|
||||
console.log(`✅ ${folder} is valid`);
|
||||
|
||||
// Check for duplicate names (only if no errors)
|
||||
const metadata = parseSkillMetadata(folderPath);
|
||||
if (metadata) {
|
||||
if (usedNames.has(metadata.name)) {
|
||||
console.error(
|
||||
`❌ Duplicate skill name "${metadata.name}" found in ${folder}`
|
||||
);
|
||||
hasErrors = true;
|
||||
} else {
|
||||
usedNames.add(metadata.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors) {
|
||||
console.log(`\n✅ All ${skillFolders.length} skills are valid`);
|
||||
}
|
||||
|
||||
return !hasErrors;
|
||||
}
|
||||
|
||||
// Run validation
|
||||
try {
|
||||
const isValid = validateSkills();
|
||||
if (!isValid) {
|
||||
console.error("\n❌ Skill validation failed");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\n🎉 Skill validation passed");
|
||||
} catch (error) {
|
||||
console.error(`Error during validation: ${error.message}`);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// YAML parser for collection files and frontmatter parsing using vfile-matter
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import yaml from "js-yaml";
|
||||
import { VFile } from "vfile";
|
||||
import { matter } from "vfile-matter";
|
||||
@@ -137,11 +138,56 @@ function extractMcpServerConfigs(filePath) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SKILL.md frontmatter and list bundled assets in a skill folder
|
||||
* @param {string} skillPath - Path to skill folder
|
||||
* @returns {object|null} Skill metadata with name, description, and assets array
|
||||
*/
|
||||
function parseSkillMetadata(skillPath) {
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
const skillFile = path.join(skillPath, "SKILL.md");
|
||||
if (!fs.existsSync(skillFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const frontmatter = parseFrontmatter(skillFile);
|
||||
|
||||
// Validate required fields
|
||||
if (!frontmatter?.name || !frontmatter?.description) {
|
||||
console.warn(
|
||||
`Invalid skill at ${skillPath}: missing name or description in frontmatter`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// List bundled assets (all files except SKILL.md)
|
||||
const assets = fs
|
||||
.readdirSync(skillPath)
|
||||
.filter((file) => {
|
||||
const filePath = path.join(skillPath, file);
|
||||
return file !== "SKILL.md" && fs.statSync(filePath).isFile();
|
||||
})
|
||||
.sort();
|
||||
|
||||
return {
|
||||
name: frontmatter.name,
|
||||
description: frontmatter.description,
|
||||
assets,
|
||||
path: skillPath,
|
||||
};
|
||||
},
|
||||
skillPath,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
extractAgentMetadata,
|
||||
extractMcpServers,
|
||||
extractMcpServerConfigs,
|
||||
parseSkillMetadata,
|
||||
safeFileOperation,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user