mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-02-05 09:33:07 +00:00
Compare commits
1 Commits
dickson/va
...
fix/yaml-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d041227f |
273
.github/scripts/validate-frontmatter.ts
vendored
273
.github/scripts/validate-frontmatter.ts
vendored
@@ -1,273 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
/**
|
|
||||||
* Validates YAML frontmatter in agent, skill, and command .md files.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* bun validate-frontmatter.ts # scan current directory
|
|
||||||
* bun validate-frontmatter.ts /path/to/dir # scan specific directory
|
|
||||||
* bun validate-frontmatter.ts file1.md file2.md # validate specific files
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { parse as parseYaml } from "yaml";
|
|
||||||
import { readdir, readFile } from "fs/promises";
|
|
||||||
import { basename, join, relative, resolve } from "path";
|
|
||||||
|
|
||||||
// Characters that require quoting in YAML values when unquoted:
|
|
||||||
// {} [] flow indicators, * anchor/alias, & anchor, # comment,
|
|
||||||
// ! tag, | > block scalars, % directive, @ ` reserved
|
|
||||||
const YAML_SPECIAL_CHARS = /[{}[\]*&#!|>%@`]/;
|
|
||||||
const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)---\s*\n?/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pre-process frontmatter text to quote values containing special YAML
|
|
||||||
* characters. This allows glob patterns like **\/*.{ts,tsx} to parse.
|
|
||||||
*/
|
|
||||||
function quoteSpecialValues(text: string): string {
|
|
||||||
const lines = text.split("\n");
|
|
||||||
const result: string[] = [];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const match = line.match(/^([a-zA-Z_-]+):\s+(.+)$/);
|
|
||||||
if (match) {
|
|
||||||
const [, key, value] = match;
|
|
||||||
if (!key || !value) {
|
|
||||||
result.push(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Skip already-quoted values
|
|
||||||
if (
|
|
||||||
(value.startsWith('"') && value.endsWith('"')) ||
|
|
||||||
(value.startsWith("'") && value.endsWith("'"))
|
|
||||||
) {
|
|
||||||
result.push(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (YAML_SPECIAL_CHARS.test(value)) {
|
|
||||||
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
||||||
result.push(`${key}: "${escaped}"`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParseResult {
|
|
||||||
frontmatter: Record<string, unknown>;
|
|
||||||
content: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFrontmatter(markdown: string): ParseResult {
|
|
||||||
const match = markdown.match(FRONTMATTER_REGEX);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
return {
|
|
||||||
frontmatter: {},
|
|
||||||
content: markdown,
|
|
||||||
error: "No frontmatter found",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const frontmatterText = quoteSpecialValues(match[1] || "");
|
|
||||||
const content = markdown.slice(match[0].length);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = parseYaml(frontmatterText);
|
|
||||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
||||||
return { frontmatter: parsed as Record<string, unknown>, content };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
frontmatter: {},
|
|
||||||
content,
|
|
||||||
error: `YAML parsed but result is not an object (got ${typeof parsed}${Array.isArray(parsed) ? " array" : ""})`,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
frontmatter: {},
|
|
||||||
content,
|
|
||||||
error: `YAML parse failed: ${err instanceof Error ? err.message : err}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Validation ---
|
|
||||||
|
|
||||||
type FileType = "agent" | "skill" | "command";
|
|
||||||
|
|
||||||
interface ValidationIssue {
|
|
||||||
level: "error" | "warning";
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateAgent(
|
|
||||||
frontmatter: Record<string, unknown>
|
|
||||||
): ValidationIssue[] {
|
|
||||||
const issues: ValidationIssue[] = [];
|
|
||||||
|
|
||||||
if (!frontmatter["name"] || typeof frontmatter["name"] !== "string") {
|
|
||||||
issues.push({ level: "error", message: 'Missing required "name" field' });
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!frontmatter["description"] ||
|
|
||||||
typeof frontmatter["description"] !== "string"
|
|
||||||
) {
|
|
||||||
issues.push({
|
|
||||||
level: "error",
|
|
||||||
message: 'Missing required "description" field',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateSkill(
|
|
||||||
frontmatter: Record<string, unknown>
|
|
||||||
): ValidationIssue[] {
|
|
||||||
const issues: ValidationIssue[] = [];
|
|
||||||
|
|
||||||
if (!frontmatter["description"] && !frontmatter["when_to_use"]) {
|
|
||||||
issues.push({
|
|
||||||
level: "error",
|
|
||||||
message: 'Missing required "description" field',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateCommand(
|
|
||||||
frontmatter: Record<string, unknown>
|
|
||||||
): ValidationIssue[] {
|
|
||||||
const issues: ValidationIssue[] = [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!frontmatter["description"] ||
|
|
||||||
typeof frontmatter["description"] !== "string"
|
|
||||||
) {
|
|
||||||
issues.push({
|
|
||||||
level: "error",
|
|
||||||
message: 'Missing required "description" field',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- File type detection ---
|
|
||||||
|
|
||||||
function detectFileType(filePath: string): FileType | null {
|
|
||||||
if (filePath.includes("/agents/")) return "agent";
|
|
||||||
if (filePath.includes("/skills/") && basename(filePath) === "SKILL.md")
|
|
||||||
return "skill";
|
|
||||||
if (filePath.includes("/commands/")) return "command";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- File discovery ---
|
|
||||||
|
|
||||||
async function findMdFiles(
|
|
||||||
baseDir: string
|
|
||||||
): Promise<{ path: string; type: FileType }[]> {
|
|
||||||
const results: { path: string; type: FileType }[] = [];
|
|
||||||
|
|
||||||
async function walk(dir: string) {
|
|
||||||
const entries = await readdir(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
await walk(fullPath);
|
|
||||||
} else if (entry.name.endsWith(".md")) {
|
|
||||||
const type = detectFileType(fullPath);
|
|
||||||
if (type) {
|
|
||||||
results.push({ path: fullPath, type });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await walk(baseDir);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Main ---
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
|
|
||||||
let files: { path: string; type: FileType }[];
|
|
||||||
let baseDir: string;
|
|
||||||
|
|
||||||
if (args.length > 0 && args.every((a) => a.endsWith(".md"))) {
|
|
||||||
baseDir = process.cwd();
|
|
||||||
files = [];
|
|
||||||
for (const arg of args) {
|
|
||||||
const fullPath = resolve(arg);
|
|
||||||
const type = detectFileType(fullPath);
|
|
||||||
if (type) {
|
|
||||||
files.push({ path: fullPath, type });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
baseDir = args[0] || process.cwd();
|
|
||||||
files = await findMdFiles(baseDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalErrors = 0;
|
|
||||||
let totalWarnings = 0;
|
|
||||||
|
|
||||||
console.log(`Validating ${files.length} frontmatter files...\n`);
|
|
||||||
|
|
||||||
for (const { path: filePath, type } of files) {
|
|
||||||
const rel = relative(baseDir, filePath);
|
|
||||||
const content = await readFile(filePath, "utf-8");
|
|
||||||
const result = parseFrontmatter(content);
|
|
||||||
|
|
||||||
const issues: ValidationIssue[] = [];
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
issues.push({ level: "error", message: result.error });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.error) {
|
|
||||||
switch (type) {
|
|
||||||
case "agent":
|
|
||||||
issues.push(...validateAgent(result.frontmatter));
|
|
||||||
break;
|
|
||||||
case "skill":
|
|
||||||
issues.push(...validateSkill(result.frontmatter));
|
|
||||||
break;
|
|
||||||
case "command":
|
|
||||||
issues.push(...validateCommand(result.frontmatter));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (issues.length > 0) {
|
|
||||||
console.log(`${rel} (${type})`);
|
|
||||||
for (const issue of issues) {
|
|
||||||
const prefix = issue.level === "error" ? " ERROR" : " WARN ";
|
|
||||||
console.log(`${prefix}: ${issue.message}`);
|
|
||||||
if (issue.level === "error") totalErrors++;
|
|
||||||
else totalWarnings++;
|
|
||||||
}
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("---");
|
|
||||||
console.log(
|
|
||||||
`Validated ${files.length} files: ${totalErrors} errors, ${totalWarnings} warnings`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (totalErrors > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error("Fatal error:", err);
|
|
||||||
process.exit(2);
|
|
||||||
});
|
|
||||||
34
.github/workflows/validate-frontmatter.yml
vendored
34
.github/workflows/validate-frontmatter.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Validate Frontmatter
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**/agents/*.md'
|
|
||||||
- '**/skills/*/SKILL.md'
|
|
||||||
- '**/commands/*.md'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: cd .github/scripts && bun install yaml
|
|
||||||
|
|
||||||
- name: Get changed frontmatter files
|
|
||||||
id: changed
|
|
||||||
run: |
|
|
||||||
FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep -E '(agents/.*\.md|skills/.*/SKILL\.md|commands/.*\.md)$' || true)
|
|
||||||
echo "files<<EOF" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "$FILES" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Validate frontmatter
|
|
||||||
if: steps.changed.outputs.files != ''
|
|
||||||
run: |
|
|
||||||
echo "${{ steps.changed.outputs.files }}" | xargs bun .github/scripts/validate-frontmatter.ts
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: agent-creator
|
name: agent-creator
|
||||||
description: Use this agent when the user asks to "create an agent", "generate an agent", "build a new agent", "make me an agent that...", or describes agent functionality they need. Trigger when user wants to create autonomous agents for plugins. Examples:
|
description: |
|
||||||
|
Use this agent when the user asks to "create an agent", "generate an agent", "build a new agent", "make me an agent that...", or describes agent functionality they need. Trigger when user wants to create autonomous agents for plugins. Examples:
|
||||||
|
|
||||||
<example>
|
<example>
|
||||||
Context: User wants to create a code review agent
|
Context: User wants to create a code review agent
|
||||||
@@ -28,7 +29,6 @@ assistant: "I'll use the agent-creator agent to generate a configuration validat
|
|||||||
Plugin development with agent addition, trigger agent-creator.
|
Plugin development with agent addition, trigger agent-creator.
|
||||||
</commentary>
|
</commentary>
|
||||||
</example>
|
</example>
|
||||||
|
|
||||||
model: sonnet
|
model: sonnet
|
||||||
color: magenta
|
color: magenta
|
||||||
tools: ["Write", "Read"]
|
tools: ["Write", "Read"]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: plugin-validator
|
name: plugin-validator
|
||||||
description: Use this agent when the user asks to "validate my plugin", "check plugin structure", "verify plugin is correct", "validate plugin.json", "check plugin files", or mentions plugin validation. Also trigger proactively after user creates or modifies plugin components. Examples:
|
description: |
|
||||||
|
Use this agent when the user asks to "validate my plugin", "check plugin structure", "verify plugin is correct", "validate plugin.json", "check plugin files", or mentions plugin validation. Also trigger proactively after user creates or modifies plugin components. Examples:
|
||||||
|
|
||||||
<example>
|
<example>
|
||||||
Context: User finished creating a new plugin
|
Context: User finished creating a new plugin
|
||||||
@@ -30,7 +31,6 @@ Manifest modified, validate to ensure correctness.
|
|||||||
</commentary>
|
</commentary>
|
||||||
assistant: "I'll use the plugin-validator agent to check the manifest."
|
assistant: "I'll use the plugin-validator agent to check the manifest."
|
||||||
</example>
|
</example>
|
||||||
|
|
||||||
model: inherit
|
model: inherit
|
||||||
color: yellow
|
color: yellow
|
||||||
tools: ["Read", "Grep", "Glob", "Bash"]
|
tools: ["Read", "Grep", "Glob", "Bash"]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: skill-reviewer
|
name: skill-reviewer
|
||||||
description: Use this agent when the user has created or modified a skill and needs quality review, asks to "review my skill", "check skill quality", "improve skill description", or wants to ensure skill follows best practices. Trigger proactively after skill creation. Examples:
|
description: |
|
||||||
|
Use this agent when the user has created or modified a skill and needs quality review, asks to "review my skill", "check skill quality", "improve skill description", or wants to ensure skill follows best practices. Trigger proactively after skill creation. Examples:
|
||||||
|
|
||||||
<example>
|
<example>
|
||||||
Context: User just created a new skill
|
Context: User just created a new skill
|
||||||
@@ -29,7 +30,6 @@ assistant: "I'll use the skill-reviewer agent to review the changes."
|
|||||||
Skill description modified, review for triggering effectiveness.
|
Skill description modified, review for triggering effectiveness.
|
||||||
</commentary>
|
</commentary>
|
||||||
</example>
|
</example>
|
||||||
|
|
||||||
model: inherit
|
model: inherit
|
||||||
color: cyan
|
color: cyan
|
||||||
tools: ["Read", "Grep", "Glob"]
|
tools: ["Read", "Grep", "Glob"]
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
---
|
---
|
||||||
name: code-simplifier
|
name: code-simplifier
|
||||||
description: Use this agent when code has been written or modified and needs to be simplified for clarity, consistency, and maintainability while preserving all functionality. This agent should be triggered automatically after completing a coding task or writing a logical chunk of code. It simplifies code by following project best practices while retaining all functionality. The agent focuses only on recently modified code unless instructed otherwise.\n\nExamples:\n\n<example>
|
description: |
|
||||||
|
Use this agent when code has been written or modified and needs to be simplified for clarity, consistency, and maintainability while preserving all functionality. This agent should be triggered automatically after completing a coding task or writing a logical chunk of code. It simplifies code by following project best practices while retaining all functionality. The agent focuses only on recently modified code unless instructed otherwise.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
<example>
|
||||||
Context: The assistant has just implemented a new feature that adds user authentication to an API endpoint.
|
Context: The assistant has just implemented a new feature that adds user authentication to an API endpoint.
|
||||||
user: "Please add authentication to the /api/users endpoint"
|
user: "Please add authentication to the /api/users endpoint"
|
||||||
assistant: "I've implemented the authentication for the /api/users endpoint. Here's the code:"
|
assistant: "I've implemented the authentication for the /api/users endpoint. Here's the code:"
|
||||||
|
|||||||
Reference in New Issue
Block a user