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:
39
AGENTS.md
39
AGENTS.md
@@ -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
|
- **Prompts** - Task-specific prompts for code generation and problem-solving
|
||||||
- **Instructions** - Coding standards and best practices applied to specific file patterns
|
- **Instructions** - Coding standards and best practices applied to specific file patterns
|
||||||
- **Skills** - Self-contained folders with instructions and bundled resources for specialized tasks
|
- **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
|
- **Collections** - Curated collections organized around specific themes and workflows
|
||||||
|
|
||||||
## Repository Structure
|
## 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)
|
├── prompts/ # Task-specific prompts (.prompt.md files)
|
||||||
├── instructions/ # Coding standards and guidelines (.instructions.md files)
|
├── instructions/ # Coding standards and guidelines (.instructions.md files)
|
||||||
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
|
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
|
||||||
|
├── hooks/ # Automated workflow hooks (.hook.md files)
|
||||||
├── collections/ # Curated collections of resources (.md files)
|
├── collections/ # Curated collections of resources (.md files)
|
||||||
├── docs/ # Documentation for different resource types
|
├── docs/ # Documentation for different resource types
|
||||||
├── eng/ # Build and automation scripts
|
├── eng/ # Build and automation scripts
|
||||||
@@ -48,9 +50,9 @@ npm run skill:create -- --name <skill-name>
|
|||||||
|
|
||||||
## Development Workflow
|
## 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`), instruction files (`*.instructions.md`), and hook files (`*.hook.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets:
|
||||||
|
|
||||||
#### Agent Files (*.agent.md)
|
#### Agent Files (*.agent.md)
|
||||||
- Must have `description` field (wrapped in single quotes)
|
- 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)
|
- Asset files should be reasonably sized (under 5MB per file)
|
||||||
- Skills follow the [Agent Skills specification](https://agentskills.io/specification)
|
- 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
|
### 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:**
|
**For Agents, Prompts, and Instructions:**
|
||||||
1. Create the file with proper front matter
|
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`
|
3. Update the README.md by running: `npm run build`
|
||||||
4. Verify the resource appears in the generated README
|
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:**
|
**For Skills:**
|
||||||
1. Run `npm run skill:create` to scaffold a new skill folder
|
1. Run `npm run skill:create` to scaffold a new skill folder
|
||||||
2. Edit the generated SKILL.md file with your instructions
|
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
|
- [ ] Any bundled assets are referenced in SKILL.md
|
||||||
- [ ] Bundled assets are under 5MB per file
|
- [ ] 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
|
## Contributing
|
||||||
|
|
||||||
This is a community-driven project. Contributions are welcome! Please see:
|
This is a community-driven project. Contributions are welcome! Please see:
|
||||||
|
|||||||
@@ -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 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 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 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 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 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
|
- **👉 [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.
|
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?
|
## 🎯 Why Use Awesome GitHub Copilot?
|
||||||
|
|
||||||
- **Productivity**: Pre-built agents, prompts and instructions save time and provide consistent results.
|
- **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:
|
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
|
- Improve existing content
|
||||||
- Report issues or suggest enhancements
|
- 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 ✨
|
## Contributors ✨
|
||||||
|
|
||||||
|
|||||||
31
docs/README.hooks.md
Normal file
31
docs/README.hooks.md
Normal 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` |
|
||||||
@@ -158,6 +158,8 @@ function getDisplayName(filePath, kind) {
|
|||||||
return basename.replace(".agent.md", "");
|
return basename.replace(".agent.md", "");
|
||||||
} else if (kind === "instruction") {
|
} else if (kind === "instruction") {
|
||||||
return basename.replace(".instructions.md", "");
|
return basename.replace(".instructions.md", "");
|
||||||
|
} else if (kind === "hook") {
|
||||||
|
return basename.replace(".hook.md", "");
|
||||||
} else if (kind === "skill") {
|
} else if (kind === "skill") {
|
||||||
return path.basename(filePath);
|
return path.basename(filePath);
|
||||||
}
|
}
|
||||||
@@ -221,6 +223,23 @@ function generateReadme(collection, items) {
|
|||||||
lines.push("");
|
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
|
// Skills
|
||||||
const skills = items.filter((item) => item.kind === "skill");
|
const skills = items.filter((item) => item.kind === "skill");
|
||||||
if (skills.length > 0) {
|
if (skills.length > 0) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import path from "path";
|
import path, { dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { dirname } from "path";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
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
|
- Browse the skills table below to find relevant capabilities
|
||||||
- Copy the skill folder to your local skills directory
|
- Copy the skill folder to your local skills directory
|
||||||
- Reference skills in your prompts or let the agent discover them automatically`,
|
- 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 =
|
const vscodeInstallImage =
|
||||||
@@ -115,6 +142,7 @@ const AKA_INSTALL_URLS = {
|
|||||||
instructions: "https://aka.ms/awesome-copilot/install/instructions",
|
instructions: "https://aka.ms/awesome-copilot/install/instructions",
|
||||||
prompt: "https://aka.ms/awesome-copilot/install/prompt",
|
prompt: "https://aka.ms/awesome-copilot/install/prompt",
|
||||||
agent: "https://aka.ms/awesome-copilot/install/agent",
|
agent: "https://aka.ms/awesome-copilot/install/agent",
|
||||||
|
hook: "https://aka.ms/awesome-copilot/install/hook",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROOT_FOLDER = path.join(__dirname, "..");
|
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 PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
|
||||||
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
||||||
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
|
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 COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
|
||||||
const COOKBOOK_DIR = path.join(ROOT_FOLDER, "cookbook");
|
const COOKBOOK_DIR = path.join(ROOT_FOLDER, "cookbook");
|
||||||
const MAX_COLLECTION_ITEMS = 50;
|
const MAX_COLLECTION_ITEMS = 50;
|
||||||
@@ -135,23 +164,7 @@ const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
|
|||||||
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
|
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
|
||||||
|
|
||||||
export {
|
export {
|
||||||
TEMPLATES,
|
AGENTS_DIR, AKA_INSTALL_URLS, COLLECTIONS_DIR,
|
||||||
vscodeInstallImage,
|
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
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,24 +7,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path, { dirname } from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import {
|
import {
|
||||||
AGENTS_DIR,
|
AGENTS_DIR,
|
||||||
COLLECTIONS_DIR,
|
COLLECTIONS_DIR,
|
||||||
COOKBOOK_DIR,
|
COOKBOOK_DIR,
|
||||||
INSTRUCTIONS_DIR,
|
HOOKS_DIR,
|
||||||
PROMPTS_DIR,
|
INSTRUCTIONS_DIR,
|
||||||
ROOT_FOLDER,
|
PROMPTS_DIR,
|
||||||
SKILLS_DIR,
|
ROOT_FOLDER,
|
||||||
|
SKILLS_DIR
|
||||||
} from "./constants.mjs";
|
} from "./constants.mjs";
|
||||||
import {
|
|
||||||
parseCollectionYaml,
|
|
||||||
parseFrontmatter,
|
|
||||||
parseSkillMetadata,
|
|
||||||
parseYamlFile,
|
|
||||||
} from "./yaml-parser.mjs";
|
|
||||||
import { getGitFileDates } from "./utils/git-dates.mjs";
|
import { getGitFileDates } from "./utils/git-dates.mjs";
|
||||||
|
import {
|
||||||
|
parseCollectionYaml,
|
||||||
|
parseFrontmatter,
|
||||||
|
parseSkillMetadata,
|
||||||
|
parseHookMetadata,
|
||||||
|
parseYamlFile,
|
||||||
|
} from "./yaml-parser.mjs";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
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
|
* Generate prompts metadata
|
||||||
*/
|
*/
|
||||||
@@ -539,6 +610,7 @@ function generateSearchIndex(
|
|||||||
agents,
|
agents,
|
||||||
prompts,
|
prompts,
|
||||||
instructions,
|
instructions,
|
||||||
|
hooks,
|
||||||
skills,
|
skills,
|
||||||
collections
|
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) {
|
for (const skill of skills) {
|
||||||
index.push({
|
index.push({
|
||||||
type: "skill",
|
type: "skill",
|
||||||
@@ -720,7 +806,7 @@ async function main() {
|
|||||||
// Load git dates for all resource files (single efficient git command)
|
// Load git dates for all resource files (single efficient git command)
|
||||||
console.log("Loading git history for last updated dates...");
|
console.log("Loading git history for last updated dates...");
|
||||||
const gitDates = getGitFileDates(
|
const gitDates = getGitFileDates(
|
||||||
["agents/", "prompts/", "instructions/", "skills/", "collections/"],
|
["agents/", "prompts/", "instructions/", "hooks/", "skills/", "collections/"],
|
||||||
ROOT_FOLDER
|
ROOT_FOLDER
|
||||||
);
|
);
|
||||||
console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
|
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)`
|
`✓ 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 promptsData = generatePromptsData(gitDates);
|
||||||
const prompts = promptsData.items;
|
const prompts = promptsData.items;
|
||||||
console.log(
|
console.log(
|
||||||
@@ -771,6 +863,7 @@ async function main() {
|
|||||||
agents,
|
agents,
|
||||||
prompts,
|
prompts,
|
||||||
instructions,
|
instructions,
|
||||||
|
hooks,
|
||||||
skills,
|
skills,
|
||||||
collections
|
collections
|
||||||
);
|
);
|
||||||
@@ -782,6 +875,11 @@ async function main() {
|
|||||||
JSON.stringify(agentsData, null, 2)
|
JSON.stringify(agentsData, null, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(WEBSITE_DATA_DIR, "hooks.json"),
|
||||||
|
JSON.stringify(hooksData, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(WEBSITE_DATA_DIR, "prompts.json"),
|
path.join(WEBSITE_DATA_DIR, "prompts.json"),
|
||||||
JSON.stringify(promptsData, null, 2)
|
JSON.stringify(promptsData, null, 2)
|
||||||
|
|||||||
@@ -4,24 +4,26 @@ import fs from "fs";
|
|||||||
import path, { dirname } from "path";
|
import path, { dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import {
|
import {
|
||||||
AGENTS_DIR,
|
AGENTS_DIR,
|
||||||
AKA_INSTALL_URLS,
|
AKA_INSTALL_URLS,
|
||||||
COLLECTIONS_DIR,
|
COLLECTIONS_DIR,
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
INSTRUCTIONS_DIR,
|
HOOKS_DIR,
|
||||||
PROMPTS_DIR,
|
INSTRUCTIONS_DIR,
|
||||||
repoBaseUrl,
|
PROMPTS_DIR,
|
||||||
ROOT_FOLDER,
|
repoBaseUrl,
|
||||||
SKILLS_DIR,
|
ROOT_FOLDER,
|
||||||
TEMPLATES,
|
SKILLS_DIR,
|
||||||
vscodeInsidersInstallImage,
|
TEMPLATES,
|
||||||
vscodeInstallImage,
|
vscodeInsidersInstallImage,
|
||||||
|
vscodeInstallImage,
|
||||||
} from "./constants.mjs";
|
} from "./constants.mjs";
|
||||||
import {
|
import {
|
||||||
extractMcpServerConfigs,
|
extractMcpServerConfigs,
|
||||||
parseCollectionYaml,
|
parseCollectionYaml,
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
parseSkillMetadata,
|
parseSkillMetadata,
|
||||||
|
parseHookMetadata,
|
||||||
} from "./yaml-parser.mjs";
|
} from "./yaml-parser.mjs";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
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
|
* 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 promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
|
||||||
const agentsHeader = TEMPLATES.agentsSection.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 skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
|
||||||
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
||||||
/^##\s/m,
|
/^##\s/m,
|
||||||
@@ -1031,6 +1095,15 @@ async function main() {
|
|||||||
registryNames
|
registryNames
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Generate hooks README
|
||||||
|
const hooksReadme = buildCategoryReadme(
|
||||||
|
generateHooksSection,
|
||||||
|
HOOKS_DIR,
|
||||||
|
hooksHeader,
|
||||||
|
TEMPLATES.hooksUsage,
|
||||||
|
registryNames
|
||||||
|
);
|
||||||
|
|
||||||
// Generate skills README
|
// Generate skills README
|
||||||
const skillsReadme = buildCategoryReadme(
|
const skillsReadme = buildCategoryReadme(
|
||||||
generateSkillsSection,
|
generateSkillsSection,
|
||||||
@@ -1061,6 +1134,7 @@ async function main() {
|
|||||||
);
|
);
|
||||||
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
||||||
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
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.skills.md"), skillsReadme);
|
||||||
writeFileIfChanged(
|
writeFileIfChanged(
|
||||||
path.join(DOCS_DIR, "README.collections.md"),
|
path.join(DOCS_DIR, "README.collections.md"),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {
|
import {
|
||||||
COLLECTIONS_DIR,
|
COLLECTIONS_DIR,
|
||||||
MAX_COLLECTION_ITEMS,
|
MAX_COLLECTION_ITEMS,
|
||||||
ROOT_FOLDER,
|
ROOT_FOLDER,
|
||||||
} from "./constants.mjs";
|
} from "./constants.mjs";
|
||||||
import { parseCollectionYaml, parseFrontmatter } from "./yaml-parser.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) {
|
function validateCollectionItems(items) {
|
||||||
if (!items || !Array.isArray(items)) {
|
if (!items || !Array.isArray(items)) {
|
||||||
return "Items is required and must be an array";
|
return "Items is required and must be an array";
|
||||||
@@ -177,10 +212,10 @@ function validateCollectionItems(items) {
|
|||||||
if (!item.kind || typeof item.kind !== "string") {
|
if (!item.kind || typeof item.kind !== "string") {
|
||||||
return `Item ${i + 1} must have a 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 ${
|
return `Item ${
|
||||||
i + 1
|
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
|
// Validate file path exists
|
||||||
@@ -208,6 +243,11 @@ function validateCollectionItems(items) {
|
|||||||
i + 1
|
i + 1
|
||||||
} kind is "agent" but path doesn't end with .agent.md`;
|
} 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
|
// Validate agent-specific frontmatter
|
||||||
if (item.kind === "agent") {
|
if (item.kind === "agent") {
|
||||||
@@ -216,6 +256,14 @@ function validateCollectionItems(items) {
|
|||||||
return agentValidation;
|
return agentValidation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate hook-specific frontmatter
|
||||||
|
if (item.kind === "hook") {
|
||||||
|
const hookValidation = validateHookFile(filePath, i + 1);
|
||||||
|
if (hookValidation) {
|
||||||
|
return hookValidation;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// YAML parser for collection files and frontmatter parsing using vfile-matter
|
// YAML parser for collection files and frontmatter parsing using vfile-matter
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
import path from "path";
|
||||||
import { VFile } from "vfile";
|
import { VFile } from "vfile";
|
||||||
import { matter } from "vfile-matter";
|
import { matter } from "vfile-matter";
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ function parseSkillMetadata(skillPath) {
|
|||||||
const relativePath = path.relative(skillPath, filePath);
|
const relativePath = path.relative(skillPath, filePath);
|
||||||
if (relativePath !== "SKILL.md") {
|
if (relativePath !== "SKILL.md") {
|
||||||
// Normalize path separators to forward slashes for cross-platform consistency
|
// 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)
|
* Parse a generic YAML file (used for tools.yml and other config files)
|
||||||
* @param {string} filePath - Path to the YAML file
|
* @param {string} filePath - Path to the YAML file
|
||||||
@@ -212,12 +289,13 @@ function parseYamlFile(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
extractAgentMetadata,
|
||||||
|
extractMcpServerConfigs,
|
||||||
|
extractMcpServers,
|
||||||
parseCollectionYaml,
|
parseCollectionYaml,
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
extractAgentMetadata,
|
|
||||||
extractMcpServers,
|
|
||||||
extractMcpServerConfigs,
|
|
||||||
parseSkillMetadata,
|
parseSkillMetadata,
|
||||||
|
parseHookMetadata,
|
||||||
parseYamlFile,
|
parseYamlFile,
|
||||||
safeFileOperation,
|
safeFileOperation,
|
||||||
};
|
};
|
||||||
|
|||||||
90
hooks/session-auto-commit/README.md
Normal file
90
hooks/session-auto-commit/README.md
Normal 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
|
||||||
45
hooks/session-auto-commit/auto-commit.sh
Executable file
45
hooks/session-auto-commit/auto-commit.sh
Executable 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
|
||||||
12
hooks/session-auto-commit/hooks.json
Normal file
12
hooks/session-auto-commit/hooks.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"hooks": {
|
||||||
|
"sessionEnd": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"bash": ".github/hooks/session-auto-commit/auto-commit.sh",
|
||||||
|
"timeoutSec": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
64
hooks/session-logger/README.md
Normal file
64
hooks/session-logger/README.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
- User prompts and questions
|
||||||
|
- Session duration
|
||||||
|
- Working directory context
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Complete Audit Trail**: Track all Copilot interactions
|
||||||
|
- **Structured Logging**: JSON format for easy parsing
|
||||||
|
- **Searchable History**: Review past sessions and prompts
|
||||||
|
- **Analytics Ready**: Export data for usage analysis
|
||||||
|
- **Privacy Aware**: Configurable to exclude sensitive data
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
Logs are written to `logs/copilot/session.log` in JSON format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2024-01-15T10:30:00Z",
|
||||||
|
"event": "sessionStart",
|
||||||
|
"sessionId": "abc123",
|
||||||
|
"cwd": "/workspace/project"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
32
hooks/session-logger/hooks.json
Normal file
32
hooks/session-logger/hooks.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24
hooks/session-logger/log-prompt.sh
Executable file
24
hooks/session-logger/log-prompt.sh
Executable 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
|
||||||
25
hooks/session-logger/log-session-end.sh
Executable file
25
hooks/session-logger/log-session-end.sh
Executable 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
|
||||||
26
hooks/session-logger/log-session-start.sh
Executable file
26
hooks/session-logger/log-session-start.sh
Executable 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
|
||||||
|
echo "{\"timestamp\":\"$TIMESTAMP\",\"event\":\"sessionStart\",\"cwd\":\"$CWD\"}" >> logs/copilot/session.log
|
||||||
|
|
||||||
|
echo "📝 Session logged"
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user