New awesome agent primitive

This commit is contained in:
Harald Kirschner
2025-12-17 15:27:05 -08:00
parent 8baf6d7223
commit 88956de414
11 changed files with 812 additions and 17 deletions

View File

@@ -39,3 +39,19 @@ The following instructions are only to be applied when performing a code review.
- [ ] The file name is lower case, with words separated by hyphens. - [ ] The file name is lower case, with words separated by hyphens.
- [ ] Encourage the use of `tools`, but it's not required. - [ ] Encourage the use of `tools`, but it's not required.
- [ ] Strongly encourage the use of `model` to specify the model that the chat mode is optimised for. - [ ] Strongly encourage the use of `model` to specify the model that the chat mode is optimised for.
## Agent Skills guide
**Only apply to folders in the `skills/` directory**
- [ ] The skill folder contains a `SKILL.md` file.
- [ ] The SKILL.md has markdown front matter.
- [ ] The SKILL.md has a `name` field.
- [ ] The `name` field value is lowercase with words separated by hyphens.
- [ ] The `name` field matches the folder name.
- [ ] The SKILL.md has a `description` field.
- [ ] The `description` field is not empty, at least 10 characters, and maximum 1024 characters.
- [ ] The `description` field value is wrapped in single quotes.
- [ ] The folder name is lower case, with words separated by hyphens.
- [ ] Any bundled assets (scripts, templates, data files) are referenced in the SKILL.md instructions.
- [ ] Bundled assets are reasonably sized (under 5MB per file).

View File

