reduxe the agents.md file size (#20)

This commit is contained in:
Pedro Rodrigues
2026-01-27 21:38:29 +00:00
committed by GitHub
parent 5da9a5ee37
commit a3b815155c
6 changed files with 347 additions and 1856 deletions

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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

View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -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/