Skip to content

Commit 939f148

Browse files
authored
Merge pull request #650 from github/copilot/add-llms-txt-file
Add llms.txt generation for GitHub Pages deployment
2 parents b492cfe + fd509d2 commit 939f148

4 files changed

Lines changed: 264 additions & 1 deletion

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ reports/
77
.DS_Store
88
*.tmp
99

10+
# Generated files
11+
/llms.txt
12+
1013
# Website build artifacts
1114
website/dist/
1215
website/.astro/
1316
website/public/data/*
17+
website/public/llms.txt

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ To make it easy to add these customizations to your editor, we have created a [M
5454

5555
</details>
5656

57+
## 📄 llms.txt
58+
59+
An [`llms.txt`](https://github.github.io/awesome-copilot/llms.txt) file following the [llmstxt.org](https://llmstxt.org/) specification is available on the GitHub Pages site. This machine-readable file makes it easy for Large Language Models to discover and understand all available agents, prompts, instructions, and skills, providing a structured overview of the repository's resources with names and descriptions.
60+
5761
## 🔧 How to Use
5862

5963
### 🤖 Custom Agents

eng/generate-llms-txt.mjs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#!/usr/bin/env node
2+
3+
import fs from "fs";
4+
import path from "path";
5+
import {
6+
AGENTS_DIR,
7+
INSTRUCTIONS_DIR,
8+
PROMPTS_DIR,
9+
SKILLS_DIR,
10+
ROOT_FOLDER,
11+
} from "./constants.mjs";
12+
import { parseFrontmatter, parseSkillMetadata } from "./yaml-parser.mjs";
13+
14+
/**
15+
* Extracts the name from a file based on its frontmatter or filename
16+
* @param {string} filePath - Path to the file (or directory for skills)
17+
* @param {string} fileType - Type of file (agent, prompt, instruction, skill)
18+
* @returns {string} - The name of the resource
19+
*/
20+
function extractName(filePath, fileType) {
21+
try {
22+
if (fileType === "skill") {
23+
// For skills, filePath is the skill directory
24+
const skillMetadata = parseSkillMetadata(filePath);
25+
return skillMetadata?.name || path.basename(filePath);
26+
}
27+
28+
const frontmatter = parseFrontmatter(filePath);
29+
if (frontmatter?.name) {
30+
return frontmatter.name;
31+
}
32+
33+
// Fallback to filename
34+
const basename = path.basename(filePath, path.extname(filePath));
35+
return basename
36+
.replace(/\.(agent|prompt|instructions)$/, "")
37+
.replace(/[-_]/g, " ")
38+
.replace(/\b\w/g, (l) => l.toUpperCase());
39+
} catch (error) {
40+
console.error(`Error extracting name from ${filePath}: ${error.message}`);
41+
const basename = path.basename(filePath, path.extname(filePath));
42+
return basename.replace(/[-_]/g, " ");
43+
}
44+
}
45+
46+
/**
47+
* Extracts the description from a file's frontmatter
48+
* @param {string} filePath - Path to the file (or directory for skills)
49+
* @param {string} fileType - Type of file (agent, prompt, instruction, skill)
50+
* @returns {string} - The description of the resource (single line)
51+
*/
52+
function extractDescription(filePath, fileType) {
53+
try {
54+
if (fileType === "skill") {
55+
// For skills, filePath is the skill directory
56+
const skillMetadata = parseSkillMetadata(filePath);
57+
const description = skillMetadata?.description || "No description available";
58+
// Convert multiline descriptions to single line
59+
return description.replace(/\s+/g, " ").trim();
60+
}
61+
62+
const frontmatter = parseFrontmatter(filePath);
63+
const description = frontmatter?.description || "No description available";
64+
// Convert multiline descriptions to single line
65+
return description.replace(/\s+/g, " ").trim();
66+
} catch (error) {
67+
console.error(
68+
`Error extracting description from ${filePath}: ${error.message}`
69+
);
70+
return "No description available";
71+
}
72+
}
73+
74+
/**
75+
* Gets the relative URL path for a resource
76+
* @param {string} filePath - Full path to the file
77+
* @param {string} baseDir - Base directory to calculate relative path from
78+
* @returns {string} - Relative URL path
79+
*/
80+
function getRelativeUrl(filePath, baseDir) {
81+
const basePath = baseDir || ROOT_FOLDER;
82+
const relativePath = path.relative(basePath, filePath);
83+
return relativePath.replace(/\\/g, "/");
84+
}
85+
86+
/**
87+
* Generates llms.txt content according to the llmstxt.org specification
88+
* @returns {string} - The llms.txt file content
89+
*/
90+
function generateLlmsTxt() {
91+
let content = "";
92+
93+
// H1 header (required)
94+
content += "# Awesome GitHub Copilot\n\n";
95+
96+
// Summary blockquote (optional but recommended)
97+
content +=
98+
"> A community-driven collection of custom agents, prompts, instructions, and skills to enhance GitHub Copilot experiences across various domains, languages, and use cases.\n\n";
99+
100+
// Add overview section
101+
content += "## Overview\n\n";
102+
content +=
103+
"This repository provides resources to customize and enhance GitHub Copilot:\n\n";
104+
content +=
105+
"- **Agents**: Specialized GitHub Copilot agents that integrate with MCP servers\n";
106+
content +=
107+
"- **Prompts**: Task-specific prompts for code generation and problem-solving\n";
108+
content +=
109+
"- **Instructions**: Coding standards and best practices applied to specific file patterns\n";
110+
content +=
111+
"- **Skills**: Self-contained folders with instructions and bundled resources for specialized tasks\n\n";
112+
113+
// Process Agents
114+
content += "## Agents\n\n";
115+
const agentFiles = fs
116+
.readdirSync(AGENTS_DIR)
117+
.filter((file) => file.endsWith(".agent.md"))
118+
.sort();
119+
120+
agentFiles.forEach((file) => {
121+
const filePath = path.join(AGENTS_DIR, file);
122+
const name = extractName(filePath, "agent");
123+
const description = extractDescription(filePath, "agent");
124+
const url = getRelativeUrl(filePath, ROOT_FOLDER);
125+
126+
content += `- [${name}](${url}): ${description}\n`;
127+
});
128+
129+
content += "\n";
130+
131+
// Process Prompts
132+
content += "## Prompts\n\n";
133+
const promptFiles = fs
134+
.readdirSync(PROMPTS_DIR)
135+
.filter((file) => file.endsWith(".prompt.md"))
136+
.sort();
137+
138+
promptFiles.forEach((file) => {
139+
const filePath = path.join(PROMPTS_DIR, file);
140+
const name = extractName(filePath, "prompt");
141+
const description = extractDescription(filePath, "prompt");
142+
const url = getRelativeUrl(filePath, ROOT_FOLDER);
143+
144+
content += `- [${name}](${url}): ${description}\n`;
145+
});
146+
147+
content += "\n";
148+
149+
// Process Instructions
150+
content += "## Instructions\n\n";
151+
const instructionFiles = fs
152+
.readdirSync(INSTRUCTIONS_DIR)
153+
.filter((file) => file.endsWith(".instructions.md"))
154+
.sort();
155+
156+
instructionFiles.forEach((file) => {
157+
const filePath = path.join(INSTRUCTIONS_DIR, file);
158+
const name = extractName(filePath, "instruction");
159+
const description = extractDescription(filePath, "instruction");
160+
const url = getRelativeUrl(filePath, ROOT_FOLDER);
161+
162+
content += `- [${name}](${url}): ${description}\n`;
163+
});
164+
165+
content += "\n";
166+
167+
// Process Skills
168+
content += "## Skills\n\n";
169+
const skillDirs = fs
170+
.readdirSync(SKILLS_DIR)
171+
.filter((item) => {
172+
const itemPath = path.join(SKILLS_DIR, item);
173+
return fs.statSync(itemPath).isDirectory();
174+
})
175+
.sort();
176+
177+
skillDirs.forEach((dir) => {
178+
const skillDirPath = path.join(SKILLS_DIR, dir);
179+
const skillPath = path.join(skillDirPath, "SKILL.md");
180+
181+
if (fs.existsSync(skillPath)) {
182+
const name = extractName(skillDirPath, "skill");
183+
const description = extractDescription(skillDirPath, "skill");
184+
const url = getRelativeUrl(skillPath, ROOT_FOLDER);
185+
186+
content += `- [${name}](${url}): ${description}\n`;
187+
} else {
188+
console.warn(`Warning: Skill directory '${dir}' found without SKILL.md file`);
189+
}
190+
});
191+
192+
content += "\n";
193+
194+
// Add documentation links
195+
content += "## Documentation\n\n";
196+
content +=
197+
"- [README.md](README.md): Main documentation and getting started guide\n";
198+
content +=
199+
"- [CONTRIBUTING.md](CONTRIBUTING.md): Guidelines for contributing to this repository\n";
200+
content +=
201+
"- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md): Community standards and expectations\n";
202+
content += "- [SECURITY.md](SECURITY.md): Security policies and reporting\n";
203+
content += "- [AGENTS.md](AGENTS.md): Project overview and setup commands\n\n";
204+
205+
// Add repository information
206+
content += "## Repository\n\n";
207+
content +=
208+
"- **GitHub**: https://github.com/github/awesome-copilot\n";
209+
content += "- **License**: MIT\n";
210+
content +=
211+
"- **Website**: https://github.github.io/awesome-copilot\n";
212+
213+
return content;
214+
}
215+
216+
/**
217+
* Main function to generate and write the llms.txt file
218+
* @param {string} [outputDir] - Optional output directory. Defaults to repository root.
219+
*/
220+
function main(outputDir = ROOT_FOLDER) {
221+
console.log("Generating llms.txt file...");
222+
223+
try {
224+
const content = generateLlmsTxt();
225+
const outputPath = path.join(outputDir, "llms.txt");
226+
227+
// Ensure output directory exists
228+
if (!fs.existsSync(outputDir)) {
229+
fs.mkdirSync(outputDir, { recursive: true });
230+
}
231+
232+
fs.writeFileSync(outputPath, content, "utf8");
233+
234+
console.log(`✓ llms.txt generated successfully at ${outputPath}`);
235+
236+
// Count resources using a helper function
237+
const countResources = (dirName) => {
238+
const lines = content.split("\n");
239+
return lines.filter((l) => l.startsWith("- [") && l.includes(`${dirName}/`)).length;
240+
};
241+
242+
console.log(` - ${countResources("agents")} agents`);
243+
console.log(` - ${countResources("prompts")} prompts`);
244+
console.log(` - ${countResources("instructions")} instructions`);
245+
console.log(` - ${countResources("skills")} skills`);
246+
} catch (error) {
247+
console.error(`Error generating llms.txt: ${error.message}`);
248+
process.exit(1);
249+
}
250+
}
251+
252+
// Support command-line argument for output directory
253+
const outputDir = process.argv[2];
254+
main(outputDir);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"start": "npm run build",
99
"build": "node ./eng/update-readme.mjs",
10+
"llms:generate": "node ./eng/generate-llms-txt.mjs",
1011
"contributors:add": "all-contributors add",
1112
"contributors:report": "node ./eng/contributor-report.mjs",
1213
"contributors:generate": "all-contributors generate",
@@ -17,7 +18,7 @@
1718
"skill:create": "node ./eng/create-skill.mjs",
1819
"website:data": "node ./eng/generate-website-data.mjs",
1920
"website:dev": "npm run website:data && npm run --prefix website dev",
20-
"website:build": "npm run build && npm run website:data && npm run --prefix website build",
21+
"website:build": "npm run build && npm run website:data && node ./eng/generate-llms-txt.mjs website/public && npm run --prefix website build",
2122
"website:preview": "npm run --prefix website preview"
2223
},
2324
"repository": {

0 commit comments

Comments
 (0)