@@ -7,6 +7,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
- **Agents** - Specialized GitHub Copilot agents that integrate with MCP servers - **Agents** - Specialized GitHub Copilot agents that integrate with MCP servers
- **Prompts** - Task-specific prompts for code generation and problem-solving - **Prompts** - Task-specific prompts for code generation and problem-solving
- **Instructions** - Coding standards and best practices applied to specific file patterns - **Instructions** - Coding standards and best practices applied to specific file patterns
- **Skills** - Self-contained folders with instructions and bundled resources for specialized tasks
- **Collections** - Curated collections organized around specific themes and workflows - **Collections** - Curated collections organized around specific themes and workflows
## Repository Structure ## Repository Structure
@@ -16,6 +17,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
├── agents/ # Custom GitHub Copilot agent definitions (.agent.md files) ├── agents/ # Custom GitHub Copilot agent definitions (.agent.md files)
├── prompts/ # Task-specific prompts (.prompt.md files) ├── prompts/ # Task-specific prompts (.prompt.md files)
├── instructions/ # Coding standards and guidelines (.instructions.md files) ├── instructions/ # Coding standards and guidelines (.instructions.md files)
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
├── collections/ # Curated collections of resources (.md files) ├── collections/ # Curated collections of resources (.md files)
├── docs/ # Documentation for different resource types ├── docs/ # Documentation for different resource types
├── eng/ # Build and automation scripts ├── eng/ # Build and automation scripts
@@ -36,13 +38,19 @@ npm run collection:validate
# Create a new collection # Create a new collection
npm run collection:create -- --id <collection-id> --tags <tags> npm run collection:create -- --id <collection-id> --tags <tags>
# Validate agent skills
npm run skill:validate
# Create a new skill
npm run skill:create -- --name <skill-name>
``` ```
## Development Workflow ## Development Workflow
### Working with Agents, Prompts, and Instructions ### Working with Agents, Prompts, Instructions, and Skills
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter: All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets:
#### Agent Files (*.agent.md) #### Agent Files (*.agent.md)
- Must have `description` field (wrapped in single quotes) - Must have `description` field (wrapped in single quotes)
@@ -62,20 +70,40 @@ All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction fi
- Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`) - Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`)
- File names should be lower case with words separated by hyphens - File names should be lower case with words separated by hyphens
#### Agent Skills (skills/*/SKILL.md)
- Each skill is a folder containing a `SKILL.md` file
- SKILL.md must have `name` field (lowercase with hyphens, matching folder name, max 64 characters)
- SKILL.md must have `description` field (wrapped in single quotes, 10-1024 characters)
- Folder names should be lower case with words separated by hyphens
- Skills can include bundled assets (scripts, templates, data files)
- Bundled assets should be referenced in the SKILL.md instructions
- Asset files should be reasonably sized (under 5MB per file)
- Skills follow the [Agent Skills specification](https://github.com/anthropics/skills/blob/main/spec/skill-client-integration.md)
### Adding New Resources ### Adding New Resources
When adding a new agent, prompt, or instruction file: When adding a new agent, prompt, instruction, or skill:
**For Agents, Prompts, and Instructions:**
1. Create the file with proper front matter 1. Create the file with proper front matter
2. Add the file to the appropriate directory 2. Add the file to the appropriate directory
3. Update the README.md by running: `npm run build` 3. Update the README.md by running: `npm run build`
4. Verify the resource appears in the generated README 4. Verify the resource appears in the generated README
**For Skills:**
1. Run `npm run skill:create` to scaffold a new skill folder
2. Edit the generated SKILL.md file with your instructions
3. Add any bundled assets (scripts, templates, data) to the skill folder
4. Run `npm run skill:validate` to validate the skill structure
5. Update the README.md by running: `npm run build`
6. Verify the skill appears in the generated README
### Testing Instructions ### Testing Instructions
```bash ```bash
# Run all validation checks # Run all validation checks
npm run collection:validate npm run collection:validate
npm run skill:validate
# Build and verify README generation # Build and verify README generation
npm run build npm run build
@@ -148,6 +176,15 @@ For agent files (*.agent.md):
- [ ] Includes `model` field (strongly recommended) - [ ] Includes `model` field (strongly recommended)
- [ ] Considers using `tools` field - [ ] Considers using `tools` field
For skills (skills/*/):
- [ ] Folder contains a SKILL.md file
- [ ] SKILL.md has markdown front matter
- [ ] Has `name` field matching folder name (lowercase with hyphens, max 64 characters)
- [ ] Has non-empty `description` field wrapped in single quotes (10-1024 characters)
- [ ] Folder name is lower case with hyphens
- [ ] Any bundled assets are referenced in SKILL.md
- [ ] Bundled assets are under 5MB per file
## Contributing ## Contributing
This is a community-driven project. Contributions are welcome! Please see: This is a community-driven project. Contributions are welcome! Please see:

25
docs/README.skills.md Normal file
View File

@@ -0,0 +1,25 @@
# 🎯 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/agentskills/agentskills), 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.
### 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
| Name | Description | Bundled Assets |
| ---- | ----------- | -------------- |
| [webapp-testing](../skills/webapp-testing/SKILL.md) | Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. | `test-helper.js` |

View File

@@ -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) - 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 - Agents will have access to tools from configured MCP servers
- Follow agent-specific instructions for optimal usage`, - 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 = const vscodeInstallImage =
@@ -98,9 +121,16 @@ const ROOT_FOLDER = path.join(__dirname, "..");
const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions"); const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts"); const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents"); 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 COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
const MAX_COLLECTION_ITEMS = 50; 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"); const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
export { export {
@@ -113,8 +143,13 @@ export {
INSTRUCTIONS_DIR, INSTRUCTIONS_DIR,
PROMPTS_DIR, PROMPTS_DIR,
AGENTS_DIR, AGENTS_DIR,
SKILLS_DIR,
COLLECTIONS_DIR, COLLECTIONS_DIR,
MAX_COLLECTION_ITEMS, MAX_COLLECTION_ITEMS,
SKILL_NAME_MIN_LENGTH,
SKILL_NAME_MAX_LENGTH,
SKILL_DESCRIPTION_MIN_LENGTH,
SKILL_DESCRIPTION_MAX_LENGTH,
DOCS_DIR, DOCS_DIR,
}; };

219
eng/create-skill.mjs Normal file
View 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();

View File

@@ -9,6 +9,7 @@ import {
extractMcpServers, extractMcpServers,
extractMcpServerConfigs, extractMcpServerConfigs,
parseFrontmatter, parseFrontmatter,
parseSkillMetadata,
} from "./yaml-parser.mjs"; } from "./yaml-parser.mjs";
import { import {
TEMPLATES, TEMPLATES,
@@ -19,6 +20,7 @@ import {
ROOT_FOLDER, ROOT_FOLDER,
PROMPTS_DIR, PROMPTS_DIR,
AGENTS_DIR, AGENTS_DIR,
SKILLS_DIR,
COLLECTIONS_DIR, COLLECTIONS_DIR,
INSTRUCTIONS_DIR, INSTRUCTIONS_DIR,
DOCS_DIR, DOCS_DIR,
@@ -51,34 +53,34 @@ let MCP_REGISTRY_SET = null;
*/ */
async function loadMcpRegistryNames() { async function loadMcpRegistryNames() {
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET; if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
try { try {
console.log('Fetching MCP registry from API...'); console.log('Fetching MCP registry from API...');
const allServers = []; const allServers = [];
let cursor = null; let cursor = null;
const apiUrl = 'https://api.mcp.github.com/v0.1/servers/'; const apiUrl = 'https://api.mcp.github.com/v0.1/servers/';
// Fetch all pages using cursor-based pagination // Fetch all pages using cursor-based pagination
do { do {
const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl; const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`API returned status ${response.status}`); throw new Error(`API returned status ${response.status}`);
} }
const json = await response.json(); const json = await response.json();
const servers = json?.servers || []; const servers = json?.servers || [];
// Extract server names and displayNames from the response // Extract server names and displayNames from the response
for (const entry of servers) { for (const entry of servers) {
const serverName = entry?.server?.name; const serverName = entry?.server?.name;
if (serverName) { if (serverName) {
// Try to get displayName from GitHub metadata, fall back to server name // Try to get displayName from GitHub metadata, fall back to server name
const displayName = const displayName =
entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName || entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName ||
serverName; serverName;
allServers.push({ allServers.push({
name: serverName, name: serverName,
displayName: displayName.toLowerCase(), displayName: displayName.toLowerCase(),
@@ -87,18 +89,18 @@ async function loadMcpRegistryNames() {
}); });
} }
} }
// Get next cursor for pagination // Get next cursor for pagination
cursor = json?.metadata?.nextCursor || null; cursor = json?.metadata?.nextCursor || null;
} while (cursor); } while (cursor);
console.log(`Loaded ${allServers.length} servers from MCP registry`); console.log(`Loaded ${allServers.length} servers from MCP registry`);
MCP_REGISTRY_SET = allServers; MCP_REGISTRY_SET = allServers;
} catch (e) { } catch (e) {
console.warn(`Failed to load MCP registry from API: ${e.message}`); console.warn(`Failed to load MCP registry from API: ${e.message}`);
MCP_REGISTRY_SET = []; MCP_REGISTRY_SET = [];
} }
return MCP_REGISTRY_SET; return MCP_REGISTRY_SET;
} }
@@ -435,7 +437,7 @@ function generateMcpServerLinks(servers, registryNames) {
if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) { if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) {
return true; return true;
} }
// Check if the serverName matches a part of the full name after a slash // Check if the serverName matches a part of the full name after a slash
// e.g., "apify" matches "com.apify/apify-mcp-server" // e.g., "apify" matches "com.apify/apify-mcp-server"
const nameParts = entry.fullName.split('/'); const nameParts = entry.fullName.split('/');
@@ -446,7 +448,7 @@ function generateMcpServerLinks(servers, registryNames) {
return true; return true;
} }
} }
// Check if serverName matches the displayName ignoring case // Check if serverName matches the displayName ignoring case
return entry.displayName === serverNameLower; 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) * Unified generator for chat modes & agents (future consolidation)
* @param {Object} cfg * @param {Object} cfg
@@ -886,6 +946,7 @@ async function main() {
); );
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# "); const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# "); const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
const collectionsHeader = TEMPLATES.collectionsSection.replace( const collectionsHeader = TEMPLATES.collectionsSection.replace(
/^##\s/m, /^##\s/m,
"# " "# "
@@ -914,6 +975,15 @@ async function main() {
registryNames registryNames
); );
// Generate skills README
const skillsReadme = buildCategoryReadme(
generateSkillsSection,
SKILLS_DIR,
skillsHeader,
TEMPLATES.skillsUsage,
registryNames
);
// Generate collections README // Generate collections README
const collectionsReadme = buildCategoryReadme( const collectionsReadme = buildCategoryReadme(
generateCollectionsSection, generateCollectionsSection,
@@ -935,6 +1005,7 @@ async function main() {
); );
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
writeFileIfChanged( writeFileIfChanged(
path.join(DOCS_DIR, "README.collections.md"), path.join(DOCS_DIR, "README.collections.md"),
collectionsReadme collectionsReadme

172
eng/validate-skills.mjs Normal file
View 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);
}

View File

@@ -1,5 +1,6 @@
// YAML parser for collection files and frontmatter parsing using vfile-matter // YAML parser for collection files and frontmatter parsing using vfile-matter
import fs from "fs"; import fs from "fs";
import path from "path";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { VFile } from "vfile"; import { VFile } from "vfile";
import { matter } from "vfile-matter"; 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 { export {
parseCollectionYaml, parseCollectionYaml,
parseFrontmatter, parseFrontmatter,
extractAgentMetadata, extractAgentMetadata,
extractMcpServers, extractMcpServers,
extractMcpServerConfigs, extractMcpServerConfigs,
parseSkillMetadata,
safeFileOperation, safeFileOperation,
}; };

View File

@@ -11,7 +11,9 @@
"contributors:generate": "all-contributors generate", "contributors:generate": "all-contributors generate",
"contributors:check": "all-contributors check", "contributors:check": "all-contributors check",
"collection:validate": "node ./eng/validate-collections.mjs", "collection:validate": "node ./eng/validate-collections.mjs",
"collection:create": "node ./eng/create-collection.mjs" "collection:create": "node ./eng/create-collection.mjs",
"skill:validate": "node ./eng/validate-skills.mjs",
"skill:create": "node ./eng/create-skill.mjs"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -0,0 +1,116 @@
---
name: webapp-testing
description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
---
# Web Application Testing
This skill enables comprehensive testing and debugging of local web applications using Playwright automation.
## When to Use This Skill
Use this skill when you need to:
- Test frontend functionality in a real browser
- Verify UI behavior and interactions
- Debug web application issues
- Capture screenshots for documentation or debugging
- Inspect browser console logs
- Validate form submissions and user flows
- Check responsive design across viewports
## Prerequisites
- Node.js installed on the system
- A locally running web application (or accessible URL)
- Playwright will be installed automatically if not present
## Core Capabilities
### 1. Browser Automation
- Navigate to URLs
- Click buttons and links
- Fill form fields
- Select dropdowns
- Handle dialogs and alerts
### 2. Verification
- Assert element presence
- Verify text content
- Check element visibility
- Validate URLs
- Test responsive behavior
### 3. Debugging
- Capture screenshots
- View console logs
- Inspect network requests
- Debug failed tests
## Usage Examples
### Example 1: Basic Navigation Test
```javascript
// Navigate to a page and verify title
await page.goto('http://localhost:3000');
const title = await page.title();
console.log('Page title:', title);
```
### Example 2: Form Interaction
```javascript
// Fill out and submit a form
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
```
### Example 3: Screenshot Capture
```javascript
// Capture a screenshot for debugging
await page.screenshot({ path: 'debug.png', fullPage: true });
```
## Guidelines
1. **Always verify the app is running** - Check that the local server is accessible before running tests
2. **Use explicit waits** - Wait for elements or navigation to complete before interacting
3. **Capture screenshots on failure** - Take screenshots to help debug issues
4. **Clean up resources** - Always close the browser when done
5. **Handle timeouts gracefully** - Set reasonable timeouts for slow operations
6. **Test incrementally** - Start with simple interactions before complex flows
7. **Use selectors wisely** - Prefer data-testid or role-based selectors over CSS classes
## Common Patterns
### Pattern: Wait for Element
```javascript
await page.waitForSelector('#element-id', { state: 'visible' });
```
### Pattern: Check if Element Exists
```javascript
const exists = await page.locator('#element-id').count() > 0;
```
### Pattern: Get Console Logs
```javascript
page.on('console', msg => console.log('Browser log:', msg.text()));
```
### Pattern: Handle Errors
```javascript
try {
await page.click('#button');
} catch (error) {
await page.screenshot({ path: 'error.png' });
throw error;
}
```
## Limitations
- Requires Node.js environment
- Cannot test native mobile apps (use React Native Testing Library instead)
- May have issues with complex authentication flows
- Some modern frameworks may require specific configuration

View File

@@ -0,0 +1,56 @@
/**
* Helper utilities for web application testing with Playwright
*/
/**
* Wait for a condition to be true with timeout
* @param {Function} condition - Function that returns boolean
* @param {number} timeout - Timeout in milliseconds
* @param {number} interval - Check interval in milliseconds
*/
async function waitForCondition(condition, timeout = 5000, interval = 100) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await condition()) {
return true;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error('Condition not met within timeout');
}
/**
* Capture browser console logs
* @param {Page} page - Playwright page object
* @returns {Array} Array of console messages
*/
function captureConsoleLogs(page) {
const logs = [];
page.on('console', msg => {
logs.push({
type: msg.type(),
text: msg.text(),
timestamp: new Date().toISOString()
});
});
return logs;
}
/**
* Take screenshot with automatic naming
* @param {Page} page - Playwright page object
* @param {string} name - Base name for screenshot
*/
async function captureScreenshot(page, name) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${name}-${timestamp}.png`;
await page.screenshot({ path: filename, fullPage: true });
console.log(`Screenshot saved: ${filename}`);
return filename;
}
module.exports = {
waitForCondition,
captureConsoleLogs,
captureScreenshot
};