Merge pull request #685 from github/hooks

Hooks
This commit is contained in:
Aaron Powell
2026-02-11 14:16:50 +11:00
committed by GitHub
23 changed files with 1232 additions and 66 deletions

View File

@@ -8,6 +8,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
- **Prompts** - Task-specific prompts for code generation and problem-solving
- **Instructions** - Coding standards and best practices applied to specific file patterns
- **Skills** - Self-contained folders with instructions and bundled resources for specialized tasks
- **Hooks** - Automated workflows triggered by specific events during development
- **Collections** - Curated collections organized around specific themes and workflows
## Repository Structure
@@ -18,6 +19,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
├── prompts/ # Task-specific prompts (.prompt.md files)
├── instructions/ # Coding standards and guidelines (.instructions.md files)
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
├── hooks/ # Automated workflow hooks (folders with README.md + hooks.json)
├── collections/ # Curated collections of resources (.md files)
├── docs/ # Documentation for different resource types
├── eng/ # Build and automation scripts
@@ -48,9 +50,9 @@ npm run skill:create -- --name <skill-name>
## Development Workflow
### Working with Agents, Prompts, Instructions, and Skills
### Working with Agents, Prompts, Instructions, Skills, and Hooks
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets:
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets. Hooks are folders containing a `README.md` with frontmatter and a `hooks.json` configuration file:
#### Agent Files (*.agent.md)
- Must have `description` field (wrapped in single quotes)
@@ -80,9 +82,20 @@ All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction fi
- Asset files should be reasonably sized (under 5MB per file)
- Skills follow the [Agent Skills specification](https://agentskills.io/specification)
#### Hook Folders (hooks/*/README.md)
- Each hook is a folder containing a `README.md` file with frontmatter
- README.md must have `name` field (human-readable name)
- README.md must have `description` field (wrapped in single quotes, not empty)
- Must include a `hooks.json` file with hook configuration (hook events extracted from this file)
- Folder names should be lower case with words separated by hyphens
- Can include bundled assets (scripts, utilities, configuration files)
- Bundled scripts should be referenced in the README.md and hooks.json
- Follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- Optionally includes `tags` field for categorization
### Adding New Resources
When adding a new agent, prompt, instruction, or skill:
When adding a new agent, prompt, instruction, skill, or hook:
**For Agents, Prompts, and Instructions:**
1. Create the file with proper front matter
@@ -90,6 +103,16 @@ When adding a new agent, prompt, instruction, or skill:
3. Update the README.md by running: `npm run build`
4. Verify the resource appears in the generated README
**For Hooks:**
1. Create a new folder in `hooks/` with a descriptive name
2. Create `README.md` with proper frontmatter (name, description, hooks, tags)
3. Create `hooks.json` with hook configuration following GitHub Copilot hooks spec
4. Add any bundled scripts or assets to the folder
5. Make scripts executable: `chmod +x script.sh`
6. Update the README.md by running: `npm run build`
7. Verify the hook appears in the generated README
**For Skills:**
1. Run `npm run skill:create` to scaffold a new skill folder
2. Edit the generated SKILL.md file with your instructions
@@ -186,6 +209,16 @@ For skills (skills/*/):
- [ ] Any bundled assets are referenced in SKILL.md
- [ ] Bundled assets are under 5MB per file
For hook folders (hooks/*/):
- [ ] Folder contains a README.md file with markdown front matter
- [ ] Has `name` field with human-readable name
- [ ] Has non-empty `description` field wrapped in single quotes
- [ ] Has `hooks.json` file with valid hook configuration (hook events extracted from this file)
- [ ] Folder name is lower case with hyphens
- [ ] Any bundled scripts are executable and referenced in README.md
- [ ] Follows [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- [ ] Optionally includes `tags` array field for categorization
## Contributing
This is a community-driven project. Contributions are welcome! Please see:

View File

@@ -11,6 +11,7 @@ This repository provides a comprehensive toolkit for enhancing GitHub Copilot wi
- **👉 [Awesome Agents](docs/README.agents.md)** - Specialized GitHub Copilot agents that integrate with MCP servers to provide enhanced capabilities for specific workflows and tools
- **👉 [Awesome Prompts](docs/README.prompts.md)** - Focused, task-specific prompts for generating code, documentation, and solving specific problems
- **👉 [Awesome Instructions](docs/README.instructions.md)** - Comprehensive coding standards and best practices that apply to specific file patterns or entire projects
- **👉 [Awesome Hooks](docs/README.hooks.md)** - Automated workflows triggered by specific events during development, testing, and deployment
- **👉 [Awesome Skills](docs/README.skills.md)** - Self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks
- **👉 [Awesome Collections](docs/README.collections.md)** - Curated collections of related prompts, instructions, agents, and skills organized around specific themes and workflows
- **👉 [Awesome Cookbook Recipes](cookbook/README.md)** - Practical, copy-paste-ready code snippets and real-world examples for working with GitHub Copilot tools and features
@@ -96,6 +97,10 @@ Use the `/` command in GitHub Copilot Chat to access prompts:
Instructions automatically apply to files based on their patterns and provide contextual guidance for coding standards, frameworks, and best practices.
### 🪝 Hooks
Hooks enable automated workflows triggered by specific events during GitHub Copilot coding agent sessions (like sessionStart, sessionEnd, userPromptSubmitted). They can automate tasks like logging, auto-committing changes, or integrating with external services.
## 🎯 Why Use Awesome GitHub Copilot?
- **Productivity**: Pre-built agents, prompts and instructions save time and provide consistent results.
@@ -107,7 +112,7 @@ Instructions automatically apply to files based on their patterns and provide co
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on how to:
- Add new prompts, instructions, agents, or skills
- Add new prompts, instructions, hooks, agents, or skills
- Improve existing content
- Report issues or suggest enhancements
@@ -148,7 +153,7 @@ The customizations in this repository are sourced from and created by third-part
---
**Ready to supercharge your coding experience?** Start exploring our [prompts](docs/README.prompts.md), [instructions](docs/README.instructions.md), and [custom agents](docs/README.agents.md)!
**Ready to supercharge your coding experience?** Start exploring our [prompts](docs/README.prompts.md), [instructions](docs/README.instructions.md), [hooks](docs/README.hooks.md), and [custom agents](docs/README.agents.md)!
## Contributors ✨

31
docs/README.hooks.md Normal file
View File

@@ -0,0 +1,31 @@
# 🪝 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.
### 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
| Name | Description | Events | Bundled Assets |
| ---- | ----------- | ------ | -------------- |
| [Session Auto-Commit](../hooks/session-auto-commit/README.md) | Automatically commits and pushes changes when a Copilot coding agent session ends | sessionEnd | `auto-commit.sh`<br />`hooks.json` |
| [Session Logger](../hooks/session-logger/README.md) | Logs all Copilot coding agent session activity for audit and analysis | sessionStart, sessionEnd, userPromptSubmitted | `hooks.json`<br />`log-prompt.sh`<br />`log-session-end.sh`<br />`log-session-start.sh` |

View File

@@ -4,7 +4,11 @@ import fs from "fs";
import path from "path";
import readline from "readline";
import { COLLECTIONS_DIR, ROOT_FOLDER } from "./constants.mjs";
import { parseCollectionYaml, parseFrontmatter } from "./yaml-parser.mjs";
import {
parseCollectionYaml,
parseFrontmatter,
parseHookMetadata,
} from "./yaml-parser.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
@@ -158,6 +162,12 @@ function getDisplayName(filePath, kind) {
return basename.replace(".agent.md", "");
} else if (kind === "instruction") {
return basename.replace(".instructions.md", "");
} else if (kind === "hook") {
// For folder-based hooks like hooks/<hook>/README.md, use the folder name.
if (basename.toLowerCase() === "readme.md") {
return path.basename(path.dirname(filePath));
}
return basename.replace(".hook.md", "");
} else if (kind === "skill") {
return path.basename(filePath);
}
@@ -221,6 +231,27 @@ 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;
// Extract events from hooks.json rather than frontmatter
const hookFolderPath = path.join(ROOT_FOLDER, path.dirname(item.path));
const hookMeta = parseHookMetadata(hookFolderPath);
const event =
hookMeta?.hooks?.length > 0 ? hookMeta.hooks.join(", ") : "N/A";
lines.push(`| \`${name}\` | ${description} | ${event} |`);
}
lines.push("");
}
// Skills
const skills = items.filter((item) => item.kind === "skill");
if (skills.length > 0) {

View File

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

View File

@@ -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)
@@ -825,6 +923,7 @@ async function main() {
prompts: prompts.length,
instructions: instructions.length,
skills: skills.length,
hooks: hooks.length,
collections: collections.length,
tools: tools.length,
samples: samplesData.totalRecipes,

View File

@@ -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"),

View File

@@ -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,15 @@ function validateCollectionItems(items) {
i + 1
} kind is "agent" but path doesn't end with .agent.md`;
}
if (item.kind === "hook") {
const isValidHookPath =
item.path.startsWith("hooks/") && item.path.endsWith("/README.md");
if (!isValidHookPath) {
return `Item ${
i + 1
} kind is "hook" but path must be hooks/<hook>/README.md`;
}
}
// Validate agent-specific frontmatter
if (item.kind === "agent") {
@@ -216,6 +260,14 @@ function validateCollectionItems(items) {
return agentValidation;
}
}
// Validate hook-specific frontmatter
if (item.kind === "hook") {
const hookValidation = validateHookFile(filePath);
if (hookValidation) {
return hookValidation;
}
}
}
return null;
}

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

