mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
chore: build system skill body (#42)
* feat: extract SKILL.md body into AGENTS.md with H1 title and Overview section Build system now parses SKILL.md body to extract H1 heading as the AGENTS.md title and places remaining content under an Overview section. Adds validation that SKILL.md body starts with H1, directory name is kebab-case, and name field matches directory name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: AGENTS.md is now SKILL.md body with frontmatter stripped Build now generates AGENTS.md by extracting the SKILL.md markdown body (everything after YAML frontmatter). CLAUDE.md remains a symlink to AGENTS.md. Removes content generation logic (Structure, Usage, Overview, Reference Categories, Available References sections) — SKILL.md is the single source of truth for agent instructions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Structure and Usage sections to AGENTS.md, validate H1 title matches directory name Build now generates AGENTS.md as: H1 Title > Structure > Usage > rest of SKILL.md body. Validates that SKILL.md body starts with H1 heading and that the title in kebab-case matches the directory name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ Guidance for AI coding agents working with this repository.
|
||||
skills/
|
||||
{skill-name}/
|
||||
SKILL.md # Required: skill manifest (Agent Skills spec)
|
||||
AGENTS.md # Generated: navigation guide for agents
|
||||
AGENTS.md # Generated: SKILL.md body (frontmatter stripped)
|
||||
CLAUDE.md # Generated: symlink to AGENTS.md
|
||||
references/
|
||||
_sections.md # Required: section definitions
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
discoverSkills,
|
||||
getSkillPaths,
|
||||
@@ -77,78 +77,19 @@ function parseSections(rulesDir: string): Section[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SKILL.md frontmatter to extract metadata
|
||||
* Extract the markdown body from SKILL.md (everything after frontmatter)
|
||||
*/
|
||||
function parseSkillFrontmatter(content: string): Record<string, unknown> {
|
||||
function extractSkillBody(content: string): string {
|
||||
if (!content.startsWith("---")) {
|
||||
return {};
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
const endIndex = content.indexOf("---", 3);
|
||||
if (endIndex === -1) {
|
||||
return {};
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
const frontmatterContent = content.slice(3, endIndex).trim();
|
||||
const result: Record<string, unknown> = {};
|
||||
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;
|
||||
|
||||
const 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;
|
||||
return content.slice(endIndex + 3).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,22 +160,29 @@ function getReferenceFiles(referencesDir: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert skill name to title (e.g., "postgres-best-practices" -> "Postgres Best Practices")
|
||||
* Parse the SKILL.md body into its H1 title and the content after it.
|
||||
* Returns the title text and the remaining body content.
|
||||
*/
|
||||
function skillNameToTitle(skillName: string): string {
|
||||
return skillName
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
function parseSkillBodySections(body: string): {
|
||||
title: string | null;
|
||||
content: string;
|
||||
} {
|
||||
const lines = body.split("\n");
|
||||
const firstLine = lines[0]?.trim() ?? "";
|
||||
|
||||
const h1Match = firstLine.match(/^#\s+(.+)$/);
|
||||
if (!h1Match) {
|
||||
return { title: null, content: body };
|
||||
}
|
||||
|
||||
const content = lines.slice(1).join("\n").trim();
|
||||
return { title: h1Match[1].trim(), content };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CLAUDE.md symlink pointing to AGENTS.md
|
||||
* Create a symlink, removing any existing file or symlink at the target path
|
||||
*/
|
||||
function createClaudeSymlink(paths: SkillPaths): void {
|
||||
const symlinkPath = paths.claudeSymlink;
|
||||
|
||||
// Remove existing symlink or file if it exists
|
||||
function createSymlink(symlinkPath: string, target: string): void {
|
||||
if (existsSync(symlinkPath)) {
|
||||
const stat = lstatSync(symlinkPath);
|
||||
if (stat.isSymbolicLink() || stat.isFile()) {
|
||||
@@ -242,46 +190,33 @@ function createClaudeSymlink(paths: SkillPaths): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Create symlink (relative path so it works across environments)
|
||||
symlinkSync("AGENTS.md", symlinkPath);
|
||||
console.log(` Created symlink: CLAUDE.md -> AGENTS.md`);
|
||||
symlinkSync(target, symlinkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build AGENTS.md for a specific skill
|
||||
*
|
||||
* AGENTS.md is a concise navigation guide for AI agents, NOT a comprehensive
|
||||
* documentation dump. It helps agents understand the skill directory structure
|
||||
* and how to find information.
|
||||
* Structure: H1 Title > Structure > Usage > rest of SKILL.md body
|
||||
* CLAUDE.md = symlink to AGENTS.md.
|
||||
*/
|
||||
function buildSkill(paths: SkillPaths): void {
|
||||
console.log(`[${paths.name}] Building AGENTS.md...`);
|
||||
|
||||
// Read SKILL.md for metadata
|
||||
// Read SKILL.md and strip frontmatter
|
||||
const skillContent = existsSync(paths.skillFile)
|
||||
? readFileSync(paths.skillFile, "utf-8")
|
||||
: "";
|
||||
const frontmatter = parseSkillFrontmatter(skillContent);
|
||||
const skillTitle = skillNameToTitle(paths.name);
|
||||
const description =
|
||||
(frontmatter.description as string) || `${skillTitle} skill for AI agents.`;
|
||||
const body = extractSkillBody(skillContent);
|
||||
const { title, content: skillBodyContent } = parseSkillBodySections(body);
|
||||
|
||||
// Parse sections if available
|
||||
const sections = parseAllSections(paths.referencesDir);
|
||||
const referenceFiles = getReferenceFiles(paths.referencesDir);
|
||||
|
||||
// Generate concise AGENTS.md
|
||||
const output: string[] = [];
|
||||
|
||||
// Header
|
||||
output.push(`# ${paths.name}\n`);
|
||||
output.push(`> **Note:** \`CLAUDE.md\` is a symlink to this file.\n`);
|
||||
// 1. Title (from SKILL.md H1)
|
||||
if (title) {
|
||||
output.push(`# ${title}\n`);
|
||||
}
|
||||
|
||||
// Brief description
|
||||
output.push(`## Overview\n`);
|
||||
output.push(`${description}\n`);
|
||||
|
||||
// Directory structure
|
||||
// 2. Structure
|
||||
output.push(`## Structure\n`);
|
||||
output.push("```");
|
||||
output.push(`${paths.name}/`);
|
||||
@@ -293,7 +228,7 @@ function buildSkill(paths: SkillPaths): void {
|
||||
}
|
||||
output.push("```\n");
|
||||
|
||||
// How to use
|
||||
// 3. Usage
|
||||
output.push(`## Usage\n`);
|
||||
output.push(`1. Read \`SKILL.md\` for the main skill instructions`);
|
||||
output.push(
|
||||
@@ -303,59 +238,19 @@ function buildSkill(paths: SkillPaths): void {
|
||||
`3. Reference files are loaded on-demand - read only what you need\n`,
|
||||
);
|
||||
|
||||
// Reference sections (if available)
|
||||
if (sections.length > 0) {
|
||||
output.push(`## Reference Categories\n`);
|
||||
output.push(`| Priority | Category | Impact | Prefix |`);
|
||||
output.push(`|----------|----------|--------|--------|`);
|
||||
for (const section of sections.sort((a, b) => a.number - b.number)) {
|
||||
output.push(
|
||||
`| ${section.number} | ${section.title} | ${section.impact} | \`${section.prefix}-\` |`,
|
||||
);
|
||||
}
|
||||
output.push("");
|
||||
output.push(
|
||||
`Reference files are named \`{prefix}-{topic}.md\` (e.g., \`query-missing-indexes.md\`).\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// Reference file list (just filenames, not content)
|
||||
if (referenceFiles.length > 0) {
|
||||
output.push(`## Available References\n`);
|
||||
const grouped = new Map<string, string[]>();
|
||||
|
||||
for (const file of referenceFiles) {
|
||||
const name = basename(file, ".md");
|
||||
const prefix = name.split("-")[0];
|
||||
const group = grouped.get(prefix) || [];
|
||||
group.push(name);
|
||||
grouped.set(prefix, group);
|
||||
}
|
||||
|
||||
for (const [prefix, files] of grouped) {
|
||||
const section = sections.find((s) => s.prefix === prefix);
|
||||
const title = section ? section.title : prefix;
|
||||
output.push(`**${title}** (\`${prefix}-\`):`);
|
||||
for (const file of files.sort()) {
|
||||
output.push(`- \`references/${file}.md\``);
|
||||
}
|
||||
// 4. Rest of SKILL.md body (after H1 title)
|
||||
if (skillBodyContent) {
|
||||
output.push(skillBodyContent);
|
||||
output.push("");
|
||||
}
|
||||
}
|
||||
|
||||
// Stats
|
||||
output.push(`---\n`);
|
||||
output.push(
|
||||
`*${referenceFiles.length} reference files across ${sections.length} categories*`,
|
||||
);
|
||||
|
||||
// Write AGENTS.md
|
||||
writeFileSync(paths.agentsOutput, output.join("\n"));
|
||||
console.log(` Generated: ${paths.agentsOutput}`);
|
||||
console.log(` Total references: ${referenceFiles.length}`);
|
||||
|
||||
// Create CLAUDE.md symlink
|
||||
createClaudeSymlink(paths);
|
||||
// Create CLAUDE.md -> AGENTS.md symlink
|
||||
createSymlink(paths.claudeSymlink, "AGENTS.md");
|
||||
console.log(` Created symlink: CLAUDE.md -> AGENTS.md`);
|
||||
}
|
||||
|
||||
// Run build when executed directly
|
||||
@@ -397,11 +292,12 @@ if (isMainModule) {
|
||||
|
||||
export {
|
||||
buildSkill,
|
||||
extractSkillBody,
|
||||
generateSectionMap,
|
||||
getMarkdownFiles,
|
||||
getMarkdownFilesRecursive, // deprecated, use getMarkdownFiles
|
||||
getReferenceFiles,
|
||||
parseAllSections,
|
||||
parseSkillBodySections,
|
||||
parseSections,
|
||||
skillNameToTitle,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import {
|
||||
extractSkillBody,
|
||||
generateSectionMap,
|
||||
getMarkdownFiles,
|
||||
parseAllSections,
|
||||
parseSections,
|
||||
parseSkillBodySections,
|
||||
} from "./build.js";
|
||||
import {
|
||||
discoverSkills,
|
||||
@@ -142,12 +144,102 @@ export function validateRuleFile(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the `name` field from SKILL.md frontmatter
|
||||
*/
|
||||
function extractSkillName(skillFilePath: string): string | null {
|
||||
if (!existsSync(skillFilePath)) return null;
|
||||
|
||||
const content = readFileSync(skillFilePath, "utf-8");
|
||||
if (!content.startsWith("---")) return null;
|
||||
|
||||
const endIndex = content.indexOf("---", 3);
|
||||
if (endIndex === -1) return null;
|
||||
|
||||
const frontmatter = content.slice(3, endIndex);
|
||||
for (const line of frontmatter.split("\n")) {
|
||||
const match = line.match(/^name:\s*(.+)/);
|
||||
if (match) return match[1].trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a title to kebab-case (e.g., "Supabase Postgres Best Practices" -> "supabase-postgres-best-practices")
|
||||
*/
|
||||
function titleToKebab(title: string): string {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SKILL.md structure:
|
||||
* - `name` field matches directory name (kebab-case)
|
||||
* - Body starts with an H1 heading
|
||||
* - H1 title in kebab-case matches directory name
|
||||
*/
|
||||
function validateSkillStructure(paths: SkillPaths): string[] {
|
||||
const errors: string[] = [];
|
||||
const skillName = extractSkillName(paths.skillFile);
|
||||
|
||||
if (!skillName) {
|
||||
errors.push("SKILL.md is missing the `name` field in frontmatter");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (skillName !== paths.name) {
|
||||
errors.push(
|
||||
`SKILL.md name "${skillName}" does not match directory name "${paths.name}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate kebab-case format
|
||||
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(paths.name)) {
|
||||
errors.push(
|
||||
`Directory name "${paths.name}" is not valid kebab-case (lowercase alphanumeric and hyphens only, no leading/trailing/consecutive hyphens)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate body starts with H1 and title matches directory name in kebab-case
|
||||
if (existsSync(paths.skillFile)) {
|
||||
const content = readFileSync(paths.skillFile, "utf-8");
|
||||
const body = extractSkillBody(content);
|
||||
const { title } = parseSkillBodySections(body);
|
||||
|
||||
if (!title) {
|
||||
errors.push(
|
||||
"SKILL.md body must start with an H1 heading (e.g., `# Skill Title`)",
|
||||
);
|
||||
} else {
|
||||
const kebabTitle = titleToKebab(title);
|
||||
if (kebabTitle !== paths.name) {
|
||||
errors.push(
|
||||
`H1 title "${title}" in kebab-case is "${kebabTitle}", but directory name is "${paths.name}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all reference files for a skill
|
||||
*/
|
||||
function validateSkill(paths: SkillPaths): boolean {
|
||||
console.log(`[${paths.name}] Validating...`);
|
||||
|
||||
// Validate skill structure (name, kebab-case, H1 heading)
|
||||
const nameErrors = validateSkillStructure(paths);
|
||||
if (nameErrors.length > 0) {
|
||||
for (const error of nameErrors) {
|
||||
console.log(` ERROR: ${error}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if references directory exists
|
||||
if (!existsSync(paths.referencesDir)) {
|
||||
console.log(` No references directory found.`);
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
# supabase-postgres-best-practices
|
||||
|
||||
> **Note:** `CLAUDE.md` is a symlink to this file.
|
||||
|
||||
## Overview
|
||||
|
||||
Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.
|
||||
# Supabase Postgres Best Practices
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -22,7 +16,19 @@ supabase-postgres-best-practices/
|
||||
2. Browse `references/` for detailed documentation on specific topics
|
||||
3. Reference files are loaded on-demand - read only what you need
|
||||
|
||||
## Reference Categories
|
||||
Comprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Writing SQL queries or designing schemas
|
||||
- Implementing indexes or query optimization
|
||||
- Reviewing database performance issues
|
||||
- Configuring connection pooling or scaling
|
||||
- Optimizing for Postgres-specific features
|
||||
- Working with Row-Level Security (RLS)
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
@@ -35,57 +41,28 @@ supabase-postgres-best-practices/
|
||||
| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |
|
||||
| 8 | Advanced Features | LOW | `advanced-` |
|
||||
|
||||
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
|
||||
## How to Use
|
||||
|
||||
## Available References
|
||||
Read individual rule files for detailed explanations and SQL examples:
|
||||
|
||||
**Advanced Features** (`advanced-`):
|
||||
- `references/advanced-full-text-search.md`
|
||||
- `references/advanced-jsonb-indexing.md`
|
||||
```
|
||||
references/query-missing-indexes.md
|
||||
references/schema-partial-indexes.md
|
||||
references/_sections.md
|
||||
```
|
||||
|
||||
**Connection Management** (`conn-`):
|
||||
- `references/conn-idle-timeout.md`
|
||||
- `references/conn-limits.md`
|
||||
- `references/conn-pooling.md`
|
||||
- `references/conn-prepared-statements.md`
|
||||
Each rule file contains:
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect SQL example with explanation
|
||||
- Correct SQL example with explanation
|
||||
- Optional EXPLAIN output or metrics
|
||||
- Additional context and references
|
||||
- Supabase-specific notes (when applicable)
|
||||
|
||||
**Data Access Patterns** (`data-`):
|
||||
- `references/data-batch-inserts.md`
|
||||
- `references/data-n-plus-one.md`
|
||||
- `references/data-pagination.md`
|
||||
- `references/data-upsert.md`
|
||||
## References
|
||||
|
||||
**Concurrency & Locking** (`lock-`):
|
||||
- `references/lock-advisory.md`
|
||||
- `references/lock-deadlock-prevention.md`
|
||||
- `references/lock-short-transactions.md`
|
||||
- `references/lock-skip-locked.md`
|
||||
|
||||
**Monitoring & Diagnostics** (`monitor-`):
|
||||
- `references/monitor-explain-analyze.md`
|
||||
- `references/monitor-pg-stat-statements.md`
|
||||
- `references/monitor-vacuum-analyze.md`
|
||||
|
||||
**Query Performance** (`query-`):
|
||||
- `references/query-composite-indexes.md`
|
||||
- `references/query-covering-indexes.md`
|
||||
- `references/query-index-types.md`
|
||||
- `references/query-missing-indexes.md`
|
||||
- `references/query-partial-indexes.md`
|
||||
|
||||
**Schema Design** (`schema-`):
|
||||
- `references/schema-constraints.md`
|
||||
- `references/schema-data-types.md`
|
||||
- `references/schema-foreign-key-indexes.md`
|
||||
- `references/schema-lowercase-identifiers.md`
|
||||
- `references/schema-partitioning.md`
|
||||
- `references/schema-primary-keys.md`
|
||||
|
||||
**Security & RLS** (`security-`):
|
||||
- `references/security-privileges.md`
|
||||
- `references/security-rls-basics.md`
|
||||
- `references/security-rls-performance.md`
|
||||
|
||||
---
|
||||
|
||||
*31 reference files across 8 categories*
|
||||
- https://www.postgresql.org/docs/current/
|
||||
- https://supabase.com/docs
|
||||
- https://wiki.postgresql.org/wiki/Performance_Optimization
|
||||
- https://supabase.com/docs/guides/database/overview
|
||||
- https://supabase.com/docs/guides/auth/row-level-security
|
||||
|
||||
Reference in New Issue
Block a user