Add skill-manager skill (migrated from skill-creator) with deploy action

- Skill content moved from ~/.agents/skills/skill-creator, renamed to skill-manager
- New deploy CLI action: installs any skill via absolute-path symlinks (or copies)
  into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills
- Taskfile + shell module wrappers (task deploy / cli:deploy)
- SKILL.md: deploy docs, origin-repository/origin-path metadata

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 22:32:42 +03:00
co-authored by Claude Fable 5
parent cc30987dd2
commit 6042272ccc
62 changed files with 3474 additions and 1 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# skill-creator # skill-manager
Dedicated to manage AI agent skill - which is capable to create and manage skills following these specs -> agentskills.io/specification Dedicated to manage AI agent skill - which is capable to create and manage skills following these specs -> agentskills.io/specification
+242
View File
@@ -0,0 +1,242 @@
---
name: skill-manager
description: >
Create, structure, refine, and deploy Agent Skills following the agentskills.io
specification. Use this skill when asked to build a new skill, create a SKILL.md file,
package agent instructions into a reusable skill, or improve an existing skill's
description, structure, or content. Also use when someone wants to turn a workflow,
procedure, or set of instructions into a portable skill that works across Claude Code,
GitHub Copilot, OpenAI Codex, and other compatible agents — or wants to deploy, install,
publish, or symlink any skill into the agent skill directories ($HOME/.agents/skills,
$HOME/.claude/skills, $HOME/.cline/skills), even if they just say "make this skill available".
license: Proprietary
metadata:
author: workspace-swiss-knife
version: "1.0"
spec: agentskills.io/specification
origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/skill-manager.git
origin-path: $HOME/projects-ibm/cognitive-architect/workspace-skills-code-agent/skill-manager
compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments
---
# Skill Manager
Build, manage, and deploy well-formed [Agent Skills](https://agentskills.io/specification) — the portable, open format for packaging reusable agent instructions.
## Skill structure
Every skill is a directory. The only required file is `SKILL.md`:
```
.agents/skills/<skill-name>/
├── SKILL.md # Required: YAML frontmatter + instructions
├── references/ # Optional: detailed reference docs loaded on demand
├── assets/ # Optional: templates, schemas, data files
└── scripts/ # Optional: executable scripts the agent can run
├── Taskfile.yml # Required — pure aggregator: only includes: + default task
├── package.json # Required when TypeScript/JS code is present
├── tsconfig.json # Required when TypeScript code is present
├── .gitignore # Required when scripts/ exists
├── .scripts/ # Shell modules (Taskfile → api → lib), one folder per module
│ └── <module>/ # e.g. diagram/, validator/, scaffolder/
│ ├── Taskfile.yml
│ ├── api/ # Thin wrappers — source lib/--index.sh, call one function
│ └── lib/ # --index.sh, --index-api.sh, --env-vars-*.sh, -<action>.sh
└── src/ # All non-shell source: TypeScript, Python, JS, etc.
# ❌ NO source files (.py/.ts/.js/.sh) directly in scripts/ root
# ❌ Root Taskfile.yml must NOT have inline cmds — it only includes: module Taskfiles
```
**Root `scripts/Taskfile.yml` pattern — aggregator only:**
```yaml
version: "3"
includes:
diagram: ./.scripts/diagram/Taskfile.yml
# add more modules here
tasks:
default:
cmds:
- task --list-all
silent: true
```
The root Taskfile **never** contains inline `python ...` or `bash ...` commands. All logic lives inside module Taskfiles under `.scripts/`. Tasks in the root Taskfile may delegate to a module task via `task: module:action` but must not contain logic themselves.
Default location for skills: `.agents/skills/<skill-name>/`
## Available scripts
This skill ships a `skill-tools` TypeScript CLI under `scripts/`. All commands run via `task` from the `scripts/` directory:
```bash
cd .agents/skills/skill-manager/scripts
# Validate any skill directory (TypeScript CLI via cli module)
task validate -- --skill-dir="/absolute/path/to/skill"
# or directly via cli module:
task cli:validate -- --skill-dir="/absolute/path/to/skill"
# Scaffold a new skill from the built-in template
task scaffold -- --skill-name="my-skill" --output-dir="/path/to/.agents/skills"
# or directly via cli module:
task cli:scaffold -- --skill-name="my-skill" --output-dir="/path/to/.agents/skills"
# Deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills
task deploy -- --skill-dir="/absolute/path/to/skill"
# copy instead of symlink, replace existing real directories:
task deploy -- --skill-dir="/absolute/path/to/skill" --mode=copy --force
# Build TypeScript to dist/
task build
# or directly via cli module:
task cli:build
# Shell-only validation (no Node.js required)
task validator:execute -- --skill-dir="/absolute/path/to/skill"
# Show environment vars (base module)
task base:environment:show
```
TypeScript CLI actions (run via `cli` module → `src/`):
- **`validate`** (`src/actions/validate/action.ts`) — 13-check validation against agentskills.io spec; YAML output with per-check detail
- **`scaffold`** (`src/actions/scaffold/action.ts`) — creates `<skill-name>/SKILL.md` + `references/` + `assets/`; outputs YAML with created paths and next-steps
- **`deploy`** (`src/actions/deploy/action.ts`) — deploys any skill into the agent skill directories; outputs YAML with per-target status
Shell modules under `scripts/.scripts/`:
- **`loggers/`** — logging utilities (`_loggers_info`, `_loggers_debug`, `_loggers_warn`, `_loggers_error`, `_loggers_trace`)
- **`base/`** — base utilities (`_base_ensureEnvironmentVariable`, `_base_environment_show`, `_base_mask`); exposes `task base:environment:show`
- **`cli/`** — wraps TypeScript CLI commands; exposes `task cli:build`, `task cli:validate`, `task cli:scaffold`
- **`validator/`** — pure-shell skill validator (no Node.js); exposes `task validator:execute`
## Workflow
### Step 1 — Gather domain context
Before writing anything:
- Understand **what** the skill does and **when** it should activate
- Collect real, specific knowledge — runbooks, API docs, code examples, past corrections
- Generic knowledge produces generic skills; domain-specific input produces valuable skills
### Step 2 — Create the skill directory and SKILL.md
Use `assets/SKILL-template.md` as the starting point. Copy it to `.agents/skills/<skill-name>/SKILL.md`.
Read `references/specification.md` for complete frontmatter rules (name, description, license, compatibility, metadata, allowed-tools).
**Critical:** the `name` field must exactly match the parent directory name.
### Step 3 — Write an effective description
The `description` field is the sole activation trigger — agents read only `name` + `description` at startup.
Rules:
- 1–1024 characters
- Cover both **what it does** and **when to use it**
- Use imperative phrasing: "Use this skill when…"
- Be explicit about indirect triggers: "even if the user doesn't mention X directly"
- Include domain keywords users are likely to use
Read `references/specification.md` → "description field" section for examples of good vs. poor descriptions.
### Step 4 — Write the SKILL.md body
Write **only what the agent wouldn't know without this skill**: project-specific conventions, non-obvious edge cases, specific APIs to use.
Keep `SKILL.md` under 500 lines / 5,000 tokens. Move detailed reference material to `references/` and tell the agent *when* to load each file — not just "see references/ for details."
Read `references/best-practices.md` for:
- Content principles (add what agent lacks; omit what it knows)
- Effective patterns: gotchas sections, output templates, checklists, validation loops
- Calibrating prescriptiveness to task fragility
### Step 5 — Add references/ (when SKILL.md grows too large)
Create focused files in `references/`. Reference them conditionally in `SKILL.md`:
```markdown
Read `references/api-errors.md` if the API returns a non-200 status code.
```
### Step 6 — Add scripts/ (when agents need to run code)
Scripts must: accept input via flags (not interactive prompts), expose `--help`, output structured data (JSON/CSV or YAML) to stdout, use meaningful exit codes, be idempotent.
**Scripts folder structure:**
- `scripts/Taskfile.yml` — **always required** when `scripts/` exists; maps every operator command to a module
- `scripts/src/` — **all source files** (TypeScript, JavaScript, Python, or other languages); never place scripts directly in `scripts/`
- `scripts/.scripts/` — shell scripts only: Taskfile → api → lib modules
- `scripts/package.json` + `scripts/tsconfig.json` — required when TypeScript is present; use pnpm + tsx for dev
**❌ NEVER place script files (`.py`, `.ts`, `.js`, `.sh`) directly in `scripts/`.**
Only `Taskfile.yml`, `package.json`, `tsconfig.json`, lockfiles, and `.gitignore` belong at the `scripts/` root level.
Source code always goes in a subdirectory: `scripts/src/` (any language) or `scripts/.scripts/` (shell modules).
**TypeScript CLI pattern** (follow `src/cli/commands.ts` + `src/actions/` in this skill):
1. CLI dispatcher (`commands.ts`) holds an action registry and routes `--action` to the right module
2. Each action is a directory with one `action.ts` exporting `run(options) → Record<string, unknown>`
3. Output is always YAML to stdout; `success: true/false` controls exit code
4. Run dev: `npx tsx src/cli/commands.ts --action <name> [options]`
5. Run via Taskfile: `task <name> -- [options]`
Read `references/scripts-guide.md` for:
- The full **Taskfile → api → lib** shell module architecture
- TypeScript CLI structure diagram, pnpm setup, and agentic design requirements
Validate a completed skill:
```bash
cd .agents/skills/<skill-name>/scripts && task validate -- --skill-dir="/absolute/path/to/skill"
```
### Step 7 — Add assets/ (templates and data files)
Place output format templates, schema files, and lookup tables in `assets/`. Reference them from `SKILL.md` so they load only when needed.
### Step 8 — Add .gitignore (always when scripts/ exists)
Every skill with a `scripts/` directory must have `scripts/.gitignore`. Git traverses the tree — one file scoped to where the build output lives is sufficient.
Copy `scripts/.gitignore` from this skill as the canonical template (covers `node_modules/`, `dist/`, `.env`, `.venv/`, `.DS_Store`).
Read `references/best-practices.md` → ".gitignore conventions" for the full template.
### Step 9 — Deploy the skill
Once a skill validates cleanly, deploy it so agents can discover it. Deployment always
resolves to **absolute paths** and targets both canonical agent skill directories:
- `$HOME/.agents/skills/<skill-name>`
- `$HOME/.claude/skills/<skill-name>`
- `$HOME/.cline/skills/<skill-name>`
```bash
cd <skill-manager>/scripts
task deploy -- --skill-dir="/absolute/path/to/skill"
```
Rules:
- `--skill-dir` may be given as a relative path but is always resolved to an absolute path before deployment
- Default mode is **symlink**: an absolute-path symlink is created in each target, so the source (e.g. a git repo) stays the single source of truth and updates propagate automatically
- `--mode=copy` copies the skill instead (excluding `node_modules/`, `dist/`, `.git/`) — use for machines where the source repo won't stay available
- The deployed name comes from the `name:` field in SKILL.md frontmatter (a mismatch with the directory name is reported as a warning)
- Existing symlinks at a destination are replaced; existing **real directories** are only replaced with `--force`
- `--targets="dir1,dir2"` overrides the target directories when deploying somewhere non-standard
- The action is idempotent — re-running a deploy is always safe
## Validation checklist
- [ ] `name` in frontmatter matches directory name exactly
- [ ] `name` uses only lowercase letters, numbers, hyphens; no leading/trailing/consecutive hyphens
- [ ] `description` is under 1024 characters and covers both what and when
- [ ] `SKILL.md` body is under 500 lines
- [ ] Large reference material is in `references/` with conditional load instructions
- [ ] Scripts (if any) have `--help`, avoid interactive prompts, use structured output
- [ ] If `scripts/` exists: `scripts/.gitignore` is present (node_modules/, dist/, .env, .venv/)
- [ ] If `scripts/` exists: no source files (`.py`, `.ts`, `.js`, `.sh`) sit directly in `scripts/` — they must be in `scripts/src/` or `scripts/.scripts/`
- [ ] Skill is deployed (`task deploy`) so it resolves at `$HOME/.agents/skills/<name>`, `$HOME/.claude/skills/<name>` and `$HOME/.cline/skills/<name>` via absolute paths
## If the skill isn't triggering reliably
Read `references/description-optimization.md` for the full description eval loop: designing trigger queries, train/validation splits, computing trigger rates, and the optimization cycle.
+68
View File
@@ -0,0 +1,68 @@
---
name: skill-name
description: >
[What the skill does — be specific about capabilities].
Use this skill when [specific trigger conditions — user intent, task type, domain],
even if the user doesn't explicitly mention [domain keywords].
license: Proprietary
metadata:
author: workspace-swiss-knife
version: "1.0"
# compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agents (uncomment if needed)
# allowed-tools: Bash Read Write (uncomment if needed)
---
# [Skill Title]
[One-sentence summary of what this skill does and for whom.]
## Overview
[2–3 sentences of context. What problem does this skill solve? What domain does it operate in?]
## Prerequisites
[List any tools, packages, env vars, or conditions required. Remove section if none.]
- Requires: `tool-name`
- Environment: `ENV_VAR=value`
## Available scripts
[List scripts if any. Remove section if no scripts/ directory.
Convention: Taskfile.yml is always required when scripts/ exists.
Shell scripts → scripts/.scripts/<domain>/api/ (Taskfile → api → lib modules)
Other languages → scripts/src/]
Run via Taskfile (preferred):
```bash
cd scripts && task <action> -- --flag=value
```
- **`scripts/Taskfile.yml`** — Entry point; maps tasks to `.scripts/<domain>/` modules
- **`scripts/.scripts/<domain>/api/<action>--<sub>.sh`** — [what it does]
- **`scripts/src/process.py`** — [what it does]
## Workflow
[Step-by-step instructions. Be prescriptive for fragile steps; flexible for steps with valid alternatives.]
1. [Step one]
2. [Step two]
3. [Step three]
## Gotchas
[Non-obvious facts the agent will get wrong without being told. This is the highest-value section.
Delete this section if you have no gotchas yet — add them as you discover them.]
- [Specific fact that defies reasonable assumptions]
- [Another non-obvious environment-specific detail]
## References
[Tell the agent when to load each reference file. Use conditional loading, not generic "see references/".
Delete this section if no references/ directory.]
- Read `references/api-errors.md` if the API returns a non-200 status code.
- Read `references/schema.md` before writing any database queries.
+46
View File
@@ -0,0 +1,46 @@
{
"_comment": "Eval suite for skill output quality and trigger testing. See references/description-optimization.md for the full optimization loop.",
"skill_name": "my-skill",
"trigger_queries": {
"_comment": "~20 queries split 60/40 into train and validation sets. Each has should_trigger: true/false.",
"train": [
{ "query": "I have a CSV of monthly sales data in ~/data/q4_results.csv — can you find the top 3 months by revenue?", "should_trigger": true },
{ "query": "my boss wants a chart from this data file", "should_trigger": true },
{ "query": "can you analyze this spreadsheet and tell me which rows have missing values?", "should_trigger": true },
{ "query": "whats the quickest way to convert this json file to yaml", "should_trigger": false },
{ "query": "write a fibonacci function in python", "should_trigger": false },
{ "query": "can you write a python script that reads a csv and uploads each row to postgres", "should_trigger": false }
],
"validation": [
{ "query": "hey can you clean up this csv and remove duplicates?", "should_trigger": true },
{ "query": "I need to update the formulas in my Excel budget spreadsheet", "should_trigger": false }
]
},
"output_evals": {
"_comment": "Test cases for evaluating skill output quality. Store input files in evals/files/.",
"evals": [
{
"id": 1,
"prompt": "I have a CSV of monthly sales data in data/sales_2025.csv. Can you find the top 3 months by revenue and make a bar chart?",
"expected_output": "A bar chart image showing the top 3 months by revenue, with labeled axes and values.",
"files": ["evals/files/sales_2025.csv"],
"assertions": [
"The output includes a bar chart image file",
"The chart shows exactly 3 months",
"Both axes are labeled",
"The chart title or caption mentions revenue"
]
},
{
"id": 2,
"prompt": "there's a csv in my downloads called customers.csv, some rows have missing emails — can you clean it up and tell me how many were missing?",
"expected_output": "A cleaned CSV with missing emails handled, plus a count of how many were missing.",
"files": ["evals/files/customers.csv"],
"assertions": [
"Output includes a cleaned CSV file",
"Output states the count of rows with missing emails"
]
}
]
}
}
+238
View File
@@ -0,0 +1,238 @@
# Skill Body — Best Practices
Source: https://agentskills.io/skill-creation/best-practices
## Core principle: start from real expertise
Ask an LLM to generate a skill without domain context → vague, generic output.
Feed it real runbooks, API specs, code review comments, incident reports → specific, valuable skill.
Good source material:
- Internal documentation, runbooks, style guides
- API specifications, schemas, configuration files
- Code review comments and issue trackers
- Version control history — patches and fixes reveal real patterns
- Real-world failure cases and their resolutions
---
## Content principles
### Add what the agent lacks — omit what it knows
Focus on what the agent *wouldn't* know without your skill:
- Project-specific conventions
- Domain-specific procedures
- Non-obvious edge cases
- The specific tools or APIs to use
**Too verbose:**
```markdown
## Extract PDF text
PDF (Portable Document Format) files are a common file format that contains
text, images, and other content. To extract text from a PDF, you'll need to
use a library. pdfplumber is recommended because it handles most cases well.
```
**Better:**
```markdown
## Extract PDF text
Use pdfplumber. For scanned documents, fall back to pdf2image + pytesseract.
```
Ask: "Would the agent get this wrong without this instruction?" If no → cut it.
### Provide defaults, not menus
When multiple tools could work, pick one and mention alternatives briefly.
```markdown
<!-- Too many options — the agent will hesitate -->
You can use pypdf, pdfplumber, PyMuPDF, or pdf2image...
<!-- Clear default with escape hatch -->
Use pdfplumber:
import pdfplumber
For scanned PDFs requiring OCR, use pdf2image + pytesseract instead.
```
### Favor procedures over declarations
Teach the agent *how to approach* a class of problems, not what to produce for one specific instance.
```markdown
<!-- Specific answer — only useful for this exact task -->
Join the `orders` table to `customers` on `customer_id`, filter where
`region = 'EMEA'`, and sum the `amount` column.
<!-- Reusable method — works for any analytical query -->
1. Read the schema from `references/schema.yaml` to find relevant tables
2. Join tables using the `_id` foreign key convention
3. Apply filters from the user's request as WHERE clauses
4. Aggregate numeric columns and format as a markdown table
```
---
## Effective patterns
### Gotchas section
The highest-value content in many skills. Environment-specific facts that defy reasonable assumptions — concrete corrections to mistakes the agent will make without being told.
```markdown
## Gotchas
- The `users` table uses soft deletes. Always include `WHERE deleted_at IS NULL`
or results will include deactivated accounts.
- The user ID is `user_id` in the database, `uid` in the auth service,
and `accountId` in the billing API. All three refer to the same value.
- The `/health` endpoint returns 200 even if the database connection is down.
Use `/ready` to check full service health.
```
Keep gotchas in `SKILL.md` — not in a reference file. The agent must read them before encountering the situation.
**When to add a gotcha:** whenever an agent makes a mistake you have to correct, add the correction here.
### Output format template
When the agent must produce a specific format, provide a template. More reliable than describing the format in prose — agents pattern-match well against concrete structures.
Short templates → inline in `SKILL.md`. Long templates or conditional-only templates → `assets/` and reference them:
```markdown
## Report structure
Use this template (full template in `assets/report-template.md`):
# [Analysis Title]
## Executive summary
[One-paragraph overview]
## Key findings
- Finding 1 with supporting data
## Recommendations
1. Specific actionable recommendation
```
### Checklist for multi-step workflows
An explicit checklist helps the agent track progress and avoid skipping steps.
```markdown
## Progress
- [ ] Step 1: Analyze the form (run `scripts/analyze_form.py`)
- [ ] Step 2: Create field mapping (edit `fields.json`)
- [ ] Step 3: Validate mapping (run `scripts/validate_fields.py`)
- [ ] Step 4: Fill the form (run `scripts/fill_form.py`)
- [ ] Step 5: Verify output (run `scripts/verify_output.py`)
```
### Validation loop
Instruct the agent to validate its own work before moving on.
```markdown
## Editing workflow
1. Make your edits
2. Run validation: `python scripts/validate.py output/`
3. If validation fails:
- Review the error message
- Fix the issues
- Run validation again
4. Only proceed when validation passes
```
### Plan-validate-execute (for batch/destructive operations)
Have the agent create an intermediate plan, validate it against a source of truth, then execute.
```markdown
## Form filling workflow
1. Extract form fields: `python scripts/analyze_form.py input.pdf` → `form_fields.json`
2. Create `field_values.json` mapping each field name to its intended value
3. Validate: `python scripts/validate_fields.py form_fields.json field_values.json`
(checks that field names exist, types are compatible, required fields are present)
4. If validation fails, revise `field_values.json` and re-validate
5. Fill: `python scripts/fill_form.py input.pdf field_values.json output.pdf`
```
---
## Calibrating prescriptiveness
Not every step needs the same specificity. Match it to the fragility of the task.
**Give the agent freedom** when multiple approaches are valid and variation is acceptable:
```markdown
## Code review process
1. Check all database queries for SQL injection (use parameterized queries)
2. Verify authentication checks on every endpoint
3. Look for race conditions in concurrent code paths
4. Confirm error messages don't leak internal details
```
**Be prescriptive** when operations are fragile or exact sequence matters:
```markdown
## Database migration
Run exactly this sequence:
\```bash
python scripts/migrate.py --verify --backup
\```
Do not modify the command or add additional flags.
```
Most skills have a mix — calibrate each section independently.
---
## Sizing and progressive disclosure
- Keep `SKILL.md` under **500 lines / 5,000 tokens** — just core instructions the agent needs on every run
- Move detailed reference material to `references/` files
- Tell the agent *when* to load each file: "Read `references/api-errors.md` if the API returns a non-200 status code" — not a generic "see references/ for more"
- Design coherent units: a skill that queries a database and formats results may be one coherent unit; one that also covers DB administration is probably too broad
---
## Refining with real execution
1. Run the skill against real tasks
2. Read the execution traces (not just final outputs) — wasted steps reveal vague instructions; wrong approaches reveal instructions that don't apply
3. Add missed corrections to the gotchas section
4. Feed failed results + current `SKILL.md` to an LLM and ask for improvements
When prompting for improvements:
- Generalize from feedback — fix the underlying issue, not the specific test case
- Keep the skill lean — fewer better instructions outperform exhaustive rules
- Explain the why — "Do X because Y tends to cause Z" works better than "ALWAYS do X"
- Bundle repeated work — if the agent reinvents the same helper script every run, add it to `scripts/`
---
## .gitignore conventions
Every skill with a `scripts/` directory **must** have a `scripts/.gitignore`. One file, scoped to where the build output lives:
```gitignore
# Node / pnpm
node_modules/
dist/
.pnpm-store/
# Environment
.env
.env.local
.env.*.local
# Python virtual environments
.venv/
__pycache__/
*.pyc
# OS
.DS_Store
```
Place it at `<skill-name>/scripts/.gitignore`. Git traverses the tree and picks it up regardless of where the repo root is — no need for a skill-root `.gitignore` with `scripts/node_modules/` path prefixes.
**Rule:** Before the first `pnpm install` or `tsc` run, create `scripts/.gitignore`. Copy `scripts/.gitignore` from this skill as the canonical template.
+146
View File
@@ -0,0 +1,146 @@
# Description Optimization — Trigger Eval Loop
Source: https://agentskills.io/skill-creation/optimizing-descriptions
The `description` field is the sole activation trigger. Agents read only `name` + `description` at startup. An under-specified description means the skill won't trigger when it should; an over-broad description means it triggers when it shouldn't.
---
## Step 1 — Design trigger eval queries
Create ~20 queries: **8–10 should-trigger**, **8–10 should-not-trigger**.
Store them in `evals/eval_queries.json` (see `assets/evals-template.json` for the full format).
### Should-trigger queries
Vary along several axes:
- **Phrasing**: formal, casual, with typos
- **Explicitness**: some name the domain directly; others describe the need without naming it
- **Detail**: terse prompts alongside context-heavy ones with file paths and column names
- **Complexity**: single-step tasks alongside multi-step workflows
The most useful should-trigger queries are ones where the skill *would* help but the connection isn't obvious — these are where description wording makes the difference.
### Should-not-trigger queries (near-misses)
The most valuable negatives share keywords or concepts with your skill but need something different.
**Weak negatives** (obviously irrelevant — tests nothing):
- "Write a fibonacci function"
- "What's the weather today?"
**Strong negatives** (near-misses — tests precision):
- "I need to update formulas in my Excel budget spreadsheet" — shares "spreadsheet" but needs Excel editing, not CSV analysis
- "can you write a python script that reads a csv and uploads each row to postgres" — involves CSV, but the task is database ETL, not analysis
### Tips for realism
Include in your queries:
- File paths (`~/Downloads/report_final_v2.xlsx`)
- Personal context ("my manager asked me to…")
- Specific details (column names, company names, data values)
- Casual language, abbreviations, occasional typos
---
## Step 2 — Test trigger rates
Run each query through the agent with the skill installed. Observe whether the agent loads the skill's `SKILL.md`.
A query passes if:
- `should_trigger: true` → skill was invoked
- `should_trigger: false` → skill was not invoked
### Multiple runs (nondeterminism)
Run each query 3 times and compute a **trigger rate** (fraction of runs where skill was invoked).
- Should-trigger passes if trigger rate ≥ 0.5
- Should-not-trigger passes if trigger rate < 0.5
Example shell script for Claude Code:
```bash
#!/bin/bash
QUERIES_FILE="${1:?Usage: $0 <queries.json>}"
SKILL_NAME="my-skill"
RUNS=3
check_triggered() {
local query="$1"
claude -p "$query" --output-format json 2>/dev/null \
| jq -e --arg skill "$SKILL_NAME" \
'any(.messages[].content[]; .type == "tool_use" and .name == "Skill" and .input.skill == $skill)' \
> /dev/null 2>&1
}
count=$(jq length "$QUERIES_FILE")
for i in $(seq 0 $((count - 1))); do
query=$(jq -r ".[$i].query" "$QUERIES_FILE")
should_trigger=$(jq -r ".[$i].should_trigger" "$QUERIES_FILE")
triggers=0
for run in $(seq 1 $RUNS); do
check_triggered "$query" && triggers=$((triggers + 1))
done
jq -n \
--arg query "$query" \
--argjson should_trigger "$should_trigger" \
--argjson triggers "$triggers" \
--argjson runs "$RUNS" \
'{query: $query, should_trigger: $should_trigger, triggers: $triggers, runs: $runs, trigger_rate: ($triggers / $runs)}'
done | jq -s '.'
```
---
## Step 3 — Train/validation split
Split your ~20 queries:
- **Train set (~60%, ~12 queries)**: guide improvements
- **Validation set (~40%, ~8 queries)**: check whether improvements generalize
Keep both sets proportionally mixed (should-trigger and should-not-trigger). Fix the split across iterations.
---
## Step 4 — The optimization loop
1. **Evaluate** on train + validation sets
2. **Identify failures** in train set only:
- Should-trigger failures → description too narrow → broaden scope, add more "when to use" context
- Should-not-trigger false-positives → description too broad → add specificity, clarify what the skill does *not* do
3. **Revise the description**:
- Address the general category that failed queries represent — don't add specific keywords from failed queries (overfitting)
- If stuck after several iterations, try a structurally different approach rather than incremental tweaks
- Check that description stays under 1024 characters
4. **Repeat** steps 1–3 until train set passes or improvement plateaus
5. **Select the best iteration** by validation pass rate — the best may be an earlier iteration, not the last
Five iterations is usually enough.
---
## Step 5 — Apply the result
1. Update the `description` field in `SKILL.md` frontmatter
2. Verify it is under 1024 characters
3. Try 5–10 fresh queries (never part of optimization) as a final sanity check
**Before and after example:**
```yaml
# Before
description: Process CSV files.
# After
description: >
Analyze CSV and tabular data files — compute summary statistics,
add derived columns, generate charts, and clean messy data. Use this
skill when the user has a CSV, TSV, or Excel file and wants to
explore, transform, or visualize the data, even if they don't
explicitly mention "CSV" or "analysis."
```
The improved description is more specific about what the skill does (stats, derived columns, charts, cleaning) and broader about when it applies (CSV, TSV, Excel; even without explicit keywords).
+418
View File
@@ -0,0 +1,418 @@
# Using Scripts in Skills
Source: https://agentskills.io/skill-creation/using-scripts
Scripts in `scripts/` let agents run executable code as part of a skill's workflow. This reference covers the folder convention, one-off commands, self-contained bundled scripts, and design principles for agentic use.
---
## Scripts folder convention
```
scripts/
├── Taskfile.yml # Required: maps operator commands to modules
├── package.json # Required when TypeScript/JS code exists
├── tsconfig.json # Required when TypeScript code exists
├── pnpm-lock.yaml # Committed lockfile
├── .scripts/ # Shell scripts (.sh) — always placed here
└── src/ # TypeScript, JavaScript, Python, or other language source
├── cli/
│ └── commands.ts # CLI dispatcher (action registry pattern)
├── actions/
│ └── <action>/
│ └── action.ts # One action per directory; exports run()
└── services/ # Shared logic reused across actions
```
**Rules:**
- `scripts/Taskfile.yml` is **always required** when a `scripts/` directory exists — it is the single entry point for every operator command
- All `.sh` files go in `scripts/.scripts/` — never directly in `scripts/`
- Shell modules follow the `Taskfile → api → lib` architecture described below
- TypeScript/JS source goes in `scripts/src/` with pnpm + tsx for dev, tsc for build
- Supporting config files (`package.json`, `tsconfig.json`, `pnpm-lock.yaml`, etc.) live in `scripts/` alongside the subdirectories
**Run via Taskfile (preferred — all languages):**
```bash
cd scripts && task validate -- --skill-dir="/path/to/skill"
cd scripts && task scaffold -- --skill-name="my-skill"
cd scripts && task build
```
**Run TypeScript directly with tsx (dev):**
```bash
cd scripts && npx tsx src/cli/commands.ts --action validate --skill-dir /path/to/skill
```
**Run shell API directly:**
```bash
bash scripts/.scripts/validator/api/skill--execute.sh --skill-dir="/path/to/skill"
```
**Run Python:**
```bash
uv run scripts/src/process.py --input file.json
```
---
## Shell module architecture (Taskfile → api → lib)
When a skill ships shell scripts, organise them as **modules** rather than flat files. This is the `wrapper-first` pattern: every public command is a thin API wrapper; all logic lives in composable lib functions.
### Why this matters
- **Agents run the Taskfile task**; they never need to know the internal paths
- **Operators run the Taskfile task**; the api/ file is the only public surface
- **Logic is testable** in isolation inside lib/; the api/ file has zero logic
### Module layout
```
scripts/
├── Taskfile.yml # Root aggregator — includes: + TypeScript tasks
└── .scripts/
└── <module-noun>/ # e.g. validator, scaffolder, parser
├── Taskfile.yml # Module-level: defines this module's tasks
├── api/
│ └── <action>--<sub-action>.sh # Thin wrapper — sources lib, calls one function
└── lib/
├── --index.sh # Composes: sources env-reader, env-validator, index-api
├── --index-api.sh # Sources every lib function file in this module
├── --env-vars-reader.sh # Reads env vars (no-op for CLI-only modules)
├── --env-vars-validator.sh # Validates required env vars (no-op for CLI-only modules)
└── -<action>--<sub-action>.sh # Implementation: one function per file
```
**Naming rules:**
| Artefact | Convention | Example |
|---|---|---|
| Domain folder | `<module-noun>` — what the module *is* | `validator` |
| API file | `<action>--<sub-action>.sh` — what it *does* | `skill--execute.sh` |
| Lib function file | `-<action>--<sub-action>.sh` | `-skill--execute.sh` |
| Shell function name | `_<module>__<action>__<sub_action>` | `_validator__skill__execute` |
| Taskfile task | descriptive verb phrase | `validate-shell` |
**Module vs action — the key distinction:**
- The **module** (domain folder) is a noun describing *what the module is*: `validator`, `scaffolder`, `parser`
- The **action** (api file + function suffix) is a verb describing *what it does*: `execute`, `run`, `parse`, `build`
- A module named `validate` is wrong — `validate` is an action, not a module identity
### Module-level Taskfile.yml (inside .scripts/<module>/)
Each shell module has its own `Taskfile.yml`. It only knows about its own actions:
```yaml
# .scripts/validator/Taskfile.yml
version: "3"
tasks:
execute:
desc: -- --skill-dir="<path>"
cmds:
- |
./.scripts/validator/api/skill--execute.sh {{ .CLI_ARGS }}
silent: true
```
- One task per action in the module
- Task names are action verbs: `execute`, `run`, `build`, `parse`
- `{{ .CLI_ARGS }}` forwards all flags to the api script
- `silent: true` keeps output clean
### Root Taskfile.yml (scripts/Taskfile.yml)
The root Taskfile is an **aggregator** — it imports shell modules via `includes:` and adds any TypeScript/Python tasks inline:
```yaml
# scripts/Taskfile.yml
version: "3"
includes:
validator: ./.scripts/validator/Taskfile.yml
# Add more modules here as they are created:
# scaffolder: ./.scripts/scaffolder/Taskfile.yml
tasks:
default:
cmds:
- task --list-all
silent: true
validate:
desc: Validate via TypeScript CLI -- --skill-dir="<path>"
cmds:
- npx tsx src/cli/commands.ts --action validate {{ .CLI_ARGS }}
silent: true
```
- Shell modules are namespaced automatically: `validator:execute`, `scaffolder:run`
- TypeScript/Python tasks are defined inline (no sub-Taskfile needed)
- `default` task runs `task --list-all` so the operator can always discover what's available
- Adding a new shell module = one new line under `includes:`
### api/<action>--<sub-action>.sh (thin wrapper)
```bash
#!/bin/bash
# Thin wrapper. No logic here.
. ./.scripts/validator/lib/--index.sh
_validator__skill__execute "$@"
```
- Sources `lib/--index.sh` (relative to where the task runs — `scripts/`)
- Calls exactly one lib function and forwards `"$@"`
- Never contains conditionals, loops, or string manipulation
### lib/--index.sh (bootstrap)
```bash
#!/bin/bash
. ./.scripts/validator/lib/--env-vars-reader.sh
. ./.scripts/validator/lib/--env-vars-validator.sh
. ./.scripts/validator/lib/--index-api.sh
```
- Fixed order: env-reader → env-validator → index-api
- No logic — only `source` statements
### lib/--index-api.sh (function loader)
```bash
#!/bin/bash
. ./.scripts/validator/lib/-skill--execute.sh
```
- Sources every lib function file in the module
- Add one line per function file; no other content
### lib/--env-vars-reader.sh and lib/--env-vars-validator.sh
For CLI-only modules (all input via flags), these are no-ops:
```bash
#!/bin/bash
# CLI-only module — no env vars required
```
For modules that consume env vars, `--env-vars-reader.sh` exports them and `--env-vars-validator.sh` fails fast with a clear message if required vars are missing.
### lib/-<action>--<sub-action>.sh (implementation)
```bash
#!/bin/bash
_validator__skill__execute() {
local skill_dir=""
# ... parse flags, validate, implement
}
```
- One function per file; filename mirrors the function name (minus the `_domain__` prefix)
- The function contains all logic; the api wrapper has none
### Implementation workflow
1. Create the domain folder: `scripts/.scripts/<domain>/api/` and `scripts/.scripts/<domain>/lib/`
2. Write `lib/-<action>--<sub-action>.sh` with the full implementation
3. Write `lib/--index-api.sh` sourcing it
4. Write `lib/--index.sh` with the three bootstrap sources
5. Write `lib/--env-vars-reader.sh` and `lib/--env-vars-validator.sh` (even if no-op)
6. Write `api/<action>--<sub-action>.sh` as the thin wrapper
7. `chmod +x` all `.sh` files in the module
8. Add the Taskfile task
9. Test: `cd scripts && task <action> -- --flag=value`
---
## One-off commands (no scripts/ directory needed)
When an existing package already does what you need, reference it directly in `SKILL.md`:
| Runner | Command | Notes |
|--------|---------|-------|
| `uvx` | `uvx ruff@0.8.0 check .` | Python. Ships with uv. Fast, aggressive caching. |
| `pipx` | `pipx run 'black==24.10.0' .` | Python. Available via OS package managers. |
| `npx` | `npx eslint@9 --fix .` | Node.js packages. Ships with npm. |
| `bunx` | `bunx eslint@9 --fix .` | Bun's npx equivalent. Bun-only environments. |
| `go run` | `go run golang.org/x/tools/cmd/goimports@v0.28.0 .` | Go. Built into go toolchain. |
**Tips:**
- Pin versions (`npx eslint@9.0.0`) for reproducibility
- State prerequisites in `SKILL.md` (e.g., "Requires Node.js 18+")
- Move complex multi-flag commands into scripts — a tested script is more reliable than a growing one-liner
---
## Self-contained scripts with inline dependencies
Bundle scripts in `scripts/`. Each script declares its own dependencies — no separate manifest or install step required.
### Python (PEP 723) — recommended
```python
# scripts/process.py
# /// script
# dependencies = [
# "requests>=2.31",
# "beautifulsoup4>=4.12,<5",
# ]
# requires-python = ">=3.11"
# ///
from bs4 import BeautifulSoup
import sys
# ... script content
```
Run with:
```bash
uv run scripts/process.py --input data.json
pipx run scripts/process.py --input data.json # alternative
```
`uv run` creates an isolated environment, installs dependencies, and runs the script. Use `uv lock --script` for a full lockfile.
### Bash — for simple shell operations
```bash
#!/usr/bin/env bash
# scripts/.scripts/validate.sh
set -euo pipefail
# ... script content
```
Run with:
```bash
bash scripts/.scripts/validate.sh "$INPUT_FILE"
```
### Deno TypeScript — self-contained by default
```typescript
// scripts/extract.ts
#!/usr/bin/env -S deno run
import * as cheerio from "npm:cheerio@1.0.0";
// ... script content
```
Run with: `deno run scripts/extract.ts`
---
## Referencing scripts from SKILL.md
Use relative paths from the skill directory root:
```markdown
## Available scripts
- **`scripts/.scripts/validate.sh`** — Validates configuration files
- **`scripts/src/process.py`** — Processes input data and produces a summary report
## Workflow
1. Validate: `bash scripts/.scripts/validate.sh "$INPUT_FILE"`
2. Process: `uv run scripts/src/process.py --input results.json`
```
The same convention applies in `references/*.md` files — paths are relative to the skill root.
---
## Designing scripts for agentic use
### Hard requirement: no interactive prompts
Agents operate in non-interactive shells. A script that blocks on interactive input will hang indefinitely.
```bash
# Bad: hangs waiting for input
$ python scripts/deploy.py
Target environment: _
# Good: clear error with guidance
$ python scripts/deploy.py
Error: --env is required. Options: development, staging, production.
Usage: python scripts/deploy.py --env staging --tag v1.2.3
```
Accept all input via:
- Command-line flags (`--env staging`)
- Environment variables (`TARGET_ENV=staging`)
- Stdin (pipe-safe, non-blocking)
### Expose --help
`--help` output is the primary way an agent learns your script's interface:
```
Usage: scripts/process.py [OPTIONS] INPUT_FILE
Process input data and produce a summary report.
Options:
--format FORMAT Output format: json, csv, table (default: json)
--output FILE Write output to FILE instead of stdout
--verbose Print progress to stderr
Examples:
scripts/process.py data.csv
scripts/process.py --format csv --output report.csv data.csv
```
Keep it concise — the output enters the agent's context window.
### Write helpful error messages
```
Error: --format must be one of: json, csv, table.
Received: "xml"
```
Not: `Error: invalid input`
An opaque error wastes a turn. The message should say what went wrong, what was expected, what to try.
### Use structured output
Prefer JSON, CSV, TSV over free-form text. Structured formats can be consumed by both the agent and standard tools (`jq`, `cut`, `awk`).
```
# Hard to parse
NAME STATUS CREATED
my-service running 2025-01-15
# Machine-readable
{"name": "my-service", "status": "running", "created": "2025-01-15"}
```
Separate data from diagnostics:
- **stdout** → structured data output
- **stderr** → progress messages, warnings, diagnostics
### Further design requirements
| Requirement | Why |
|-------------|-----|
| **Idempotency** | Agents may retry commands. "Create if not exists" is safer than "create and fail on duplicate." |
| **Input validation** | Reject ambiguous input with a clear error rather than guessing. Use enums and closed sets. |
| **`--dry-run` support** | For destructive/stateful operations, let the agent preview what will happen. |
| **Meaningful exit codes** | Use distinct codes for different failure types (not found, invalid args, auth failure). Document them in `--help`. |
| **Safe defaults** | Destructive operations should require explicit flags (`--confirm`, `--force`). |
| **Predictable output size** | Agent harnesses often truncate tool output beyond ~10–30K characters. Default to summaries; support `--offset` for pagination or require `--output FILE` to opt in to large stdout. |
---
## When to bundle a script
Signal: the agent independently writes the same helper logic (a chart builder, a data parser, a validator) across multiple test runs.
When you see that pattern:
1. Extract the repeated logic into a tested script
2. Place it in `scripts/`
3. Document it in `SKILL.md` under "Available scripts"
4. Reference it with a specific run instruction
This is more reliable than letting the agent reinvent the logic each time, and it gives you a stable artifact to test and maintain.
+213
View File
@@ -0,0 +1,213 @@
# Agent Skills — Full Specification Reference
Source: https://agentskills.io/specification
## Directory structure
```
skill-name/
├── SKILL.md # Required: metadata + instructions
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation loaded on demand
├── assets/ # Optional: templates, resources
└── ... # Any additional files or directories
```
## SKILL.md format
The file must contain YAML frontmatter followed by Markdown content.
---
## Frontmatter fields
| Field | Required | Constraints |
|-------|----------|-------------|
| `name` | Yes | Max 64 chars. Lowercase letters, numbers, hyphens only. No leading/trailing/consecutive hyphens. Must match the parent directory name. |
| `description` | Yes | Max 1024 chars. Non-empty. Describes what the skill does and when to use it. |
| `license` | No | License name or reference to a bundled license file. |
| `compatibility` | No | Max 500 chars. Indicates environment requirements (product, packages, network, etc.). |
| `metadata` | No | Arbitrary key-value mapping (string → string) for additional metadata. |
| `allowed-tools` | No | Space-separated string of pre-approved tools. (Experimental) |
---
## `name` field
Rules:
- 1–64 characters
- Only: lowercase letters `a-z`, digits `0-9`, hyphens `-`
- Must not start or end with a hyphen
- Must not contain consecutive hyphens `--`
- **Must match the parent directory name exactly**
Valid examples:
```yaml
name: pdf-processing
name: data-analysis
name: code-review
```
Invalid examples:
```yaml
name: PDF-Processing # uppercase not allowed
name: -pdf # cannot start with hyphen
name: pdf--processing # consecutive hyphens not allowed
```
---
## `description` field
Rules:
- 1–1024 characters
- Should describe both **what** the skill does and **when** to use it
- Include specific keywords that help agents identify relevant tasks
**Good example:**
```yaml
description: >
Extracts text and tables from PDF files, fills PDF forms, and merges
multiple PDFs. Use when working with PDF documents or when the user
mentions PDFs, forms, or document extraction.
```
**Poor example:**
```yaml
description: Helps with PDFs.
```
Principles for effective descriptions:
- Use imperative phrasing: "Use this skill when…" not "This skill does…"
- Focus on user intent, not internal mechanics
- Be explicit about indirect triggers: "even if they don't explicitly mention 'CSV'"
- Err on the side of being specific and slightly pushy
- Keep it concise — a few sentences to a short paragraph
---
## `license` field
- Specifies the license applied to the skill
- Keep it short — either the name of a license or the name of a bundled license file
Example:
```yaml
license: Apache-2.0
license: Proprietary. LICENSE.txt has complete terms.
```
---
## `compatibility` field
- 1–500 characters if provided
- Only include if your skill has specific environment requirements
- Can indicate: intended product, required system packages, network access
Examples:
```yaml
compatibility: Designed for Claude Code (or similar products)
compatibility: Requires git, docker, jq, and access to the internet
compatibility: Requires Python 3.14+ and uv
```
Most skills do not need this field.
---
## `metadata` field
- A map from string keys to string values
- Use for storing additional properties not defined by the spec
- Make key names reasonably unique to avoid conflicts
Example:
```yaml
metadata:
author: example-org
version: "1.0"
```
---
## `allowed-tools` field
- A space-separated string of tools pre-approved to run
- Experimental — support varies between agent implementations
Example:
```yaml
allowed-tools: Bash(git:*) Bash(jq:*) Read
```
---
## Body content
The Markdown body after the frontmatter contains the skill instructions.
Recommended sections:
- Step-by-step instructions
- Examples of inputs and outputs
- Common edge cases
The agent loads the entire body when the skill activates. Keep it under 500 lines. Move longer reference material to separate files.
---
## Progressive disclosure
Skills are loaded in three stages:
| Stage | Content | Size |
|-------|---------|------|
| Startup | `name` + `description` only | ~100 tokens |
| Activation | Full `SKILL.md` body | < 5,000 tokens recommended |
| On demand | Files in `scripts/`, `references/`, `assets/` | As needed |
Keep `SKILL.md` under 500 lines. Move detailed reference material to separate files and tell the agent *when* to load them — not just "see references/ for details."
---
## File references
Use relative paths from the skill root:
```markdown
See [the reference guide](references/REFERENCE.md) for details.
Run the extraction script:
scripts/extract.py
```
Keep file references one level deep from `SKILL.md`. Avoid deeply nested reference chains.
---
## Optional directories
### `scripts/`
Contains executable code agents can run. Scripts should:
- Be self-contained or clearly document dependencies
- Include helpful error messages
- Handle edge cases gracefully
Supported languages depend on the agent implementation. Common: Python, Bash, JavaScript.
### `references/`
Contains documentation agents read on demand:
- `REFERENCE.md` — Detailed technical reference
- `FORMS.md` — Form templates or structured data formats
- Domain-specific files (`finance.md`, `legal.md`, etc.)
Keep individual reference files focused. Agents load these on demand — smaller files = less context used.
### `assets/`
Contains static resources:
- Templates (document, configuration)
- Images (diagrams, examples)
- Data files (lookup tables, schemas)
+16
View File
@@ -0,0 +1,16 @@
# Node / pnpm
node_modules/
dist/
.pnpm-store/
# Build artefacts
*.js.map
*.d.ts.map
# Environment
.env
.env.local
.env.*.local
# OS
.DS_Store
+11
View File
@@ -0,0 +1,11 @@
# https://taskfile.dev
version: "3"
tasks:
environment:show:
desc: "Show environment vars"
cmds:
- |
./.scripts/base/api/environment-show.sh {{ .CLI_ARGS }}
silent: true
@@ -0,0 +1,9 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/base/lib/--index.sh
_base_environment_show "$@"
@@ -0,0 +1,28 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
export LOCAL_HOME_DIR=$(pwd)
export LOCAL_HOME_DIR_NAME=${PWD##*/}
export LOCAL_GIT_ROOT_DIR=$(dirname "${LOCAL_HOME_DIR}")
TMP_BASE_FILE_DOTENV=".env"
if
[ -e "${TMP_BASE_FILE_DOTENV}" ]
then
export $(
grep -v '^#' "${TMP_BASE_FILE_DOTENV}" | xargs
) >/dev/null 2>&1
fi
ALL_ARGS=("$@")
while [[ "$#" -gt 0 ]]; do
case $1 in
*) ;;
esac
shift
done
set -- "${ALL_ARGS[@]}"
@@ -0,0 +1,10 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/base/lib/--index-api.sh
# No required environment variables for skill-manager base
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/base/lib/-ensure-environment-variable.sh
. ./.scripts/base/lib/-environment-show.sh
. ./.scripts/base/lib/-mask.sh
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index.sh
#
# --> passed parameters are read & exported environment variables
#
. ./.scripts/base/lib/--env-vars-reader.sh
#
# --> required environment variables are validated for existence
#
. ./.scripts/base/lib/--env-vars-validator.sh
#
# --> available functions are imported/exported
#
. ./.scripts/base/lib/--index-api.sh
# -------------------------------------------------------------------------------------
@@ -0,0 +1,18 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index-api.sh
_base_ensureEnvironmentVariable() {
local FUNCTION_NAME="_base_ensureEnvironmentVariable"
local ENV_VAR_NAME="$1"
_loggers_info "${FUNCTION_NAME}" "ENV_VAR_NAME: ${ENV_VAR_NAME}"
if [ -z "${!ENV_VAR_NAME}" ]; then
_loggers_error "${FUNCTION_NAME}" "Missing required environment variable: ${ENV_VAR_NAME}. Check .env or .env-* files!"
exit 1
fi
}
@@ -0,0 +1,16 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index-api.sh
_base_environment_show() {
local FUNCTION_NAME="_base_environment_show"
_loggers_info "${FUNCTION_NAME}" "LOCAL_HOME_DIR: ${LOCAL_HOME_DIR}"
_loggers_info "${FUNCTION_NAME}" "LOCAL_HOME_DIR_NAME: ${LOCAL_HOME_DIR_NAME}"
}
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_base_mask() {
local original_value="$1"
local RET_VAL
if
[[ -n "$original_value" && ${#original_value} -gt 2 ]]
then
RET_VAL="${original_value:0:1}$(printf '%*s' $((${#original_value} - 2)) '' | tr ' ' '*')${original_value: -1}"
else
RET_VAL="$(printf '%*s' ${#original_value} '' | tr ' ' '*')"
fi
echo "${RET_VAL}"
}
+38
View File
@@ -0,0 +1,38 @@
# https://taskfile.dev
version: "3"
tasks:
build:
desc: Compile TypeScript source to dist/
cmds:
- |
./.scripts/cli/api/build.sh {{ .CLI_ARGS }}
silent: true
validate:
desc: |
Validate a skill directory against the agentskills.io spec.
Usage: task cli:validate -- --skill-dir="/absolute/path/to/skill"
cmds:
- |
./.scripts/cli/api/validate.sh {{ .CLI_ARGS }}
silent: true
scaffold:
desc: |
Scaffold a new skill directory from the built-in template.
Usage: task cli:scaffold -- --skill-name="my-skill" [--output-dir="/path/to/.agents/skills"]
cmds:
- |
./.scripts/cli/api/scaffold.sh {{ .CLI_ARGS }}
silent: true
deploy:
desc: |
Deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills (absolute paths).
Usage: task cli:deploy -- --skill-dir="/absolute/path/to/skill" [--mode=symlink|copy] [--force]
cmds:
- |
./.scripts/cli/api/deploy.sh {{ .CLI_ARGS }}
silent: true
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
set -e
. ./.scripts/cli/lib/--index.sh
_cli__build "$@"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
set -e
. ./.scripts/cli/lib/--index.sh
_cli__deploy "$@"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
set -e
. ./.scripts/cli/lib/--index.sh
_cli__scaffold "$@"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
set -e
. ./.scripts/cli/lib/--index.sh
_cli__validate "$@"
@@ -0,0 +1,15 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
ALL_ARGS=("$@")
while [[ "$#" -gt 0 ]]; do
case $1 in
*) ;;
esac
shift
done
set -- "${ALL_ARGS[@]}"
@@ -0,0 +1,10 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/cli/lib/--index-api.sh
# No required environment variables for cli module
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/cli/lib/-build.sh
. ./.scripts/cli/lib/-validate.sh
. ./.scripts/cli/lib/-scaffold.sh
. ./.scripts/cli/lib/-deploy.sh
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--index.sh
. ./.scripts/base/lib/--index.sh
#
# --> passed parameters are read & exported environment variables
#
. ./.scripts/cli/lib/--env-vars-reader.sh
#
# --> required environment variables are validated for existence
#
. ./.scripts/cli/lib/--env-vars-validator.sh
#
# --> available functions are imported/exported
#
. ./.scripts/cli/lib/--index-api.sh
# -------------------------------------------------------------------------------------
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_cli__build() {
local FUNCTION_NAME="_cli__build"
_loggers_info "${FUNCTION_NAME}" "Compiling TypeScript source to dist/"
pnpm run build
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_cli__deploy() {
local FUNCTION_NAME="_cli__deploy"
_loggers_info "${FUNCTION_NAME}" "Deploying skill into agent skill directories via TypeScript CLI"
npx tsx src/cli/commands.ts --action deploy "$@"
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_cli__scaffold() {
local FUNCTION_NAME="_cli__scaffold"
_loggers_info "${FUNCTION_NAME}" "Running skill scaffolder via TypeScript CLI"
npx tsx src/cli/commands.ts --action scaffold "$@"
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_cli__validate() {
local FUNCTION_NAME="_cli__validate"
_loggers_info "${FUNCTION_NAME}" "Running skill validator via TypeScript CLI"
npx tsx src/cli/commands.ts --action validate "$@"
}
@@ -0,0 +1,18 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
export LOGGER_TRAILING_NEW_LINE_ENABLED="TRUE"
export LOGGER_IS_ENABLED_ERROR=true
export LOGGER_IS_ENABLED_INFO=true
export LOGGER_IS_ENABLED_WARN=true
export LOGGER_IS_ENABLED_DEBUG=true
export LOGGER_IS_ENABLED_TRACE=false
export LOCAL_STRING_005_SPACES=" "
export LOCAL_STRING_010_SPACES="${LOCAL_STRING_005_SPACES}${LOCAL_STRING_005_SPACES}"
export LOCAL_STRING_050_SPACES="${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}"
export LOCAL_STRING_070_SPACES="${LOCAL_STRING_050_SPACES}${LOCAL_STRING_010_SPACES}${LOCAL_STRING_010_SPACES}"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/--env-vars-reader.sh
. ./.scripts/loggers/lib/--index.sh
@@ -0,0 +1,22 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
. ./.scripts/loggers/lib/-info.sh
. ./.scripts/loggers/lib/-trace.sh
. ./.scripts/loggers/lib/-debug.sh
. ./.scripts/loggers/lib/-error.sh
. ./.scripts/loggers/lib/-warn.sh
. ./.scripts/loggers/lib/-printf--debug.sh
. ./.scripts/loggers/lib/-printf--info.sh
. ./.scripts/loggers/lib/-printf--trace.sh
. ./.scripts/loggers/lib/-printf--warn.sh
. ./.scripts/loggers/lib/-empty-line.sh
. ./.scripts/loggers/lib/-waiting-dot.sh
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
#
# --> passed parameters are read & exported environment variables
#
. ./.scripts/loggers/lib/--env-vars-reader.sh
#
# --> available functions are imported/exported
#
. ./.scripts/loggers/lib/--index-api.sh
# -------------------------------------------------------------------------------------
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_debug() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_DEBUG}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# DEBUG # ${TMP_1ST_PARAM} # ${2}"
echo -e "\033[1;36m${TMP_LINE}\033[0m" >&2
fi
}
@@ -0,0 +1,13 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_emptyLine() {
_loggers_enableLoggerTrailingNewLine
echo -e "" >&2
}
@@ -0,0 +1,15 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
_loggers_enableLoggerTrailingNewLine() {
if
[ -z "$LOGGER_TRAILING_NEW_LINE_DISABLED" ]
then
echo "" >&2
export LOGGER_TRAILING_NEW_LINE_DISABLED="TRUE"
fi
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_error() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_ERROR}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# ERROR # ${TMP_1ST_PARAM} # $2"
echo -e "\033[1;31m${TMP_LINE}\033[0m" >&2
fi
}
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_info() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_INFO}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# INFO # ${TMP_1ST_PARAM} # ${2}"
echo -e "${TMP_LINE}" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__debug() {
if
[ "${LOGGER_IS_ENABLED_DEBUG}" = true ]
then
printf '\033[1;36m%s\033[0m\n' "$*" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__info() {
if
[ "${LOGGER_IS_ENABLED_INFO}" = true ]
then
printf '\033[1;32m%s\033[0m\n' "$*" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__trace() {
if
[ "${LOGGER_IS_ENABLED_TRACE}" = true ]
then
printf '\033[0;90m%s\033[0m\n' "$*" >&2
fi
}
@@ -0,0 +1,14 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
_loggers_printf__warn() {
if
[ "${LOGGER_IS_ENABLED_WARN}" = true ]
then
printf '\033[1;33m%s\033[0m\n' "$*" >&2
fi
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_trace() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_TRACE}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# TRACE # ${TMP_1ST_PARAM} # $2"
echo -e "\033[0;94m${TMP_LINE}\033[0m" >&2
fi
}
@@ -0,0 +1,13 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_waitingDot() {
echo -n "." >&2
export LOGGER_TRAILING_NEW_LINE_DISABLED=""
}
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
# TOBE ADDED - Licence & Copyright Disclaimer
#
# -------------------------------------------------------------------------------------
. ./.scripts/loggers/lib/-enable-trailing-new-line.sh
_loggers_warn() {
_loggers_enableLoggerTrailingNewLine
if
[ "${LOGGER_IS_ENABLED_WARN}" = true ]
then
local TMP_1ST_PARAM="${1}${LOCAL_STRING_070_SPACES}"
TMP_1ST_PARAM="${TMP_1ST_PARAM:0:70}"
local TMP_LINE="# WARN # ${TMP_1ST_PARAM} # $2"
echo -e "\033[1;33m${TMP_LINE}\033[0m" >&2
fi
}
+11
View File
@@ -0,0 +1,11 @@
# https://taskfile.dev
version: "3"
tasks:
execute:
desc: -- --skill-dir="<path>"
cmds:
- |
./.scripts/validator/api/skill--execute.sh {{ .CLI_ARGS }}
silent: true
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# validator/api/skill--execute.sh — thin API wrapper for skill validation
. ./.scripts/validator/lib/--index.sh
_validator__skill__execute "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# validate/lib/--env-vars-reader.sh — environment loading for the validate module
# This module operates without env files (all input via CLI flags).
# No env loading required.
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# validate/lib/--env-vars-validator.sh — env validation for the validate module
# This module takes all input via --skill-dir CLI flag.
# No environment variables required.
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
. ./.scripts/validator/lib/-skill--execute.sh
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# -------------------------------------------------------------------------------------
#
#
#
# -------------------------------------------------------------------------------------
. ./.scripts/base/lib/--index.sh
#
# --> passed parameters are read & exported environment variables
#
. ./.scripts/validator/lib/--env-vars-reader.sh
#
# --> required environment variables are validated for existence
#
. ./.scripts/validator/lib/--env-vars-validator.sh
#
# --> available functions are imported/exported
#
. ./.scripts/validator/lib/--index-api.sh
# -------------------------------------------------------------------------------------
+212
View File
@@ -0,0 +1,212 @@
#!/bin/bash
# validator/lib/-skill--execute.sh — validate a skill directory against the agentskills.io spec
_validator__skill__execute() {
local FUNCTION_NAME="_validator__skill__execute"
local SKILL_DIR=""
# ── Parse args ──────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--skill-dir=*) SKILL_DIR="${1#*=}"; shift ;;
--skill-dir) SKILL_DIR="$2"; shift 2 ;;
--help|-h)
cat <<EOF
Usage: task -t scripts/Taskfile.yml validate-shell -- --skill-dir="<path>"
Validate an Agent Skill directory against the agentskills.io specification.
Options:
--skill-dir PATH Path to the skill directory to validate (required)
Checks:
- SKILL.md exists
- YAML frontmatter is present (--- delimiters)
- 'name' field is present and valid (a-z, 0-9, hyphens only)
- 'name' matches the directory name
- 'name' is <= 64 characters, no leading/trailing/consecutive hyphens
- 'description' field is present and <= 1024 characters
- SKILL.md body is <= 500 lines (warning if exceeded)
- Optional directories: scripts/, references/, assets/
Exit codes:
0 All checks passed
1 One or more checks failed
2 Usage error
Examples:
task -t scripts/Taskfile.yml validate-shell -- --skill-dir=".agents/skills/my-skill"
EOF
return 0
;;
*) echo "Error: unknown argument: $1" >&2; return 2 ;;
esac
done
# ── Validate required args ──────────────────────────────────────────────────
if [[ -z "$SKILL_DIR" ]]; then
echo "Error: --skill-dir is required." >&2
echo "Usage: task -t scripts/Taskfile.yml validate-shell -- --skill-dir=\"<path>\"" >&2
return 2
fi
SKILL_DIR="${SKILL_DIR%/}" # strip trailing slash
# ── Counters ────────────────────────────────────────────────────────────────
local PASS=0 FAIL=0 WARN=0
_pass() { echo " ✅ $1"; ((PASS++)) || true; }
_fail() { echo " ❌ $1"; ((FAIL++)) || true; }
_warn() { echo " ⚠️ $1"; ((WARN++)) || true; }
_info() { echo " ℹ️ $1"; }
echo ""
echo "Validating skill: $SKILL_DIR"
echo "──────────────────────────────────────────"
# ── Check: directory exists ─────────────────────────────────────────────────
if [[ ! -d "$SKILL_DIR" ]]; then
echo "Error: directory not found: $SKILL_DIR" >&2
return 2
fi
local SKILL_FILE="$SKILL_DIR/SKILL.md"
local DIR_NAME
DIR_NAME="$(basename "$SKILL_DIR")"
# ── Check: SKILL.md exists ──────────────────────────────────────────────────
if [[ ! -f "$SKILL_FILE" ]]; then
_fail "SKILL.md not found in $SKILL_DIR"
echo ""; echo "Result: ❌ FAILED"; return 1
fi
_pass "SKILL.md exists"
# ── Check: frontmatter delimiters ───────────────────────────────────────────
local CONTENT
CONTENT="$(cat "$SKILL_FILE")"
if ! echo "$CONTENT" | grep -q "^---$"; then
_fail "SKILL.md is missing YAML frontmatter (--- delimiters)"
echo ""; echo "Result: ❌ FAILED"; return 1
fi
_pass "YAML frontmatter delimiters found"
# ── Extract frontmatter ─────────────────────────────────────────────────────
local FRONTMATTER
FRONTMATTER="$(awk '/^---$/{if(p==0){p=1;next}else{exit}} p{print}' "$SKILL_FILE")"
# ── Check: name field ───────────────────────────────────────────────────────
local NAME_RAW NAME
NAME_RAW="$(echo "$FRONTMATTER" | grep "^name:" | head -1 | sed 's/^name:[[:space:]]*//')"
NAME="$(echo "$NAME_RAW" | tr -d "'\"" | tr -d '[:space:]')"
if [[ -z "$NAME" ]]; then
_fail "'name' field is missing or empty"
else
_pass "'name' field is present: $NAME"
if [[ "$NAME" != "$DIR_NAME" ]]; then
_fail "'name' ($NAME) does not match directory name ($DIR_NAME)"
else
_pass "'name' matches directory name"
fi
local NAME_LEN="${#NAME}"
if [[ $NAME_LEN -gt 64 ]]; then
_fail "'name' is $NAME_LEN characters (max 64)"
else
_pass "'name' length is $NAME_LEN (max 64)"
fi
if echo "$NAME" | grep -q '[^a-z0-9-]'; then
_fail "'name' contains invalid characters (only a-z, 0-9, - allowed)"
else
_pass "'name' uses only allowed characters (a-z, 0-9, -)"
fi
if [[ "$NAME" == -* ]]; then
_fail "'name' must not start with a hyphen"
else
_pass "'name' does not start with a hyphen"
fi
if [[ "$NAME" == *- ]]; then
_fail "'name' must not end with a hyphen"
else
_pass "'name' does not end with a hyphen"
fi
if echo "$NAME" | grep -q "\-\-"; then
_fail "'name' must not contain consecutive hyphens (--)"
else
_pass "'name' has no consecutive hyphens"
fi
fi
# ── Check: description field ────────────────────────────────────────────────
local DESC_LINE
DESC_LINE="$(echo "$FRONTMATTER" | grep "^description:" | head -1)"
if [[ -z "$DESC_LINE" ]]; then
_fail "'description' field is missing"
else
local DESC_VALUE
DESC_VALUE="$(echo "$DESC_LINE" | sed 's/^description:[[:space:]]*//')"
local DESC_FULL
if [[ "$DESC_VALUE" == ">" || "$DESC_VALUE" == "|" || -z "$DESC_VALUE" ]]; then
local DESC_BODY
DESC_BODY="$(awk '/^description:/{found=1; next} found && /^ /{print; next} found && /^[^ ]/{exit}' "$SKILL_FILE" | sed 's/^ //')"
DESC_FULL="$(echo "$DESC_BODY" | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
else
DESC_FULL="$DESC_VALUE"
fi
if [[ -z "$DESC_FULL" ]]; then
_fail "'description' is empty"
else
local DESC_LEN="${#DESC_FULL}"
_pass "'description' is present (${DESC_LEN} chars)"
if [[ $DESC_LEN -gt 1024 ]]; then
_fail "'description' is $DESC_LEN characters (max 1024)"
else
_pass "'description' length is $DESC_LEN (max 1024)"
fi
fi
fi
# ── Check: body length ──────────────────────────────────────────────────────
local BODY_LINES
BODY_LINES="$(awk '/^---$/{n++} n>=2{print}' "$SKILL_FILE" | wc -l | tr -d ' ')"
if [[ $BODY_LINES -gt 500 ]]; then
_warn "SKILL.md body is $BODY_LINES lines (recommended max 500). Consider moving detail to references/."
else
_pass "SKILL.md body is $BODY_LINES lines (max 500 recommended)"
fi
# ── Optional directories ────────────────────────────────────────────────────
echo ""
_info "Optional directories:"
for dir in scripts references assets; do
if [[ -d "$SKILL_DIR/$dir" ]]; then
local FILE_COUNT
FILE_COUNT="$(find "$SKILL_DIR/$dir" -type f | wc -l | tr -d ' ')"
_info " $dir/ — found ($FILE_COUNT files)"
else
_info " $dir/ — not present"
fi
done
# ── Summary ─────────────────────────────────────────────────────────────────
echo ""
echo "──────────────────────────────────────────"
echo "Passed: $PASS | Failed: $FAIL | Warnings: $WARN"
echo ""
if [[ $FAIL -gt 0 ]]; then
echo "Result: ❌ FAILED"; return 1
elif [[ $WARN -gt 0 ]]; then
echo "Result: ✅ PASSED (with warnings)"; return 0
else
echo "Result: ✅ PASSED"; return 0
fi
}
+50
View File
@@ -0,0 +1,50 @@
# https://taskfile.dev
version: "3"
includes:
base: ./.scripts/base/Taskfile.yml
cli: ./.scripts/cli/Taskfile.yml
validator: ./.scripts/validator/Taskfile.yml
tasks:
default:
cmds:
- task --list-all
silent: true
build:
desc: Compile TypeScript source to dist/
cmds:
- task: cli:build
silent: true
validate:
desc: |
Validate a skill directory against the agentskills.io spec.
Usage: task validate -- --skill-dir="/absolute/path/to/skill"
cmds:
- task: cli:validate
vars:
CLI_ARGS: "{{ .CLI_ARGS }}"
silent: true
scaffold:
desc: |
Scaffold a new skill directory from the built-in template.
Usage: task scaffold -- --skill-name="my-skill" [--output-dir="/path/to/.agents/skills"]
cmds:
- task: cli:scaffold
vars:
CLI_ARGS: "{{ .CLI_ARGS }}"
silent: true
deploy:
desc: |
Deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills (absolute paths).
Usage: task deploy -- --skill-dir="/absolute/path/to/skill" [--mode=symlink|copy] [--force]
cmds:
- task: cli:deploy
vars:
CLI_ARGS: "{{ .CLI_ARGS }}"
silent: true
+23
View File
@@ -0,0 +1,23 @@
{
"name": "skill-tools",
"version": "0.1.0",
"description": "CLI tools for creating and validating Agent Skills",
"type": "module",
"packageManager": "pnpm@9.15.0",
"bin": {
"skill-tools": "dist/cli/commands.js"
},
"scripts": {
"build": "tsc",
"cli": "tsx src/cli/commands.ts"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
},
"dependencies": {
"js-yaml": "^4.1.0"
}
}
+354
View File
@@ -0,0 +1,354 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
js-yaml:
specifier: ^4.1.0
version: 4.3.0
devDependencies:
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
'@types/node':
specifier: ^22.0.0
version: 22.20.1
tsx:
specifier: ^4.19.0
version: 4.23.1
typescript:
specifier: ^5.7.0
version: 5.9.3
packages:
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
hasBin: true
tsx@4.23.1:
resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==}
engines: {node: '>=18.0.0'}
hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.28.1':
optional: true
'@types/js-yaml@4.0.9': {}
'@types/node@22.20.1':
dependencies:
undici-types: 6.21.0
argparse@2.0.1: {}
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
fsevents@2.3.3:
optional: true
js-yaml@4.3.0:
dependencies:
argparse: 2.0.1
tsx@4.23.1:
dependencies:
esbuild: 0.28.1
optionalDependencies:
fsevents: 2.3.3
typescript@5.9.3: {}
undici-types@6.21.0: {}
+191
View File
@@ -0,0 +1,191 @@
/**
* Action: deploy
*
* Deploys a skill directory into the agent skill directories so compatible
* agents (Claude Code, Cline, Copilot, Codex, …) can discover it.
*
* Default targets (always resolved to absolute paths):
* $HOME/.agents/skills/<skill-name>
* $HOME/.claude/skills/<skill-name>
* $HOME/.cline/skills/<skill-name>
*
* Modes:
* symlink (default) — creates an absolute-path symlink to the skill source.
* The source stays the single source of truth (e.g. a git repo).
* copy — copies the skill directory (excluding node_modules/, dist/, .git/).
*
* Existing symlinks at the destination are replaced. Existing real directories
* are only replaced when --force is given.
*
* Usage:
* skill-tools --action deploy --skill-dir /abs/path/to/skill [--mode symlink|copy] [--targets dir1,dir2] [--force]
*/
import {
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
unlinkSync,
} from "node:fs";
import { homedir } from "node:os";
import { basename, join, resolve } from "node:path";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const DEFAULT_TARGETS = [".agents/skills", ".claude/skills", ".cline/skills"];
const COPY_EXCLUDES = new Set(["node_modules", "dist", ".git"]);
function readSkillName(skillDir: string): string | undefined {
const skillMdPath = join(skillDir, "SKILL.md");
const content = readFileSync(skillMdPath, "utf-8");
const match = content.match(/^---\n[\s\S]*?^name:\s*(\S+)\s*$/m);
return match?.[1];
}
function safeRealpath(path: string): string {
try {
return realpathSync(path);
} catch {
return resolve(path);
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function run(options: Record<string, string | undefined>): Record<string, unknown> {
const rawSkillDir = options["skill-dir"];
if (!rawSkillDir) {
return {
action: "deploy",
success: false,
error: "Missing required option: --skill-dir",
};
}
const skillDir = resolve(rawSkillDir);
if (!existsSync(skillDir)) {
return {
action: "deploy",
success: false,
error: `Skill directory does not exist: ${skillDir}`,
};
}
if (!existsSync(join(skillDir, "SKILL.md"))) {
return {
action: "deploy",
success: false,
error: `Not a skill directory (missing SKILL.md): ${skillDir}`,
};
}
const mode = options["mode"] ?? "symlink";
if (mode !== "symlink" && mode !== "copy") {
return {
action: "deploy",
success: false,
error: `'mode' must be 'symlink' or 'copy'. Got: '${mode}'`,
};
}
const force = options["force"] === "true";
const skillName = readSkillName(skillDir) ?? basename(skillDir);
const dirName = basename(skillDir);
const warnings: string[] = [];
if (skillName !== dirName) {
warnings.push(
`Frontmatter name '${skillName}' does not match directory name '${dirName}' — deploying as '${skillName}'`
);
}
const targets = (
options["targets"]
? options["targets"].split(",").map((t) => t.trim()).filter(Boolean)
: DEFAULT_TARGETS.map((t) => join(homedir(), t))
).map((t) => resolve(t));
const sourceReal = safeRealpath(skillDir);
const deployments: Record<string, unknown>[] = [];
let success = true;
for (const targetDir of targets) {
const destination = join(targetDir, skillName);
try {
mkdirSync(targetDir, { recursive: true });
let existing: ReturnType<typeof lstatSync> | undefined;
try {
existing = lstatSync(destination);
} catch {
existing = undefined;
}
if (existing && !existing.isSymbolicLink() && safeRealpath(destination) === sourceReal) {
deployments.push({
target: destination,
status: "skipped",
reason: "Destination is the source directory itself",
});
continue;
}
if (existing) {
if (existing.isSymbolicLink()) {
unlinkSync(destination);
} else if (force) {
rmSync(destination, { recursive: true });
} else {
deployments.push({
target: destination,
status: "skipped",
reason: "Destination exists and is not a symlink — pass --force to replace",
});
success = false;
continue;
}
}
if (mode === "symlink") {
symlinkSync(skillDir, destination);
} else {
cpSync(skillDir, destination, {
recursive: true,
filter: (src) => !COPY_EXCLUDES.has(basename(src)),
});
}
deployments.push({ target: destination, status: "deployed", mode });
} catch (err) {
deployments.push({
target: destination,
status: "failed",
reason: err instanceof Error ? err.message : String(err),
});
success = false;
}
}
const result: Record<string, unknown> = {
action: "deploy",
success,
"skill-name": skillName,
"skill-dir": skillDir,
mode,
deployments,
};
if (warnings.length > 0) {
result["warnings"] = warnings;
}
return result;
}
+171
View File
@@ -0,0 +1,171 @@
/**
* Action: scaffold
*
* Scaffolds a new skill directory following the agentskills.io structure.
*
* Creates:
* <output-dir>/<skill-name>/
* ├── SKILL.md — populated from built-in template
* ├── references/ — created empty (ready for docs)
* └── assets/ — created empty (ready for templates)
*
* The SKILL.md is pre-filled with the standard frontmatter template:
* - name set to <skill-name>
* - description placeholder with imperative phrasing hint
* - license, metadata, compatibility stubs
*
* Usage:
* skill-tools --action scaffold --skill-name my-skill [--output-dir /path]
*/
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
// ---------------------------------------------------------------------------
// SKILL.md template
// ---------------------------------------------------------------------------
function buildSkillMd(skillName: string): string {
return `---
name: ${skillName}
description: >
[What the skill does — be specific about capabilities].
Use this skill when [specific trigger conditions — user intent, task type, domain],
even if the user doesn't explicitly mention [domain keywords].
license: Proprietary
metadata:
author: workspace-swiss-knife
version: "1.0"
# compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agents (uncomment if needed)
# allowed-tools: Bash Read Write (uncomment if needed)
---
# [Skill Title]
[One-sentence summary of what this skill does and for whom.]
## Overview
[2–3 sentences of context. What problem does this skill solve? What domain does it operate in?]
## Prerequisites
[List any tools, packages, env vars, or conditions required. Remove section if none.]
- Requires: \`tool-name\`
- Environment: \`ENV_VAR=value\`
## Workflow
[Step-by-step instructions. Be prescriptive for fragile steps; flexible for steps with valid alternatives.]
1. [Step one]
2. [Step two]
3. [Step three]
## Gotchas
[Non-obvious facts the agent will get wrong without being told. This is the highest-value section.
Delete this section if you have no gotchas yet — add them as you discover them.]
- [Specific fact that defies reasonable assumptions]
- [Another non-obvious environment-specific detail]
## References
[Tell the agent when to load each reference file. Use conditional loading, not generic "see references/".
Delete this section if no references/ directory.]
- Read \`references/api-errors.md\` if the API returns a non-200 status code.
- Read \`references/schema.md\` before writing any database queries.
`;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function run(options: Record<string, string | undefined>): Record<string, unknown> {
const skillName = options["skill-name"];
if (!skillName) {
return {
action: "scaffold",
success: false,
error: "Missing required option: --skill-name",
};
}
// Validate skill name
if (/[^a-z0-9-]/.test(skillName)) {
return {
action: "scaffold",
success: false,
error: `'skill-name' must use only a-z, 0-9, hyphens. Got: '${skillName}'`,
};
}
if (skillName.startsWith("-") || skillName.endsWith("-")) {
return {
action: "scaffold",
success: false,
error: `'skill-name' must not start or end with a hyphen. Got: '${skillName}'`,
};
}
if (/--/.test(skillName)) {
return {
action: "scaffold",
success: false,
error: `'skill-name' must not contain consecutive hyphens. Got: '${skillName}'`,
};
}
if (skillName.length > 64) {
return {
action: "scaffold",
success: false,
error: `'skill-name' must be ≤ 64 characters. Got: ${skillName.length}`,
};
}
// Resolve output directory
const rawOutputDir = options["output-dir"] ?? join(process.cwd(), "..", "..", ".agents", "skills");
const outputDir = resolve(rawOutputDir);
const skillDir = join(outputDir, skillName);
if (existsSync(skillDir)) {
return {
action: "scaffold",
success: false,
error: `Directory already exists: ${skillDir}`,
};
}
// Create directories
const created: string[] = [];
mkdirSync(skillDir, { recursive: true });
created.push(skillDir);
const refsDir = join(skillDir, "references");
const assetsDir = join(skillDir, "assets");
mkdirSync(refsDir);
created.push(refsDir);
mkdirSync(assetsDir);
created.push(assetsDir);
// Write SKILL.md
const skillMdPath = join(skillDir, "SKILL.md");
writeFileSync(skillMdPath, buildSkillMd(skillName), "utf-8");
created.push(skillMdPath);
return {
action: "scaffold",
success: true,
"skill-name": skillName,
"skill-dir": skillDir,
created,
"next-steps": [
`Edit ${skillMdPath} — fill in description, title, overview, workflow`,
`Run validation: skill-tools --action validate --skill-dir ${skillDir}`,
"Add reference docs to references/ as the skill grows",
"Add templates/data files to assets/ if needed",
],
};
}
+287
View File
@@ -0,0 +1,287 @@
/**
* Action: validate
*
* Validates a skill directory against the agentskills.io specification.
*
* Checks performed:
* 1. SKILL.md exists
* 2. YAML frontmatter delimiters (--- ... ---) are present
* 3. 'name' field is present in frontmatter
* 4. 'name' matches the directory name (basename)
* 5. 'name' length ≤ 64 characters
* 6. 'name' uses only a-z, 0-9, hyphens
* 7. 'name' does not start with a hyphen
* 8. 'name' does not end with a hyphen
* 9. 'name' has no consecutive hyphens
* 10. 'description' field is present and non-empty
* 11. 'description' length ≤ 1024 characters
* 12. SKILL.md body (lines after frontmatter) ≤ 500 lines (recommendation)
* 13. If scripts/ exists, Taskfile.yml must be present inside it
*
* Output: YAML with success flag, counts, and per-check detail list.
*/
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join, basename } from "node:path";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface CheckResult {
id: number;
pass: boolean;
level: "error" | "warning" | "info";
message: string;
}
interface DirectoryInfo {
found: boolean;
files?: number;
"Taskfile.yml"?: boolean;
}
interface ValidateOutput {
action: "validate";
"skill-dir": string;
success: boolean;
summary: {
passed: number;
failed: number;
warnings: number;
};
checks: CheckResult[];
optional: {
"scripts/": DirectoryInfo;
"references/": DirectoryInfo;
"assets/": DirectoryInfo;
};
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function countFilesSync(dir: string): number {
try {
const entries = readdirSync(dir, { withFileTypes: true });
let count = 0;
for (const e of entries) {
if (e.isDirectory()) {
count += countFilesSync(join(dir, e.name));
} else {
count++;
}
}
return count;
} catch {
return 0;
}
}
// ---------------------------------------------------------------------------
// Frontmatter parser — handles scalar and > block values
// ---------------------------------------------------------------------------
function parseFrontmatter(raw: string): Record<string, string> {
const result: Record<string, string> = {};
const lines = raw.split("\n");
let i = 0;
while (i < lines.length) {
const line = lines[i];
const keyMatch = line.match(/^([a-zA-Z_-]+)\s*:\s*(.*)/);
if (keyMatch) {
const key = keyMatch[1];
const val = keyMatch[2].trim();
if (val === ">" || val === "|") {
// Multi-line block scalar — collect indented continuation lines
const parts: string[] = [];
i++;
while (
i < lines.length &&
(lines[i].startsWith(" ") || lines[i].startsWith("\t") || lines[i].trim() === "")
) {
parts.push(lines[i].trim());
i++;
}
result[key] = parts.join(" ").trim();
continue;
} else {
result[key] = val;
}
}
i++;
}
return result;
}
// ---------------------------------------------------------------------------
// Optional directories inspector
// ---------------------------------------------------------------------------
function inspectOptional(skillDir: string): ValidateOutput["optional"] {
function dirInfo(subdir: string): DirectoryInfo {
const p = join(skillDir, subdir);
if (!existsSync(p)) return { found: false };
return { found: true, files: countFilesSync(p) };
}
const scriptsDir = join(skillDir, "scripts");
const scriptsInfo = dirInfo("scripts");
if (scriptsInfo.found) {
scriptsInfo["Taskfile.yml"] = existsSync(join(scriptsDir, "Taskfile.yml"));
}
return {
"scripts/": scriptsInfo,
"references/": dirInfo("references"),
"assets/": dirInfo("assets"),
};
}
// ---------------------------------------------------------------------------
// Output builder
// ---------------------------------------------------------------------------
function buildOutput(
skillDir: string,
checks: CheckResult[],
optional: ValidateOutput["optional"]
): Record<string, unknown> {
const passed = checks.filter((c) => c.pass).length;
const failed = checks.filter((c) => !c.pass && c.level === "error").length;
const warnings = checks.filter((c) => !c.pass && c.level === "warning").length;
return {
action: "validate",
"skill-dir": skillDir,
success: failed === 0,
summary: { passed, failed, warnings },
checks,
optional,
};
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function run(options: Record<string, string | undefined>): Record<string, unknown> {
const skillDir = options["skill-dir"];
if (!skillDir) {
return {
action: "validate",
success: false,
error: "Missing required option: --skill-dir",
};
}
const checks: CheckResult[] = [];
let checkId = 1;
const ok = (message: string, level: CheckResult["level"] = "error"): CheckResult =>
({ id: checkId++, pass: true, level, message });
const err = (message: string, level: CheckResult["level"] = "error"): CheckResult =>
({ id: checkId++, pass: false, level, message });
// ── Check 1: SKILL.md exists ─────────────────────────────────────────────
const skillMdPath = join(skillDir, "SKILL.md");
if (!existsSync(skillMdPath)) {
checks.push(err("SKILL.md does not exist in skill directory"));
return buildOutput(skillDir, checks, inspectOptional(skillDir));
}
checks.push(ok("SKILL.md exists"));
const raw = readFileSync(skillMdPath, "utf-8");
// ── Check 2: Frontmatter delimiters ──────────────────────────────────────
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
if (!fmMatch) {
checks.push(err("YAML frontmatter delimiters (--- ... ---) not found"));
return buildOutput(skillDir, checks, inspectOptional(skillDir));
}
checks.push(ok("YAML frontmatter delimiters found"));
const fm = parseFrontmatter(fmMatch[1]);
const body = fmMatch[2];
// ── Checks 3–9: name field ───────────────────────────────────────────────
const name = fm["name"];
if (!name) {
checks.push(err("'name' field is missing from frontmatter"));
} else {
checks.push(ok(`'name' field is present: ${name}`));
const dirName = basename(skillDir);
checks.push(
name === dirName
? ok("'name' matches directory name")
: err(`'name' (${name}) does not match directory name (${dirName})`)
);
checks.push(
name.length <= 64
? ok(`'name' length is ${name.length} (max 64)`)
: err(`'name' length is ${name.length} (max 64)`)
);
checks.push(
/[^a-z0-9-]/.test(name)
? err("'name' contains characters outside a-z, 0-9, hyphen")
: ok("'name' uses only allowed characters (a-z, 0-9, -)")
);
checks.push(
name.startsWith("-")
? err("'name' must not start with a hyphen")
: ok("'name' does not start with a hyphen")
);
checks.push(
name.endsWith("-")
? err("'name' must not end with a hyphen")
: ok("'name' does not end with a hyphen")
);
checks.push(
/--/.test(name)
? err("'name' contains consecutive hyphens")
: ok("'name' has no consecutive hyphens")
);
}
// ── Checks 10–11: description field ──────────────────────────────────────
const description = fm["description"];
if (!description || description.trim() === "") {
checks.push(err("'description' field is missing or empty"));
} else {
const descLen = description.trim().length;
checks.push(ok(`'description' is present (${descLen} chars)`));
checks.push(
descLen <= 1024
? ok(`'description' length is ${descLen} (max 1024)`)
: err(`'description' length is ${descLen} (max 1024)`)
);
}
// ── Check 12: Body line count ─────────────────────────────────────────────
const bodyLines = body.split("\n").length;
checks.push(
bodyLines <= 500
? ok(`SKILL.md body is ${bodyLines} lines (max 500 recommended)`)
: err(`SKILL.md body is ${bodyLines} lines (max 500 recommended)`, "warning")
);
// ── Check 13: scripts/ must have Taskfile.yml ─────────────────────────────
const scriptsDir = join(skillDir, "scripts");
if (existsSync(scriptsDir)) {
checks.push(
existsSync(join(scriptsDir, "Taskfile.yml"))
? ok("scripts/Taskfile.yml exists")
: err("scripts/ exists but Taskfile.yml is missing — it is required")
);
}
return buildOutput(skillDir, checks, inspectOptional(skillDir));
}
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env node
/**
* skill-tools CLI dispatcher
*
* Usage:
* skill-tools --action <action-name> [options]
*
* Actions:
* validate — validate a skill directory against the agentskills.io spec
* scaffold — scaffold a new skill directory from the built-in template
* deploy — deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills, $HOME/.cline/skills
*
* Output is always YAML to stdout. Exit 0 on success, 1 on error.
*/
import { parseArgs } from "node:util";
import * as yaml from "js-yaml";
// ---------------------------------------------------------------------------
// Action registry
// ---------------------------------------------------------------------------
type ActionModule = {
run: (options: Record<string, string | undefined>) => Record<string, unknown>;
};
const ACTIONS: Record<string, () => Promise<ActionModule>> = {
validate: () => import("../actions/validate/action.js"),
scaffold: () => import("../actions/scaffold/action.js"),
deploy: () => import("../actions/deploy/action.js"),
};
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
let values: {
action?: string;
"skill-dir"?: string;
"skill-name"?: string;
"output-dir"?: string;
mode?: string;
targets?: string;
force?: boolean;
help?: boolean;
};
try {
({ values } = parseArgs({
args: process.argv.slice(2),
options: {
action: { type: "string", short: "a" },
"skill-dir": { type: "string" },
"skill-name": { type: "string" },
"output-dir": { type: "string" },
mode: { type: "string" },
targets: { type: "string" },
force: { type: "boolean", short: "f" },
help: { type: "boolean", short: "h" },
},
strict: true,
}));
} catch (err) {
printError(String(err));
process.exit(1);
}
if (values.help || !values.action) {
console.log(`skill-tools --action <action> [options]
Actions:
validate Validate a skill directory against the agentskills.io spec
--skill-dir <path> Path to the skill root directory (required)
scaffold Scaffold a new skill directory from the built-in template
--skill-name <name> Skill name (lowercase, hyphens only, required)
--output-dir <path> Parent directory to create the skill in (default: .agents/skills)
deploy Deploy a skill into the agent skill directories (absolute paths)
--skill-dir <path> Path to the skill root directory (required)
--mode <mode> symlink (default) or copy
--targets <dirs> Comma-separated target directories
(default: $HOME/.agents/skills,$HOME/.claude/skills,$HOME/.cline/skills)
--force Replace an existing real directory at the destination
Examples:
skill-tools --action validate --skill-dir /path/to/my-skill
skill-tools --action scaffold --skill-name my-skill --output-dir /path/to/.agents/skills
skill-tools --action deploy --skill-dir /path/to/my-skill
skill-tools --action deploy --skill-dir /path/to/my-skill --mode copy --force
`);
process.exit(values.help ? 0 : 1);
}
const actionName = values.action;
if (!ACTIONS[actionName]) {
printError(
`Unknown action '${actionName}'. Available: ${Object.keys(ACTIONS).join(", ")}`
);
process.exit(1);
}
try {
const mod = await ACTIONS[actionName]();
const result = mod.run({
"skill-dir": values["skill-dir"],
"skill-name": values["skill-name"],
"output-dir": values["output-dir"],
mode: values.mode,
targets: values.targets,
force: values.force ? "true" : undefined,
});
process.stdout.write(
yaml.dump(result, { noRefs: true, sortKeys: false, lineWidth: 120 })
);
const success = (result["success"] as boolean | undefined) ?? true;
process.exit(success ? 0 : 1);
} catch (err) {
const errorOutput = {
success: false,
action: actionName,
error: err instanceof Error ? err.message : String(err),
};
process.stdout.write(yaml.dump(errorOutput, { noRefs: true, sortKeys: false }));
process.exit(1);
}
}
function printError(msg: string): void {
process.stderr.write(`Error: ${msg}\n`);
process.stderr.write(
`Usage: skill-tools --action <${Object.keys(ACTIONS).join("|")}> [options]\n`
);
}
main();
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}