View File

@@ -0,0 +1,90 @@
---
name: 'Session Auto-Commit'
description: 'Automatically commits and pushes changes when a Copilot coding agent session ends'
tags: ['automation', 'git', 'productivity']
---
# Session Auto-Commit Hook
Automatically commits and pushes changes when a GitHub Copilot coding agent session ends, ensuring your work is always saved and backed up.
## Overview
This hook runs at the end of each Copilot coding agent session and automatically:
- Detects if there are uncommitted changes
- Stages all changes
- Creates a timestamped commit
- Pushes to the remote repository
## Features
- **Automatic Backup**: Never lose work from a Copilot session
- **Timestamped Commits**: Each auto-commit includes the session end time
- **Safe Execution**: Only commits when there are actual changes
- **Error Handling**: Gracefully handles push failures
## Installation
1. Copy this hook folder to your repository's `.github/hooks/` directory:
```bash
cp -r hooks/session-auto-commit .github/hooks/
```
2. Ensure the script is executable:
```bash
chmod +x .github/hooks/session-auto-commit/auto-commit.sh
```
3. Commit the hook configuration to your repository's default branch
## Configuration
The hook is configured in `hooks.json` to run on the `sessionEnd` event:
```json
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"type": "command",
"bash": ".github/hooks/session-auto-commit/auto-commit.sh",
"timeoutSec": 30
}
]
}
}
```
## How It Works
1. When a Copilot coding agent session ends, the hook executes
2. Checks if inside a Git repository
3. Detects uncommitted changes using `git status`
4. Stages all changes with `git add -A`
5. Creates a commit with format: `auto-commit: YYYY-MM-DD HH:MM:SS`
6. Attempts to push to remote
7. Reports success or failure
## Customization
You can customize the hook by modifying `auto-commit.sh`:
- **Commit Message Format**: Change the timestamp format or message prefix
- **Selective Staging**: Use specific git add patterns instead of `-A`
- **Branch Selection**: Push to specific branches only
- **Notifications**: Add desktop notifications or Slack messages
## Disabling
To temporarily disable auto-commits:
1. Remove or comment out the `sessionEnd` hook in `hooks.json`
2. Or set an environment variable: `export SKIP_AUTO_COMMIT=true`
## Notes
- The hook uses `--no-verify` to avoid triggering pre-commit hooks
- Failed pushes won't block session termination
- Requires appropriate git credentials configured
- Works with both Copilot coding agent and GitHub Copilot CLI

