chore: comply with agent skills open stardard (#14)

This commit is contained in:
Pedro Rodrigues
2026-01-26 15:22:35 +00:00
committed by GitHub
parent 1712854a51
commit f421451c79
44 changed files with 311 additions and 116 deletions

View File

@@ -45,10 +45,117 @@ function parseSections(rulesDir: string): Section[] {
}
/**
* Load metadata from metadata.json
* Parse SKILL.md frontmatter to extract metadata
*/
function loadMetadata(metadataFile: string, skillName: string): Metadata {
if (!existsSync(metadataFile)) {
function parseSkillFrontmatter(content: string): Record<string, unknown> {
if (!content.startsWith("---")) {
return {};
}
const endIndex = content.indexOf("---", 3);
if (endIndex === -1) {
return {};
}
const frontmatterContent = content.slice(3, endIndex).trim();
const result: Record<string, unknown> = {};
let currentKey = "";
let inMetadata = false;
const metadataObj: Record<string, string> = {};
for (const line of frontmatterContent.split("\n")) {
// Check for metadata block start
if (line.trim() === "metadata:") {
inMetadata = true;
continue;
}
// Handle metadata nested values
if (inMetadata && line.startsWith(" ")) {
const colonIndex = line.indexOf(":");
if (colonIndex !== -1) {
const key = line.slice(0, colonIndex).trim();
let value = line.slice(colonIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
metadataObj[key] = value;
}
continue;
}
// End metadata block when we hit a non-indented line
if (inMetadata && !line.startsWith(" ") && line.trim()) {
inMetadata = false;
result.metadata = metadataObj;
}
// Handle top-level key-value
const colonIndex = line.indexOf(":");
if (colonIndex === -1) continue;
currentKey = line.slice(0, colonIndex).trim();
let value = line.slice(colonIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (value) {
result[currentKey] = value;
}
}
// Ensure metadata is captured if file ends in metadata block
if (inMetadata && Object.keys(metadataObj).length > 0) {
result.metadata = metadataObj;
}
return result;
}
/**
* Extract references from SKILL.md body
*/
function extractReferencesFromBody(content: string): string[] {
const references: string[] = [];
const lines = content.split("\n");
let inReferencesSection = false;
for (const line of lines) {
if (line.match(/^##\s+References/i)) {
inReferencesSection = true;
continue;
}
if (inReferencesSection) {
// Stop at next heading
if (line.startsWith("## ")) {
break;
}
// Match list items with URLs
const urlMatch = line.match(/^-\s*(https?:\/\/[^\s]+)/);
if (urlMatch) {
references.push(urlMatch[1]);
}
}
}
return references;
}
/**
* Load metadata from SKILL.md frontmatter (Agent Skills spec compliant)
*/
function loadMetadata(skillFile: string, skillName: string): Metadata {
if (!existsSync(skillFile)) {
return {
version: "1.0.0",
organization: "Supabase",
@@ -61,7 +168,25 @@ function loadMetadata(metadataFile: string, skillName: string): Metadata {
};
}
return JSON.parse(readFileSync(metadataFile, "utf-8"));
const content = readFileSync(skillFile, "utf-8");
const frontmatter = parseSkillFrontmatter(content);
const metadata = (frontmatter.metadata as Record<string, string>) || {};
return {
version: metadata.version || "1.0.0",
organization: metadata.organization || "Supabase",
date:
metadata.date ||
new Date().toLocaleDateString("en-US", {
month: "long",
year: "numeric",
}),
abstract:
metadata.abstract ||
(frontmatter.description as string) ||
`${skillName} guide for developers.`,
references: extractReferencesFromBody(content),
};
}
/**
@@ -104,14 +229,14 @@ function buildSkill(paths: SkillPaths): void {
console.log(`[${paths.name}] Building AGENTS.md...`);
// Load metadata and sections
const metadata = loadMetadata(paths.metadataFile, paths.name);
const sections = parseSections(paths.rulesDir);
const metadata = loadMetadata(paths.skillFile, paths.name);
const sections = parseSections(paths.referencesDir);
const sectionMap = generateSectionMap(sections);
const skillTitle = skillNameToTitle(paths.name);
// Check if rules directory exists
if (!existsSync(paths.rulesDir)) {
console.log(` No rules directory found. Generating empty AGENTS.md.`);
// Check if references directory exists
if (!existsSync(paths.referencesDir)) {
console.log(` No references directory found. Generating empty AGENTS.md.`);
writeFileSync(
paths.agentsOutput,
`# ${skillTitle}\n\nNo rules defined yet.\n`,
@@ -119,19 +244,19 @@ function buildSkill(paths: SkillPaths): void {
return;
}
// Get all rule files
const ruleFiles = readdirSync(paths.rulesDir)
// Get all reference files
const referenceFiles = readdirSync(paths.referencesDir)
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
.map((f) => join(paths.rulesDir, f));
.map((f) => join(paths.referencesDir, f));
if (ruleFiles.length === 0) {
console.log(` No rule files found. Generating empty AGENTS.md.`);
if (referenceFiles.length === 0) {
console.log(` No reference files found. Generating empty AGENTS.md.`);
}
// Parse and validate all rules
const rules: Rule[] = [];
for (const file of ruleFiles) {
for (const file of referenceFiles) {
const validation = validateRuleFile(file, sectionMap);
if (!validation.valid) {
console.error(` Skipping invalid file ${basename(file)}:`);

View File

@@ -15,18 +15,18 @@ export const SKILLS_ROOT = join(BUILD_DIR, "../../skills");
export interface SkillPaths {
name: string;
skillDir: string;
rulesDir: string;
referencesDir: string;
agentsOutput: string;
metadataFile: string;
skillFile: string;
}
// Discover all valid skills (directories with metadata.json)
// Discover all valid skills (directories with SKILL.md per Agent Skills spec)
export function discoverSkills(): string[] {
if (!existsSync(SKILLS_ROOT)) return [];
return readdirSync(SKILLS_ROOT, { withFileTypes: true })
.filter((d) => d.isDirectory())
.filter((d) => existsSync(join(SKILLS_ROOT, d.name, "metadata.json")))
.filter((d) => existsSync(join(SKILLS_ROOT, d.name, "SKILL.md")))
.map((d) => d.name);
}
@@ -36,16 +36,16 @@ export function getSkillPaths(skillName: string): SkillPaths {
return {
name: skillName,
skillDir,
rulesDir: join(skillDir, "rules"),
referencesDir: join(skillDir, "references"),
agentsOutput: join(skillDir, "AGENTS.md"),
metadataFile: join(skillDir, "metadata.json"),
skillFile: join(skillDir, "SKILL.md"),
};
}
// Validate skill exists
export function validateSkillExists(skillName: string): boolean {
const paths = getSkillPaths(skillName);
return existsSync(paths.metadataFile);
return existsSync(paths.skillFile);
}
// Valid impact levels in priority order

View File

@@ -44,14 +44,14 @@ function isGoodExample(label: string): boolean {
export function validateRuleFile(
filePath: string,
sectionMap?: Record<string, number>,
rulesDir?: string,
referencesDir?: string,
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Generate section map if not provided
if (!sectionMap && rulesDir) {
const sections = parseSections(rulesDir);
if (!sectionMap && referencesDir) {
const sections = parseSections(referencesDir);
sectionMap = generateSectionMap(sections);
} else if (!sectionMap) {
sectionMap = {};
@@ -138,25 +138,25 @@ export function validateRuleFile(
}
/**
* Validate all rule files for a skill
* Validate all reference files for a skill
*/
function validateSkill(paths: SkillPaths): boolean {
console.log(`[${paths.name}] Validating...`);
// Check if rules directory exists
if (!existsSync(paths.rulesDir)) {
console.log(` No rules directory found.`);
// Check if references directory exists
if (!existsSync(paths.referencesDir)) {
console.log(` No references directory found.`);
return true;
}
// Get section map
const sections = parseSections(paths.rulesDir);
const sections = parseSections(paths.referencesDir);
const sectionMap = generateSectionMap(sections);
// Get all markdown files (excluding _ prefixed files)
const files = readdirSync(paths.rulesDir)
const files = readdirSync(paths.referencesDir)
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
.map((f) => join(paths.rulesDir, f));
.map((f) => join(paths.referencesDir, f));
if (files.length === 0) {
console.log(` No rule files found.`);
@@ -168,7 +168,7 @@ function validateSkill(paths: SkillPaths): boolean {
let hasErrors = false;
for (const file of files) {
const result = validateRuleFile(file, sectionMap, paths.rulesDir);
const result = validateRuleFile(file, sectionMap, paths.referencesDir);
const filename = basename(file);
if (result.valid) {