Skip to content

Commit 15913ca

Browse files
committed
Add agent-skills discovery plugin and integrate into build process
1 parent 5e0e7f7 commit 15913ca

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

astro/astro.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as fs from "node:fs";
1717
import { duendeOpenGraphImage } from "./src/plugins/duende-og-image.js";
1818
import removeMarkdownExtensions from "./src/plugins/remove-markdown-extensions.js";
1919
import staticRedirects from "./src/plugins/static-redirects.js";
20+
import agentSkillsDiscovery from "./src/plugins/agent-skills-discovery.js";
2021

2122
// https://astro.build/config
2223
export default defineConfig({
@@ -233,6 +234,7 @@ export default defineConfig({
233234
contentDir: "./src/content/docs",
234235
}),
235236
staticRedirects(),
237+
agentSkillsDiscovery(),
236238
opengraphImages({
237239
options: {
238240
fonts: [
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import path from "node:path";
2+
import fs from "node:fs/promises";
3+
import { createHash } from "node:crypto";
4+
import url from "node:url";
5+
import type { AstroIntegrationLogger } from "astro";
6+
7+
const SKILLS_REPO = "DuendeSoftware/duende-skills";
8+
const SKILLS_BRANCH = "main";
9+
const SKILLS_PATH = "skills";
10+
const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/${SKILLS_REPO}/${SKILLS_BRANCH}`;
11+
const GITHUB_API_BASE = `https://api.github.com/repos/${SKILLS_REPO}/contents`;
12+
13+
interface SkillFrontmatter {
14+
name: string;
15+
description: string;
16+
}
17+
18+
interface SkillEntry {
19+
name: string;
20+
type: "skill-md";
21+
description: string;
22+
url: string;
23+
digest: string;
24+
}
25+
26+
function parseFrontmatter(content: string): SkillFrontmatter | null {
27+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
28+
if (!match) return null;
29+
30+
const yaml = match[1];
31+
const name = yaml.match(/^name:\s*(.+)$/m)?.[1]?.trim();
32+
const description = yaml.match(/^description:\s*(.+)$/m)?.[1]?.trim();
33+
34+
if (!name || !description) return null;
35+
return { name, description };
36+
}
37+
38+
function computeDigest(content: string): string {
39+
const hash = createHash("sha256").update(content).digest("hex");
40+
return `sha256:${hash}`;
41+
}
42+
43+
async function fetchSkillDirectories(
44+
logger: AstroIntegrationLogger,
45+
): Promise<string[]> {
46+
const apiUrl = `${GITHUB_API_BASE}/${SKILLS_PATH}?ref=${SKILLS_BRANCH}`;
47+
logger.info(`Fetching skill list from ${apiUrl}`);
48+
49+
const response = await fetch(apiUrl, {
50+
headers: {
51+
Accept: "application/vnd.github.v3+json",
52+
"User-Agent": "duende-docs-agent-skills",
53+
},
54+
});
55+
56+
if (!response.ok) {
57+
throw new Error(
58+
`Failed to fetch skills directory: ${response.status} ${response.statusText}`,
59+
);
60+
}
61+
62+
const entries = (await response.json()) as Array<{
63+
name: string;
64+
type: string;
65+
}>;
66+
return entries.filter((e) => e.type === "dir").map((e) => e.name);
67+
}
68+
69+
async function fetchSkillContent(skillName: string): Promise<string> {
70+
const skillUrl = `${GITHUB_RAW_BASE}/${SKILLS_PATH}/${skillName}/SKILL.md`;
71+
const response = await fetch(skillUrl, {
72+
headers: { "User-Agent": "duende-docs-agent-skills" },
73+
});
74+
75+
if (!response.ok) {
76+
throw new Error(
77+
`Failed to fetch ${skillUrl}: ${response.status} ${response.statusText}`,
78+
);
79+
}
80+
81+
return await response.text();
82+
}
83+
84+
async function generateAgentSkills(
85+
outDir: string,
86+
logger: AstroIntegrationLogger,
87+
): Promise<void> {
88+
const wellKnownDir = path.join(outDir, ".well-known", "agent-skills");
89+
await fs.mkdir(wellKnownDir, { recursive: true });
90+
91+
// Fetch list of skill directories
92+
const skillDirs = await fetchSkillDirectories(logger);
93+
logger.info(`Found ${skillDirs.length} skills to process`);
94+
95+
const skills: SkillEntry[] = [];
96+
97+
for (const skillName of skillDirs) {
98+
try {
99+
const content = await fetchSkillContent(skillName);
100+
const frontmatter = parseFrontmatter(content);
101+
102+
if (!frontmatter) {
103+
logger.warn(`Skipping ${skillName}: could not parse frontmatter`);
104+
continue;
105+
}
106+
107+
// Write SKILL.md to output
108+
const skillDir = path.join(wellKnownDir, skillName);
109+
await fs.mkdir(skillDir, { recursive: true });
110+
await fs.writeFile(path.join(skillDir, "SKILL.md"), content);
111+
112+
// Add to index
113+
skills.push({
114+
name: frontmatter.name,
115+
type: "skill-md",
116+
description: frontmatter.description,
117+
url: `/.well-known/agent-skills/${skillName}/SKILL.md`,
118+
digest: computeDigest(content),
119+
});
120+
121+
logger.info(` ✓ ${frontmatter.name}`);
122+
} catch (err) {
123+
logger.warn(
124+
`Failed to process skill ${skillName}: ${err instanceof Error ? err.message : err}`,
125+
);
126+
}
127+
}
128+
129+
// Write index.json
130+
const index = {
131+
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
132+
skills,
133+
};
134+
135+
await fs.writeFile(
136+
path.join(wellKnownDir, "index.json"),
137+
JSON.stringify(index, null, 2),
138+
);
139+
140+
logger.info(
141+
`Generated agent-skills discovery index with ${skills.length} skills`,
142+
);
143+
}
144+
145+
export default function agentSkillsDiscovery() {
146+
return {
147+
name: "agent-skills-discovery",
148+
hooks: {
149+
"astro:build:done": async (hookOptions: any) => {
150+
const outDir: string = url.fileURLToPath(hookOptions.dir);
151+
const logger: AstroIntegrationLogger = hookOptions.logger;
152+
153+
try {
154+
await generateAgentSkills(outDir, logger);
155+
} catch (err) {
156+
logger.error(
157+
`Failed to generate agent-skills discovery: ${err instanceof Error ? err.message : err}`,
158+
);
159+
}
160+
},
161+
},
162+
};
163+
}

0 commit comments

Comments
 (0)