View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Session Auto-Commit Hook
# Automatically commits and pushes changes when a Copilot session ends
set -euo pipefail
# Check if SKIP_AUTO_COMMIT is set
if [[ "${SKIP_AUTO_COMMIT:-}" == "true" ]]; then
echo "⏭️ Auto-commit skipped (SKIP_AUTO_COMMIT=true)"
exit 0
fi
# Check if we're in a git repository
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
echo "⚠️ Not in a git repository"
exit 0
fi
# Check for uncommitted changes
if [[ -z "$(git status --porcelain)" ]]; then
echo "✨ No changes to commit"
exit 0
fi
echo "📦 Auto-committing changes from Copilot session..."
# Stage all changes
git add -A
# Create timestamped commit
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
git commit -m "auto-commit: $TIMESTAMP" --no-verify 2>/dev/null || {
echo "⚠️ Commit failed"
exit 0
}
# Attempt to push
if git push 2>/dev/null; then
echo "✅ Changes committed and pushed successfully"
else
echo "⚠️ Push failed - changes committed locally"
fi
exit 0

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"type": "command",
"bash": ".github/hooks/session-auto-commit/auto-commit.sh",
"timeoutSec": 30
}
]
}
}

View File

@@ -0,0 +1,58 @@
---
name: 'Session Logger'
description: 'Logs all Copilot coding agent session activity for audit and analysis'
tags: ['logging', 'audit', 'analytics']
---
# Session Logger Hook
Comprehensive logging for GitHub Copilot coding agent sessions, tracking session starts, ends, and user prompts for audit trails and usage analytics.
## Overview
This hook provides detailed logging of Copilot coding agent activity:
- Session start/end times with working directory context
- User prompt submission events
- Configurable log levels
## Features
- **Session Tracking**: Log session start and end events
- **Prompt Logging**: Record when user prompts are submitted
- **Structured Logging**: JSON format for easy parsing
- **Privacy Aware**: Configurable to disable logging entirely
## Installation
1. Copy this hook folder to your repository's `.github/hooks/` directory:
```bash
cp -r hooks/session-logger .github/hooks/
```
2. Create the logs directory:
```bash
mkdir -p logs/copilot
```
3. Ensure scripts are executable:
```bash
chmod +x .github/hooks/session-logger/*.sh
```
4. Commit the hook configuration to your repository's default branch
## Log Format
Session events are written to `logs/copilot/session.log` and prompt events to `logs/copilot/prompts.log` in JSON format:
```json
{"timestamp":"2024-01-15T10:30:00Z","event":"sessionStart","cwd":"/workspace/project"}
{"timestamp":"2024-01-15T10:35:00Z","event":"sessionEnd"}
```
## Privacy & Security
- Add `logs/` to `.gitignore` to avoid committing session data
- Use `LOG_LEVEL=ERROR` to only log errors
- Set `SKIP_LOGGING=true` environment variable to disable
- Logs are stored locally only

