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:
Aaron Powell
2026-02-09 16:44:53 +11:00
parent d99ba71986
commit acb5ad4ce8
17 changed files with 783 additions and 66 deletions

View File

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