Feat/cursor plugin (#47)

* cursor plugin generation

* gitignore
This commit is contained in:
shaun smith
2026-02-20 10:24:20 +00:00
committed by GitHub
parent 3f4f55d626
commit c1cd47139f
7 changed files with 349 additions and 13 deletions

View File

@@ -0,0 +1,23 @@
{
"name": "huggingface-skills",
"skills": "skills",
"mcpServers": ".mcp.json",
"description": "Agent Skills for AI/ML tasks including dataset creation, model training, evaluation, and research paper publishing on Hugging Face Hub",
"version": "1.0.0",
"author": {
"name": "Hugging Face"
},
"homepage": "https://github.com/huggingface/skills",
"repository": "https://github.com/huggingface/skills",
"license": "Apache-2.0",
"keywords": [
"huggingface",
"machine-learning",
"datasets",
"training",
"evaluation",
"papers",
"fine-tuning",
"llm"
]
}

View File

@@ -1,13 +1,20 @@
name: Check AGENTS.md and marketplace.json
name: Validate generated agent/plugin artifacts
on:
pull_request:
paths:
- "scripts/AGENTS_TEMPLATE.md"
- "scripts/generate_agents.py"
- "scripts/generate_cursor_plugin.py"
- "scripts/publish.sh"
- "**/SKILL.md"
- "agents/AGENTS.md"
- "README.md"
- ".claude-plugin/marketplace.json"
- ".claude-plugin/plugin.json"
- "gemini-extension.json"
- ".cursor-plugin/plugin.json"
- ".mcp.json"
jobs:
validate:
@@ -19,12 +26,5 @@ jobs:
- name: Set up uv
uses: astral-sh/setup-uv@v7
- name: Generate AGENTS.md and validate marketplace.json
run: uv run scripts/generate_agents.py
- name: Ensure AGENTS.md is up to date
run: |
git diff --quiet -- agents/AGENTS.md || {
echo "::error::agents/AGENTS.md is outdated. Run 'python scripts/generate_agents.py' and commit the changes."
exit 1
}
- name: Ensure generated files are up to date
run: ./scripts/publish.sh --check

3
.gitignore vendored
View File

@@ -205,4 +205,5 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/
.claude
.claude
.fast-agent/

7
.mcp.json Normal file
View File

@@ -0,0 +1,7 @@
{
"mcpServers": {
"huggingface-skills": {
"url": "https://huggingface.co/mcp?login"
}
}
}

View File

@@ -16,7 +16,7 @@ In practice, skills are self-contained folders that package instructions, script
## Installation
Hugging Face skills are compatible with Claude Code, Codex, and Gemini CLI. With integrations Cursor, Windsurf, and Continue, on the way.
Hugging Face skills are compatible with Claude Code, Codex, Gemini CLI, and Cursor.
### Claude Code
@@ -66,6 +66,21 @@ gemini extensions install https://github.com/huggingface/skills.git --consent
4. See [Gemini CLI extensions docs](https://geminicli.com/docs/extensions/#installing-an-extension) for more help.
### Cursor
This repository includes Cursor plugin manifests:
- `.cursor-plugin/plugin.json`
- `.mcp.json` (configured with the Hugging Face MCP server URL)
Install from repository URL (or local checkout) via the Cursor plugin flow.
For contributors, regenerate manifests with:
```bash
./scripts/publish.sh
```
## Skills
This repository contains a few skills to get you started. You can also contribute your own skills to the repository.
@@ -112,7 +127,11 @@ Your coding agent automatically loads the corresponding `SKILL.md` instructions
```
3. Add or edit supporting scripts, templates, and documents referenced by your instructions.
4. Add an entry to `.claude-plugin/marketplace.json` with a concise, human-readable description.
5. Run `python scripts/generate_agents.py` to validate the structure.
5. Run:
```bash
./scripts/publish.sh
```
to regenerate and validate all generated metadata.
6. Reinstall or reload the skill bundle in your coding agent so the updated folder is available.
### Marketplace

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""Generate Cursor plugin artifacts from existing repo metadata.
Outputs:
- .cursor-plugin/plugin.json
- .mcp.json
Design goals:
- Keep Claude + Cursor metadata in sync.
- Reuse .claude-plugin/plugin.json as primary metadata source.
- Discover skills from skills/*/SKILL.md.
- Reuse MCP URL from gemini-extension.json when available.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CLAUDE_PLUGIN_MANIFEST = ROOT / ".claude-plugin" / "plugin.json"
GEMINI_EXTENSION = ROOT / "gemini-extension.json"
CURSOR_PLUGIN_DIR = ROOT / ".cursor-plugin"
CURSOR_PLUGIN_MANIFEST = CURSOR_PLUGIN_DIR / "plugin.json"
CURSOR_MCP_CONFIG = ROOT / ".mcp.json"
DEFAULT_MCP_SERVER_NAME = "huggingface-skills"
DEFAULT_MCP_URL = "https://huggingface.co/mcp?login"
PLUGIN_NAME_RE = re.compile(r"^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$")
def load_json(path: Path) -> dict:
if not path.exists():
raise FileNotFoundError(f"Missing required file: {path}")
return json.loads(path.read_text(encoding="utf-8"))
def parse_frontmatter(text: str) -> dict[str, str]:
match = re.search(r"^---\s*\n(.*?)\n---\s*", text, re.DOTALL)
if not match:
return {}
data: dict[str, str] = {}
for line in match.group(1).splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data
def collect_skills() -> list[str]:
skills: list[str] = []
for skill_md in sorted(ROOT.glob("skills/*/SKILL.md")):
meta = parse_frontmatter(skill_md.read_text(encoding="utf-8"))
name = meta.get("name", "").strip()
if not name:
continue
skills.append(name)
return skills
def validate_plugin_name(name: str) -> None:
if not PLUGIN_NAME_RE.match(name):
raise ValueError(
"Invalid plugin name in .claude-plugin/plugin.json: "
f"'{name}'. Must be lowercase and match {PLUGIN_NAME_RE.pattern}"
)
def build_cursor_plugin_manifest() -> dict:
src = load_json(CLAUDE_PLUGIN_MANIFEST)
name = src.get("name")
if not isinstance(name, str) or not name:
raise ValueError(".claude-plugin/plugin.json must define a non-empty 'name'")
validate_plugin_name(name)
skills = collect_skills()
if not skills:
raise ValueError("No skills discovered under skills/*/SKILL.md")
manifest: dict = {"name": name, "skills": "skills", "mcpServers": ".mcp.json"}
# Copy optional metadata fields when present.
for key in [
"description",
"version",
"author",
"homepage",
"repository",
"license",
"keywords",
"logo",
]:
if key in src:
manifest[key] = src[key]
return manifest
def extract_mcp_from_gemini() -> tuple[str, str]:
"""Return (server_name, url) from gemini-extension when available."""
if not GEMINI_EXTENSION.exists():
return DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_URL
data = load_json(GEMINI_EXTENSION)
servers = data.get("mcpServers")
if not isinstance(servers, dict) or not servers:
return DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_URL
# Use first configured server as source of truth.
server_name = next(iter(servers.keys()))
server_cfg = servers[server_name]
if not isinstance(server_cfg, dict):
return DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_URL
url = server_cfg.get("url") or server_cfg.get("httpUrl") or DEFAULT_MCP_URL
if not isinstance(url, str) or not url.strip():
url = DEFAULT_MCP_URL
return server_name, url
def build_mcp_config() -> dict:
server_name, url = extract_mcp_from_gemini()
return {
"mcpServers": {
server_name: {
"url": url,
}
}
}
def render_json(data: dict) -> str:
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
def write_or_check(path: Path, content: str, check: bool) -> bool:
"""Return True when file is already up-to-date (or after writing in non-check mode)."""
current = path.read_text(encoding="utf-8") if path.exists() else None
if current == content:
return True
if check:
return False
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
return True
def main() -> None:
parser = argparse.ArgumentParser(description="Generate Cursor plugin manifest + MCP config")
parser.add_argument(
"--check",
action="store_true",
help="Validate generated files are up-to-date without writing changes.",
)
args = parser.parse_args()
plugin_manifest = render_json(build_cursor_plugin_manifest())
mcp_config = render_json(build_mcp_config())
ok_plugin = write_or_check(CURSOR_PLUGIN_MANIFEST, plugin_manifest, check=args.check)
ok_mcp = write_or_check(CURSOR_MCP_CONFIG, mcp_config, check=args.check)
if args.check:
outdated = []
if not ok_plugin:
outdated.append(str(CURSOR_PLUGIN_MANIFEST.relative_to(ROOT)))
if not ok_mcp:
outdated.append(str(CURSOR_MCP_CONFIG.relative_to(ROOT)))
if outdated:
print("Generated Cursor artifacts are out of date:", file=sys.stderr)
for item in outdated:
print(f" - {item}", file=sys.stderr)
print("Run: uv run scripts/generate_cursor_plugin.py", file=sys.stderr)
sys.exit(1)
print("Cursor plugin artifacts are up to date.")
return
print(f"Wrote {CURSOR_PLUGIN_MANIFEST.relative_to(ROOT)}")
print(f"Wrote {CURSOR_MCP_CONFIG.relative_to(ROOT)}")
if __name__ == "__main__":
main()

87
scripts/publish.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
GENERATED_FILES=(
"agents/AGENTS.md"
"README.md"
".cursor-plugin/plugin.json"
".mcp.json"
)
file_sig() {
local path="$1"
if [[ -f "$path" ]]; then
sha256sum "$path" | awk '{print $1}'
else
echo "__MISSING__"
fi
}
run_generate() {
uv run scripts/generate_agents.py
uv run scripts/generate_cursor_plugin.py
}
run_check() {
declare -A before
local changed=()
for path in "${GENERATED_FILES[@]}"; do
before["$path"]="$(file_sig "$path")"
done
run_generate
for path in "${GENERATED_FILES[@]}"; do
if [[ "${before[$path]}" != "$(file_sig "$path")" ]]; then
changed+=("$path")
fi
done
if [[ ${#changed[@]} -gt 0 ]]; then
echo "Generated artifacts are outdated."
echo "Run: ./scripts/publish.sh"
echo
echo "Changed files:"
for path in "${changed[@]}"; do
echo "$path"
done
exit 1
fi
# Extra explicit check for cursor-only artifacts
uv run scripts/generate_cursor_plugin.py --check
echo "All generated artifacts are up to date."
}
case "${1:-}" in
"")
run_generate
echo "Publish artifacts generated successfully."
;;
"--check")
run_check
;;
"-h"|"--help")
cat <<'EOF'
Usage:
./scripts/publish.sh Generate all publish artifacts
./scripts/publish.sh --check Verify generated artifacts are up to date
This script regenerates:
- agents/AGENTS.md
- README.md (skills table section)
- .cursor-plugin/plugin.json
- .mcp.json
EOF
;;
*)
echo "Unknown option: $1" >&2
echo "Use --help for usage." >&2
exit 2
;;
esac