mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
reduxe the agents.md file size (#20)
This commit is contained in:
@@ -10,7 +10,8 @@ Guidance for AI coding agents working with this repository.
|
||||
skills/
|
||||
{skill-name}/
|
||||
SKILL.md # Required: skill manifest (Agent Skills spec)
|
||||
AGENTS.md # Generated: compiled references
|
||||
AGENTS.md # Generated: navigation guide for agents
|
||||
CLAUDE.md # Generated: symlink to AGENTS.md
|
||||
references/
|
||||
_sections.md # Required: section definitions
|
||||
{prefix}-{name}.md # Reference files
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
@@ -12,215 +15,12 @@ import {
|
||||
type SkillPaths,
|
||||
validateSkillExists,
|
||||
} from "./config.js";
|
||||
import { parseRuleFile } from "./parser.js";
|
||||
import type { Metadata, Rule, Section } from "./types.js";
|
||||
import { validateRuleFile } from "./validate.js";
|
||||
|
||||
/**
|
||||
* Parse section definitions from _sections.md
|
||||
*/
|
||||
function parseSections(rulesDir: string): Section[] {
|
||||
const sectionsFile = join(rulesDir, "_sections.md");
|
||||
if (!existsSync(sectionsFile)) {
|
||||
console.warn("Warning: _sections.md not found, using empty sections");
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = readFileSync(sectionsFile, "utf-8");
|
||||
const sections: Section[] = [];
|
||||
|
||||
// Match format: Impact and Description on separate lines
|
||||
// ## 1. Query Performance (query)
|
||||
// **Impact:** CRITICAL
|
||||
// **Description:** Description text
|
||||
const sectionMatches = content.matchAll(
|
||||
/##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g,
|
||||
);
|
||||
|
||||
for (const match of sectionMatches) {
|
||||
sections.push({
|
||||
number: parseInt(match[1], 10),
|
||||
title: match[2].trim(),
|
||||
prefix: match[3].trim(),
|
||||
impact: match[4].trim() as Section["impact"],
|
||||
description: match[5].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SKILL.md frontmatter to extract metadata
|
||||
*/
|
||||
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",
|
||||
date: new Date().toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}),
|
||||
abstract: `${skillName} guide for developers.`,
|
||||
references: [],
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate anchor from title
|
||||
*/
|
||||
function toAnchor(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert skill name to title (e.g., "postgres-best-practices" -> "Postgres Best Practices")
|
||||
*/
|
||||
function skillNameToTitle(skillName: string): string {
|
||||
return skillName
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
import type { Section } from "./types.js";
|
||||
|
||||
/**
|
||||
* Generate SECTION_MAP from parsed sections
|
||||
*/
|
||||
export function generateSectionMap(
|
||||
sections: Section[],
|
||||
): Record<string, number> {
|
||||
function generateSectionMap(sections: Section[]): Record<string, number> {
|
||||
const map: Record<string, number> = {};
|
||||
for (const section of sections) {
|
||||
map[section.prefix] = section.number;
|
||||
@@ -260,6 +60,116 @@ function getMarkdownFilesRecursive(dir: string): string[] {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse section definitions from _sections.md (legacy function for validation)
|
||||
*/
|
||||
function parseSections(rulesDir: string): Section[] {
|
||||
const sectionsFile = join(rulesDir, "_sections.md");
|
||||
if (!existsSync(sectionsFile)) {
|
||||
return [];
|
||||
}
|
||||
return parseSectionsFromFile(sectionsFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SKILL.md frontmatter to extract metadata
|
||||
*/
|
||||
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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse section definitions from a _sections.md file
|
||||
*/
|
||||
function parseSectionsFromFile(filePath: string): Section[] {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const sections: Section[] = [];
|
||||
|
||||
const sectionMatches = content.matchAll(
|
||||
/##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g,
|
||||
);
|
||||
|
||||
for (const match of sectionMatches) {
|
||||
sections.push({
|
||||
number: parseInt(match[1], 10),
|
||||
title: match[2].trim(),
|
||||
prefix: match[3].trim(),
|
||||
impact: match[4].trim() as Section["impact"],
|
||||
description: match[5].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all _sections.md files from references directory and subdirectories
|
||||
*/
|
||||
@@ -290,202 +200,180 @@ function parseAllSections(referencesDir: string): Section[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse section definitions from a specific _sections.md file
|
||||
* Get all reference files (excluding _sections.md)
|
||||
*/
|
||||
function parseSectionsFromFile(filePath: string): Section[] {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const sections: Section[] = [];
|
||||
function getReferenceFiles(referencesDir: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
const sectionMatches = content.matchAll(
|
||||
/##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g,
|
||||
);
|
||||
|
||||
for (const match of sectionMatches) {
|
||||
sections.push({
|
||||
number: parseInt(match[1], 10),
|
||||
title: match[2].trim(),
|
||||
prefix: match[3].trim(),
|
||||
impact: match[4].trim() as Section["impact"],
|
||||
description: match[5].trim(),
|
||||
});
|
||||
if (!existsSync(referencesDir)) {
|
||||
return files;
|
||||
}
|
||||
|
||||
return sections;
|
||||
const entries = readdirSync(referencesDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip files starting with underscore
|
||||
if (entry.startsWith("_")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = join(referencesDir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// Recursively scan subdirectories
|
||||
const subEntries = readdirSync(fullPath);
|
||||
for (const subEntry of subEntries) {
|
||||
if (!subEntry.startsWith("_") && subEntry.endsWith(".md")) {
|
||||
files.push(join(fullPath, subEntry));
|
||||
}
|
||||
}
|
||||
} else if (entry.endsWith(".md")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert skill name to title (e.g., "postgres-best-practices" -> "Postgres Best Practices")
|
||||
*/
|
||||
function skillNameToTitle(skillName: string): string {
|
||||
return skillName
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CLAUDE.md symlink pointing to AGENTS.md
|
||||
*/
|
||||
function createClaudeSymlink(paths: SkillPaths): void {
|
||||
const symlinkPath = paths.claudeSymlink;
|
||||
|
||||
// Remove existing symlink or file if it exists
|
||||
if (existsSync(symlinkPath)) {
|
||||
const stat = lstatSync(symlinkPath);
|
||||
if (stat.isSymbolicLink() || stat.isFile()) {
|
||||
unlinkSync(symlinkPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Create symlink (relative path so it works across environments)
|
||||
symlinkSync("AGENTS.md", symlinkPath);
|
||||
console.log(` Created symlink: CLAUDE.md -> AGENTS.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function buildSkill(paths: SkillPaths): void {
|
||||
console.log(`[${paths.name}] Building AGENTS.md...`);
|
||||
|
||||
// Load metadata and sections (including from subdirectories)
|
||||
const metadata = loadMetadata(paths.skillFile, paths.name);
|
||||
const sections = parseAllSections(paths.referencesDir);
|
||||
const sectionMap = generateSectionMap(sections);
|
||||
// Read SKILL.md for metadata
|
||||
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.`;
|
||||
|
||||
// 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`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Parse sections if available
|
||||
const sections = parseAllSections(paths.referencesDir);
|
||||
const referenceFiles = getReferenceFiles(paths.referencesDir);
|
||||
|
||||
// Get all reference files recursively
|
||||
const referenceFiles = getMarkdownFilesRecursive(paths.referencesDir);
|
||||
|
||||
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 referenceFiles) {
|
||||
const validation = validateRuleFile(file, sectionMap);
|
||||
if (!validation.valid) {
|
||||
console.error(` Skipping invalid file ${basename(file)}:`);
|
||||
for (const e of validation.errors) {
|
||||
console.error(` - ${e}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = parseRuleFile(file, sectionMap);
|
||||
if (result.success && result.rule) {
|
||||
rules.push(result.rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Group rules by section and assign IDs
|
||||
const rulesBySection = new Map<number, Rule[]>();
|
||||
|
||||
for (const rule of rules) {
|
||||
const sectionRules = rulesBySection.get(rule.section) || [];
|
||||
sectionRules.push(rule);
|
||||
rulesBySection.set(rule.section, sectionRules);
|
||||
}
|
||||
|
||||
// Sort rules within each section and assign IDs
|
||||
for (const [sectionNum, sectionRules] of rulesBySection) {
|
||||
sectionRules.sort((a, b) => a.title.localeCompare(b.title));
|
||||
sectionRules.forEach((rule, index) => {
|
||||
rule.id = `${sectionNum}.${index + 1}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate markdown output
|
||||
// Generate concise AGENTS.md
|
||||
const output: string[] = [];
|
||||
|
||||
// Header
|
||||
output.push(`# ${skillTitle}\n`);
|
||||
output.push(`**Version ${metadata.version}**`);
|
||||
output.push(`${metadata.organization}`);
|
||||
output.push(`${metadata.date}\n`);
|
||||
output.push(`# ${paths.name}\n`);
|
||||
output.push(`> **Note:** \`CLAUDE.md\` is a symlink to this file.\n`);
|
||||
|
||||
// Brief description
|
||||
output.push(`## Overview\n`);
|
||||
output.push(`${description}\n`);
|
||||
|
||||
// Directory structure
|
||||
output.push(`## Structure\n`);
|
||||
output.push("```");
|
||||
output.push(`${paths.name}/`);
|
||||
output.push(` SKILL.md # Main skill file - read this first`);
|
||||
output.push(` AGENTS.md # This navigation guide`);
|
||||
output.push(` CLAUDE.md # Symlink to AGENTS.md`);
|
||||
if (existsSync(paths.referencesDir)) {
|
||||
output.push(` references/ # Detailed reference files`);
|
||||
}
|
||||
output.push("```\n");
|
||||
|
||||
// How to use
|
||||
output.push(`## Usage\n`);
|
||||
output.push(`1. Read \`SKILL.md\` for the main skill instructions`);
|
||||
output.push(
|
||||
"> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n",
|
||||
`2. Browse \`references/\` for detailed documentation on specific topics`,
|
||||
);
|
||||
output.push(
|
||||
`3. Reference files are loaded on-demand - read only what you need\n`,
|
||||
);
|
||||
output.push("---\n");
|
||||
|
||||
// Abstract
|
||||
output.push("## Abstract\n");
|
||||
output.push(`${metadata.abstract}\n`);
|
||||
output.push("---\n");
|
||||
|
||||
// Table of Contents
|
||||
output.push("## Table of Contents\n");
|
||||
|
||||
for (const section of sections) {
|
||||
const sectionRules = rulesBySection.get(section.number) || [];
|
||||
// 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(
|
||||
`${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`,
|
||||
`Reference files are named \`{prefix}-{topic}.md\` (e.g., \`query-missing-indexes.md\`).\n`,
|
||||
);
|
||||
|
||||
for (const rule of sectionRules) {
|
||||
output.push(
|
||||
` - ${rule.id} [${rule.title}](#${toAnchor(`${rule.id}-${rule.title}`)})`,
|
||||
);
|
||||
}
|
||||
|
||||
output.push("");
|
||||
}
|
||||
|
||||
output.push("---\n");
|
||||
// Reference file list (just filenames, not content)
|
||||
if (referenceFiles.length > 0) {
|
||||
output.push(`## Available References\n`);
|
||||
const grouped = new Map<string, string[]>();
|
||||
|
||||
// Sections and Rules
|
||||
for (const section of sections) {
|
||||
const sectionRules = rulesBySection.get(section.number) || [];
|
||||
|
||||
output.push(`## ${section.number}. ${section.title}\n`);
|
||||
output.push(`**Impact: ${section.impact}**\n`);
|
||||
output.push(`${section.description}\n`);
|
||||
|
||||
if (sectionRules.length === 0) {
|
||||
output.push(
|
||||
"*No rules defined yet. See rules/_template.md for creating new rules.*\n",
|
||||
);
|
||||
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 rule of sectionRules) {
|
||||
output.push(`### ${rule.id} ${rule.title}\n`);
|
||||
|
||||
if (rule.impactDescription) {
|
||||
output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`);
|
||||
} else {
|
||||
output.push(`**Impact: ${rule.impact}**\n`);
|
||||
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\``);
|
||||
}
|
||||
|
||||
output.push(`${rule.explanation}\n`);
|
||||
|
||||
for (const example of rule.examples) {
|
||||
if (example.description) {
|
||||
output.push(`**${example.label} (${example.description}):**\n`);
|
||||
} else {
|
||||
output.push(`**${example.label}:**\n`);
|
||||
}
|
||||
|
||||
output.push(`\`\`\`${example.language || "sql"}`);
|
||||
output.push(example.code);
|
||||
output.push("```\n");
|
||||
|
||||
if (example.additionalText) {
|
||||
output.push(`${example.additionalText}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.references && rule.references.length > 0) {
|
||||
if (rule.references.length === 1) {
|
||||
output.push(`Reference: ${rule.references[0]}\n`);
|
||||
} else {
|
||||
output.push("References:");
|
||||
for (const ref of rule.references) {
|
||||
output.push(`- ${ref}`);
|
||||
}
|
||||
output.push("");
|
||||
}
|
||||
}
|
||||
|
||||
output.push("---\n");
|
||||
output.push("");
|
||||
}
|
||||
}
|
||||
|
||||
// References section
|
||||
if (metadata.references && metadata.references.length > 0) {
|
||||
output.push("## References\n");
|
||||
for (const ref of metadata.references) {
|
||||
output.push(`- ${ref}`);
|
||||
}
|
||||
output.push("");
|
||||
}
|
||||
// Stats
|
||||
output.push(`---\n`);
|
||||
output.push(
|
||||
`*${referenceFiles.length} reference files across ${sections.length} categories*`,
|
||||
);
|
||||
|
||||
// Write output
|
||||
// Write AGENTS.md
|
||||
writeFileSync(paths.agentsOutput, output.join("\n"));
|
||||
console.log(` Generated: ${paths.agentsOutput}`);
|
||||
console.log(` Total rules: ${rules.length}`);
|
||||
console.log(` Total references: ${referenceFiles.length}`);
|
||||
|
||||
// Create CLAUDE.md symlink
|
||||
createClaudeSymlink(paths);
|
||||
}
|
||||
|
||||
// Run build when executed directly
|
||||
@@ -527,7 +415,10 @@ if (isMainModule) {
|
||||
|
||||
export {
|
||||
buildSkill,
|
||||
generateSectionMap,
|
||||
getMarkdownFilesRecursive,
|
||||
getReferenceFiles,
|
||||
parseAllSections,
|
||||
parseSections,
|
||||
skillNameToTitle,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface SkillPaths {
|
||||
skillDir: string;
|
||||
referencesDir: string;
|
||||
agentsOutput: string;
|
||||
claudeSymlink: string;
|
||||
skillFile: string;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ export function getSkillPaths(skillName: string): SkillPaths {
|
||||
skillDir,
|
||||
referencesDir: join(skillDir, "references"),
|
||||
agentsOutput: join(skillDir, "AGENTS.md"),
|
||||
claudeSymlink: join(skillDir, "CLAUDE.md"),
|
||||
skillFile: join(skillDir, "SKILL.md"),
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1
skills/supabase-postgres-best-practices/CLAUDE.md
Symbolic link
1
skills/supabase-postgres-best-practices/CLAUDE.md
Symbolic link
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -55,10 +55,6 @@ Each rule file contains:
|
||||
- Additional context and references
|
||||
- Supabase-specific notes (when applicable)
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
|
||||
## References
|
||||
|
||||
- https://www.postgresql.org/docs/current/
|
||||
|
||||
Reference in New Issue
Block a user