mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-03-16 22:23:07 +00:00
210 lines
6.4 KiB
Python
Executable File
210 lines
6.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Copy a skill directory with metadata tracking.
|
|
|
|
Creates a copy of a skill directory and adds a META.yaml file
|
|
to track lineage, changes, and performance metrics.
|
|
"""
|
|
|
|
import argparse
|
|
import shutil
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
def create_meta_yaml(
|
|
dest: Path,
|
|
parent: str | None,
|
|
changes: str | None,
|
|
score: float | None,
|
|
iteration: int | None,
|
|
) -> None:
|
|
"""Create META.yaml file in the destination directory."""
|
|
created_at = datetime.now(timezone.utc).isoformat()
|
|
|
|
# Build YAML content manually to avoid external dependencies
|
|
lines = ["# Skill iteration metadata", ""]
|
|
|
|
# Helper to format YAML values
|
|
def yaml_value(val):
|
|
if val is None:
|
|
return "null"
|
|
if isinstance(val, bool):
|
|
return "true" if val else "false"
|
|
if isinstance(val, (int, float)):
|
|
return str(val)
|
|
if isinstance(val, str):
|
|
# Quote strings that might be ambiguous
|
|
if val in ("null", "true", "false") or val.startswith(("'", '"', "[", "{")):
|
|
return f'"{val}"'
|
|
# Quote strings with special characters
|
|
if any(c in val for c in (":", "#", "\n", '"', "'")):
|
|
escaped = val.replace("\\", "\\\\").replace('"', '\\"')
|
|
return f'"{escaped}"'
|
|
return val
|
|
return str(val)
|
|
|
|
lines.append(f"parent: {yaml_value(parent)}")
|
|
lines.append(f"changes: {yaml_value(changes)}")
|
|
lines.append(f"score: {yaml_value(score)}")
|
|
lines.append(f"iteration: {yaml_value(iteration)}")
|
|
lines.append(f"created_at: {yaml_value(created_at)}")
|
|
lines.append("")
|
|
|
|
meta_path = dest / "META.yaml"
|
|
meta_path.write_text("\n".join(lines))
|
|
|
|
|
|
def copy_skill(
|
|
source: Path,
|
|
dest: Path,
|
|
parent: str | None = None,
|
|
changes: str | None = None,
|
|
score: float | None = None,
|
|
iteration: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Copy a skill directory and create version directory structure.
|
|
|
|
Creates a version directory with:
|
|
- skill/ : The actual skill files (copied from source)
|
|
- runs/ : Created by executor during execution (run-1/, run-2/, run-3/)
|
|
- improvements/ : For improvement suggestions (if not v0)
|
|
- META.yaml : Version metadata
|
|
|
|
The runs/ directory structure is created on-demand by the executor:
|
|
- runs/run-1/transcript.md, outputs/, evaluation.json
|
|
- runs/run-2/...
|
|
- runs/run-3/...
|
|
|
|
Args:
|
|
source: Path to the source skill directory (or source/skill/ if copying from another version)
|
|
dest: Path to the destination version directory (e.g., workspace/v1)
|
|
parent: Name/path of the parent skill iteration
|
|
changes: Description of changes from parent
|
|
score: Evaluation score for this iteration
|
|
iteration: Iteration number
|
|
"""
|
|
source = Path(source).resolve()
|
|
dest = Path(dest).resolve()
|
|
|
|
if not source.exists():
|
|
raise FileNotFoundError(f"Source directory does not exist: {source}")
|
|
|
|
if not source.is_dir():
|
|
raise ValueError(f"Source must be a directory: {source}")
|
|
|
|
if dest.exists():
|
|
raise FileExistsError(f"Destination already exists: {dest}")
|
|
|
|
# Create the version directory structure
|
|
dest.mkdir(parents=True)
|
|
skill_dest = dest / "skill"
|
|
(dest / "runs").mkdir()
|
|
|
|
# Create improvements directory for non-baseline versions
|
|
if iteration is not None and iteration > 0:
|
|
(dest / "improvements").mkdir()
|
|
|
|
# Copy the skill files to skill/ subdirectory
|
|
shutil.copytree(source, skill_dest)
|
|
|
|
# Create metadata file at the version root
|
|
create_meta_yaml(dest, parent, changes, score, iteration)
|
|
|
|
print(f"Copied skill from {source} to {skill_dest}")
|
|
print(f"Created version directory structure at {dest}")
|
|
print(f" - skill/ : Skill files")
|
|
print(f" - runs/ : For execution runs (run-1/, run-2/, run-3/)")
|
|
if iteration is not None and iteration > 0:
|
|
print(f" - improvements/ : Improvement suggestions")
|
|
print(f" - META.yaml : Version metadata")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Copy a skill directory with metadata tracking and version structure.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# Create baseline v0 from an existing skill
|
|
python copy_skill.py ./skills/public/pdf ./skill_iterations/v0 --iteration 0
|
|
|
|
# Create v1 from v0's skill directory
|
|
python copy_skill.py ./skill_iterations/v0/skill ./skill_iterations/v1 \\
|
|
--parent v0 \\
|
|
--changes "Added error handling for empty cells" \\
|
|
--iteration 1
|
|
|
|
# Create v2 with score from evaluation
|
|
python copy_skill.py ./skill_iterations/v1/skill ./skill_iterations/v2 \\
|
|
--parent v1 \\
|
|
--changes "Improved coordinate guidance" \\
|
|
--score 7.5 \\
|
|
--iteration 2
|
|
|
|
Output structure:
|
|
dest/
|
|
├── META.yaml # Version metadata
|
|
├── skill/ # The actual skill files
|
|
├── runs/ # Execution runs (created by executor)
|
|
│ ├── run-1/
|
|
│ │ ├── transcript.md
|
|
│ │ ├── outputs/
|
|
│ │ └── evaluation.json
|
|
│ ├── run-2/
|
|
│ └── run-3/
|
|
└── improvements/ # Improvement suggestions (v1+)
|
|
""",
|
|
)
|
|
|
|
parser.add_argument("source", type=Path, help="Source skill directory to copy")
|
|
|
|
parser.add_argument("dest", type=Path, help="Destination path for the copy")
|
|
|
|
parser.add_argument(
|
|
"--parent",
|
|
type=str,
|
|
default=None,
|
|
help="Name or path of the parent skill iteration",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--changes",
|
|
type=str,
|
|
default=None,
|
|
help="Description of changes from the parent version",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--score",
|
|
type=float,
|
|
default=None,
|
|
help="Evaluation score for this iteration (e.g., 7.5)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--iteration",
|
|
type=int,
|
|
default=None,
|
|
help="Iteration number (e.g., 1, 2, 3)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
copy_skill(
|
|
source=args.source,
|
|
dest=args.dest,
|
|
parent=args.parent,
|
|
changes=args.changes,
|
|
score=args.score,
|
|
iteration=args.iteration,
|
|
)
|
|
except (FileNotFoundError, FileExistsError, ValueError) as e:
|
|
parser.error(str(e))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|