View File

@@ -0,0 +1,32 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": ".github/hooks/session-logger/log-session-start.sh",
"cwd": ".",
"timeoutSec": 5
}
],
"sessionEnd": [
{
"type": "command",
"bash": ".github/hooks/session-logger/log-session-end.sh",
"cwd": ".",
"timeoutSec": 5
}
],
"userPromptSubmitted": [
{
"type": "command",
"bash": ".github/hooks/session-logger/log-prompt.sh",
"cwd": ".",
"env": {
"LOG_LEVEL": "INFO"
},
"timeoutSec": 5
}
]
}
}

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Log user prompt submission
set -euo pipefail
# Skip if logging disabled
if [[ "${SKIP_LOGGING:-}" == "true" ]]; then
exit 0
fi
# Read input from Copilot (contains prompt info)
INPUT=$(cat)
# Create logs directory if it doesn't exist
mkdir -p logs/copilot
# Extract timestamp
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Log prompt (you can parse INPUT for more details)
echo "{\"timestamp\":\"$TIMESTAMP\",\"event\":\"userPromptSubmitted\",\"level\":\"${LOG_LEVEL:-INFO}\"}" >> logs/copilot/prompts.log
exit 0

View File

@@ -0,0 +1,25 @@
#!/bin/bash
# Log session end event
set -euo pipefail
# Skip if logging disabled
if [[ "${SKIP_LOGGING:-}" == "true" ]]; then
exit 0
fi
# Read input from Copilot
INPUT=$(cat)
# Create logs directory if it doesn't exist
mkdir -p logs/copilot
# Extract timestamp
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Log session end
echo "{\"timestamp\":\"$TIMESTAMP\",\"event\":\"sessionEnd\"}" >> logs/copilot/session.log
echo "📝 Session end logged"
exit 0

View File

@@ -0,0 +1,26 @@
#!/bin/bash
# Log session start event
set -euo pipefail
# Skip if logging disabled
if [[ "${SKIP_LOGGING:-}" == "true" ]]; then
exit 0
fi
# Read input from Copilot
INPUT=$(cat)
# Create logs directory if it doesn't exist
mkdir -p logs/copilot
# Extract timestamp and session info
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
CWD=$(pwd)
# Log session start (use jq for proper JSON encoding)
jq -Rn --arg timestamp "$TIMESTAMP" --arg cwd "$CWD" '{"timestamp":$timestamp,"event":"sessionStart","cwd":$cwd}' >> logs/copilot/session.log
echo "📝 Session logged"
exit 0

