This commit is contained in:
jack
2026-02-05 10:18:53 +08:00
parent 717f8c1747
commit e8a4b2d318
364 changed files with 453102 additions and 0 deletions
@@ -0,0 +1,51 @@
---
name: pull_brainskill
description: Pulls valid Claude Skills from a ZIP URL (preferred), Git repository, or local directory. Only folders containing a strictly named SKILL.md file are imported.
allowed-tools: Bash
---
# Pull BRAIN Skill
This skill imports skill folders from a remote source or local directory.
**Strict Validation**: A folder is considered a valid skill ONLY if it contains a `SKILL.md` file (strictly case-sensitive). Folders with `skill.md` or `Skill.md` will be skipped.
## How to use
1. **Locate the script**:
- Project path: `.claude/skills/pull_BRAINSkill/scripts/pull_skills.py`
- Global path (Windows): `%USERPROFILE%\.claude\skills\pull_BRAINSkill\scripts\pull_skills.py`
2. **Run the script**: Provide a ZIP URL (Recommended), Git URL, or local path.
### Example 1: Pull via ZIP (Recommended First Choice)
This method is faster and works best in restricted network environments. To do this, you must firstly parse the repository into a ZIP file URL by appending `/archive/refs/heads/main.zip` to the repo URL. For example, for a repository at `https://github.com/GitRepoAuthorName/RepoName`, the ZIP URL would be `https://github.com/GitRepoAuthorName/RepoName/archive/refs/heads/main.zip`.
```bash
python ".claude/skills/pull_BRAINSkill/scripts/pull_skills.py" "https://github.com/GitRepoAuthorName/RepoName/archive/refs/heads/main.zip" --overwrite
```
### Example 2: Pull via Git
Use this if you need a specific branch or have git configured.
```bash
python ".claude/skills/pull_BRAINSkill/scripts/pull_skills.py" "https://github.com/GitRepoAuthorName/RepoName.git"
```
### Example 3: Import from Local Directory
```bash
python ".claude/skills/pull_BRAINSkill/scripts/pull_skills.py" "C:/Downloads/my-skills-repo"
```
Options:
- `--dest <path>`: Destination directory for skills (default: `.claude/skills` relative to current project).
- `--branch <branch>`: Specify branch to checkout.
- `--overwrite`: Overwrite existing skill folders with the same name.
## Behavior
- Clones the repository shallowly (`--depth 1`) into a temp directory.
- Scans top-level folders for `SKILL.md`.
- Copies valid skill folders into the destination `.claude/skills` directory.
## Notes
- Paths use forward slashes for compatibility.
- Requires `git` in PATH.
- Only checks for the presence of `SKILL.md`, not content validity.
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Pull Skill Folders from a Git repository into a local .claude/skills directory.
Usage:
python pull_skills.py <git_repo_url> [--dest <dest_path>] [--branch <branch>] [--overwrite]
- git_repo_url: HTTPS git URL, e.g., https://github.com/GitRepoAuthorName/-.git
- dest_path: Destination folder for skills (default: <cwd>/.claude/skills)
- branch: Branch to checkout (default: auto, uses repo default)
- overwrite: If set, overwrite existing skill folders (default: off)
Behavior:
- Clones the repo into a temporary directory (depth=1)
- Scans top-level subfolders for a SKILL.md file
- Copies those folders into the destination .claude/skills directory
Notes:
- Requires Git installed and available in PATH.
- Only validates existence of SKILL.md, does not validate YAML content.
- Copies one level deep (top-level folders only).
"""
import os
import sys
import json
import shutil
import tempfile
import subprocess
from typing import List, Dict
def run(cmd: List[str], cwd: str | None = None) -> subprocess.CompletedProcess:
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
def ensure_dir(path: str) -> None:
os.makedirs(path, exist_ok=True)
def copy_skill_folder(src_folder: str, dest_root: str, overwrite: bool) -> Dict[str, str]:
name = os.path.basename(src_folder.rstrip("/\\"))
dest_folder = os.path.join(dest_root, name)
# STRICT check for SKILL.md existence (case-sensitive)
# os.listdir returns the actual filename on the filesystem
if "SKILL.md" not in os.listdir(src_folder):
return {"status": "skipped", "folder": name, "reason": "no SKILL.md found (strictly case-sensitive)"}
if os.path.exists(dest_folder):
if overwrite:
shutil.rmtree(dest_folder)
else:
return {"status": "skipped", "folder": name, "reason": "exists"}
shutil.copytree(src_folder, dest_folder)
return {"status": "copied", "folder": name, "dest": dest_folder}
def main():
if len(sys.argv) < 2:
print(json.dumps({
"ok": False,
"error": "Missing git repo URL",
"usage": "python pull_skills.py <git_repo_url> [--dest <dest_path>] [--branch <branch>] [--overwrite]"
}, ensure_ascii=False))
sys.exit(1)
repo_url = sys.argv[1]
dest = None
branch = None
overwrite = False
# Parse optional args
args = sys.argv[2:]
i = 0
while i < len(args):
a = args[i]
if a == "--dest" and i + 1 < len(args):
dest = args[i + 1]
i += 2
elif a == "--branch" and i + 1 < len(args):
branch = args[i + 1]
i += 2
elif a == "--overwrite":
overwrite = True
i += 1
else:
i += 1
if not dest:
dest = os.path.join(os.getcwd(), ".claude", "skills")
ensure_dir(dest)
tempdir = tempfile.mkdtemp(prefix="pull_skills_")
repo_dir = os.path.join(tempdir, "repo")
# Handle local directory vs git URL vs ZIP URL
if os.path.isdir(repo_url):
# Case 1: Local directory
try:
shutil.copytree(repo_url, repo_dir, dirs_exist_ok=True)
except Exception as e:
print(json.dumps({"ok": False, "stage": "copy_local", "error": str(e)}, ensure_ascii=False))
sys.exit(2)
elif repo_url.lower().endswith(".zip"):
# Case 2: ZIP URL (direct download)
import urllib.request
import zipfile
try:
zip_path = os.path.join(tempdir, "repo.zip")
# print(f"Downloading {repo_url}...", file=sys.stderr)
urllib.request.urlretrieve(repo_url, zip_path)
extract_dir = os.path.join(tempdir, "extracted")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
# Handle GitHub zip structure: usually extract_dir/<repo-branch>/...
# We want repo_dir to point to the folder containing the skills
contents = os.listdir(extract_dir)
if len(contents) == 1 and os.path.isdir(os.path.join(extract_dir, contents[0])):
# Move the inner content to repo_dir
shutil.move(os.path.join(extract_dir, contents[0]), repo_dir)
else:
shutil.move(extract_dir, repo_dir)
except Exception as e:
print(json.dumps({
"ok": False,
"stage": "download_zip",
"error": str(e),
"url": repo_url
}, ensure_ascii=False))
sys.exit(2)
else:
# Case 3: Git Clone
clone_cmd = ["git", "clone", "--depth", "1"]
if branch:
clone_cmd += ["--branch", branch]
clone_cmd += [repo_url, repo_dir]
cp = run(clone_cmd)
if cp.returncode != 0:
print(json.dumps({
"ok": False,
"stage": "clone",
"cmd": " ".join(clone_cmd),
"stderr": cp.stderr.strip(),
"stdout": cp.stdout.strip()
}, ensure_ascii=False))
sys.exit(2)
# Scan top-level subfolders
copied: List[Dict[str, str]] = []
skipped: List[Dict[str, str]] = []
for entry in os.listdir(repo_dir):
sub = os.path.join(repo_dir, entry)
if os.path.isdir(sub):
# Check for skill.md case-insensitively
res = copy_skill_folder(sub, dest, overwrite)
if res["status"] == "copied":
copied.append(res)
else:
# If "no SKILL.md found", we just skip silently effectively (or log as skipped)
# But to avoid cluttering output with non-skill folders, strict filtering is good.
pass
result = {
"ok": True,
"repo": repo_url,
"dest": dest,
"copied": copied,
"skipped": skipped
}
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()