Repository navigation
Restructure skills catalog into categories #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8eab812
Move skills into category directories
njb90 6249033
Add skills discovery check
njb90 bbb8972
Document catalog layout and authoring conventions
njb90 4ba491f
Disable colour in discovery check output
njb90 90cb20e
Add link-tracking and version-bump checks
njb90 341c5c8
Assert discovered skill names and fail link check on zero matches
njb90 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: Skills checks | ||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [main] | ||
| jobs: | ||
| discovery: | ||
| name: Every SKILL.md is discovered | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| - run: scripts/check-discovery.sh | ||
|
|
||
| links: | ||
| name: Doc links carry tracking params | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - run: scripts/check-links.sh | ||
|
|
||
| versions: | ||
| name: Changed skills bump their version | ||
| if: github.event_name == 'pull_request' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - run: scripts/check-versions.sh "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| specs/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # Contributing | ||
|
|
||
| Rules for adding or changing skills in this repo. The `skills` CLI is unforgiving | ||
| about layout and frontmatter: a skill that breaks these rules is dropped from | ||
| discovery with no warning, so read this before opening a PR. | ||
|
|
||
| ## Layout | ||
|
|
||
| Every skill lives at `skills/<category>/<name>/SKILL.md`, exactly two directories | ||
| deep. The CLI does not look any deeper, so a skill nested at depth three is | ||
| invisible to users. | ||
|
|
||
| Categories: | ||
|
|
||
| - `platform` — reference skills for a Cloudinary capability (uploads, | ||
| transformations, docs lookup) | ||
| - `frameworks` — patterns for using Cloudinary in a specific framework or SDK | ||
| - `use-cases` — end-to-end recipes that combine several capabilities | ||
| - `utilities` — tools that do a job using Cloudinary | ||
|
|
||
| Category directories are created with their first skill. Do not add a README or | ||
| placeholder to an empty category. | ||
|
|
||
| Users install skills by name, not by path, so the category is invisible to them. | ||
| Moving a skill between categories does not affect existing installs. | ||
|
|
||
| ## Frontmatter | ||
|
|
||
| ```yaml | ||
| --- | ||
| name: cloudinary-example | ||
| description: "Reference for <capability>. Use when <trigger phrases>." | ||
| license: MIT | ||
| metadata: | ||
| author: cloudinary | ||
| version: '1.0.0' | ||
| --- | ||
| ``` | ||
|
|
||
| - `name` must equal the directory name. | ||
| - `name` and `description` must both be strings. If either is missing, or the | ||
| frontmatter fails to parse, the skill silently vanishes from discovery. | ||
| - A description containing a colon must be quoted. Unquoted, YAML reads the colon | ||
| as a nested key, the description is no longer a string, and the skill vanishes. | ||
| - Include `license`, `metadata.author`, and `metadata.version` (semver). | ||
| - Set `metadata.internal: true` on a work-in-progress skill that should not appear | ||
| in the default install. Users can reveal it with `INSTALL_INTERNAL_SKILLS=1`. | ||
|
|
||
| ## Description shapes | ||
|
|
||
| Agents see the catalog as a flat list of names and descriptions, so every | ||
| description does two jobs. It opens with a clause that self-locates the skill in | ||
| that list, then follows with a "Use when..." sentence that carries the trigger | ||
| phrases. | ||
|
|
||
| | Category | Opening shape | | ||
| |---|---| | ||
| | Platform | `Reference for <capability>.` | | ||
| | Frameworks | `Patterns for using Cloudinary in <framework>.` | | ||
| | Use-case | `End-to-end recipe for <outcome>.` | | ||
| | Utility | `Tool that <does X> using Cloudinary.` | | ||
|
|
||
| Rules: | ||
|
|
||
| - The opening clause never contains a colon before its first period unless the | ||
| whole description is quoted. | ||
| - Trigger phrases live in the "Use when" sentence, not in the opening clause. | ||
|
|
||
| Existing skills predate these shapes and are not yet retrofitted. New skills must | ||
| follow them. | ||
|
|
||
| ## Referencing other skills | ||
|
|
||
| Refer to another skill by its name only, for example "use cloudinary-docs for | ||
| anything outside this skill's scope". Never reference another skill by file path. | ||
| Installed paths differ per agent and per install scope, so a path that works on | ||
| your machine will be wrong on someone else's. | ||
|
|
||
| ## Versioning | ||
|
|
||
| Bump `metadata.version` on any content change to a skill, including its | ||
| references and assets. Moving or renaming files with identical content does not | ||
| bump the version; the lock-file hash is unchanged and `skills update` correctly | ||
| reports nothing to do. CI enforces this on every PR: a skill with changed | ||
| content and an unchanged version fails the `versions` job. | ||
|
|
||
| ## Documentation links | ||
|
|
||
| Every link to a Cloudinary docs `.md` page or `llms.txt` must carry | ||
| `?install_source=skillspack&referrer=<skill>-skill` so traffic from skills is | ||
| attributable. Use the same `referrer` value throughout a skill, for example | ||
| `trans-skill` or `react-skill`. CI fails the `links` job on any link missing | ||
| either param. | ||
|
|
||
| ## Local check | ||
|
|
||
| Before opening a PR, run the same checks CI runs: | ||
|
|
||
| ```bash | ||
| scripts/check-discovery.sh | ||
| scripts/check-links.sh | ||
| scripts/check-versions.sh origin/main HEAD | ||
| ``` | ||
|
|
||
| The discovery script compares the `name` in every `SKILL.md` under `skills/` | ||
| against the names the skills CLI actually lists, and names any skill that is | ||
| missing. A missing skill means it is being dropped: check for bad frontmatter, | ||
| an unquoted colon in the description, a missing `name` or `description`, or a | ||
| directory nested at the wrong depth. The other two scripts report any untracked | ||
| doc link (and fail if they find no links at all) and any changed skill whose | ||
| version was not bumped. CI (`.github/workflows/skills-discovery.yml`) runs all | ||
| three and fails the PR on the same conditions. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| #!/usr/bin/env bash | ||
| # Every SKILL.md under skills/ must be discovered by the skills CLI, by name. | ||
| # The CLI drops a skill silently (bad frontmatter, wrong depth, missing | ||
| # name/description) whenever at least one other skill is found, so a count | ||
| # alone is not enough. Usage: scripts/check-discovery.sh [dir] (default: .) | ||
| set -u | ||
| dir="${1:-.}" | ||
|
|
||
| # Expected: the frontmatter name of every SKILL.md. | ||
| expected=$(find "$dir/skills" -name SKILL.md | while IFS= read -r f; do | ||
| # First name: key inside the leading frontmatter block only. | ||
| sed -nE '1,/^---$/{s/^name:[[:space:]]*['"'"'"]?([^'"'"'"]+)['"'"'"]?[[:space:]]*$/\1/p;}' "$f" | head -1 | ||
| done | LC_ALL=C sort -u) | ||
|
|
||
| if [ -z "$expected" ]; then | ||
| echo "::error::No SKILL.md with a name found under $dir/skills." | ||
| exit 1 | ||
| fi | ||
|
|
||
| output=$(INSTALL_INTERNAL_SKILLS=1 NO_COLOR=1 npx -y skills@latest add "$dir" --list 2>&1 || true) | ||
| # Strip ANSI escapes (the CLI colours output whenever CI is set), then keep | ||
| # the lines between "Available Skills" and the trailing hint. Skill names are | ||
| # printed alone on a line; descriptions are multi-word, so a line that is a | ||
| # single slug token is a skill name. | ||
| found=$(printf '%s\n' "$output" \ | ||
| | sed -E 's/\x1b\[[0-9;?]*[a-zA-Z]//g' \ | ||
| | sed -n '/Available Skills/,/Use --skill/p' \ | ||
| | sed -E 's/^[^[:alnum:]]*//; s/[[:space:]]+$//' \ | ||
| | grep -xE '[a-z0-9][a-z0-9._-]*' \ | ||
| | LC_ALL=C sort -u) | ||
|
|
||
| missing=$(comm -23 <(printf '%s\n' "$expected") <(printf '%s\n' "$found")) | ||
| extra=$(comm -13 <(printf '%s\n' "$expected") <(printf '%s\n' "$found")) | ||
|
|
||
| if [ -n "$missing" ] || [ -n "$extra" ]; then | ||
| for n in $missing; do | ||
| echo "::error::Skill '$n' has a SKILL.md but is not discovered by the skills CLI (bad frontmatter, wrong depth, or missing name/description)." | ||
| done | ||
| for n in $extra; do | ||
| echo "::error::Skills CLI reports '$n' but no SKILL.md declares that name." | ||
| done | ||
| echo "--- expected ---"; printf '%s\n' "$expected" | ||
| echo "--- found ---"; printf '%s\n' "$found" | ||
| echo "--- cli output ---"; printf '%s\n' "$output" | ||
| exit 1 | ||
| fi | ||
|
|
||
| count=$(printf '%s\n' "$expected" | wc -l | tr -d ' ') | ||
| echo "All $count skills discovered by name:" | ||
| printf ' %s\n' $expected |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| #!/usr/bin/env bash | ||
| # Every cloudinary.com link to a .md page or llms.txt must carry the | ||
| # install_source and referrer query params so docs traffic from skills is | ||
| # attributable. Usage: scripts/check-links.sh [dir] (default: skills) | ||
| set -u | ||
| dir="${1:-skills}" | ||
| url_re='https?://[A-Za-z0-9.-]*cloudinary\.com/[^[:space:])>"'"'"'`]*(\.md|/llms\.txt)([?#][^[:space:])>"'"'"'`]*)?' | ||
|
|
||
| fail=0 | ||
| checked=0 | ||
| while IFS= read -r hit; do | ||
| checked=$((checked + 1)) | ||
| file="${hit%%:*}"; rest="${hit#*:}"; line="${rest%%:*}"; url="${rest#*:}" | ||
| missing="" | ||
| case "$url" in (*install_source=*) ;; (*) missing="install_source";; esac | ||
| case "$url" in (*referrer=*) ;; (*) missing="${missing:+$missing, }referrer";; esac | ||
| if [ -n "$missing" ]; then | ||
| echo "::error file=$file,line=$line::Missing $missing on $url" | ||
| fail=1 | ||
| fi | ||
| done < <(grep -rnoE "$url_re" "$dir" --include='*.md' --include='*.txt' --include='*.ts' --include='*.tsx' --include='*.js' | LC_ALL=C sort -u) | ||
|
|
||
| if [ "$checked" -eq 0 ]; then | ||
| echo "::error::No cloudinary.com .md or llms.txt links found under '$dir'. The check matched nothing, so it cannot pass." | ||
| exit 1 | ||
| fi | ||
| if [ "$fail" -ne 0 ]; then | ||
| echo "Add '?install_source=skillspack&referrer=<skill>-skill' to each link above (see CONTRIBUTING.md)." | ||
| exit 1 | ||
| fi | ||
| echo "All $checked .md and llms.txt links carry install_source and referrer." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #!/usr/bin/env bash | ||
| # Any skill whose content changed between BASE and HEAD must bump | ||
| # metadata.version in its SKILL.md. Pure moves/renames (identical content) do | ||
| # not count as changes. New skills pass. Usage: scripts/check-versions.sh BASE HEAD | ||
| set -u | ||
| base="${1:?base ref}"; head="${2:?head ref}" | ||
|
|
||
| version_at() { # <ref> <path> -> version string or empty | ||
| git show "$1:$2" 2>/dev/null | sed -nE "s/^ version: *['\"]?([^'\"]+)['\"]?[[:space:]]*$/\1/p" | head -1 | ||
| } | ||
| skill_name_of() { # <ref> <path> -> name of the skill (dir holding nearest SKILL.md), or empty | ||
| local d; d=$(dirname "$2") | ||
| while [ "$d" != "." ] && [ "$d" != "skills" ]; do | ||
| if git cat-file -e "$1:$d/SKILL.md" 2>/dev/null; then basename "$d"; return; fi | ||
| d=$(dirname "$d") | ||
| done | ||
| } | ||
| skill_md_in() { # <ref> <name> -> path of that skill's SKILL.md in ref, or empty | ||
| git ls-tree -r --name-only "$1" -- skills | grep -E "(^|/)$2/SKILL\.md$" | head -1 | ||
| } | ||
|
|
||
| # Collect the names of skills touched by non-identical changes. | ||
| changed_names() { | ||
| git diff --name-status -M100% "$base" "$head" -- skills | while read -r status p1 p2; do | ||
| case "$status" in | ||
| (R100) continue ;; # pure move, identical content | ||
| (D) skill_name_of "$base" "$p1" ;; # deletion: file lived in base | ||
| (R*) skill_name_of "$head" "$p2" ;; | ||
| (*) skill_name_of "$head" "$p1" ;; | ||
| esac | ||
| done | sort -u | ||
| } | ||
| names=$(changed_names) | ||
|
|
||
| if [ -z "$names" ]; then | ||
| echo "No skill content changed." | ||
| exit 0 | ||
| fi | ||
|
|
||
| fail=0 | ||
| for name in $names; do | ||
| head_md=$(skill_md_in "$head" "$name") | ||
| base_md=$(skill_md_in "$base" "$name") | ||
| if [ -z "$head_md" ]; then echo "skip $name: removed"; continue; fi | ||
| hv=$(version_at "$head" "$head_md") | ||
| if [ -z "$base_md" ]; then echo "ok $name: new skill ($hv)"; continue; fi | ||
| bv=$(version_at "$base" "$base_md") | ||
| if [ -z "$hv" ]; then | ||
| echo "::error file=$head_md::$name has no metadata.version"; fail=1; continue | ||
| fi | ||
| if [ "$bv" = "$hv" ]; then | ||
| echo "::error file=$head_md,line=7::$name changed but metadata.version is still $hv. Bump it (see CONTRIBUTING.md, Versioning)." | ||
| fail=1 | ||
| else | ||
| echo "ok $name: $bv -> $hv" | ||
| fi | ||
| done | ||
| exit $fail |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth fixing: right now the script can pass even if it doesn’t find/check any links. We should fail when zero URLs are matched so we don’t get a false success.