mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
feat: add hooks functionality with automated workflows
- Introduced hooks to enable automated workflows triggered by specific events during GitHub Copilot sessions. - Added documentation for hooks in AGENTS.md and README.md. - Created a new directory structure for hooks, including README.md and hooks.json files. - Implemented two example hooks: Session Auto-Commit and Session Logger. - Developed scripts for logging session events and auto-committing changes. - Enhanced validation and parsing for hook metadata. - Updated build and validation scripts to accommodate new hooks functionality.
This commit is contained in:
@@ -158,6 +158,8 @@ function getDisplayName(filePath, kind) {
|
||||
return basename.replace(".agent.md", "");
|
||||
} else if (kind === "instruction") {
|
||||
return basename.replace(".instructions.md", "");
|
||||
} else if (kind === "hook") {
|
||||
return basename.replace(".hook.md", "");
|
||||
} else if (kind === "skill") {
|
||||
return path.basename(filePath);
|
||||
}
|
||||
@@ -221,6 +223,23 @@ function generateReadme(collection, items) {
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// Hooks
|
||||
const hooks = items.filter((item) => item.kind === "hook");
|
||||
if (hooks.length > 0) {
|
||||
lines.push("### Hooks");
|
||||
lines.push("");
|
||||
lines.push("| Hook | Description | Event |");
|
||||
lines.push("|------|-------------|-------|");
|
||||
for (const item of hooks) {
|
||||
const name = getDisplayName(item.path, "hook");
|
||||
const description =
|
||||
item.frontmatter?.description || item.frontmatter?.name || name;
|
||||
const event = item.frontmatter?.event || "N/A";
|
||||
lines.push(`| \`${name}\` | ${description} | ${event} |`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// Skills
|
||||
const skills = items.filter((item) => item.kind === "skill");
|
||||
if (skills.length > 0) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from "path";
|
||||
import path, { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -100,6 +99,34 @@ Skills differ from other primitives by supporting bundled assets (scripts, code
|
||||
- Browse the skills table below to find relevant capabilities
|
||||
- Copy the skill folder to your local skills directory
|
||||
- Reference skills in your prompts or let the agent discover them automatically`,
|
||||
|
||||
hooksSection: `## 🪝 Hooks
|
||||
|
||||
Hooks enable automated workflows triggered by specific events during GitHub Copilot coding agent sessions, such as session start, session end, user prompts, and tool usage.`,
|
||||
|
||||
hooksUsage: `### How to Use Hooks
|
||||
|
||||
**What's Included:**
|
||||
- Each hook is a folder containing a \`README.md\` file and a \`hooks.json\` configuration
|
||||
- Hooks may include helper scripts, utilities, or other bundled assets
|
||||
- Hooks follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
|
||||
|
||||
**To Install:**
|
||||
- Copy the hook folder to your repository's \`.github/hooks/\` directory
|
||||
- Ensure any bundled scripts are executable (\`chmod +x script.sh\`)
|
||||
- Commit the hook to your repository's default branch
|
||||
|
||||
**To Activate/Use:**
|
||||
- Hooks automatically execute during Copilot coding agent sessions
|
||||
- Configure hook events in the \`hooks.json\` file
|
||||
- Available events: \`sessionStart\`, \`sessionEnd\`, \`userPromptSubmitted\`, \`preToolUse\`, \`postToolUse\`, \`errorOccurred\`
|
||||
|
||||
**When to Use:**
|
||||
- Automate session logging and audit trails
|
||||
- Auto-commit changes at session end
|
||||
- Track usage analytics
|
||||
- Integrate with external tools and services
|
||||
- Custom session workflows`,
|
||||
};
|
||||
|
||||
const vscodeInstallImage =
|
||||
@@ -115,6 +142,7 @@ const AKA_INSTALL_URLS = {
|
||||
instructions: "https://aka.ms/awesome-copilot/install/instructions",
|
||||
prompt: "https://aka.ms/awesome-copilot/install/prompt",
|
||||
agent: "https://aka.ms/awesome-copilot/install/agent",
|
||||
hook: "https://aka.ms/awesome-copilot/install/hook",
|
||||
};
|
||||
|
||||
const ROOT_FOLDER = path.join(__dirname, "..");
|
||||
@@ -122,6 +150,7 @@ const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
|
||||
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
|
||||
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
||||
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
|
||||
const HOOKS_DIR = path.join(ROOT_FOLDER, "hooks");
|
||||
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
|
||||
const COOKBOOK_DIR = path.join(ROOT_FOLDER, "cookbook");
|
||||
const MAX_COLLECTION_ITEMS = 50;
|
||||
@@ -135,23 +164,7 @@ const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
|
||||
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
|
||||
|
||||
export {
|
||||
TEMPLATES,
|
||||
vscodeInstallImage,
|
||||
vscodeInsidersInstallImage,
|
||||
repoBaseUrl,
|
||||
AKA_INSTALL_URLS,
|
||||
ROOT_FOLDER,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
AGENTS_DIR,
|
||||
SKILLS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
COOKBOOK_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
SKILL_NAME_MIN_LENGTH,
|
||||
SKILL_NAME_MAX_LENGTH,
|
||||
SKILL_DESCRIPTION_MIN_LENGTH,
|
||||
SKILL_DESCRIPTION_MAX_LENGTH,
|
||||
DOCS_DIR,
|
||||
AGENTS_DIR, AKA_INSTALL_URLS, COLLECTIONS_DIR,
|
||||
COOKBOOK_DIR, DOCS_DIR, HOOKS_DIR, INSTRUCTIONS_DIR, MAX_COLLECTION_ITEMS, PROMPTS_DIR, repoBaseUrl, ROOT_FOLDER, SKILL_DESCRIPTION_MAX_LENGTH, SKILL_DESCRIPTION_MIN_LENGTH, SKILL_NAME_MAX_LENGTH, SKILL_NAME_MIN_LENGTH, SKILLS_DIR, TEMPLATES, vscodeInsidersInstallImage, vscodeInstallImage
|
||||
};
|
||||
|
||||
|
||||
@@ -7,24 +7,26 @@
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path, { dirname } from "path";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import {
|
||||
AGENTS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
COOKBOOK_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
AGENTS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
COOKBOOK_DIR,
|
||||
HOOKS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR
|
||||
} from "./constants.mjs";
|
||||
import {
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
parseYamlFile,
|
||||
} from "./yaml-parser.mjs";
|
||||
import { getGitFileDates } from "./utils/git-dates.mjs";
|
||||
import {
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
parseHookMetadata,
|
||||
parseYamlFile,
|
||||
} from "./yaml-parser.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
@@ -122,6 +124,75 @@ function generateAgentsData(gitDates) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate hooks metadata
|
||||
*/
|
||||
/**
|
||||
* Generate hooks metadata (similar to skills - folder-based)
|
||||
*/
|
||||
function generateHooksData(gitDates) {
|
||||
const hooks = [];
|
||||
|
||||
// Check if hooks directory exists
|
||||
if (!fs.existsSync(HOOKS_DIR)) {
|
||||
return {
|
||||
items: hooks,
|
||||
filters: {
|
||||
hooks: [],
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Get all hook folders (directories)
|
||||
const hookFolders = fs.readdirSync(HOOKS_DIR).filter((file) => {
|
||||
const filePath = path.join(HOOKS_DIR, file);
|
||||
return fs.statSync(filePath).isDirectory();
|
||||
});
|
||||
|
||||
// Track all unique values for filters
|
||||
const allHookTypes = new Set();
|
||||
const allTags = new Set();
|
||||
|
||||
for (const folder of hookFolders) {
|
||||
const hookPath = path.join(HOOKS_DIR, folder);
|
||||
const metadata = parseHookMetadata(hookPath);
|
||||
if (!metadata) continue;
|
||||
|
||||
const relativePath = path
|
||||
.relative(ROOT_FOLDER, hookPath)
|
||||
.replace(/\\/g, "/");
|
||||
const readmeRelativePath = `${relativePath}/README.md`;
|
||||
|
||||
// Track unique values
|
||||
(metadata.hooks || []).forEach((h) => allHookTypes.add(h));
|
||||
(metadata.tags || []).forEach((t) => allTags.add(t));
|
||||
|
||||
hooks.push({
|
||||
id: folder,
|
||||
title: metadata.name,
|
||||
description: metadata.description,
|
||||
hooks: metadata.hooks || [],
|
||||
tags: metadata.tags || [],
|
||||
assets: metadata.assets || [],
|
||||
path: relativePath,
|
||||
readmeFile: readmeRelativePath,
|
||||
lastUpdated: gitDates.get(readmeRelativePath) || null,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort and return with filter metadata
|
||||
const sortedHooks = hooks.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedHooks,
|
||||
filters: {
|
||||
hooks: Array.from(allHookTypes).sort(),
|
||||
tags: Array.from(allTags).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate prompts metadata
|
||||
*/
|
||||
@@ -539,6 +610,7 @@ function generateSearchIndex(
|
||||
agents,
|
||||
prompts,
|
||||
instructions,
|
||||
hooks,
|
||||
skills,
|
||||
collections
|
||||
) {
|
||||
@@ -584,6 +656,20 @@ function generateSearchIndex(
|
||||
});
|
||||
}
|
||||
|
||||
for (const hook of hooks) {
|
||||
index.push({
|
||||
type: "hook",
|
||||
id: hook.id,
|
||||
title: hook.title,
|
||||
description: hook.description,
|
||||
path: hook.readmeFile,
|
||||
lastUpdated: hook.lastUpdated,
|
||||
searchText: `${hook.title} ${hook.description} ${hook.hooks.join(
|
||||
" "
|
||||
)} ${hook.tags.join(" ")}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const skill of skills) {
|
||||
index.push({
|
||||
type: "skill",
|
||||
@@ -720,7 +806,7 @@ async function main() {
|
||||
// Load git dates for all resource files (single efficient git command)
|
||||
console.log("Loading git history for last updated dates...");
|
||||
const gitDates = getGitFileDates(
|
||||
["agents/", "prompts/", "instructions/", "skills/", "collections/"],
|
||||
["agents/", "prompts/", "instructions/", "hooks/", "skills/", "collections/"],
|
||||
ROOT_FOLDER
|
||||
);
|
||||
console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
|
||||
@@ -732,6 +818,12 @@ async function main() {
|
||||
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
|
||||
);
|
||||
|
||||
const hooksData = generateHooksData(gitDates);
|
||||
const hooks = hooksData.items;
|
||||
console.log(
|
||||
`✓ Generated ${hooks.length} hooks (${hooksData.filters.hooks.length} hook types, ${hooksData.filters.tags.length} tags)`
|
||||
);
|
||||
|
||||
const promptsData = generatePromptsData(gitDates);
|
||||
const prompts = promptsData.items;
|
||||
console.log(
|
||||
@@ -771,6 +863,7 @@ async function main() {
|
||||
agents,
|
||||
prompts,
|
||||
instructions,
|
||||
hooks,
|
||||
skills,
|
||||
collections
|
||||
);
|
||||
@@ -782,6 +875,11 @@ async function main() {
|
||||
JSON.stringify(agentsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "hooks.json"),
|
||||
JSON.stringify(hooksData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "prompts.json"),
|
||||
JSON.stringify(promptsData, null, 2)
|
||||
|
||||
@@ -4,24 +4,26 @@ import fs from "fs";
|
||||
import path, { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import {
|
||||
AGENTS_DIR,
|
||||
AKA_INSTALL_URLS,
|
||||
COLLECTIONS_DIR,
|
||||
DOCS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
repoBaseUrl,
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
TEMPLATES,
|
||||
vscodeInsidersInstallImage,
|
||||
vscodeInstallImage,
|
||||
AGENTS_DIR,
|
||||
AKA_INSTALL_URLS,
|
||||
COLLECTIONS_DIR,
|
||||
DOCS_DIR,
|
||||
HOOKS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
repoBaseUrl,
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
TEMPLATES,
|
||||
vscodeInsidersInstallImage,
|
||||
vscodeInstallImage,
|
||||
} from "./constants.mjs";
|
||||
import {
|
||||
extractMcpServerConfigs,
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
extractMcpServerConfigs,
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
parseHookMetadata,
|
||||
} from "./yaml-parser.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -515,6 +517,67 @@ function generateAgentsSection(agentsDir, registryNames = []) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the hooks section with a table of all hooks
|
||||
*/
|
||||
function generateHooksSection(hooksDir) {
|
||||
if (!fs.existsSync(hooksDir)) {
|
||||
console.log(`Hooks directory does not exist: ${hooksDir}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get all hook folders (directories)
|
||||
const hookFolders = fs.readdirSync(hooksDir).filter((file) => {
|
||||
const filePath = path.join(hooksDir, file);
|
||||
return fs.statSync(filePath).isDirectory();
|
||||
});
|
||||
|
||||
// Parse each hook folder
|
||||
const hookEntries = hookFolders
|
||||
.map((folder) => {
|
||||
const hookPath = path.join(hooksDir, folder);
|
||||
const metadata = parseHookMetadata(hookPath);
|
||||
if (!metadata) return null;
|
||||
|
||||
return {
|
||||
folder,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
hooks: metadata.hooks,
|
||||
tags: metadata.tags,
|
||||
assets: metadata.assets,
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry !== null)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log(`Found ${hookEntries.length} hook(s)`);
|
||||
|
||||
if (hookEntries.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create table header
|
||||
let content =
|
||||
"| Name | Description | Events | Bundled Assets |\n| ---- | ----------- | ------ | -------------- |\n";
|
||||
|
||||
// Generate table rows for each hook
|
||||
for (const hook of hookEntries) {
|
||||
const link = `../hooks/${hook.folder}/README.md`;
|
||||
const events = hook.hooks.length > 0 ? hook.hooks.join(", ") : "N/A";
|
||||
const assetsList =
|
||||
hook.assets.length > 0
|
||||
? hook.assets.map((a) => `\`${a}\``).join("<br />")
|
||||
: "None";
|
||||
|
||||
content += `| [${hook.name}](${link}) | ${formatTableCell(
|
||||
hook.description
|
||||
)} | ${events} | ${assetsList} |\n`;
|
||||
}
|
||||
|
||||
return `${TEMPLATES.hooksSection}\n${TEMPLATES.hooksUsage}\n\n${content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the skills section with a table of all skills
|
||||
*/
|
||||
@@ -1002,6 +1065,7 @@ async function main() {
|
||||
);
|
||||
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
|
||||
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
|
||||
const hooksHeader = TEMPLATES.hooksSection.replace(/^##\s/m, "# ");
|
||||
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
|
||||
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
||||
/^##\s/m,
|
||||
@@ -1031,6 +1095,15 @@ async function main() {
|
||||
registryNames
|
||||
);
|
||||
|
||||
// Generate hooks README
|
||||
const hooksReadme = buildCategoryReadme(
|
||||
generateHooksSection,
|
||||
HOOKS_DIR,
|
||||
hooksHeader,
|
||||
TEMPLATES.hooksUsage,
|
||||
registryNames
|
||||
);
|
||||
|
||||
// Generate skills README
|
||||
const skillsReadme = buildCategoryReadme(
|
||||
generateSkillsSection,
|
||||
@@ -1061,6 +1134,7 @@ async function main() {
|
||||
);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.hooks.md"), hooksReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
|
||||
writeFileIfChanged(
|
||||
path.join(DOCS_DIR, "README.collections.md"),
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
COLLECTIONS_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
ROOT_FOLDER,
|
||||
COLLECTIONS_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
ROOT_FOLDER,
|
||||
} from "./constants.mjs";
|
||||
import { parseCollectionYaml, parseFrontmatter } from "./yaml-parser.mjs";
|
||||
|
||||
@@ -155,6 +155,41 @@ function validateAgentFile(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateHookFile(filePath) {
|
||||
try {
|
||||
const hook = parseFrontmatter(filePath);
|
||||
|
||||
if (!hook) {
|
||||
return `Item ${filePath} hook file could not be parsed`;
|
||||
}
|
||||
|
||||
// Validate name field
|
||||
if (!hook.name || typeof hook.name !== "string") {
|
||||
return `Item ${filePath} hook must have a 'name' field`;
|
||||
}
|
||||
if (hook.name.length < 1 || hook.name.length > 50) {
|
||||
return `Item ${filePath} hook name must be between 1 and 50 characters`;
|
||||
}
|
||||
|
||||
// Validate description field
|
||||
if (!hook.description || typeof hook.description !== "string") {
|
||||
return `Item ${filePath} hook must have a 'description' field`;
|
||||
}
|
||||
if (hook.description.length < 1 || hook.description.length > 500) {
|
||||
return `Item ${filePath} hook description must be between 1 and 500 characters`;
|
||||
}
|
||||
|
||||
// Validate event field (optional but recommended)
|
||||
if (hook.event !== undefined && typeof hook.event !== "string") {
|
||||
return `Item ${filePath} hook 'event' must be a string`;
|
||||
}
|
||||
|
||||
return null; // All validations passed
|
||||
} catch (error) {
|
||||
return `Item ${filePath} hook file validation failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function validateCollectionItems(items) {
|
||||
if (!items || !Array.isArray(items)) {
|
||||
return "Items is required and must be an array";
|
||||
@@ -177,10 +212,10 @@ function validateCollectionItems(items) {
|
||||
if (!item.kind || typeof item.kind !== "string") {
|
||||
return `Item ${i + 1} must have a kind string`;
|
||||
}
|
||||
if (!["prompt", "instruction", "agent", "skill"].includes(item.kind)) {
|
||||
if (!["prompt", "instruction", "agent", "skill", "hook"].includes(item.kind)) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind must be one of: prompt, instruction, agent, skill`;
|
||||
} kind must be one of: prompt, instruction, agent, skill, hook`;
|
||||
}
|
||||
|
||||
// Validate file path exists
|
||||
@@ -208,6 +243,11 @@ function validateCollectionItems(items) {
|
||||
i + 1
|
||||
} kind is "agent" but path doesn't end with .agent.md`;
|
||||
}
|
||||
if (item.kind === "hook" && !item.path.endsWith(".hook.md")) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind is "hook" but path doesn't end with .hook.md`;
|
||||
}
|
||||
|
||||
// Validate agent-specific frontmatter
|
||||
if (item.kind === "agent") {
|
||||
@@ -216,6 +256,14 @@ function validateCollectionItems(items) {
|
||||
return agentValidation;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate hook-specific frontmatter
|
||||
if (item.kind === "hook") {
|
||||
const hookValidation = validateHookFile(filePath, i + 1);
|
||||
if (hookValidation) {
|
||||
return hookValidation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// YAML parser for collection files and frontmatter parsing using vfile-matter
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import { VFile } from "vfile";
|
||||
import { matter } from "vfile-matter";
|
||||
|
||||
@@ -173,7 +173,7 @@ function parseSkillMetadata(skillPath) {
|
||||
const relativePath = path.relative(skillPath, filePath);
|
||||
if (relativePath !== "SKILL.md") {
|
||||
// Normalize path separators to forward slashes for cross-platform consistency
|
||||
arrayOfFiles.push(relativePath.replace(/\\/g, '/'));
|
||||
arrayOfFiles.push(relativePath.replace(/\\/g, "/"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -195,6 +195,83 @@ function parseSkillMetadata(skillPath) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse hook metadata from a hook folder (similar to skills)
|
||||
* @param {string} hookPath - Path to the hook folder
|
||||
* @returns {object|null} Hook metadata or null on error
|
||||
*/
|
||||
function parseHookMetadata(hookPath) {
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
const readmeFile = path.join(hookPath, "README.md");
|
||||
if (!fs.existsSync(readmeFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const frontmatter = parseFrontmatter(readmeFile);
|
||||
|
||||
// Validate required fields
|
||||
if (!frontmatter?.name || !frontmatter?.description) {
|
||||
console.warn(
|
||||
`Invalid hook at ${hookPath}: missing name or description in frontmatter`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract hook events from hooks.json if it exists
|
||||
let hookEvents = [];
|
||||
const hooksJsonPath = path.join(hookPath, "hooks.json");
|
||||
if (fs.existsSync(hooksJsonPath)) {
|
||||
try {
|
||||
const hooksJsonContent = fs.readFileSync(hooksJsonPath, "utf8");
|
||||
const hooksConfig = JSON.parse(hooksJsonContent);
|
||||
// Extract all hook event names from the hooks object
|
||||
if (hooksConfig.hooks && typeof hooksConfig.hooks === "object") {
|
||||
hookEvents = Object.keys(hooksConfig.hooks);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to parse hooks.json at ${hookPath}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// List bundled assets (all files except README.md), recursing through subdirectories
|
||||
const getAllFiles = (dirPath, arrayOfFiles = []) => {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(dirPath, file);
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
|
||||
} else {
|
||||
const relativePath = path.relative(hookPath, filePath);
|
||||
if (relativePath !== "README.md") {
|
||||
// Normalize path separators to forward slashes for cross-platform consistency
|
||||
arrayOfFiles.push(relativePath.replace(/\\/g, "/"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFiles;
|
||||
};
|
||||
|
||||
const assets = getAllFiles(hookPath).sort();
|
||||
|
||||
return {
|
||||
name: frontmatter.name,
|
||||
description: frontmatter.description,
|
||||
hooks: hookEvents,
|
||||
tags: frontmatter.tags || [],
|
||||
assets,
|
||||
path: hookPath,
|
||||
};
|
||||
},
|
||||
hookPath,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a generic YAML file (used for tools.yml and other config files)
|
||||
* @param {string} filePath - Path to the YAML file
|
||||
@@ -212,12 +289,13 @@ function parseYamlFile(filePath) {
|
||||
}
|
||||
|
||||
export {
|
||||
extractAgentMetadata,
|
||||
extractMcpServerConfigs,
|
||||
extractMcpServers,
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
extractAgentMetadata,
|
||||
extractMcpServers,
|
||||
extractMcpServerConfigs,
|
||||
parseSkillMetadata,
|
||||
parseHookMetadata,
|
||||
parseYamlFile,
|
||||
safeFileOperation,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user