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/
|
skills/
|
||||||
{skill-name}/
|
{skill-name}/
|
||||||
SKILL.md # Required: skill manifest (Agent Skills spec)
|
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/
|
references/
|
||||||
_sections.md # Required: section definitions
|
_sections.md # Required: section definitions
|
||||||
{prefix}-{name}.md # Reference files
|
{prefix}-{name}.md # Reference files
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
existsSync,
|
existsSync,
|
||||||
|
lstatSync,
|
||||||
readdirSync,
|
readdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
statSync,
|
statSync,
|
||||||
|
symlinkSync,
|
||||||
|
unlinkSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { basename, join } from "node:path";
|
import { basename, join } from "node:path";
|
||||||
@@ -12,215 +15,12 @@ import {
|
|||||||
type SkillPaths,
|
type SkillPaths,
|
||||||
validateSkillExists,
|
validateSkillExists,
|
||||||
} from "./config.js";
|
} from "./config.js";
|
||||||
import { parseRuleFile } from "./parser.js";
|
import type { Section } from "./types.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(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate SECTION_MAP from parsed sections
|
* Generate SECTION_MAP from parsed sections
|
||||||
*/
|
*/
|
||||||
export function generateSectionMap(
|
function generateSectionMap(sections: Section[]): Record<string, number> {
|
||||||
sections: Section[],
|
|
||||||
): Record<string, number> {
|
|
||||||
const map: Record<string, number> = {};
|
const map: Record<string, number> = {};
|
||||||
for (const section of sections) {
|
for (const section of sections) {
|
||||||
map[section.prefix] = section.number;
|
map[section.prefix] = section.number;
|
||||||
@@ -260,6 +60,116 @@ function getMarkdownFilesRecursive(dir: string): string[] {
|
|||||||
return files;
|
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
|
* 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[] {
|
function getReferenceFiles(referencesDir: string): string[] {
|
||||||
const content = readFileSync(filePath, "utf-8");
|
const files: string[] = [];
|
||||||
const sections: Section[] = [];
|
|
||||||
|
|
||||||
const sectionMatches = content.matchAll(
|
if (!existsSync(referencesDir)) {
|
||||||
/##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g,
|
return files;
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
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
|
* 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 {
|
function buildSkill(paths: SkillPaths): void {
|
||||||
console.log(`[${paths.name}] Building AGENTS.md...`);
|
console.log(`[${paths.name}] Building AGENTS.md...`);
|
||||||
|
|
||||||
// Load metadata and sections (including from subdirectories)
|
// Read SKILL.md for metadata
|
||||||
const metadata = loadMetadata(paths.skillFile, paths.name);
|
const skillContent = existsSync(paths.skillFile)
|
||||||
const sections = parseAllSections(paths.referencesDir);
|
? readFileSync(paths.skillFile, "utf-8")
|
||||||
const sectionMap = generateSectionMap(sections);
|
: "";
|
||||||
|
const frontmatter = parseSkillFrontmatter(skillContent);
|
||||||
const skillTitle = skillNameToTitle(paths.name);
|
const skillTitle = skillNameToTitle(paths.name);
|
||||||
|
const description =
|
||||||
|
(frontmatter.description as string) || `${skillTitle} skill for AI agents.`;
|
||||||
|
|
||||||
// Check if references directory exists
|
// Parse sections if available
|
||||||
if (!existsSync(paths.referencesDir)) {
|
const sections = parseAllSections(paths.referencesDir);
|
||||||
console.log(` No references directory found. Generating empty AGENTS.md.`);
|
const referenceFiles = getReferenceFiles(paths.referencesDir);
|
||||||
writeFileSync(
|
|
||||||
paths.agentsOutput,
|
|
||||||
`# ${skillTitle}\n\nNo rules defined yet.\n`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all reference files recursively
|
// Generate concise AGENTS.md
|
||||||
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
|
|
||||||
const output: string[] = [];
|
const output: string[] = [];
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
output.push(`# ${skillTitle}\n`);
|
output.push(`# ${paths.name}\n`);
|
||||||
output.push(`**Version ${metadata.version}**`);
|
output.push(`> **Note:** \`CLAUDE.md\` is a symlink to this file.\n`);
|
||||||
output.push(`${metadata.organization}`);
|
|
||||||
output.push(`${metadata.date}\n`);
|
|
||||||
output.push(
|
|
||||||
"> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n",
|
|
||||||
);
|
|
||||||
output.push("---\n");
|
|
||||||
|
|
||||||
// Abstract
|
// Brief description
|
||||||
output.push("## Abstract\n");
|
output.push(`## Overview\n`);
|
||||||
output.push(`${metadata.abstract}\n`);
|
output.push(`${description}\n`);
|
||||||
output.push("---\n");
|
|
||||||
|
|
||||||
// Table of Contents
|
// Directory structure
|
||||||
output.push("## Table of Contents\n");
|
output.push(`## Structure\n`);
|
||||||
|
output.push("```");
|
||||||
for (const section of sections) {
|
output.push(`${paths.name}/`);
|
||||||
const sectionRules = rulesBySection.get(section.number) || [];
|
output.push(` SKILL.md # Main skill file - read this first`);
|
||||||
output.push(
|
output.push(` AGENTS.md # This navigation guide`);
|
||||||
`${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`,
|
output.push(` CLAUDE.md # Symlink to AGENTS.md`);
|
||||||
);
|
if (existsSync(paths.referencesDir)) {
|
||||||
|
output.push(` references/ # Detailed reference files`);
|
||||||
for (const rule of sectionRules) {
|
|
||||||
output.push(
|
|
||||||
` - ${rule.id} [${rule.title}](#${toAnchor(`${rule.id}-${rule.title}`)})`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push("");
|
|
||||||
}
|
|
||||||
|
|
||||||
output.push("---\n");
|
|
||||||
|
|
||||||
// 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 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`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
output.push("```\n");
|
||||||
|
|
||||||
if (example.additionalText) {
|
// How to use
|
||||||
output.push(`${example.additionalText}\n`);
|
output.push(`## Usage\n`);
|
||||||
|
output.push(`1. Read \`SKILL.md\` for the main skill instructions`);
|
||||||
|
output.push(
|
||||||
|
`2. Browse \`references/\` for detailed documentation on specific topics`,
|
||||||
|
);
|
||||||
|
output.push(
|
||||||
|
`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`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule.references && rule.references.length > 0) {
|
// Reference file list (just filenames, not content)
|
||||||
if (rule.references.length === 1) {
|
if (referenceFiles.length > 0) {
|
||||||
output.push(`Reference: ${rule.references[0]}\n`);
|
output.push(`## Available References\n`);
|
||||||
} else {
|
const grouped = new Map<string, string[]>();
|
||||||
output.push("References:");
|
|
||||||
for (const ref of rule.references) {
|
for (const file of referenceFiles) {
|
||||||
output.push(`- ${ref}`);
|
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\``);
|
||||||
}
|
}
|
||||||
output.push("");
|
output.push("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push("---\n");
|
// Stats
|
||||||
}
|
output.push(`---\n`);
|
||||||
}
|
output.push(
|
||||||
|
`*${referenceFiles.length} reference files across ${sections.length} categories*`,
|
||||||
|
);
|
||||||
|
|
||||||
// References section
|
// Write AGENTS.md
|
||||||
if (metadata.references && metadata.references.length > 0) {
|
|
||||||
output.push("## References\n");
|
|
||||||
for (const ref of metadata.references) {
|
|
||||||
output.push(`- ${ref}`);
|
|
||||||
}
|
|
||||||
output.push("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write output
|
|
||||||
writeFileSync(paths.agentsOutput, output.join("\n"));
|
writeFileSync(paths.agentsOutput, output.join("\n"));
|
||||||
console.log(` Generated: ${paths.agentsOutput}`);
|
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
|
// Run build when executed directly
|
||||||
@@ -527,7 +415,10 @@ if (isMainModule) {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
buildSkill,
|
buildSkill,
|
||||||
|
generateSectionMap,
|
||||||
getMarkdownFilesRecursive,
|
getMarkdownFilesRecursive,
|
||||||
|
getReferenceFiles,
|
||||||
parseAllSections,
|
parseAllSections,
|
||||||
parseSections,
|
parseSections,
|
||||||
|
skillNameToTitle,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface SkillPaths {
|
|||||||
skillDir: string;
|
skillDir: string;
|
||||||
referencesDir: string;
|
referencesDir: string;
|
||||||
agentsOutput: string;
|
agentsOutput: string;
|
||||||
|
claudeSymlink: string;
|
||||||
skillFile: string;
|
skillFile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export function getSkillPaths(skillName: string): SkillPaths {
|
|||||||
skillDir,
|
skillDir,
|
||||||
referencesDir: join(skillDir, "references"),
|
referencesDir: join(skillDir, "references"),
|
||||||
agentsOutput: join(skillDir, "AGENTS.md"),
|
agentsOutput: join(skillDir, "AGENTS.md"),
|
||||||
|
claudeSymlink: join(skillDir, "CLAUDE.md"),
|
||||||
skillFile: join(skillDir, "SKILL.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
|
- Additional context and references
|
||||||
- Supabase-specific notes (when applicable)
|
- Supabase-specific notes (when applicable)
|
||||||
|
|
||||||
## Full Compiled Document
|
|
||||||
|
|
||||||
For the complete guide with all rules expanded: `AGENTS.md`
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- https://www.postgresql.org/docs/current/
|
- https://www.postgresql.org/docs/current/
|
||||||
|
|||||||
Reference in New Issue
Block a user