23
.cursor-plugin/plugin.json
Normal file
23
.cursor-plugin/plugin.json
Normal 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"
|
||||
]
|
||||
}
|
||||
20
.github/workflows/generate-agents.yml
vendored
20
.github/workflows/generate-agents.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -205,4 +205,5 @@ cython_debug/
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
.claude
|
||||
.claude
|
||||
.fast-agent/
|
||||
|
||||
7
.mcp.json
Normal file
7
.mcp.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"huggingface-skills": {
|
||||
"url": "https://huggingface.co/mcp?login"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
README.md
23
README.md
@@ -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
|
||||
|
||||
199
scripts/generate_cursor_plugin.py
Normal file
199
scripts/generate_cursor_plugin.py
Normal 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
87
scripts/publish.sh
Executable 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
|
||||
Reference in New Issue
Block a user