release: v2.1.1 — skill optimization, agents, commands, reference splits (#297)

This commit is contained in:
Alireza Rezvani
2026-03-09 15:54:25 +01:00
committed by GitHub
parent 3c34297a6b
commit 8902ba79d7
567 changed files with 17859 additions and 22442 deletions

68
scripts/gemini-install.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
#
# Gemini CLI Installation Script for Claude Skills Library
#
# Sets up the workspace for Gemini CLI by generating symlinks and an index.
#
# Usage:
# ./scripts/gemini-install.sh [--dry-run]
#
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
GEMINI_SYNC_SCRIPT="$SCRIPT_DIR/sync-gemini-skills.py"
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Print colored output
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
# Banner
echo ""
echo "========================================"
echo " Claude Skills - Gemini CLI Setup"
echo "========================================"
echo ""
# Check for Python
if ! command -v python3 &> /dev/null; then
echo "Error: python3 is required for this setup."
exit 1
fi
# Run the sync script
print_info "Synchronizing skills for Gemini CLI..."
python3 "$GEMINI_SYNC_SCRIPT" "$@"
# Post-installation instructions
echo ""
print_success "Gemini CLI setup complete!"
echo ""
echo "How to use these skills in Gemini CLI:"
echo "--------------------------------------"
echo "1. Activate any skill by name:"
echo " > activate_skill(name=\"senior-architect\")"
echo ""
echo "2. Activate an agent persona:"
echo " > activate_skill(name=\"cs-engineering-lead\")"
echo ""
echo "3. Run a custom command:"
echo " > activate_skill(name=\"tdd\")"
echo ""
echo "The skills are indexed in .gemini/skills/ for discovery."
echo "Each skill folder contains its own SKILL.md instructions."
echo ""
print_info "Read GEMINI.md in the root for more details."
echo ""

View File

@@ -28,6 +28,10 @@ SKILL_DOMAINS = {
"category": "engineering",
"description": "Software engineering and technical skills"
},
"engineering": {
"category": "engineering-advanced",
"description": "Advanced engineering skills - agents, RAG, MCP, CI/CD, databases, observability"
},
"product-team": {
"category": "product",
"description": "Product management and design skills"

View File

@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
Sync Gemini Skills - Generate symlinks and index for Gemini CLI compatibility.
This script scans the entire repository for SKILL.md files and creates:
1. Symlinks in .gemini/skills/ directory
2. skills-index.json manifest for tooling
Usage:
python scripts/sync-gemini-skills.py [--dry-run] [--verbose]
"""
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional
# Domain mapping for categories based on top-level folder
DOMAIN_MAP = {
"marketing-skill": "marketing",
"engineering-team": "engineering",
"engineering": "engineering-advanced",
"product-team": "product",
"c-level-advisor": "c-level",
"project-management": "project-management",
"ra-qm-team": "ra-qm",
"business-growth": "business-growth",
"finance": "finance"
}
def find_skills(repo_root: Path) -> List[Dict]:
"""
Scan repository for all skills (SKILL.md files).
"""
skills = []
seen_names = set()
# 1. Find all SKILL.md files recursively
for skill_md in repo_root.rglob("SKILL.md"):
# Skip internal .gemini directory
if ".gemini" in skill_md.parts:
continue
# Skip evaluation workspaces, assets, and gitignored directories
if "eval-workspace" in skill_md.parts or "assets" in skill_md.parts or "evals" in skill_md.parts:
if "sample-skill" not in skill_md.parts: # Keep sample if it's for testing
continue
# Skip directories not in DOMAIN_MAP (e.g. gitignored local folders)
top_level = skill_md.relative_to(repo_root).parts[0]
if top_level not in DOMAIN_MAP and top_level not in ("agents", "commands"):
continue
skill_dir = skill_md.parent
# Determine skill name
if skill_dir == repo_root:
continue # Root SKILL.md (unlikely)
# For domain-level SKILL.md, name it after the domain
if skill_dir.name in DOMAIN_MAP:
skill_name = f"{skill_dir.name}-bundle"
else:
skill_name = skill_dir.name
# Handle duplicates by appending parent name
if skill_name in seen_names:
skill_name = f"{skill_dir.parent.name}-{skill_name}"
seen_names.add(skill_name)
# Determine category based on top-level folder
category = "general"
for folder, cat in DOMAIN_MAP.items():
if folder in skill_md.parts:
category = cat
break
description = extract_skill_description(skill_md)
# Calculate relative path (3 levels up from .gemini/skills/{name}/SKILL.md)
rel_path = skill_md.relative_to(repo_root)
source_path = "../../../" + str(rel_path)
skills.append({
"name": skill_name,
"source": source_path,
"category": category,
"description": description or f"Skill from {rel_path.parent}"
})
# 2. Agents as Skills
agents_path = repo_root / "agents"
if agents_path.exists():
for agent_file in agents_path.rglob("*.md"):
if agent_file.name == "CLAUDE.md" or ".gemini" in agent_file.parts:
continue
agent_name = agent_file.stem
if agent_name in seen_names:
agent_name = f"agent-{agent_name}"
seen_names.add(agent_name)
description = extract_skill_description(agent_file)
rel_path = agent_file.relative_to(repo_root)
source_path = "../../../" + str(rel_path)
skills.append({
"name": agent_name,
"source": source_path,
"category": "agent",
"description": description or f"Agent from {agent_file.parent.name}"
})
# 3. Commands as Skills
commands_path = repo_root / "commands"
if commands_path.exists():
for cmd_file in commands_path.glob("*.md"):
if cmd_file.name == ".gitkeep":
continue
cmd_name = cmd_file.stem
if cmd_name in seen_names:
cmd_name = f"cmd-{cmd_name}"
seen_names.add(cmd_name)
description = extract_skill_description(cmd_file)
rel_path = cmd_file.relative_to(repo_root)
source_path = "../../../" + str(rel_path)
skills.append({
"name": cmd_name,
"source": source_path,
"category": "command",
"description": description or "Custom slash command"
})
skills.sort(key=lambda s: (s["category"], s["name"]))
return skills
def extract_skill_description(skill_md_path: Path) -> Optional[str]:
"""
Extract description from YAML frontmatter.
"""
try:
content = skill_md_path.read_text(encoding="utf-8")
if not content.startswith("---"):
return None
end_idx = content.find("---", 3)
if end_idx == -1:
return None
frontmatter = content[3:end_idx]
for line in frontmatter.split("\n"):
line = line.strip()
if line.startswith("description:"):
desc = line[len("description:"):].strip()
if (desc.startswith('"') and desc.endswith('"')) or (desc.startswith("'") and desc.endswith("'")):
desc = desc[1:-1]
return desc
return None
except Exception:
return None
def create_symlinks(repo_root: Path, skills: List[Dict], dry_run: bool = False, verbose: bool = False) -> Dict:
"""
Create symlinks in .gemini/skills/ directory.
"""
gemini_skills_dir = repo_root / ".gemini" / "skills"
# Optional: Clean existing skills to remove stale ones
# if not dry_run and gemini_skills_dir.exists():
# import shutil
# shutil.rmtree(gemini_skills_dir)
created, updated, unchanged, errors = [], [], [], []
if not dry_run:
gemini_skills_dir.mkdir(parents=True, exist_ok=True)
for skill in skills:
skill_name = skill["name"]
skill_dest_dir = gemini_skills_dir / skill_name
if not dry_run:
skill_dest_dir.mkdir(exist_ok=True)
symlink_path = skill_dest_dir / "SKILL.md"
target = skill["source"]
try:
if symlink_path.is_symlink():
current_target = os.readlink(symlink_path)
if current_target == target:
unchanged.append(skill_name)
else:
if not dry_run:
symlink_path.unlink()
symlink_path.symlink_to(target)
updated.append(skill_name)
elif symlink_path.exists():
errors.append(f"{skill_name}: path exists but is not a symlink")
else:
if not dry_run:
symlink_path.symlink_to(target)
created.append(skill_name)
except Exception as e:
errors.append(f"{skill_name}: {str(e)}")
return {"created": created, "updated": updated, "unchanged": unchanged, "errors": errors}
def generate_skills_index(repo_root: Path, skills: List[Dict], dry_run: bool = False) -> Dict:
"""
Generate .gemini/skills-index.json manifest.
"""
categories = {}
for skill in skills:
cat = skill["category"]
if cat not in categories:
categories[cat] = {"count": 0, "description": f"{cat.capitalize()} resources"}
categories[cat]["count"] += 1
index = {
"version": "1.0.0",
"name": "gemini-cli-skills",
"total_skills": len(skills),
"skills": [{"name": s["name"], "category": s["category"], "description": s["description"]} for s in skills],
"categories": categories
}
if not dry_run:
index_path = repo_root / ".gemini" / "skills-index.json"
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text(json.dumps(index, indent=2) + "\n", encoding="utf-8")
return index
def main():
parser = argparse.ArgumentParser(description="Sync Gemini skills")
parser.add_argument("--dry-run", "-n", action="store_true")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
skills = find_skills(repo_root)
if not skills:
print("No skills found.")
sys.exit(1)
symlink_results = create_symlinks(repo_root, skills, args.dry_run, args.verbose)
generate_skills_index(repo_root, skills, args.dry_run)
print(f"Total items synced for Gemini CLI: {len(skills)}")
print(f"Created: {len(symlink_results['created'])}")
print(f"Updated: {len(symlink_results['updated'])}")
print(f"Unchanged: {len(symlink_results['unchanged'])}")
if symlink_results['errors']:
print(f"Errors: {len(symlink_results['errors'])}")
if __name__ == "__main__":
main()