mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
chore: comply with agent skills open stardard (#14)
This commit is contained in:
@@ -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)}:`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user