View File

@@ -86,6 +86,10 @@ try {
href={`${base}skills/`}
class:list={[{ active: activeNav === "skills" }]}>Skills</a
>
<a
href={`${base}hooks/`}
class:list={[{ active: activeNav === "hooks" }]}>Hooks</a
>
<a
href={`${base}collections/`}
class:list={[{ active: activeNav === "collections" }]}

View File

@@ -0,0 +1,54 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Modal from '../components/Modal.astro';
---
<BaseLayout title="Hooks" description="Automated workflows triggered by Copilot coding agent events" activeNav="hooks">
<main id="main-content">
<div class="page-header">
<div class="container">
<h1>🪝 Hooks</h1>
<p>Automated workflows triggered by Copilot coding agent events</p>
</div>
</div>
<div class="page-content">
<div class="container">
<div class="search-bar">
<label for="search-input" class="sr-only">Search hooks</label>
<input type="text" id="search-input" placeholder="Search hooks..." autocomplete="off">
</div>
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-hook">Hook Event:</label>
<select id="filter-hook" multiple aria-label="Filter by hook event"></select>
</div>
<div class="filter-group">
<label for="filter-tag">Tag:</label>
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" aria-label="Sort by">
<option value="title">Name (A-Z)</option>
<option value="lastUpdated">Recently Updated</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>
<div class="results-count" id="results-count" aria-live="polite"></div>
<div class="resource-list" id="resource-list" role="list">
<div class="loading" aria-live="polite">Loading hooks...</div>
</div>
</div>
</div>
</main>
<Modal />
<script>
import '../scripts/pages/hooks';
</script>
</BaseLayout>

View File

@@ -57,6 +57,14 @@ const base = import.meta.env.BASE_URL;
</div>
<div class="card-count" data-count="skills" aria-label="Skill count">-</div>
</a>
<a href={`${base}hooks/`} class="card card-with-count" id="card-hooks">
<div class="card-icon" aria-hidden="true">🪝</div>
<div class="card-content">
<h3>Hooks</h3>
<p>Automated workflows triggered by agent events</p>
</div>
<div class="card-count" data-count="hooks" aria-label="Hook count">-</div>
</a>
<a href={`${base}collections/`} class="card card-with-count" id="card-collections">
<div class="card-icon" aria-hidden="true">📦</div>
<div class="card-content">

View File

@@ -0,0 +1,348 @@
/**
* Hooks page functionality
*/
import { createChoices, getChoicesValues, type Choices } from "../choices";
import { FuzzySearch, SearchItem } from "../search";
import {
fetchData,
debounce,
escapeHtml,
getGitHubUrl,
getRawGitHubUrl,
showToast,
getLastUpdatedHtml,
} from "../utils";
import { setupModal, openFileModal } from "../modal";
import JSZip from "../jszip";
interface Hook extends SearchItem {
id: string;
path: string;
readmeFile: string;
hooks: string[];
tags: string[];
assets: string[];
lastUpdated?: string | null;
}
interface HooksData {
items: Hook[];
filters: {
hooks: string[];
tags: string[];
};
}
type SortOption = "title" | "lastUpdated";
const resourceType = "hook";
let allItems: Hook[] = [];
let search = new FuzzySearch<Hook>();
let hookSelect: Choices;
let tagSelect: Choices;
let currentFilters = {
hooks: [] as string[],
tags: [] as string[],
};
let currentSort: SortOption = "title";
function sortItems(items: Hook[]): Hook[] {
return [...items].sort((a, b) => {
if (currentSort === "lastUpdated") {
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
return dateB - dateA;
}
return a.title.localeCompare(b.title);
});
}
function applyFiltersAndRender(): void {
const searchInput = document.getElementById(
"search-input"
) as HTMLInputElement;
const countEl = document.getElementById("results-count");
const query = searchInput?.value || "";
let results = query ? search.search(query) : [...allItems];
if (currentFilters.hooks.length > 0) {
results = results.filter((item) =>
item.hooks.some((h) => currentFilters.hooks.includes(h))
);
}
if (currentFilters.tags.length > 0) {
results = results.filter((item) =>
item.tags.some((t) => currentFilters.tags.includes(t))
);
}
results = sortItems(results);
renderItems(results, query);
const activeFilters: string[] = [];
if (currentFilters.hooks.length > 0)
activeFilters.push(
`${currentFilters.hooks.length} hook event${
currentFilters.hooks.length > 1 ? "s" : ""
}`
);
if (currentFilters.tags.length > 0)
activeFilters.push(
`${currentFilters.tags.length} tag${
currentFilters.tags.length > 1 ? "s" : ""
}`
);
let countText = `${results.length} of ${allItems.length} hooks`;
if (activeFilters.length > 0) {
countText += ` (filtered by ${activeFilters.join(", ")})`;
}
if (countEl) countEl.textContent = countText;
}
function renderItems(items: Hook[], query = ""): void {
const list = document.getElementById("resource-list");
if (!list) return;
if (items.length === 0) {
list.innerHTML =
'<div class="empty-state"><h3>No hooks found</h3><p>Try a different search term or adjust filters</p></div>';
return;
}
list.innerHTML = items
.map(
(item) => `
<div class="resource-item" data-path="${escapeHtml(
item.readmeFile
)}" data-hook-id="${escapeHtml(item.id)}">
<div class="resource-info">
<div class="resource-title">${
query ? search.highlight(item.title, query) : escapeHtml(item.title)
}</div>
<div class="resource-description">${escapeHtml(
item.description || "No description"
)}</div>
<div class="resource-meta">
${item.hooks
.map(
(h) =>
`<span class="resource-tag tag-hook">${escapeHtml(h)}</span>`
)
.join("")}
${item.tags
.map(
(t) =>
`<span class="resource-tag tag-tag">${escapeHtml(t)}</span>`
)
.join("")}
${
item.assets.length > 0
? `<span class="resource-tag tag-assets">${
item.assets.length
} asset${item.assets.length === 1 ? "" : "s"}</span>`
: ""
}
${getLastUpdatedHtml(item.lastUpdated)}
</div>
</div>
<div class="resource-actions">
<button class="btn btn-primary download-hook-btn" data-hook-id="${escapeHtml(
item.id
)}" title="Download as ZIP">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"/>
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"/>
</svg>
Download
</button>
<a href="${getGitHubUrl(
item.path
)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
</div>
</div>
`
)
.join("");
// Add click handlers for opening modal
list.querySelectorAll(".resource-item").forEach((el) => {
el.addEventListener("click", (e) => {
if ((e.target as HTMLElement).closest(".resource-actions")) return;
const path = (el as HTMLElement).dataset.path;
if (path) openFileModal(path, resourceType);
});
});
// Add download handlers
list.querySelectorAll(".download-hook-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const hookId = (btn as HTMLElement).dataset.hookId;
if (hookId) downloadHook(hookId, btn as HTMLButtonElement);
});
});
}
async function downloadHook(
hookId: string,
btn: HTMLButtonElement
): Promise<void> {
const hook = allItems.find((item) => item.id === hookId);
if (!hook) {
showToast("Hook not found.", "error");
return;
}
// Build file list: README.md + all assets
const files = [
{ name: "README.md", path: hook.readmeFile },
...hook.assets.map((a) => ({
name: a,
path: `${hook.path}/${a}`,
})),
];
if (files.length === 0) {
showToast("No files found for this hook.", "error");
return;
}
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML =
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
try {
const zip = new JSZip();
const folder = zip.folder(hook.id);
const fetchPromises = files.map(async (file) => {
const url = getRawGitHubUrl(file.path);
try {
const response = await fetch(url);
if (!response.ok) return null;
const content = await response.text();
return { name: file.name, content };
} catch {
return null;
}
});
const results = await Promise.all(fetchPromises);
let addedFiles = 0;
for (const result of results) {
if (result && folder) {
folder.file(result.name, result.content);
addedFiles++;
}
}
if (addedFiles === 0) throw new Error("Failed to fetch any files");
const blob = await zip.generateAsync({ type: "blob" });
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = `${hook.id}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
btn.innerHTML =
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalContent;
}, 2000);
} catch (error) {
const message =
error instanceof Error ? error.message : "Download failed.";
showToast(message, "error");
btn.innerHTML =
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalContent;
}, 2000);
}
}
export async function initHooksPage(): Promise<void> {
const list = document.getElementById("resource-list");
const searchInput = document.getElementById(
"search-input"
) as HTMLInputElement;
const clearFiltersBtn = document.getElementById("clear-filters");
const sortSelect = document.getElementById(
"sort-select"
) as HTMLSelectElement;
const data = await fetchData<HooksData>("hooks.json");
if (!data || !data.items) {
if (list)
list.innerHTML =
'<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data.items;
search.setItems(allItems);
// Setup hook event filter
hookSelect = createChoices("#filter-hook", {
placeholderValue: "All Events",
});
hookSelect.setChoices(
data.filters.hooks.map((h) => ({ value: h, label: h })),
"value",
"label",
true
);
document.getElementById("filter-hook")?.addEventListener("change", () => {
currentFilters.hooks = getChoicesValues(hookSelect);
applyFiltersAndRender();
});
// Setup tag filter
tagSelect = createChoices("#filter-tag", {
placeholderValue: "All Tags",
});
tagSelect.setChoices(
data.filters.tags.map((t) => ({ value: t, label: t })),
"value",
"label",
true
);
document.getElementById("filter-tag")?.addEventListener("change", () => {
currentFilters.tags = getChoicesValues(tagSelect);
applyFiltersAndRender();
});
sortSelect?.addEventListener("change", () => {
currentSort = sortSelect.value as SortOption;
applyFiltersAndRender();
});
applyFiltersAndRender();
searchInput?.addEventListener(
"input",
debounce(() => applyFiltersAndRender(), 200)
);
clearFiltersBtn?.addEventListener("click", () => {
currentFilters = { hooks: [], tags: [] };
currentSort = "title";
hookSelect.removeActiveItems();
tagSelect.removeActiveItems();
if (searchInput) searchInput.value = "";
if (sortSelect) sortSelect.value = "title";
applyFiltersAndRender();
});
setupModal();
}
// Auto-initialize when DOM is ready
document.addEventListener("DOMContentLoaded", initHooksPage);

View File

@@ -11,6 +11,7 @@ interface Manifest {
prompts: number;
instructions: number;
skills: number;
hooks: number;
collections: number;
tools: number;
};
@@ -35,7 +36,7 @@ export async function initHomepage(): Promise<void> {
const manifest = await fetchData<Manifest>('manifest.json');
if (manifest && manifest.counts) {
// Populate counts in cards
const countKeys = ['agents', 'prompts', 'instructions', 'skills', 'collections', 'tools'] as const;
const countKeys = ['agents', 'prompts', 'instructions', 'skills', 'hooks', 'collections', 'tools'] as const;
countKeys.forEach(key => {
const countEl = document.querySelector(`.card-count[data-count="${key}"]`);
if (countEl && manifest.counts[key] !== undefined) {

View File

@@ -229,8 +229,10 @@ export function getResourceType(filePath: string): string {
if (filePath.endsWith(".agent.md")) return "agent";
if (filePath.endsWith(".prompt.md")) return "prompt";
if (filePath.endsWith(".instructions.md")) return "instruction";
if (filePath.includes("/skills/") && filePath.endsWith("SKILL.md"))
if (/(^|\/)skills\//.test(filePath) && filePath.endsWith("SKILL.md"))
return "skill";
if (/(^|\/)hooks\//.test(filePath) && filePath.endsWith("README.md"))
return "hook";
if (filePath.endsWith(".collection.yml")) return "collection";
return "unknown";
}
@@ -244,6 +246,7 @@ export function formatResourceType(type: string): string {
prompt: "🎯 Prompt",
instruction: "📋 Instruction",
skill: "⚡ Skill",
hook: "🪝 Hook",
collection: "📦 Collection",
};
return labels[type] || type;
@@ -258,6 +261,7 @@ export function getResourceIcon(type: string): string {
prompt: "🎯",
instruction: "📋",
skill: "⚡",
hook: "🪝",
collection: "📦",
};
return icons[type] || "📄";
@@ -499,5 +503,7 @@ export function getLastUpdatedHtml(isoDate: string | null | undefined): string {
return `<span class="last-updated">Updated: Unknown</span>`;
}
return `<span class="last-updated" title="${escapeHtml(fullDate)}">Updated ${relativeTime}</span>`;
return `<span class="last-updated" title="${escapeHtml(
fullDate
)}">Updated ${relativeTime}</span>`;
}