Merge pull request #1 from CTOTools-skills-code-agent/4.0.0/IIAA-XYZ-2026-07-23-001-skill-manager-deploy
skill-manager: deploy action + two-directory target list
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
---
|
||||||
|
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), 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 and $HOME/.claude/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>`
|
||||||
|
|
||||||
|
**About `$HOME`:** `$HOME` is the shell environment variable holding the current user's
|
||||||
|
home directory — e.g. `/Users/<username>` on macOS, `/home/<username>` on Linux. The
|
||||||
|
deploy action resolves it at runtime via Node's `os.homedir()`, so the same command works
|
||||||
|
for any user on any machine without hard-coding paths. `~` is the shell shorthand for the
|
||||||
|
same directory.
|
||||||
|
|
||||||
|
```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>` and `$HOME/.claude/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.
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
. ./.scripts/base/lib/-ensure-environment-variable.sh
|
||||||
|
|
||||||
|
. ./.scripts/base/lib/-environment-show.sh
|
||||||
|
|
||||||
|
. ./.scripts/base/lib/-mask.sh
|
||||||
@@ -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}"
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
@@ -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 and $HOME/.claude/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
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. ./.scripts/cli/lib/--index.sh
|
||||||
|
|
||||||
|
_cli__build "$@"
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. ./.scripts/cli/lib/--index.sh
|
||||||
|
|
||||||
|
_cli__deploy "$@"
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. ./.scripts/cli/lib/--index.sh
|
||||||
|
|
||||||
|
_cli__scaffold "$@"
|
||||||
Executable
+11
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Executable
+12
@@ -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 "$@"
|
||||||
|
}
|
||||||
@@ -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 "$@"
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
# -------------------------------------------------------------------------------------
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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=""
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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.
|
||||||
@@ -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
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
. ./.scripts/validator/lib/-skill--execute.sh
|
||||||
Executable
+20
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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 and $HOME/.claude/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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+354
@@ -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: {}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* 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 is the current user's home directory (e.g. /Users/<username> on macOS,
|
||||||
|
* /home/<username> on Linux), resolved at runtime via os.homedir().
|
||||||
|
*
|
||||||
|
* 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"];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#!/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 and $HOME/.claude/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 = the current user's home directory)
|
||||||
|
--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();
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user