From 88956de414289efffe3248c1d19ba710043c7db3 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Wed, 17 Dec 2025 15:27:05 -0800 Subject: [PATCH 1/7] New awesome agent primitive --- .github/copilot-instructions.md | 16 ++ AGENTS.md | 43 +++++- docs/README.skills.md | 25 +++ eng/constants.mjs | 35 +++++ eng/create-skill.mjs | 219 +++++++++++++++++++++++++++ eng/update-readme.mjs | 97 ++++++++++-- eng/validate-skills.mjs | 172 +++++++++++++++++++++ eng/yaml-parser.mjs | 46 ++++++ package.json | 4 +- skills/webapp-testing/SKILL.md | 116 ++++++++++++++ skills/webapp-testing/test-helper.js | 56 +++++++ 11 files changed, 812 insertions(+), 17 deletions(-) create mode 100644 docs/README.skills.md create mode 100644 eng/create-skill.mjs create mode 100644 eng/validate-skills.mjs create mode 100644 skills/webapp-testing/SKILL.md create mode 100644 skills/webapp-testing/test-helper.js diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d18ee5ed..93311f83 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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. - [ ] 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. + +## 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). diff --git a/AGENTS.md b/AGENTS.md index cd2961cc..c0ae3bba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 - **Prompts** - Task-specific prompts for code generation and problem-solving - **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 ## 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) ├── prompts/ # Task-specific prompts (.prompt.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) ├── docs/ # Documentation for different resource types ├── eng/ # Build and automation scripts @@ -36,13 +38,19 @@ npm run collection:validate # Create a new collection npm run collection:create -- --id --tags + +# Validate agent skills +npm run skill:validate + +# Create a new skill +npm run skill:create -- --name ``` ## 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) - 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'`) - 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 -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 2. Add the file to the appropriate directory 3. Update the README.md by running: `npm run build` 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 ```bash # Run all validation checks npm run collection:validate +npm run skill:validate # Build and verify README generation npm run build @@ -148,6 +176,15 @@ For agent files (*.agent.md): - [ ] Includes `model` field (strongly recommended) - [ ] 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 This is a community-driven project. Contributions are welcome! Please see: diff --git a/docs/README.skills.md b/docs/README.skills.md new file mode 100644 index 00000000..689356fe --- /dev/null +++ b/docs/README.skills.md @@ -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` | diff --git a/eng/constants.mjs b/eng/constants.mjs index 34372d69..879d686f 100644 --- a/eng/constants.mjs +++ b/eng/constants.mjs @@ -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, }; diff --git a/eng/create-skill.mjs b/eng/create-skill.mjs new file mode 100644 index 00000000..6f076416 --- /dev/null +++ b/eng/create-skill.mjs @@ -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(); diff --git a/eng/update-readme.mjs b/eng/update-readme.mjs index 4b098f7e..faaae7d6 100644 --- a/eng/update-readme.mjs +++ b/eng/update-readme.mjs @@ -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("
") + : "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 diff --git a/eng/validate-skills.mjs b/eng/validate-skills.mjs new file mode 100644 index 00000000..8aa05db7 --- /dev/null +++ b/eng/validate-skills.mjs @@ -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); +} diff --git a/eng/yaml-parser.mjs b/eng/yaml-parser.mjs index abb4500e..0143d319 100644 --- a/eng/yaml-parser.mjs +++ b/eng/yaml-parser.mjs @@ -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, }; diff --git a/package.json b/package.json index 4fc8d127..5f25ae57 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "contributors:generate": "all-contributors generate", "contributors:check": "all-contributors check", "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": { "type": "git", diff --git a/skills/webapp-testing/SKILL.md b/skills/webapp-testing/SKILL.md new file mode 100644 index 00000000..0184c709 --- /dev/null +++ b/skills/webapp-testing/SKILL.md @@ -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 diff --git a/skills/webapp-testing/test-helper.js b/skills/webapp-testing/test-helper.js new file mode 100644 index 00000000..df73d9fd --- /dev/null +++ b/skills/webapp-testing/test-helper.js @@ -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 +}; From 2c3d8c10720122b385e670dee32318aa7edd025e Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Wed, 17 Dec 2025 15:30:08 -0800 Subject: [PATCH 2/7] CI fix --- docs/README.skills.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.skills.md b/docs/README.skills.md index 689356fe..bbdc6844 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -1,6 +1,6 @@ # 🎯 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. +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. ### How to Use Agent Skills From 48cf3ac6c96b1bb0a156c9d669bb069ca85a0849 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 18 Dec 2025 10:48:41 +1100 Subject: [PATCH 3/7] Adding skills to readme higher up --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e6cdf1a0..58881bc9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This repository provides a comprehensive toolkit for enhancing GitHub Copilot wi - **👉 [Awesome Agents](docs/README.agents.md)** - Specialized GitHub Copilot agents that integrate with MCP servers to provide enhanced capabilities for specific workflows and tools - **👉 [Awesome Prompts](docs/README.prompts.md)** - Focused, task-specific prompts for generating code, documentation, and solving specific problems - **👉 [Awesome Instructions](docs/README.instructions.md)** - Comprehensive coding standards and best practices that apply to specific file patterns or entire projects +- **👉 [Awesome Skills](docs/README.skills.md)** - Self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks - **👉 [Awesome Collections](docs/README.collections.md)** - Curated collections of related prompts, instructions, and chat modes organized around specific themes and workflows ## 🌟 Featured Collections From 16f3220990332cb355c6fc1b21fe8d4d54a892f6 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 18 Dec 2025 10:50:33 +1100 Subject: [PATCH 4/7] Adding vscode tasks --- .vscode/tasks.json | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 364442ea..cb16ba28 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,9 +41,33 @@ "${input:tags}" ], "problemMatcher": [], - "group": "build", + "group": "none", "detail": "Creates a new collection manifest template.", "dependsOn": "npm install" + }, + { + "label": "create-skill", + "type": "shell", + "command": "npm run skill:create", + "args": [ + "--name", + "${input:skillName}", + "--description", + "${input:skillDescription}" + ], + "problemMatcher": [], + "group": "none", + "detail": "Creates a new skill template.", + "dependsOn": "npm install" + }, + { + "label": "validate-skills", + "type": "shell", + "command": "npm run skill:validate", + "problemMatcher": [], + "group": "build", + "detail": "Validates all skill manifest files.", + "dependsOn": "npm install" } ], "inputs": [ @@ -58,6 +82,18 @@ "description": "Comma separated list of tags", "default": "tag1,tag2", "type": "promptString" + }, + { + "id": "skillName", + "description": "Skill name (PascalCase)", + "default": "MySkill", + "type": "promptString" + }, + { + "id": "skillDescription", + "description": "Brief description of the skill", + "default": "A brief description of my skill.", + "type": "promptString" } ] } From 03cb605a4fa05800fc908d998d3cfc45c321a068 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 18 Dec 2025 11:02:21 +1100 Subject: [PATCH 5/7] supporting skills in collections --- .schemas/collection.schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.schemas/collection.schema.json b/.schemas/collection.schema.json index 838cdb14..22b0428b 100644 --- a/.schemas/collection.schema.json +++ b/.schemas/collection.schema.json @@ -50,13 +50,13 @@ "path": { "type": "string", "description": "Relative path from repository root to the item file", - "pattern": "^(prompts|instructions|agents)/[^/]+\\.(prompt|instructions|agent)\\.md$", + "pattern": "^(?:skills/[^/]+/SKILL\\.md|(prompts|instructions|agents)/[^/]+\\.(prompt|instructions|agent)\\.md)$", "minLength": 1 }, "kind": { "type": "string", "description": "Type of the item", - "enum": ["prompt", "instruction", "agent"] + "enum": ["prompt", "instruction", "agent", "skill"] }, "usage": { "type": "string", From 2b19cc3a3d7b5d3ed4bbb473c0b509ec4d1a1f39 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 18 Dec 2025 11:05:39 +1100 Subject: [PATCH 6/7] Skills rendering in collections --- eng/update-readme.mjs | 298 +++++++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 138 deletions(-) diff --git a/eng/update-readme.mjs b/eng/update-readme.mjs index faaae7d6..dcb4e611 100644 --- a/eng/update-readme.mjs +++ b/eng/update-readme.mjs @@ -1,30 +1,28 @@ #!/usr/bin/env node import fs from "fs"; -import path from "path"; +import path, { dirname } from "path"; import { fileURLToPath } from "url"; -import { dirname } from "path"; import { - parseCollectionYaml, - extractMcpServers, + AGENTS_DIR, + AKA_INSTALL_URLS, + COLLECTIONS_DIR, + DOCS_DIR, + INSTRUCTIONS_DIR, + PROMPTS_DIR, + repoBaseUrl, + ROOT_FOLDER, + SKILLS_DIR, + TEMPLATES, + vscodeInsidersInstallImage, + vscodeInstallImage, +} from "./constants.mjs"; +import { extractMcpServerConfigs, + parseCollectionYaml, parseFrontmatter, parseSkillMetadata, } from "./yaml-parser.mjs"; -import { - TEMPLATES, - AKA_INSTALL_URLS, - repoBaseUrl, - vscodeInstallImage, - vscodeInsidersInstallImage, - ROOT_FOLDER, - PROMPTS_DIR, - AGENTS_DIR, - SKILLS_DIR, - COLLECTIONS_DIR, - INSTRUCTIONS_DIR, - DOCS_DIR, -} from "./constants.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -55,14 +53,16 @@ async function loadMcpRegistryNames() { if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET; try { - console.log('Fetching MCP registry from API...'); + console.log("Fetching MCP registry from API..."); const allServers = []; 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 do { - const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl; + const url = cursor + ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` + : apiUrl; const response = await fetch(url); if (!response.ok) { @@ -78,8 +78,9 @@ async function loadMcpRegistryNames() { 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 || - serverName; + entry?.server?._meta?.[ + "io.modelcontextprotocol.registry/publisher-provided" + ]?.github?.displayName || serverName; allServers.push({ name: serverName, @@ -431,28 +432,31 @@ function generateMcpServerLinks(servers, registryNames) { // Match against both displayName and full name (case-insensitive) const serverNameLower = serverName.toLowerCase(); - const registryEntry = registryNames.find( - (entry) => { - // Exact match on displayName or fullName - if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) { + const registryEntry = registryNames.find((entry) => { + // Exact match on displayName or fullName + 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("/"); + if (nameParts.length > 1 && nameParts[1]) { + // Check if it matches the second part (after the slash) + const secondPart = nameParts[1] + .replace("-mcp-server", "") + .replace("-mcp", ""); + if (secondPart === 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('/'); - if (nameParts.length > 1 && nameParts[1]) { - // Check if it matches the second part (after the slash) - const secondPart = nameParts[1].replace('-mcp-server', '').replace('-mcp', ''); - if (secondPart === serverNameLower) { - return true; - } - } - - // Check if serverName matches the displayName ignoring case - return entry.displayName === serverNameLower; } - ); + + // Check if serverName matches the displayName ignoring case + return entry.displayName === serverNameLower; + }); const serverLabel = registryEntry ? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})` : serverName; @@ -489,12 +493,10 @@ function generateSkillsSection(skillsDir) { } // Get all skill folders (directories) - const skillFolders = fs - .readdirSync(skillsDir) - .filter((file) => { - const filePath = path.join(skillsDir, file); - return fs.statSync(filePath).isDirectory(); - }); + 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 @@ -768,7 +770,11 @@ function generateFeaturedCollectionsSection(collectionsDir) { * @param {string} collectionId - Collection ID * @param {{ name: string, displayName: string }[]} registryNames - Pre-loaded MCP registry names */ -function generateCollectionReadme(collection, collectionId, registryNames = []) { +function generateCollectionReadme( + collection, + collectionId, + registryNames = [] +) { if (!collection || !collection.items) { return `# ${collectionId}\n\nCollection not found or invalid.`; } @@ -820,20 +826,23 @@ function generateCollectionReadme(collection, collectionId, registryNames = []) ? "Instruction" : item.kind === "agent" ? "Agent" + : item.kind === "skill" + ? "Skill" : "Prompt"; const link = `../${item.path}`; - // Create install badges for each item - const badges = makeBadges( - item.path, + // Create install badges for each item (skills don't use chat install badges) + const badgeType = item.kind === "instruction" ? "instructions" : item.kind === "chat-mode" ? "mode" : item.kind === "agent" ? "agent" - : "prompt" - ); + : item.kind === "skill" + ? null + : "prompt"; + const badges = badgeType ? makeBadges(item.path, badgeType) : ""; const usageDescription = item.usage ? `${description} [see usage](#${title @@ -891,15 +900,21 @@ function buildCollectionRow({ kind, registryNames = [], }) { + const titleCell = badges + ? `[${title}](${link})
${badges}` + : `[${title}](${link})`; + if (hasAgents) { // Only agents currently have MCP servers; future migration may extend to chat modes. const mcpServers = kind === "agent" ? extractMcpServerConfigs(filePath) : []; const mcpServerCell = - mcpServers.length > 0 ? generateMcpServerLinks(mcpServers, registryNames) : ""; - return `| [${title}](${link})
${badges} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`; + mcpServers.length > 0 + ? generateMcpServerLinks(mcpServers, registryNames) + : ""; + return `| ${titleCell} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`; } - return `| [${title}](${link})
${badges} | ${typeDisplay} | ${usageDescription} |\n`; + return `| ${titleCell} | ${typeDisplay} | ${usageDescription} |\n`; } // Utility: write file only if content changed @@ -921,7 +936,13 @@ function writeFileIfChanged(filePath, content) { } // Build per-category README content using existing generators, upgrading headings to H1 -function buildCategoryReadme(sectionBuilder, dirPath, headerLine, usageLine, registryNames = []) { +function buildCategoryReadme( + sectionBuilder, + dirPath, + headerLine, + usageLine, + registryNames = [] +) { const section = sectionBuilder(dirPath, registryNames); if (section && section.trim()) { // Upgrade the first markdown heading level from ## to # for standalone README files @@ -984,104 +1005,106 @@ async function main() { registryNames ); - // Generate collections README - const collectionsReadme = buildCategoryReadme( - generateCollectionsSection, - COLLECTIONS_DIR, - collectionsHeader, - TEMPLATES.collectionsUsage, - registryNames - ); + // Generate collections README + const collectionsReadme = buildCategoryReadme( + generateCollectionsSection, + COLLECTIONS_DIR, + collectionsHeader, + TEMPLATES.collectionsUsage, + registryNames + ); - // Ensure docs directory exists for category outputs - if (!fs.existsSync(DOCS_DIR)) { - fs.mkdirSync(DOCS_DIR, { recursive: true }); - } + // Ensure docs directory exists for category outputs + if (!fs.existsSync(DOCS_DIR)) { + fs.mkdirSync(DOCS_DIR, { recursive: true }); + } - // Write category outputs into docs folder - writeFileIfChanged( - path.join(DOCS_DIR, "README.instructions.md"), - instructionsReadme - ); - 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 - ); + // Write category outputs into docs folder + writeFileIfChanged( + path.join(DOCS_DIR, "README.instructions.md"), + instructionsReadme + ); + 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 + ); - // Generate individual collection README files - if (fs.existsSync(COLLECTIONS_DIR)) { - console.log("Generating individual collection README files..."); + // Generate individual collection README files + if (fs.existsSync(COLLECTIONS_DIR)) { + console.log("Generating individual collection README files..."); - const collectionFiles = fs - .readdirSync(COLLECTIONS_DIR) - .filter((file) => file.endsWith(".collection.yml")); + const collectionFiles = fs + .readdirSync(COLLECTIONS_DIR) + .filter((file) => file.endsWith(".collection.yml")); - for (const file of collectionFiles) { - const filePath = path.join(COLLECTIONS_DIR, file); - const collection = parseCollectionYaml(filePath); + for (const file of collectionFiles) { + const filePath = path.join(COLLECTIONS_DIR, file); + const collection = parseCollectionYaml(filePath); - if (collection) { - const collectionId = - collection.id || path.basename(file, ".collection.yml"); - const readmeContent = generateCollectionReadme( - collection, - collectionId, - registryNames - ); - const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`); - writeFileIfChanged(readmeFile, readmeContent); + if (collection) { + const collectionId = + collection.id || path.basename(file, ".collection.yml"); + const readmeContent = generateCollectionReadme( + collection, + collectionId, + registryNames + ); + const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`); + writeFileIfChanged(readmeFile, readmeContent); + } } } - } - // Generate featured collections section and update main README.md - console.log("Updating main README.md with featured collections..."); - const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR); + // Generate featured collections section and update main README.md + console.log("Updating main README.md with featured collections..."); + const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR); - if (featuredSection) { - const mainReadmePath = path.join(ROOT_FOLDER, "README.md"); + if (featuredSection) { + const mainReadmePath = path.join(ROOT_FOLDER, "README.md"); - if (fs.existsSync(mainReadmePath)) { - let readmeContent = fs.readFileSync(mainReadmePath, "utf8"); + if (fs.existsSync(mainReadmePath)) { + let readmeContent = fs.readFileSync(mainReadmePath, "utf8"); - // Define markers to identify where to insert the featured collections - const startMarker = "## 🌟 Featured Collections"; - const endMarker = "## MCP Server"; + // Define markers to identify where to insert the featured collections + const startMarker = "## 🌟 Featured Collections"; + const endMarker = "## MCP Server"; - // Check if the section already exists - const startIndex = readmeContent.indexOf(startMarker); + // Check if the section already exists + const startIndex = readmeContent.indexOf(startMarker); - if (startIndex !== -1) { - // Section exists, replace it - const endIndex = readmeContent.indexOf(endMarker, startIndex); - if (endIndex !== -1) { - // Replace the existing section - const beforeSection = readmeContent.substring(0, startIndex); - const afterSection = readmeContent.substring(endIndex); - readmeContent = - beforeSection + featuredSection + "\n\n" + afterSection; + if (startIndex !== -1) { + // Section exists, replace it + const endIndex = readmeContent.indexOf(endMarker, startIndex); + if (endIndex !== -1) { + // Replace the existing section + const beforeSection = readmeContent.substring(0, startIndex); + const afterSection = readmeContent.substring(endIndex); + readmeContent = + beforeSection + featuredSection + "\n\n" + afterSection; + } + } else { + // Section doesn't exist, insert it before "## MCP Server" + const mcpIndex = readmeContent.indexOf(endMarker); + if (mcpIndex !== -1) { + const beforeMcp = readmeContent.substring(0, mcpIndex); + const afterMcp = readmeContent.substring(mcpIndex); + readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp; + } } + + writeFileIfChanged(mainReadmePath, readmeContent); + console.log("Main README.md updated with featured collections"); } else { - // Section doesn't exist, insert it before "## MCP Server" - const mcpIndex = readmeContent.indexOf(endMarker); - if (mcpIndex !== -1) { - const beforeMcp = readmeContent.substring(0, mcpIndex); - const afterMcp = readmeContent.substring(mcpIndex); - readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp; - } + console.warn( + "README.md not found, skipping featured collections update" + ); } - - writeFileIfChanged(mainReadmePath, readmeContent); - console.log("Main README.md updated with featured collections"); } else { - console.warn("README.md not found, skipping featured collections update"); + console.log("No featured collections found to add to README.md"); } - } else { - console.log("No featured collections found to add to README.md"); - } } catch (error) { console.error(`Error generating category README files: ${error.message}`); console.error(error.stack); @@ -1095,4 +1118,3 @@ main().catch((error) => { console.error(error.stack); process.exit(1); }); - From 9c69496a14f928dad2af74ad7cba58d3f6908d81 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Thu, 18 Dec 2025 09:53:16 -0800 Subject: [PATCH 7/7] Final skills URLs --- AGENTS.md | 2 +- docs/README.skills.md | 2 +- eng/constants.mjs | 2 +- eng/create-skill.mjs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c0ae3bba..352ba737 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,7 +78,7 @@ All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction fi - 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) +- Skills follow the [Agent Skills specification](https://agentskills.io/specification) ### Adding New Resources diff --git a/docs/README.skills.md b/docs/README.skills.md index bbdc6844..8c287652 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -1,6 +1,6 @@ # 🎯 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. +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://agentskills.io/specification), 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 diff --git a/eng/constants.mjs b/eng/constants.mjs index 879d686f..028fcea0 100644 --- a/eng/constants.mjs +++ b/eng/constants.mjs @@ -80,7 +80,7 @@ Custom agents for GitHub Copilot, making it easy for users and organizations to 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. +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://agentskills.io/specification), 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.`, diff --git a/eng/create-skill.mjs b/eng/create-skill.mjs index 6f076416..46686d29 100644 --- a/eng/create-skill.mjs +++ b/eng/create-skill.mjs @@ -202,7 +202,7 @@ Use this skill when you need to: console.log("\n📖 Resources:"); console.log( - " - Anthropic Skills Spec: https://github.com/anthropics/skills/blob/main/spec/skill-client-integration.md" + " - Anthropic Skills Spec: https://agentskills.io/specification" ); console.log( " - Project Documentation: AGENTS.md (section on Agent Skills)"