mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
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
|
||||
- **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:
|
||||
|
||||
@@ -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
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` |
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,24 +4,26 @@ import fs from "fs";
|
||||
import path, { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import {
|
||||
AGENTS_DIR,
|
||||
AKA_INSTALL_URLS,
|
||||
COLLECTIONS_DIR,
|
||||
DOCS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
repoBaseUrl,
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
TEMPLATES,
|
||||
vscodeInsidersInstallImage,
|
||||
vscodeInstallImage,
|
||||
AGENTS_DIR,
|
||||
AKA_INSTALL_URLS,
|
||||
COLLECTIONS_DIR,
|
||||
DOCS_DIR,
|
||||
HOOKS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
repoBaseUrl,
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
TEMPLATES,
|
||||
vscodeInsidersInstallImage,
|
||||
vscodeInstallImage,
|
||||
} from "./constants.mjs";
|
||||
import {
|
||||
extractMcpServerConfigs,
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
extractMcpServerConfigs,
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
parseSkillMetadata,
|
||||
parseHookMetadata,
|
||||
} from "./yaml-parser.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -515,6 +517,67 @@ function generateAgentsSection(agentsDir, registryNames = []) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the hooks section with a table of all hooks
|
||||
*/
|
||||
function generateHooksSection(hooksDir) {
|
||||
if (!fs.existsSync(hooksDir)) {
|
||||
console.log(`Hooks directory does not exist: ${hooksDir}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get all hook folders (directories)
|
||||
const hookFolders = fs.readdirSync(hooksDir).filter((file) => {
|
||||
const filePath = path.join(hooksDir, file);
|
||||
return fs.statSync(filePath).isDirectory();
|
||||
});
|
||||
|
||||
// Parse each hook folder
|
||||
const hookEntries = hookFolders
|
||||
.map((folder) => {
|
||||
const hookPath = path.join(hooksDir, folder);
|
||||
const metadata = parseHookMetadata(hookPath);
|
||||
if (!metadata) return null;
|
||||
|
||||
return {
|
||||
folder,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
hooks: metadata.hooks,
|
||||
tags: metadata.tags,
|
||||
assets: metadata.assets,
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry !== null)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log(`Found ${hookEntries.length} hook(s)`);
|
||||
|
||||
if (hookEntries.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create table header
|
||||
let content =
|
||||
"| Name | Description | Events | Bundled Assets |\n| ---- | ----------- | ------ | -------------- |\n";
|
||||
|
||||
// Generate table rows for each hook
|
||||
for (const hook of hookEntries) {
|
||||
const link = `../hooks/${hook.folder}/README.md`;
|
||||
const events = hook.hooks.length > 0 ? hook.hooks.join(", ") : "N/A";
|
||||
const assetsList =
|
||||
hook.assets.length > 0
|
||||
? hook.assets.map((a) => `\`${a}\``).join("<br />")
|
||||
: "None";
|
||||
|
||||
content += `| [${hook.name}](${link}) | ${formatTableCell(
|
||||
hook.description
|
||||
)} | ${events} | ${assetsList} |\n`;
|
||||
}
|
||||
|
||||
return `${TEMPLATES.hooksSection}\n${TEMPLATES.hooksUsage}\n\n${content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the skills section with a table of all skills
|
||||
*/
|
||||
@@ -1002,6 +1065,7 @@ async function main() {
|
||||
);
|
||||
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
|
||||
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
|
||||
const hooksHeader = TEMPLATES.hooksSection.replace(/^##\s/m, "# ");
|
||||
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
|
||||
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
||||
/^##\s/m,
|
||||
@@ -1031,6 +1095,15 @@ async function main() {
|
||||
registryNames
|
||||
);
|
||||
|
||||
// Generate hooks README
|
||||
const hooksReadme = buildCategoryReadme(
|
||||
generateHooksSection,
|
||||
HOOKS_DIR,
|
||||
hooksHeader,
|
||||
TEMPLATES.hooksUsage,
|
||||
registryNames
|
||||
);
|
||||
|
||||
// Generate skills README
|
||||
const skillsReadme = buildCategoryReadme(
|
||||
generateSkillsSection,
|
||||
@@ -1061,6 +1134,7 @@ async function main() {
|
||||
);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.hooks.md"), hooksReadme);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
|
||||
writeFileIfChanged(
|
||||
path.join(DOCS_DIR, "README.collections.md"),
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
COLLECTIONS_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
ROOT_FOLDER,
|
||||
COLLECTIONS_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
ROOT_FOLDER,
|
||||
} from "./constants.mjs";
|
||||
import { parseCollectionYaml, parseFrontmatter } from "./yaml-parser.mjs";
|
||||
|
||||
@@ -155,6 +155,41 @@ function validateAgentFile(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateHookFile(filePath) {
|
||||
try {
|
||||
const hook = parseFrontmatter(filePath);
|
||||
|
||||
if (!hook) {
|
||||
return `Item ${filePath} hook file could not be parsed`;
|
||||
}
|
||||
|
||||
// Validate name field
|
||||
if (!hook.name || typeof hook.name !== "string") {
|
||||
return `Item ${filePath} hook must have a 'name' field`;
|
||||
}
|
||||
if (hook.name.length < 1 || hook.name.length > 50) {
|
||||
return `Item ${filePath} hook name must be between 1 and 50 characters`;
|
||||
}
|
||||
|
||||
// Validate description field
|
||||
if (!hook.description || typeof hook.description !== "string") {
|
||||
return `Item ${filePath} hook must have a 'description' field`;
|
||||
}
|
||||
if (hook.description.length < 1 || hook.description.length > 500) {
|
||||
return `Item ${filePath} hook description must be between 1 and 500 characters`;
|
||||
}
|
||||
|
||||
// Validate event field (optional but recommended)
|
||||
if (hook.event !== undefined && typeof hook.event !== "string") {
|
||||
return `Item ${filePath} hook 'event' must be a string`;
|
||||
}
|
||||
|
||||
return null; // All validations passed
|
||||
} catch (error) {
|
||||
return `Item ${filePath} hook file validation failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function validateCollectionItems(items) {
|
||||
if (!items || !Array.isArray(items)) {
|
||||
return "Items is required and must be an array";
|
||||
@@ -177,10 +212,10 @@ function validateCollectionItems(items) {
|
||||
if (!item.kind || typeof item.kind !== "string") {
|
||||
return `Item ${i + 1} must have a kind string`;
|
||||
}
|
||||
if (!["prompt", "instruction", "agent", "skill"].includes(item.kind)) {
|
||||
if (!["prompt", "instruction", "agent", "skill", "hook"].includes(item.kind)) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind must be one of: prompt, instruction, agent, skill`;
|
||||
} kind must be one of: prompt, instruction, agent, skill, hook`;
|
||||
}
|
||||
|
||||
// Validate file path exists
|
||||
@@ -208,6 +243,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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
58
hooks/session-logger/README.md
Normal file
58
hooks/session-logger/README.md
Normal 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
|
||||
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 (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
|
||||
@@ -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" }]}
|
||||
|
||||
54
website/src/pages/hooks.astro
Normal file
54
website/src/pages/hooks.astro
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
348
website/src/scripts/pages/hooks.ts
Normal file
348
website/src/scripts/pages/hooks.ts
Normal 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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user