diff --git a/README.md b/README.md index cab0f2c..9bfc4e7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..2321ea3 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,242 @@ +--- +name: skill-manager +description: > + Create, structure, refine, and deploy Agent Skills following the agentskills.io + specification. Use this skill when asked to build a new skill, create a SKILL.md file, + package agent instructions into a reusable skill, or improve an existing skill's + description, structure, or content. Also use when someone wants to turn a workflow, + procedure, or set of instructions into a portable skill that works across Claude Code, + GitHub Copilot, OpenAI Codex, and other compatible agents — or wants to deploy, install, + publish, or symlink any skill into the agent skill directories ($HOME/.agents/skills, + $HOME/.claude/skills, $HOME/.cline/skills), even if they just say "make this skill available". +license: Proprietary +metadata: + author: workspace-swiss-knife + version: "1.0" + spec: agentskills.io/specification + origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/skill-manager.git + origin-path: $HOME/projects-ibm/cognitive-architect/workspace-skills-code-agent/skill-manager +compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments +--- + +# Skill Manager + +Build, manage, and deploy well-formed [Agent Skills](https://agentskills.io/specification) — the portable, open format for packaging reusable agent instructions. + +## Skill structure + +Every skill is a directory. The only required file is `SKILL.md`: + +``` +.agents/skills// +├── SKILL.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 + │ └── / # 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, -.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//` + +## Available scripts + +This skill ships a `skill-tools` TypeScript CLI under `scripts/`. All commands run via `task` from the `scripts/` directory: + +```bash +cd .agents/skills/skill-manager/scripts + +# Validate any skill directory (TypeScript CLI via cli module) +task validate -- --skill-dir="/absolute/path/to/skill" +# or directly via cli module: +task cli:validate -- --skill-dir="/absolute/path/to/skill" + +# Scaffold a new skill from the built-in template +task scaffold -- --skill-name="my-skill" --output-dir="/path/to/.agents/skills" +# or directly via cli module: +task cli:scaffold -- --skill-name="my-skill" --output-dir="/path/to/.agents/skills" + +# Deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills +task deploy -- --skill-dir="/absolute/path/to/skill" +# copy instead of symlink, replace existing real directories: +task deploy -- --skill-dir="/absolute/path/to/skill" --mode=copy --force + +# Build TypeScript to dist/ +task build +# or directly via cli module: +task cli:build + +# Shell-only validation (no Node.js required) +task validator:execute -- --skill-dir="/absolute/path/to/skill" + +# Show environment vars (base module) +task base:environment:show +``` + +TypeScript CLI actions (run via `cli` module → `src/`): +- **`validate`** (`src/actions/validate/action.ts`) — 13-check validation against agentskills.io spec; YAML output with per-check detail +- **`scaffold`** (`src/actions/scaffold/action.ts`) — creates `/SKILL.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.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` +3. Output is always YAML to stdout; `success: true/false` controls exit code +4. Run dev: `npx tsx src/cli/commands.ts --action [options]` +5. Run via Taskfile: `task -- [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//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/` +- `$HOME/.claude/skills/` +- `$HOME/.cline/skills/` + +```bash +cd /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/`, `$HOME/.claude/skills/` and `$HOME/.cline/skills/` 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. diff --git a/assets/SKILL-template.md b/assets/SKILL-template.md new file mode 100644 index 0000000..927c9f5 --- /dev/null +++ b/assets/SKILL-template.md @@ -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//api/ (Taskfile → api → lib modules) +Other languages → scripts/src/] + +Run via Taskfile (preferred): +```bash +cd scripts && task -- --flag=value +``` + +- **`scripts/Taskfile.yml`** — Entry point; maps tasks to `.scripts//` modules +- **`scripts/.scripts//api/--.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. diff --git a/assets/evals-template.json b/assets/evals-template.json new file mode 100644 index 0000000..9438540 --- /dev/null +++ b/assets/evals-template.json @@ -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" + ] + } + ] + } +} diff --git a/references/best-practices.md b/references/best-practices.md new file mode 100644 index 0000000..ecc8131 --- /dev/null +++ b/references/best-practices.md @@ -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 + +You can use pypdf, pdfplumber, PyMuPDF, or pdf2image... + + +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 + +Join the `orders` table to `customers` on `customer_id`, filter where +`region = 'EMEA'`, and sum the `amount` column. + + +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 `/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. diff --git a/references/description-optimization.md b/references/description-optimization.md new file mode 100644 index 0000000..d95c37f --- /dev/null +++ b/references/description-optimization.md @@ -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 }" +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). diff --git a/references/scripts-guide.md b/references/scripts-guide.md new file mode 100644 index 0000000..54f0f6a --- /dev/null +++ b/references/scripts-guide.md @@ -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.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/ + └── / # e.g. validator, scaffolder, parser + ├── Taskfile.yml # Module-level: defines this module's tasks + ├── api/ + │ └── --.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) + └── ---.sh # Implementation: one function per file +``` + +**Naming rules:** + +| Artefact | Convention | Example | +|---|---|---| +| Domain folder | `` — what the module *is* | `validator` | +| API file | `--.sh` — what it *does* | `skill--execute.sh` | +| Lib function file | `---.sh` | `-skill--execute.sh` | +| Shell function name | `_____` | `_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//) + +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="" + 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="" + 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/--.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/---.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//api/` and `scripts/.scripts//lib/` +2. Write `lib/---.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/--.sh` as the thin wrapper +7. `chmod +x` all `.sh` files in the module +8. Add the Taskfile task +9. Test: `cd scripts && task -- --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. diff --git a/references/specification.md b/references/specification.md new file mode 100644 index 0000000..ff868cf --- /dev/null +++ b/references/specification.md @@ -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) diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..5b98130 --- /dev/null +++ b/scripts/.gitignore @@ -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 diff --git a/scripts/.scripts/base/Taskfile.yml b/scripts/.scripts/base/Taskfile.yml new file mode 100644 index 0000000..a8d5c3f --- /dev/null +++ b/scripts/.scripts/base/Taskfile.yml @@ -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 diff --git a/scripts/.scripts/base/api/environment-show.sh b/scripts/.scripts/base/api/environment-show.sh new file mode 100644 index 0000000..036112d --- /dev/null +++ b/scripts/.scripts/base/api/environment-show.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +. ./.scripts/base/lib/--index.sh + +_base_environment_show "$@" diff --git a/scripts/.scripts/base/lib/--env-vars-reader.sh b/scripts/.scripts/base/lib/--env-vars-reader.sh new file mode 100644 index 0000000..ec67c93 --- /dev/null +++ b/scripts/.scripts/base/lib/--env-vars-reader.sh @@ -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[@]}" diff --git a/scripts/.scripts/base/lib/--env-vars-validator.sh b/scripts/.scripts/base/lib/--env-vars-validator.sh new file mode 100644 index 0000000..d55556d --- /dev/null +++ b/scripts/.scripts/base/lib/--env-vars-validator.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/base/lib/--index-api.sh + +# No required environment variables for skill-manager base diff --git a/scripts/.scripts/base/lib/--index-api.sh b/scripts/.scripts/base/lib/--index-api.sh new file mode 100644 index 0000000..b6e223f --- /dev/null +++ b/scripts/.scripts/base/lib/--index-api.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/base/lib/-ensure-environment-variable.sh + +. ./.scripts/base/lib/-environment-show.sh + +. ./.scripts/base/lib/-mask.sh diff --git a/scripts/.scripts/base/lib/--index.sh b/scripts/.scripts/base/lib/--index.sh new file mode 100644 index 0000000..45fdc99 --- /dev/null +++ b/scripts/.scripts/base/lib/--index.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 +# ------------------------------------------------------------------------------------- diff --git a/scripts/.scripts/base/lib/-ensure-environment-variable.sh b/scripts/.scripts/base/lib/-ensure-environment-variable.sh new file mode 100644 index 0000000..a745748 --- /dev/null +++ b/scripts/.scripts/base/lib/-ensure-environment-variable.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 +} diff --git a/scripts/.scripts/base/lib/-environment-show.sh b/scripts/.scripts/base/lib/-environment-show.sh new file mode 100644 index 0000000..aba067c --- /dev/null +++ b/scripts/.scripts/base/lib/-environment-show.sh @@ -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}" + +} diff --git a/scripts/.scripts/base/lib/-mask.sh b/scripts/.scripts/base/lib/-mask.sh new file mode 100644 index 0000000..11c110d --- /dev/null +++ b/scripts/.scripts/base/lib/-mask.sh @@ -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}" +} diff --git a/scripts/.scripts/cli/Taskfile.yml b/scripts/.scripts/cli/Taskfile.yml new file mode 100644 index 0000000..24bca21 --- /dev/null +++ b/scripts/.scripts/cli/Taskfile.yml @@ -0,0 +1,38 @@ +# https://taskfile.dev + +version: "3" + +tasks: + build: + desc: Compile TypeScript source to dist/ + cmds: + - | + ./.scripts/cli/api/build.sh {{ .CLI_ARGS }} + silent: true + + validate: + desc: | + Validate a skill directory against the agentskills.io spec. + Usage: task cli:validate -- --skill-dir="/absolute/path/to/skill" + cmds: + - | + ./.scripts/cli/api/validate.sh {{ .CLI_ARGS }} + silent: true + + scaffold: + desc: | + Scaffold a new skill directory from the built-in template. + Usage: task cli:scaffold -- --skill-name="my-skill" [--output-dir="/path/to/.agents/skills"] + cmds: + - | + ./.scripts/cli/api/scaffold.sh {{ .CLI_ARGS }} + silent: true + + deploy: + desc: | + Deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills (absolute paths). + Usage: task cli:deploy -- --skill-dir="/absolute/path/to/skill" [--mode=symlink|copy] [--force] + cmds: + - | + ./.scripts/cli/api/deploy.sh {{ .CLI_ARGS }} + silent: true diff --git a/scripts/.scripts/cli/api/build.sh b/scripts/.scripts/cli/api/build.sh new file mode 100644 index 0000000..b897a67 --- /dev/null +++ b/scripts/.scripts/cli/api/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +set -e + +. ./.scripts/cli/lib/--index.sh + +_cli__build "$@" diff --git a/scripts/.scripts/cli/api/deploy.sh b/scripts/.scripts/cli/api/deploy.sh new file mode 100755 index 0000000..4cce36e --- /dev/null +++ b/scripts/.scripts/cli/api/deploy.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +set -e + +. ./.scripts/cli/lib/--index.sh + +_cli__deploy "$@" diff --git a/scripts/.scripts/cli/api/scaffold.sh b/scripts/.scripts/cli/api/scaffold.sh new file mode 100644 index 0000000..90e822f --- /dev/null +++ b/scripts/.scripts/cli/api/scaffold.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +set -e + +. ./.scripts/cli/lib/--index.sh + +_cli__scaffold "$@" diff --git a/scripts/.scripts/cli/api/validate.sh b/scripts/.scripts/cli/api/validate.sh new file mode 100644 index 0000000..5f4e24e --- /dev/null +++ b/scripts/.scripts/cli/api/validate.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- +set -e + +. ./.scripts/cli/lib/--index.sh + +_cli__validate "$@" diff --git a/scripts/.scripts/cli/lib/--env-vars-reader.sh b/scripts/.scripts/cli/lib/--env-vars-reader.sh new file mode 100644 index 0000000..47e7e02 --- /dev/null +++ b/scripts/.scripts/cli/lib/--env-vars-reader.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +ALL_ARGS=("$@") +while [[ "$#" -gt 0 ]]; do + case $1 in + *) ;; + esac + shift +done +set -- "${ALL_ARGS[@]}" diff --git a/scripts/.scripts/cli/lib/--env-vars-validator.sh b/scripts/.scripts/cli/lib/--env-vars-validator.sh new file mode 100644 index 0000000..0c28e45 --- /dev/null +++ b/scripts/.scripts/cli/lib/--env-vars-validator.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/cli/lib/--index-api.sh + +# No required environment variables for cli module diff --git a/scripts/.scripts/cli/lib/--index-api.sh b/scripts/.scripts/cli/lib/--index-api.sh new file mode 100644 index 0000000..e6ebdae --- /dev/null +++ b/scripts/.scripts/cli/lib/--index-api.sh @@ -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 diff --git a/scripts/.scripts/cli/lib/--index.sh b/scripts/.scripts/cli/lib/--index.sh new file mode 100644 index 0000000..31f2275 --- /dev/null +++ b/scripts/.scripts/cli/lib/--index.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 +# ------------------------------------------------------------------------------------- diff --git a/scripts/.scripts/cli/lib/-build.sh b/scripts/.scripts/cli/lib/-build.sh new file mode 100644 index 0000000..00f9a4b --- /dev/null +++ b/scripts/.scripts/cli/lib/-build.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 +} diff --git a/scripts/.scripts/cli/lib/-deploy.sh b/scripts/.scripts/cli/lib/-deploy.sh new file mode 100755 index 0000000..a375c8d --- /dev/null +++ b/scripts/.scripts/cli/lib/-deploy.sh @@ -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 "$@" +} diff --git a/scripts/.scripts/cli/lib/-scaffold.sh b/scripts/.scripts/cli/lib/-scaffold.sh new file mode 100644 index 0000000..0d03c11 --- /dev/null +++ b/scripts/.scripts/cli/lib/-scaffold.sh @@ -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 "$@" +} diff --git a/scripts/.scripts/cli/lib/-validate.sh b/scripts/.scripts/cli/lib/-validate.sh new file mode 100644 index 0000000..6f9c1f8 --- /dev/null +++ b/scripts/.scripts/cli/lib/-validate.sh @@ -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 "$@" +} diff --git a/scripts/.scripts/loggers/lib/--env-vars-reader.sh b/scripts/.scripts/loggers/lib/--env-vars-reader.sh new file mode 100644 index 0000000..aec03dd --- /dev/null +++ b/scripts/.scripts/loggers/lib/--env-vars-reader.sh @@ -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}" diff --git a/scripts/.scripts/loggers/lib/--import.sh b/scripts/.scripts/loggers/lib/--import.sh new file mode 100644 index 0000000..d1e5edc --- /dev/null +++ b/scripts/.scripts/loggers/lib/--import.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ------------------------------------------------------------------------------------- +# +# TOBE ADDED - Licence & Copyright Disclaimer +# +# ------------------------------------------------------------------------------------- + +. ./.scripts/loggers/lib/--env-vars-reader.sh +. ./.scripts/loggers/lib/--index.sh diff --git a/scripts/.scripts/loggers/lib/--index-api.sh b/scripts/.scripts/loggers/lib/--index-api.sh new file mode 100644 index 0000000..66118fe --- /dev/null +++ b/scripts/.scripts/loggers/lib/--index-api.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 diff --git a/scripts/.scripts/loggers/lib/--index.sh b/scripts/.scripts/loggers/lib/--index.sh new file mode 100644 index 0000000..8952d78 --- /dev/null +++ b/scripts/.scripts/loggers/lib/--index.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 +# ------------------------------------------------------------------------------------- diff --git a/scripts/.scripts/loggers/lib/-debug.sh b/scripts/.scripts/loggers/lib/-debug.sh new file mode 100644 index 0000000..a4022c1 --- /dev/null +++ b/scripts/.scripts/loggers/lib/-debug.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 +} diff --git a/scripts/.scripts/loggers/lib/-empty-line.sh b/scripts/.scripts/loggers/lib/-empty-line.sh new file mode 100644 index 0000000..e44b21f --- /dev/null +++ b/scripts/.scripts/loggers/lib/-empty-line.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-enable-trailing-new-line.sh b/scripts/.scripts/loggers/lib/-enable-trailing-new-line.sh new file mode 100644 index 0000000..5a281ac --- /dev/null +++ b/scripts/.scripts/loggers/lib/-enable-trailing-new-line.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-error.sh b/scripts/.scripts/loggers/lib/-error.sh new file mode 100644 index 0000000..6fc040e --- /dev/null +++ b/scripts/.scripts/loggers/lib/-error.sh @@ -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 + +} diff --git a/scripts/.scripts/loggers/lib/-info.sh b/scripts/.scripts/loggers/lib/-info.sh new file mode 100644 index 0000000..f5b13e5 --- /dev/null +++ b/scripts/.scripts/loggers/lib/-info.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--debug.sh b/scripts/.scripts/loggers/lib/-printf--debug.sh new file mode 100644 index 0000000..1a44e33 --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--debug.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--info.sh b/scripts/.scripts/loggers/lib/-printf--info.sh new file mode 100644 index 0000000..24bcd5b --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--info.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--trace.sh b/scripts/.scripts/loggers/lib/-printf--trace.sh new file mode 100644 index 0000000..cdbe66b --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--trace.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-printf--warn.sh b/scripts/.scripts/loggers/lib/-printf--warn.sh new file mode 100644 index 0000000..a19138f --- /dev/null +++ b/scripts/.scripts/loggers/lib/-printf--warn.sh @@ -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 +} diff --git a/scripts/.scripts/loggers/lib/-trace.sh b/scripts/.scripts/loggers/lib/-trace.sh new file mode 100644 index 0000000..7b5916c --- /dev/null +++ b/scripts/.scripts/loggers/lib/-trace.sh @@ -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 + +} diff --git a/scripts/.scripts/loggers/lib/-waiting-dot.sh b/scripts/.scripts/loggers/lib/-waiting-dot.sh new file mode 100644 index 0000000..8f9787d --- /dev/null +++ b/scripts/.scripts/loggers/lib/-waiting-dot.sh @@ -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="" +} diff --git a/scripts/.scripts/loggers/lib/-warn.sh b/scripts/.scripts/loggers/lib/-warn.sh new file mode 100644 index 0000000..95bd71b --- /dev/null +++ b/scripts/.scripts/loggers/lib/-warn.sh @@ -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 +} diff --git a/scripts/.scripts/validator/Taskfile.yml b/scripts/.scripts/validator/Taskfile.yml new file mode 100644 index 0000000..dee0ee9 --- /dev/null +++ b/scripts/.scripts/validator/Taskfile.yml @@ -0,0 +1,11 @@ +# https://taskfile.dev + +version: "3" + +tasks: + execute: + desc: -- --skill-dir="" + cmds: + - | + ./.scripts/validator/api/skill--execute.sh {{ .CLI_ARGS }} + silent: true diff --git a/scripts/.scripts/validator/api/skill--execute.sh b/scripts/.scripts/validator/api/skill--execute.sh new file mode 100755 index 0000000..dc03bda --- /dev/null +++ b/scripts/.scripts/validator/api/skill--execute.sh @@ -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 "$@" diff --git a/scripts/.scripts/validator/lib/--env-vars-reader.sh b/scripts/.scripts/validator/lib/--env-vars-reader.sh new file mode 100755 index 0000000..a448ce6 --- /dev/null +++ b/scripts/.scripts/validator/lib/--env-vars-reader.sh @@ -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. diff --git a/scripts/.scripts/validator/lib/--env-vars-validator.sh b/scripts/.scripts/validator/lib/--env-vars-validator.sh new file mode 100755 index 0000000..d15742e --- /dev/null +++ b/scripts/.scripts/validator/lib/--env-vars-validator.sh @@ -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. diff --git a/scripts/.scripts/validator/lib/--index-api.sh b/scripts/.scripts/validator/lib/--index-api.sh new file mode 100755 index 0000000..962f73b --- /dev/null +++ b/scripts/.scripts/validator/lib/--index-api.sh @@ -0,0 +1,2 @@ +#!/bin/bash +. ./.scripts/validator/lib/-skill--execute.sh diff --git a/scripts/.scripts/validator/lib/--index.sh b/scripts/.scripts/validator/lib/--index.sh new file mode 100755 index 0000000..ac31265 --- /dev/null +++ b/scripts/.scripts/validator/lib/--index.sh @@ -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 +# ------------------------------------------------------------------------------------- diff --git a/scripts/.scripts/validator/lib/-skill--execute.sh b/scripts/.scripts/validator/lib/-skill--execute.sh new file mode 100755 index 0000000..cadad18 --- /dev/null +++ b/scripts/.scripts/validator/lib/-skill--execute.sh @@ -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 <&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=\"\"" >&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 +} diff --git a/scripts/Taskfile.yml b/scripts/Taskfile.yml new file mode 100644 index 0000000..f9f6e12 --- /dev/null +++ b/scripts/Taskfile.yml @@ -0,0 +1,50 @@ +# https://taskfile.dev + +version: "3" + +includes: + base: ./.scripts/base/Taskfile.yml + cli: ./.scripts/cli/Taskfile.yml + validator: ./.scripts/validator/Taskfile.yml + +tasks: + default: + cmds: + - task --list-all + silent: true + + build: + desc: Compile TypeScript source to dist/ + cmds: + - task: cli:build + silent: true + + validate: + desc: | + Validate a skill directory against the agentskills.io spec. + Usage: task validate -- --skill-dir="/absolute/path/to/skill" + cmds: + - task: cli:validate + vars: + CLI_ARGS: "{{ .CLI_ARGS }}" + silent: true + + scaffold: + desc: | + Scaffold a new skill directory from the built-in template. + Usage: task scaffold -- --skill-name="my-skill" [--output-dir="/path/to/.agents/skills"] + cmds: + - task: cli:scaffold + vars: + CLI_ARGS: "{{ .CLI_ARGS }}" + silent: true + + deploy: + desc: | + Deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills (absolute paths). + Usage: task deploy -- --skill-dir="/absolute/path/to/skill" [--mode=symlink|copy] [--force] + cmds: + - task: cli:deploy + vars: + CLI_ARGS: "{{ .CLI_ARGS }}" + silent: true diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..f741e16 --- /dev/null +++ b/scripts/package.json @@ -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" + } +} diff --git a/scripts/pnpm-lock.yaml b/scripts/pnpm-lock.yaml new file mode 100644 index 0000000..5079820 --- /dev/null +++ b/scripts/pnpm-lock.yaml @@ -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: {} diff --git a/scripts/src/actions/deploy/action.ts b/scripts/src/actions/deploy/action.ts new file mode 100644 index 0000000..a68e836 --- /dev/null +++ b/scripts/src/actions/deploy/action.ts @@ -0,0 +1,191 @@ +/** + * Action: deploy + * + * Deploys a skill directory into the agent skill directories so compatible + * agents (Claude Code, Cline, Copilot, Codex, …) can discover it. + * + * Default targets (always resolved to absolute paths): + * $HOME/.agents/skills/ + * $HOME/.claude/skills/ + * $HOME/.cline/skills/ + * + * Modes: + * symlink (default) — creates an absolute-path symlink to the skill source. + * The source stays the single source of truth (e.g. a git repo). + * copy — copies the skill directory (excluding node_modules/, dist/, .git/). + * + * Existing symlinks at the destination are replaced. Existing real directories + * are only replaced when --force is given. + * + * Usage: + * skill-tools --action deploy --skill-dir /abs/path/to/skill [--mode symlink|copy] [--targets dir1,dir2] [--force] + */ + +import { + cpSync, + existsSync, + lstatSync, + mkdirSync, + readFileSync, + realpathSync, + rmSync, + symlinkSync, + unlinkSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { basename, join, resolve } from "node:path"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const DEFAULT_TARGETS = [".agents/skills", ".claude/skills", ".cline/skills"]; + +const COPY_EXCLUDES = new Set(["node_modules", "dist", ".git"]); + +function readSkillName(skillDir: string): string | undefined { + const skillMdPath = join(skillDir, "SKILL.md"); + const content = readFileSync(skillMdPath, "utf-8"); + const match = content.match(/^---\n[\s\S]*?^name:\s*(\S+)\s*$/m); + return match?.[1]; +} + +function safeRealpath(path: string): string { + try { + return realpathSync(path); + } catch { + return resolve(path); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export function run(options: Record): Record { + 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[] = []; + let success = true; + + for (const targetDir of targets) { + const destination = join(targetDir, skillName); + + try { + mkdirSync(targetDir, { recursive: true }); + + let existing: ReturnType | 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 = { + action: "deploy", + success, + "skill-name": skillName, + "skill-dir": skillDir, + mode, + deployments, + }; + if (warnings.length > 0) { + result["warnings"] = warnings; + } + return result; +} diff --git a/scripts/src/actions/scaffold/action.ts b/scripts/src/actions/scaffold/action.ts new file mode 100644 index 0000000..3e36797 --- /dev/null +++ b/scripts/src/actions/scaffold/action.ts @@ -0,0 +1,171 @@ +/** + * Action: scaffold + * + * Scaffolds a new skill directory following the agentskills.io structure. + * + * Creates: + * // + * ├── 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 + * - 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): Record { + 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", + ], + }; +} diff --git a/scripts/src/actions/validate/action.ts b/scripts/src/actions/validate/action.ts new file mode 100644 index 0000000..8f8ee25 --- /dev/null +++ b/scripts/src/actions/validate/action.ts @@ -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 { + const result: Record = {}; + 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 { + 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): Record { + 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)); +} diff --git a/scripts/src/cli/commands.ts b/scripts/src/cli/commands.ts new file mode 100644 index 0000000..07ce8b7 --- /dev/null +++ b/scripts/src/cli/commands.ts @@ -0,0 +1,138 @@ +#!/usr/bin/env node +/** + * skill-tools CLI dispatcher + * + * Usage: + * skill-tools --action [options] + * + * Actions: + * validate — validate a skill directory against the agentskills.io spec + * scaffold — scaffold a new skill directory from the built-in template + * deploy — deploy a skill into $HOME/.agents/skills, $HOME/.claude/skills, $HOME/.cline/skills + * + * Output is always YAML to stdout. Exit 0 on success, 1 on error. + */ + +import { parseArgs } from "node:util"; +import * as yaml from "js-yaml"; + +// --------------------------------------------------------------------------- +// Action registry +// --------------------------------------------------------------------------- + +type ActionModule = { + run: (options: Record) => Record; +}; + +const ACTIONS: Record Promise> = { + validate: () => import("../actions/validate/action.js"), + scaffold: () => import("../actions/scaffold/action.js"), + deploy: () => import("../actions/deploy/action.js"), +}; + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +async function main(): Promise { + 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 [options] + +Actions: + validate Validate a skill directory against the agentskills.io spec + --skill-dir Path to the skill root directory (required) + + scaffold Scaffold a new skill directory from the built-in template + --skill-name Skill name (lowercase, hyphens only, required) + --output-dir Parent directory to create the skill in (default: .agents/skills) + + deploy Deploy a skill into the agent skill directories (absolute paths) + --skill-dir Path to the skill root directory (required) + --mode symlink (default) or copy + --targets Comma-separated target directories + (default: $HOME/.agents/skills,$HOME/.claude/skills,$HOME/.cline/skills) + --force Replace an existing real directory at the destination + +Examples: + skill-tools --action validate --skill-dir /path/to/my-skill + skill-tools --action scaffold --skill-name my-skill --output-dir /path/to/.agents/skills + skill-tools --action deploy --skill-dir /path/to/my-skill + skill-tools --action deploy --skill-dir /path/to/my-skill --mode copy --force +`); + process.exit(values.help ? 0 : 1); + } + + const actionName = values.action; + + if (!ACTIONS[actionName]) { + printError( + `Unknown action '${actionName}'. Available: ${Object.keys(ACTIONS).join(", ")}` + ); + process.exit(1); + } + + try { + const mod = await ACTIONS[actionName](); + const result = mod.run({ + "skill-dir": values["skill-dir"], + "skill-name": values["skill-name"], + "output-dir": values["output-dir"], + mode: values.mode, + targets: values.targets, + force: values.force ? "true" : undefined, + }); + process.stdout.write( + yaml.dump(result, { noRefs: true, sortKeys: false, lineWidth: 120 }) + ); + const success = (result["success"] as boolean | undefined) ?? true; + process.exit(success ? 0 : 1); + } catch (err) { + const errorOutput = { + success: false, + action: actionName, + error: err instanceof Error ? err.message : String(err), + }; + process.stdout.write(yaml.dump(errorOutput, { noRefs: true, sortKeys: false })); + process.exit(1); + } +} + +function printError(msg: string): void { + process.stderr.write(`Error: ${msg}\n`); + process.stderr.write( + `Usage: skill-tools --action <${Object.keys(ACTIONS).join("|")}> [options]\n` + ); +} + +main(); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..7bad8a4 --- /dev/null +++ b/scripts/tsconfig.json @@ -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"] +}