build-skill 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flash Brew Digital / Ben Sabic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # Build Skill
2
+
3
+ ![Flash Brew Digital OSS](https://img.shields.io/badge/Flash_Brew_Digital-OSS-6F4E37?style=for-the-badge&labelColor=E9E3DD)
4
+ ![MIT License](https://img.shields.io/badge/License-MIT-6F4E37?style=for-the-badge&labelColor=E9E3DD)
5
+
6
+ Scaffold AI agent skills quickly with the Build Skill CLI. Creates a fully-configured skills repository following the [Agent Skills Specification](https://agentskills.io/specification).
7
+
8
+ ## What are Agent Skills?
9
+
10
+ Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. They work across any AI agent that supports the [open Agent Skills standard](https://agentskills.io).
11
+
12
+ ## Quick Start
13
+
14
+ ```bash
15
+ npx build-skill
16
+ ```
17
+
18
+ This launches an interactive prompt to configure your skill.
19
+
20
+ ## Usage
21
+
22
+ ### Interactive Mode
23
+
24
+ ```bash
25
+ npx build-skill
26
+ ```
27
+
28
+ You'll be prompted for:
29
+ - **Brand/Organization name** - Your company or project name (e.g., `acme-corp`)
30
+ - **Skill name** - Name of your first skill (e.g., `data-processor`)
31
+ - **Skill description** - What the skill does and when to use it
32
+
33
+ ### Quiet Mode (CI/Scripts)
34
+
35
+ ```bash
36
+ npx build-skill --name my-skill --description "Helps with X tasks" --quiet
37
+ ```
38
+
39
+ ### CLI Options
40
+
41
+ | Option | Alias | Description | Default |
42
+ | ------ | ----- | ----------- | ------- |
43
+ | `--brand <brand>` | `-b` | Brand/organization name | Same as `--name` |
44
+ | `--name <name>` | `-n` | Skill name | *Required in quiet mode* |
45
+ | `--description <desc>` | `-d` | Skill description | *Required in quiet mode* |
46
+ | `--license <license>` | `-l` | License for the skill | `MIT` |
47
+ | `--website <url>` | `-w` | Website/docs URL | `https://example.com` |
48
+ | `--repository <repo>` | `-r` | GitHub repository (owner/repo) | `<brand>/agent-skills` |
49
+ | `--category <category>` | `-c` | Skill category | `general` |
50
+ | `--keywords <keywords>` | `-k` | Comma-separated keywords | `ai, agent, skill` |
51
+ | `--output <dir>` | `-o` | Output directory | `.` |
52
+ | `--quiet` | `-q` | Suppress prompts and visual output | `false` |
53
+ | `--force` | `-f` | Overwrite existing directory | `false` |
54
+ | `--version` | `-V` | Show version number | |
55
+ | `--help` | `-h` | Show help | |
56
+
57
+ ### Examples
58
+
59
+ ```bash
60
+ # Interactive mode
61
+ npx build-skill
62
+
63
+ # Specify brand and skill name
64
+ npx build-skill --brand acme-corp --name data-processor --description "Processes data files"
65
+
66
+ # Full configuration
67
+ npx build-skill \
68
+ --brand acme-corp \
69
+ --name data-processor \
70
+ --description "Processes CSV and JSON data files" \
71
+ --license Apache-2.0 \
72
+ --website https://acme.example.com/docs \
73
+ --repository acme-corp/agent-skills \
74
+ --category productivity \
75
+ --keywords "data, csv, json, processing" \
76
+ --quiet
77
+
78
+ # Overwrite existing directory
79
+ npx build-skill --name my-skill --description "My skill" --force --quiet
80
+ ```
81
+
82
+ ## Generated Structure
83
+
84
+ ```
85
+ <brand>-skills/
86
+ ├── .claude-plugin/
87
+ │ └── marketplace.json
88
+ ├── .github/
89
+ │ └── workflows/
90
+ │ └── process-skills.yml
91
+ ├── scripts/
92
+ │ └── sync-skills.js
93
+ ├── skills/
94
+ │ └── <skill-name>/
95
+ │ ├── .claude-plugin/
96
+ │ │ └── plugin.json
97
+ │ └── SKILL.md
98
+ └── README.md
99
+ ```
100
+
101
+ ## Included Automation
102
+
103
+ ### GitHub Actions Workflow
104
+
105
+ The generated repository includes a `process-skills.yml` workflow that:
106
+
107
+ - **On Pull Requests**: Validates all skills using the [Validate Agent Skill](https://github.com/marketplace/actions/validate-skill) action
108
+ - **On Push to Main**: Validates skills and syncs the README and marketplace.json
109
+
110
+ ### Add Skill Script
111
+
112
+ The `scripts/add-skill.js` utility creates new skills:
113
+
114
+ ```bash
115
+ node scripts/add-skill.js <skill-name> "<description>"
116
+ ```
117
+
118
+ Example:
119
+ ```bash
120
+ node scripts/add-skill.js data-processor "Processes CSV and JSON data files"
121
+ ```
122
+
123
+ This script:
124
+ - Creates the skill directory structure in `skills/`
125
+ - Generates `SKILL.md` with frontmatter
126
+ - Generates `.claude-plugin/plugin.json`
127
+ - Automatically runs sync-skills.js to update README and marketplace.json
128
+
129
+ ### Sync Script
130
+
131
+ The `scripts/sync-skills.js` utility keeps your repository in sync:
132
+
133
+ ```bash
134
+ node scripts/sync-skills.js
135
+ ```
136
+
137
+ This script:
138
+ - Scans the `skills/` directory for all skills
139
+ - Updates the "Available Skills" table in `README.md`
140
+ - Updates the `plugins` array in `.claude-plugin/marketplace.json`
141
+
142
+ Run it after modifying skills to keep everything up to date.
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ # Install dependencies
148
+ pnpm install
149
+
150
+ # Build
151
+ pnpm build
152
+
153
+ # Run tests
154
+ pnpm test
155
+
156
+ # Lint
157
+ pnpm check
158
+
159
+ # Fix lint issues
160
+ pnpm fix
161
+
162
+ # Type check
163
+ pnpm type-check
164
+ ```
165
+
166
+ ## Resources
167
+
168
+ - [Agent Skills Specification](https://agentskills.io/specification)
169
+ - [Vercel Skills CLI](https://skills.sh/)
170
+ - [Validate Agent Skill](https://github.com/marketplace/actions/validate-skill)
171
+
172
+ ## Contributing
173
+
174
+ Contributions are welcome! Please read our [Contributing Guide](.github/CONTRIBUTING.md) for more information.
175
+
176
+ ## License
177
+
178
+ [MIT License](LICENSE.md)
179
+
180
+ ## Author
181
+
182
+ [Ben Sabic](https://bensabic.ca) at [Flash Brew Digital](https://flashbrew.digital)
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { dirname, join as join2, resolve } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { log as log2 } from "@clack/prompts";
7
+ import { program } from "commander";
8
+ import pc3 from "picocolors";
9
+
10
+ // src/prompts.ts
11
+ import { cancel, group, log, outro, text } from "@clack/prompts";
12
+ import pc from "picocolors";
13
+
14
+ // src/utils.ts
15
+ import { execa } from "execa";
16
+ function normalizeName(input) {
17
+ return input.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
18
+ }
19
+ async function getGitConfig(key) {
20
+ try {
21
+ const { stdout } = await execa("git", ["config", "--get", key]);
22
+ return stdout.trim();
23
+ } catch {
24
+ return "";
25
+ }
26
+ }
27
+
28
+ // src/prompts.ts
29
+ var MAX_DESCRIPTION_LENGTH = 1024;
30
+ function validateName(value) {
31
+ if (!value?.trim()) {
32
+ return "Name is required";
33
+ }
34
+ if (!normalizeName(value)) {
35
+ return "Name must contain at least one letter or number";
36
+ }
37
+ }
38
+ function validateDescription(value) {
39
+ if (!value) {
40
+ return "Description is required";
41
+ }
42
+ if (value.length > MAX_DESCRIPTION_LENGTH) {
43
+ return `Description must be under ${MAX_DESCRIPTION_LENGTH} characters`;
44
+ }
45
+ }
46
+ async function promptForInput(providedBrand, providedName, providedDescription) {
47
+ const answers = await group(
48
+ {
49
+ brandName: () => {
50
+ if (providedBrand) {
51
+ return Promise.resolve(providedBrand);
52
+ }
53
+ return text({
54
+ message: "What is your brand/organization name?",
55
+ placeholder: "acme-corp",
56
+ validate: validateName
57
+ });
58
+ },
59
+ skillName: () => {
60
+ if (providedName) {
61
+ return Promise.resolve(providedName);
62
+ }
63
+ return text({
64
+ message: "What is the name of your first skill?",
65
+ placeholder: "my-skill",
66
+ validate: validateName
67
+ });
68
+ },
69
+ skillDescription: () => {
70
+ if (providedDescription) {
71
+ return Promise.resolve(providedDescription);
72
+ }
73
+ return text({
74
+ message: "Describe what this skill does and when to use it:",
75
+ placeholder: "Helps with X tasks. Use when working with Y or when the user mentions Z.",
76
+ validate: validateDescription
77
+ });
78
+ }
79
+ },
80
+ {
81
+ onCancel: () => {
82
+ cancel("Operation cancelled.");
83
+ process.exit(0);
84
+ }
85
+ }
86
+ );
87
+ return {
88
+ brandName: normalizeName(answers.brandName),
89
+ skillName: normalizeName(answers.skillName),
90
+ skillDescription: answers.skillDescription
91
+ };
92
+ }
93
+ function getQuietInput(providedBrand, providedName, providedDescription) {
94
+ if (!providedName) {
95
+ throw new Error("--name is required in quiet mode");
96
+ }
97
+ if (!providedDescription) {
98
+ throw new Error("--description is required in quiet mode");
99
+ }
100
+ const brand = providedBrand || providedName;
101
+ const nameError = validateName(providedName);
102
+ if (nameError) {
103
+ throw new Error(nameError);
104
+ }
105
+ const brandError = validateName(brand);
106
+ if (brandError) {
107
+ throw new Error(`Brand ${brandError.toLowerCase()}`);
108
+ }
109
+ const descError = validateDescription(providedDescription);
110
+ if (descError) {
111
+ throw new Error(descError);
112
+ }
113
+ return {
114
+ brandName: normalizeName(brand),
115
+ skillName: normalizeName(providedName),
116
+ skillDescription: providedDescription
117
+ };
118
+ }
119
+ function printSuccess(targetDir, brandName, skillName, quiet) {
120
+ if (quiet) {
121
+ console.log(targetDir);
122
+ return;
123
+ }
124
+ log.success(`Created ${pc.cyan(targetDir)}`);
125
+ log.info(`
126
+ Next steps:
127
+ ${pc.cyan(`cd ${brandName}-skills`)}
128
+ ${pc.cyan("git init")}
129
+ ${pc.cyan("git remote add origin <YOUR_REPO_URL>")}
130
+ ${pc.cyan("git branch -M main")}
131
+ ${pc.cyan("git add .")}
132
+ ${pc.cyan(`git commit -m "Initial release of ${skillName} skill"`)}
133
+ ${pc.cyan("git push -u origin main")}
134
+
135
+ Edit your skill at:
136
+ ${pc.cyan(`skills/${skillName}/SKILL.md`)}
137
+ `);
138
+ outro(pc.green("Happy building! \u{1F680}"));
139
+ }
140
+
141
+ // src/scaffold.ts
142
+ import {
143
+ access,
144
+ cp,
145
+ mkdir,
146
+ readdir,
147
+ readFile,
148
+ rename,
149
+ rm,
150
+ writeFile
151
+ } from "fs/promises";
152
+ import { join } from "path";
153
+ import { spinner } from "@clack/prompts";
154
+ import pc2 from "picocolors";
155
+ var SKILL_NAME_PLACEHOLDER = "{Skill_Name}";
156
+ async function pathExists(path) {
157
+ try {
158
+ await access(path);
159
+ return true;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+ async function copyDir(src, dest) {
165
+ await mkdir(dest, { recursive: true });
166
+ const entries = await readdir(src, { withFileTypes: true });
167
+ for (const entry of entries) {
168
+ const srcPath = join(src, entry.name);
169
+ const destPath = join(dest, entry.name);
170
+ if (entry.isDirectory()) {
171
+ await copyDir(srcPath, destPath);
172
+ } else {
173
+ await cp(srcPath, destPath);
174
+ }
175
+ }
176
+ }
177
+ async function replaceInFile(filePath, values) {
178
+ let content = await readFile(filePath, "utf-8");
179
+ for (const [key, value] of Object.entries(values)) {
180
+ const placeholder = `{${key}}`;
181
+ content = content.replaceAll(placeholder, value);
182
+ }
183
+ await writeFile(filePath, content, "utf-8");
184
+ }
185
+ async function processDirectory(dir, values) {
186
+ const entries = await readdir(dir, { withFileTypes: true });
187
+ for (const entry of entries) {
188
+ const fullPath = join(dir, entry.name);
189
+ if (entry.isDirectory()) {
190
+ if (entry.name === SKILL_NAME_PLACEHOLDER) {
191
+ const newPath = join(dir, values.Skill_Name);
192
+ await rename(fullPath, newPath);
193
+ await processDirectory(newPath, values);
194
+ } else {
195
+ await processDirectory(fullPath, values);
196
+ }
197
+ } else {
198
+ await replaceInFile(fullPath, values);
199
+ }
200
+ }
201
+ }
202
+ function formatError(error) {
203
+ if (error instanceof Error) {
204
+ if (error.message.includes("ENOENT")) {
205
+ return "Template directory not found. Please reinstall build-skill.";
206
+ }
207
+ if (error.message.includes("EACCES")) {
208
+ return "Permission denied. Check write permissions for the output directory.";
209
+ }
210
+ if (error.message.includes("ENOSPC")) {
211
+ return "No space left on device.";
212
+ }
213
+ return error.message;
214
+ }
215
+ return "An unknown error occurred";
216
+ }
217
+ async function createSkillRepository(templateDir, targetDir, values, quiet, force) {
218
+ const s = quiet ? null : spinner();
219
+ if (!await pathExists(templateDir)) {
220
+ throw new Error(
221
+ "Template directory not found. Please reinstall build-skill."
222
+ );
223
+ }
224
+ if (await pathExists(targetDir)) {
225
+ if (force) {
226
+ await rm(targetDir, { recursive: true, force: true });
227
+ } else {
228
+ throw new Error(`Directory already exists: ${targetDir}`);
229
+ }
230
+ }
231
+ s?.start(
232
+ `Creating the ${pc2.cyan(values.Brand_Name)} agent skills repository...`
233
+ );
234
+ try {
235
+ await copyDir(templateDir, targetDir);
236
+ await processDirectory(targetDir, values);
237
+ s?.stop("Agent Skills repository created!");
238
+ } catch (error) {
239
+ s?.stop("Failed to create skill repository");
240
+ try {
241
+ await rm(targetDir, { recursive: true, force: true });
242
+ } catch {
243
+ }
244
+ throw new Error(formatError(error));
245
+ }
246
+ }
247
+
248
+ // src/index.ts
249
+ var __dirname = dirname(fileURLToPath(import.meta.url));
250
+ var TEMPLATE_DIR = join2(__dirname, "..", "template");
251
+ var version = true ? "0.0.0" : "dev";
252
+ var BANNER = `
253
+ _ _ _ _ _ _ _ _
254
+ | |__ _ _(_) | __| | ___| | _(_) | |
255
+ | '_ \\| | | | | |/ _\` | / __| |/ / | | |
256
+ | |_) | |_| | | | (_| | \\__ \\ <| | | |
257
+ |_.__/ \\__,_|_|_|\\__,_| |___/_|\\_\\_|_|_|
258
+ `;
259
+ async function main() {
260
+ program.name("build-skill").description("Scaffold AI agent skills quickly").version(version).argument("[name]", "Skill name").argument("[description]", "Skill description").option("-b, --brand <brand>", "Brand/organization name").option("-n, --name <name>", "Skill name").option("-d, --description <description>", "Skill description").option("-l, --license <license>", "License for the skill").option("-w, --website <url>", "Website URL (e.g. docs) for the skill").option("-r, --repository <repo>", "GitHub repository (owner/repo)").option("-c, --category <category>", "Skill category").option("-k, --keywords <keywords>", "Comma-separated keywords").option("-o, --output <dir>", "Output directory", ".").option("-q, --quiet", "Suppress interactive prompts and visual output").option("-f, --force", "Overwrite existing directory").parse();
261
+ const options = program.opts();
262
+ const [argName, argDescription] = program.args;
263
+ const providedBrand = options.brand;
264
+ const providedName = options.name || argName;
265
+ const providedDescription = options.description || argDescription;
266
+ if (!options.quiet) {
267
+ console.log(pc3.green(BANNER));
268
+ }
269
+ const gitName = await getGitConfig("user.name");
270
+ const gitEmail = await getGitConfig("user.email");
271
+ const hasGitConfig = gitName && gitEmail;
272
+ if (!(hasGitConfig || options.quiet)) {
273
+ log2.warn(
274
+ "Could not detect git user.name or user.email. Using placeholders."
275
+ );
276
+ }
277
+ const input = options.quiet ? getQuietInput(providedBrand, providedName, providedDescription) : await promptForInput(providedBrand, providedName, providedDescription);
278
+ const { brandName, skillName, skillDescription } = input;
279
+ const outputDir = resolve(process.cwd(), options.output || ".");
280
+ const targetDir = join2(outputDir, `${brandName}-skills`);
281
+ const values = {
282
+ Brand_Name: brandName,
283
+ Skill_Name: skillName,
284
+ Skill_Description: skillDescription,
285
+ Creator_Name: gitName || "Your Name",
286
+ Creator_Email: gitEmail || "your.email@example.com",
287
+ Skill_License: options.license || "MIT",
288
+ Skill_Homepage: options.website || "https://example.com",
289
+ Skill_Repository: options.repository || `${brandName}/agent-skills`,
290
+ Skill_Category: options.category || "general",
291
+ Skill_Keywords: options.keywords || "ai, agent, skill"
292
+ };
293
+ await createSkillRepository(
294
+ TEMPLATE_DIR,
295
+ targetDir,
296
+ values,
297
+ Boolean(options.quiet),
298
+ Boolean(options.force)
299
+ );
300
+ printSuccess(targetDir, brandName, skillName, Boolean(options.quiet));
301
+ }
302
+ main().catch((error) => {
303
+ console.error(
304
+ pc3.red(
305
+ error instanceof Error ? error.message : "An unexpected error occurred"
306
+ )
307
+ );
308
+ process.exit(1);
309
+ });
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "build-skill",
3
+ "version": "0.0.0",
4
+ "description": "Scaffold AI agent skills quickly with the Build Skill CLI.",
5
+ "keywords": [
6
+ "ai",
7
+ "ai-sdk",
8
+ "agent",
9
+ "skills",
10
+ "tools"
11
+ ],
12
+ "homepage": "https://github.com/Flash-Brew-Digital/build-skill",
13
+ "bugs": {
14
+ "url": "https://github.com/Flash-Brew-Digital/build-skill/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Ben Sabic",
19
+ "url": "https://bensabic.dev"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Flash-Brew-Digital/build-skill.git"
24
+ },
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "bin": {
29
+ "build-skill": "./dist/index.js"
30
+ },
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.js",
34
+ "types": "./dist/index.d.ts"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "template",
40
+ "README.md"
41
+ ],
42
+ "type": "module",
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "test": "vitest run",
46
+ "prepublishOnly": "pnpm build",
47
+ "check": "ultracite check",
48
+ "fix": "ultracite fix",
49
+ "bump-deps": "npx npm-check-updates -u -p pnpm && pnpm install",
50
+ "type-check": "tsc --noEmit"
51
+ },
52
+ "dependencies": {
53
+ "@clack/prompts": "^1.0.0",
54
+ "commander": "^14.0.3",
55
+ "execa": "^9.6.1",
56
+ "picocolors": "^1.1.1"
57
+ },
58
+ "devDependencies": {
59
+ "@biomejs/biome": "2.3.13",
60
+ "@types/node": "^25.2.0",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.9.3",
63
+ "ultracite": "7.1.3",
64
+ "vitest": "^4.0.18"
65
+ },
66
+ "engines": {
67
+ "node": ">=20"
68
+ },
69
+ "packageManager": "pnpm@10.28.2"
70
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "{Brand_Name}-skills",
3
+ "description": "{Brand_Name} agent skills",
4
+ "owner": {
5
+ "name": "{Creator_Name}",
6
+ "email": "{Creator_Email}"
7
+ },
8
+ "metadata": {
9
+ "version": "1.0.0",
10
+ "description": "Claude Code marketplace of skills by {Brand_Name}"
11
+ },
12
+ "plugins": [
13
+ {
14
+ "name": "{Skill_Name}-skill",
15
+ "description": "{Skill_Description}",
16
+ "version": "1.0.0",
17
+ "author": {
18
+ "name": "{Creator_Name}",
19
+ "email": "{Creator_Email}"
20
+ },
21
+ "source": "./skills/{Skill_Name}",
22
+ "homepage": "{Skill_Homepage}",
23
+ "repository": "https://github.com/{Skill_Repository}",
24
+ "license": "{Skill_License}",
25
+ "category": "{Skill_Category}",
26
+ "keywords": "{Skill_Keywords}"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,86 @@
1
+ name: Process Skills
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "**/SKILL.md"
8
+ pull_request:
9
+ branches: [main]
10
+ paths:
11
+ - "**/SKILL.md"
12
+
13
+ concurrency:
14
+ group: validate-skill-${{ github.ref }}
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ detect-changes:
19
+ runs-on: ubuntu-slim
20
+ if: github.event.pull_request.draft != true && github.actor != 'dependabot[bot]'
21
+ outputs:
22
+ skills: ${{ steps.changed-skills.outputs.skills }}
23
+ steps:
24
+ - name: Checkout repository
25
+ uses: actions/checkout@v6
26
+ with:
27
+ fetch-depth: 0
28
+
29
+ - name: Get changed skills
30
+ id: changed-skills
31
+ run: |
32
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
33
+ BASE=${{ github.event.pull_request.base.sha }}
34
+ HEAD=${{ github.event.pull_request.head.sha }}
35
+ else
36
+ BASE=${{ github.event.before }}
37
+ HEAD=${{ github.event.after }}
38
+ fi
39
+
40
+ # Find changed SKILL.md files and extract skill directories
41
+ SKILLS=$(git diff --name-only $BASE $HEAD | \
42
+ grep 'SKILL.md$' | \
43
+ xargs -I {} dirname {} | \
44
+ sort -u | \
45
+ jq -R -s -c 'split("\n") | map(select(length > 0))')
46
+
47
+ echo "skills=$SKILLS" >> $GITHUB_OUTPUT
48
+ echo "Changed skills: $SKILLS"
49
+
50
+ validate:
51
+ needs: detect-changes
52
+ if: needs.detect-changes.outputs.skills != '[]'
53
+ runs-on: ubuntu-slim
54
+ strategy:
55
+ fail-fast: false
56
+ matrix:
57
+ skill: ${{ fromJson(needs.detect-changes.outputs.skills) }}
58
+ steps:
59
+ - name: Checkout repository
60
+ uses: actions/checkout@v6
61
+
62
+ - name: Validate ${{ matrix.skill }}
63
+ uses: Flash-Brew-Digital/validate-skill@v1
64
+ with:
65
+ path: ${{ matrix.skill }}
66
+
67
+ sync:
68
+ needs: validate
69
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
70
+ runs-on: ubuntu-slim
71
+ permissions:
72
+ contents: write
73
+ steps:
74
+ - name: Checkout repository
75
+ uses: actions/checkout@v6
76
+
77
+ - name: Sync skills
78
+ run: node scripts/sync-skills.js
79
+
80
+ - name: Commit changes
81
+ run: |
82
+ git config user.name "Agent Skills Bot"
83
+ git config user.email "agent-skills-bot@users.noreply.github.com"
84
+ git add README.md .claude-plugin/marketplace.json
85
+ git diff --staged --quiet || git commit -m "chore: sync skills documentation"
86
+ git push
@@ -0,0 +1,79 @@
1
+ # {Brand_Name} Agent Skills
2
+
3
+ This repository contains a collection of agent skills for {Brand_Name}. These skills are designed to enhance the capabilities of agents by providing them with specialized functionalities.
4
+
5
+ ## What are Agent Skills?
6
+
7
+ Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. They work across any AI agent that supports the [open Agent Skills standard](https://agentskills.io).
8
+
9
+ ## Available Skills
10
+ <!-- START:Available-Skills -->
11
+
12
+ | Skill | Description |
13
+ | ----- | ----------- |
14
+ | {Skill_Name} | {Skill_Description} |
15
+
16
+ <!-- END:Available-Skills -->
17
+
18
+ ## Installation
19
+
20
+ ### Option 1: Skills (Recommended)
21
+
22
+ Use the [Vercel Skills CLI](https://skills.sh/) to install skills directly:
23
+
24
+ ```bash
25
+ # Install all skills
26
+ npx skills add {Brand_Name}/agent-skills
27
+
28
+ # Install specific skills
29
+ npx skills add {Brand_Name}/agent-skills --skill {Skill_Name}
30
+
31
+ # List available skills
32
+ npx skills add {Brand_Name}/agent-skills --list
33
+ ```
34
+
35
+ ### Option 2: Claude Code Plugin
36
+
37
+ Install via Claude Code's plugin system:
38
+
39
+ ```bash
40
+ # Add the marketplace
41
+ /plugin marketplace add {Brand_Name}/agent-skills
42
+
43
+ # Install specific skill
44
+ /plugin install {Skill_Name}-skill
45
+ ```
46
+
47
+ ## Adding New Skills
48
+
49
+ Use the included script to add new skills:
50
+
51
+ ```bash
52
+ node scripts/add-skill.js <skill-name> "<description>"
53
+ ```
54
+
55
+ Example:
56
+
57
+ ```bash
58
+ node scripts/add-skill.js {Skill_Name} "{Skill_Description}"
59
+ ```
60
+
61
+ This will create the skill structure and automatically update this README and the marketplace.json.
62
+
63
+ ## Scripts
64
+
65
+ | Script | Description |
66
+ | ------ | ----------- |
67
+ | `node scripts/add-skill.js` | Add a new skill to the repository |
68
+ | `node scripts/sync-skills.js` | Sync README and marketplace.json with skills directory |
69
+
70
+ ## Contributing
71
+
72
+ 1. Fork this repository
73
+ 2. Create a new skill using `node scripts/add-skill.js`
74
+ 3. Edit the skill's `SKILL.md` with your content
75
+ 4. Submit a pull request
76
+
77
+ ## License
78
+
79
+ {Skill_License}
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * add-skill.js
5
+ *
6
+ * Adds a new skill to the repository.
7
+ *
8
+ * Usage: node scripts/add-skill.js <skill-name> <description>
9
+ *
10
+ * Example: node scripts/add-skill.js my-new-skill "Helps with X tasks"
11
+ */
12
+
13
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const ROOT_DIR = join(__dirname, "..");
19
+ const SKILLS_DIR = join(ROOT_DIR, "skills");
20
+ const MARKETPLACE_PATH = join(ROOT_DIR, ".claude-plugin", "marketplace.json");
21
+ const SYNC_SCRIPT = join(__dirname, "sync-skills.js");
22
+
23
+ const SKILLS_SUFFIX_REGEX = /-skills$/;
24
+
25
+ function normalizeName(input) {
26
+ return input
27
+ .toLowerCase()
28
+ .trim()
29
+ .replace(/\s+/g, "-")
30
+ .replace(/[^a-z0-9-]/g, "")
31
+ .replace(/-+/g, "-")
32
+ .replace(/^-|-$/g, "");
33
+ }
34
+
35
+ function printUsage() {
36
+ console.log(`
37
+ Usage: node scripts/add-skill.js <skill-name> <description>
38
+
39
+ Arguments:
40
+ skill-name Name of the skill (will be normalized to lowercase with hyphens)
41
+ description Brief description of what the skill does
42
+
43
+ Example:
44
+ node scripts/add-skill.js my-new-skill "Helps with data processing tasks"
45
+ `);
46
+ }
47
+
48
+ async function loadMarketplace() {
49
+ try {
50
+ const content = await readFile(MARKETPLACE_PATH, "utf-8");
51
+ return JSON.parse(content);
52
+ } catch {
53
+ console.error("Error: Could not read .claude-plugin/marketplace.json");
54
+ console.error("Make sure you're running this from the repository root.");
55
+ process.exit(1);
56
+ }
57
+ }
58
+
59
+ async function skillExists(skillName) {
60
+ try {
61
+ await readFile(join(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function generateSkillMd(skillName, description, marketplace) {
69
+ const brandName = marketplace.name.replace(SKILLS_SUFFIX_REGEX, "");
70
+ const license =
71
+ marketplace.plugins?.[0]?.license || marketplace.license || "MIT";
72
+
73
+ return `---
74
+ name: ${skillName}
75
+ description: ${description}
76
+ license: ${license}
77
+ metadata:
78
+ author: ${brandName}
79
+ version: "1.0.0"
80
+ ---
81
+
82
+ `;
83
+ }
84
+
85
+ function generatePluginJson(skillName, description, marketplace) {
86
+ const owner = marketplace.owner || {};
87
+ const existingPlugin = marketplace.plugins?.[0] || {};
88
+
89
+ return JSON.stringify(
90
+ {
91
+ name: skillName,
92
+ description,
93
+ version: "1.0.0",
94
+ author: {
95
+ name: owner.name || "Your Name",
96
+ email: owner.email || "your.email@example.com",
97
+ },
98
+ license: existingPlugin.license || "MIT",
99
+ homepage: existingPlugin.homepage || "https://example.com",
100
+ repository: existingPlugin.repository || "",
101
+ keywords: existingPlugin.keywords || "ai, agent, skill",
102
+ category: existingPlugin.category || "general",
103
+ },
104
+ null,
105
+ 2
106
+ );
107
+ }
108
+
109
+ async function runSyncScript() {
110
+ const { spawn } = await import("node:child_process");
111
+
112
+ return new Promise((resolve, reject) => {
113
+ const child = spawn("node", [SYNC_SCRIPT], {
114
+ stdio: "inherit",
115
+ cwd: ROOT_DIR,
116
+ });
117
+
118
+ child.on("close", (code) => {
119
+ if (code === 0) {
120
+ resolve();
121
+ } else {
122
+ reject(new Error(`sync-skills.js exited with code ${code}`));
123
+ }
124
+ });
125
+
126
+ child.on("error", reject);
127
+ });
128
+ }
129
+
130
+ async function main() {
131
+ const args = process.argv.slice(2);
132
+
133
+ if (args.length < 2 || args.includes("--help") || args.includes("-h")) {
134
+ printUsage();
135
+ process.exit(args.includes("--help") || args.includes("-h") ? 0 : 1);
136
+ }
137
+
138
+ const [rawName, ...descParts] = args;
139
+ const description = descParts.join(" ");
140
+ const skillName = normalizeName(rawName);
141
+
142
+ if (!skillName) {
143
+ console.error(
144
+ "Error: Skill name must contain at least one letter or number"
145
+ );
146
+ process.exit(1);
147
+ }
148
+
149
+ if (!description) {
150
+ console.error("Error: Description is required");
151
+ process.exit(1);
152
+ }
153
+
154
+ if (await skillExists(skillName)) {
155
+ console.error(`Error: Skill "${skillName}" already exists`);
156
+ process.exit(1);
157
+ }
158
+
159
+ console.log(`\nCreating skill: ${skillName}\n`);
160
+
161
+ const marketplace = await loadMarketplace();
162
+ const skillDir = join(SKILLS_DIR, skillName);
163
+ const pluginDir = join(skillDir, ".claude-plugin");
164
+
165
+ // Create directories
166
+ await mkdir(pluginDir, { recursive: true });
167
+
168
+ // Generate files
169
+ const skillMd = generateSkillMd(skillName, description, marketplace);
170
+ const pluginJson = generatePluginJson(skillName, description, marketplace);
171
+
172
+ await writeFile(join(skillDir, "SKILL.md"), skillMd, "utf-8");
173
+ await writeFile(join(pluginDir, "plugin.json"), `${pluginJson}\n`, "utf-8");
174
+
175
+ console.log(`✓ Created skills/${skillName}/SKILL.md`);
176
+ console.log(`✓ Created skills/${skillName}/.claude-plugin/plugin.json`);
177
+ console.log();
178
+
179
+ // Run sync script
180
+ console.log("Running sync-skills.js...\n");
181
+ await runSyncScript();
182
+
183
+ console.log(`\nSkill "${skillName}" created successfully!`);
184
+ console.log("\nNext steps:");
185
+ console.log(
186
+ ` 1. Edit skills/${skillName}/SKILL.md to add your skill content`
187
+ );
188
+ console.log(
189
+ ` 2. Update skills/${skillName}/.claude-plugin/plugin.json if needed`
190
+ );
191
+ console.log();
192
+ }
193
+
194
+ main().catch((error) => {
195
+ console.error("Error:", error.message);
196
+ process.exit(1);
197
+ });
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * sync-skills.js
5
+ *
6
+ * Syncs the Available Skills section in README.md and the plugins array
7
+ * in .claude-plugin/marketplace.json with the actual skills in skills/
8
+ *
9
+ * Usage: node scripts/sync-skills.js
10
+ */
11
+
12
+ import { readdir, readFile, writeFile } from "node:fs/promises";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const ROOT_DIR = join(__dirname, "..");
18
+ const SKILLS_DIR = join(ROOT_DIR, "skills");
19
+ const README_PATH = join(ROOT_DIR, "README.md");
20
+ const MARKETPLACE_PATH = join(ROOT_DIR, ".claude-plugin", "marketplace.json");
21
+
22
+ const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
23
+ const WHITESPACE_REGEX = /\S/;
24
+
25
+ function parseValue(raw) {
26
+ const trimmed = raw.trim();
27
+ if (
28
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
29
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
30
+ ) {
31
+ return trimmed.slice(1, -1);
32
+ }
33
+ return trimmed;
34
+ }
35
+
36
+ function parseFrontmatter(content) {
37
+ const match = content.match(FRONTMATTER_REGEX);
38
+ if (!match) {
39
+ return {};
40
+ }
41
+
42
+ const lines = match[1].split("\n");
43
+ const result = {};
44
+ let currentObject = null;
45
+
46
+ for (const line of lines) {
47
+ if (line.trim() === "") {
48
+ continue;
49
+ }
50
+
51
+ const colonIndex = line.indexOf(":");
52
+ if (colonIndex === -1) {
53
+ continue;
54
+ }
55
+
56
+ const indent = line.search(WHITESPACE_REGEX);
57
+ const key = line.slice(0, colonIndex).trim();
58
+ const rawValue = line.slice(colonIndex + 1);
59
+
60
+ if (indent > 0 && currentObject !== null) {
61
+ // Nested property
62
+ const value = parseValue(rawValue);
63
+ if (value !== "") {
64
+ currentObject[key] = value;
65
+ }
66
+ } else {
67
+ // Top-level property
68
+ const value = parseValue(rawValue);
69
+ if (value === "") {
70
+ // Start of a nested object
71
+ currentObject = {};
72
+ result[key] = currentObject;
73
+ } else {
74
+ currentObject = null;
75
+ result[key] = value;
76
+ }
77
+ }
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ async function discoverSkills() {
84
+ const skills = [];
85
+
86
+ let entries;
87
+ try {
88
+ entries = await readdir(SKILLS_DIR, { withFileTypes: true });
89
+ } catch {
90
+ console.warn("No skills directory found. Skipping sync.");
91
+ return skills;
92
+ }
93
+
94
+ for (const entry of entries) {
95
+ if (!entry.isDirectory()) {
96
+ continue;
97
+ }
98
+
99
+ const skillDir = join(SKILLS_DIR, entry.name);
100
+ const skillMdPath = join(skillDir, "SKILL.md");
101
+ const pluginJsonPath = join(skillDir, ".claude-plugin", "plugin.json");
102
+
103
+ try {
104
+ const skillMd = await readFile(skillMdPath, "utf-8");
105
+ const frontmatter = parseFrontmatter(skillMd);
106
+
107
+ let pluginJson = {};
108
+ try {
109
+ const pluginContent = await readFile(pluginJsonPath, "utf-8");
110
+ pluginJson = JSON.parse(pluginContent);
111
+ } catch {
112
+ // Ignore missing or invalid plugin.json
113
+ }
114
+
115
+ const metadata = frontmatter.metadata || {};
116
+
117
+ skills.push({
118
+ dirName: entry.name,
119
+ name: frontmatter.name || entry.name,
120
+ description: frontmatter.description || pluginJson.description || "",
121
+ license: frontmatter.license || pluginJson.license || "MIT",
122
+ version: metadata.version || pluginJson.version || "1.0.0",
123
+ author: metadata.author || pluginJson.author || {},
124
+ homepage: pluginJson.homepage || "",
125
+ repository: pluginJson.repository || "",
126
+ category: pluginJson.category || "general",
127
+ keywords: pluginJson.keywords || "",
128
+ });
129
+ } catch (error) {
130
+ console.warn(
131
+ `Warning: Could not read skill at ${entry.name}:`,
132
+ error.message
133
+ );
134
+ }
135
+ }
136
+
137
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
138
+ }
139
+
140
+ function truncate(str, maxLength = 80) {
141
+ if (str.length <= maxLength) {
142
+ return str;
143
+ }
144
+ return `${str.slice(0, maxLength - 3)}...`;
145
+ }
146
+
147
+ function generateReadmeSkillsList(skills) {
148
+ if (skills.length === 0) {
149
+ return "\n*No skills available yet.*\n";
150
+ }
151
+
152
+ const lines = ["", "| Skill | Description |", "| ----- | ----------- |"];
153
+
154
+ for (const skill of skills) {
155
+ const escapedDesc = truncate(skill.description).replace(/\|/g, "\\|");
156
+ lines.push(`| ${skill.name} | ${escapedDesc} |`);
157
+ }
158
+
159
+ lines.push("");
160
+ return lines.join("\n");
161
+ }
162
+
163
+ async function updateReadme(skills) {
164
+ const content = await readFile(README_PATH, "utf-8");
165
+ const startMarker = "<!-- START:Available-Skills -->";
166
+ const endMarker = "<!-- END:Available-Skills -->";
167
+
168
+ const startIndex = content.indexOf(startMarker);
169
+ const endIndex = content.indexOf(endMarker);
170
+
171
+ if (startIndex === -1 || endIndex === -1) {
172
+ console.warn("Warning: Could not find skill markers in README.md");
173
+ return false;
174
+ }
175
+
176
+ const skillsList = generateReadmeSkillsList(skills);
177
+ const newContent =
178
+ content.slice(0, startIndex + startMarker.length) +
179
+ skillsList +
180
+ content.slice(endIndex);
181
+
182
+ await writeFile(README_PATH, newContent, "utf-8");
183
+ return true;
184
+ }
185
+
186
+ async function updateMarketplace(skills) {
187
+ let marketplace;
188
+ try {
189
+ const content = await readFile(MARKETPLACE_PATH, "utf-8");
190
+ marketplace = JSON.parse(content);
191
+ } catch {
192
+ console.warn("Warning: Could not read marketplace.json");
193
+ return false;
194
+ }
195
+
196
+ marketplace.plugins = skills.map((skill) => ({
197
+ name: `${skill.name}-skill`,
198
+ description: skill.description,
199
+ version: skill.version,
200
+ author: skill.author,
201
+ source: `./skills/${skill.dirName}`,
202
+ homepage: skill.homepage,
203
+ repository: skill.repository,
204
+ license: skill.license,
205
+ category: skill.category,
206
+ keywords: skill.keywords,
207
+ }));
208
+
209
+ await writeFile(
210
+ MARKETPLACE_PATH,
211
+ `${JSON.stringify(marketplace, null, 2)}\n`,
212
+ "utf-8"
213
+ );
214
+ return true;
215
+ }
216
+
217
+ async function main() {
218
+ console.log("Syncing agent skill(s)...\n");
219
+
220
+ const skills = await discoverSkills();
221
+ console.log(`Found ${skills.length} agent skill(s):`);
222
+ for (const skill of skills) {
223
+ console.log(` - ${skill.name}`);
224
+ }
225
+ console.log();
226
+
227
+ const readmeUpdated = await updateReadme(skills);
228
+ if (readmeUpdated) {
229
+ console.log("✓ Updated README.md");
230
+ }
231
+
232
+ const marketplaceUpdated = await updateMarketplace(skills);
233
+ if (marketplaceUpdated) {
234
+ console.log("✓ Updated .claude-plugin/marketplace.json");
235
+ }
236
+
237
+ console.log("\nDone!");
238
+ }
239
+
240
+ main().catch((error) => {
241
+ console.error("Error:", error.message);
242
+ process.exit(1);
243
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "{Skill_Name}",
3
+ "description": "{Skill_Description}",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "{Creator_Name}",
7
+ "email": "{Creator_Email}"
8
+ },
9
+ "license": "{Skill_License}",
10
+ "homepage": "{Skill_Homepage}",
11
+ "repository": "https://github.com/{Skill_Repository}",
12
+ "keywords": "{Skill_Keywords}",
13
+ "category": "{Skill_Category}"
14
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: {Skill_Name}
3
+ description: {Skill_Description}
4
+ license: {Skill_License}
5
+ metadata:
6
+ author: {Brand_Name}
7
+ version: "1.0.0"
8
+ ---
9
+