Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/skills-discovery.yml
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

Copy link
Copy Markdown
Collaborator

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.


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 }}"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
specs/
112 changes: 112 additions & 0 deletions CONTRIBUTING.md
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.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@ The agent skills in this package help your AI coding assistant write correct Clo

## Available Skills

Skills are grouped by category below. The category is a folder in this repo only: you install a skill by its name, and the folder layout has no effect on how it is installed or used.

### Platform

| Skill | Description |
|---|---|
| `cloudinary-docs` | Selects the most relevant markdown pages from the current documentation using the latest llms.txt. Use when answering Cloudinary questions or integrating Cloudinary into code. |
| `cloudinary-transformations` | Turns natural language image and video transformation requirements into valid URL transformation strings that follow Cloudinary best practices. Use when building delivery URLs, applying transformations, optimizing media, or debugging transformation syntax errors. |

### Frameworks

| Skill | Description |
|---|---|
| `cloudinary-react` | Provides opinionated React SDK patterns for configuration, common integration scenarios, and troubleshooting for frequent errors and TypeScript pitfalls. Use when developing React apps with Cloudinary. |
| `cloudinary-next` | Provides opinionated Next.js SDK patterns for Server and Client Component boundaries, server-side uploads and deletes, and troubleshooting for frequent errors and TypeScript pitfalls. Use when developing Next.js apps with Cloudinary. |

## Install

Install the skills you need by name:

```bash
npx skills add cloudinary-devs/skills --skill cloudinary-docs,cloudinary-transformations
npx skills add cloudinary-devs/skills --skill cloudinary-next
```

Or install everything:

```bash
npx skills add cloudinary-devs/skills
```

Install all skills or select the ones you need. You can install globally or per project.
You can install globally or per project.

> **💡 Tip:** [Cloudinary MCP servers](https://cloudinary.com/documentation/cloudinary_llm_mcp#mcp_servers) complement the Cloudinary Skills. Used together, skills and MCP servers improve the end-to-end workflow. For example, our skills spell out best practices such as using named transformations to apply the same transformation across many assets. Based on that, the model can decide which MCP tools to run to create named transformations that match your needs.

Expand Down
50 changes: 50 additions & 0 deletions scripts/check-discovery.sh
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
31 changes: 31 additions & 0 deletions scripts/check-links.sh
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."
58 changes: 58 additions & 0 deletions scripts/check-versions.sh
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
Loading