Source code for ge

"""ge: GitHub Engineering tools for AI agents.

Reduces boilerplate when working on GitHub issues and PRs with AI coding agents.
Fetches context, downloads media, checks freshness, and assembles everything
into a structured document the agent can consume.

Usage::

    import ge

    # Prepare full context for an issue
    ctx = ge.prepare_issue('owner/repo', 42)

    # Prepare full context for a PR
    ctx = ge.prepare_pr('owner/repo', 7)

    # Or from a full URL
    ctx = ge.prepare('https://github.com/owner/repo/issues/42')
"""

from pathlib import Path as _Path

from ge.context import prepare_issue, prepare_pr, prepare_discussion
from ge.analysis import analyze_issue, analyze_pr
from ge.github import (
    get_issue,
    get_pr,
    get_comments,
    get_pr_diff,
    get_discussion,
    find_related_prs,
    find_related_commits,
)
from ge.media import (
    process_all_media,
    extract_video_frames,
    describe_images,
    copy_images_to_clipboard,
)
from ge.util import parse_github_url, resolve_target


[docs] def prepare( url_or_spec, number=None, *, output_dir=None, describe_media=True, **kwargs ): """Prepare context from a GitHub URL or repo+number. Automatically detects whether it's an issue, PR, or discussion. When ``describe_media`` is True (default) and the ``anthropic`` package is available, downloaded images are described via the Claude API so the agent gets visual context without manual image pasting. >>> # ctx = ge.prepare('https://github.com/owner/repo/issues/42') >>> # ctx = ge.prepare('owner/repo', 42) >>> # ctx = ge.prepare('https://github.com/owner/repo/discussions/5') """ if number is None: # Parse from URL owner, repo, number, kind = parse_github_url(url_or_spec) repo_spec = f"{owner}/{repo}" else: repo_spec = url_or_spec # Probe whether it's a PR or issue try: data = get_pr(repo_spec, number) kind = "pr" except Exception: kind = "issue" if kind == "pr": return prepare_pr( repo_spec, number, output_dir=output_dir, describe_media=describe_media, **kwargs, ) elif kind == "discussion": return prepare_discussion( repo_spec, number, output_dir=output_dir, describe_media=describe_media, **kwargs, ) else: return prepare_issue( repo_spec, number, output_dir=output_dir, describe_media=describe_media, **kwargs, )
_SKILLS_DIR = _Path(__file__).parent / "data" / "skills"
[docs] def install_skills(*, target_dir=None): """Symlink ge skills into ~/.claude/skills/ for global discovery. Creates symlinks from ``target_dir/<skill>`` to the skill folders bundled with the ge package. Existing correct symlinks are left alone; a warning is printed if a symlink points elsewhere. Args: target_dir: Destination directory. Defaults to ``~/.claude/skills/``. """ if target_dir is None: target_dir = _Path.home() / ".claude" / "skills" else: target_dir = _Path(target_dir) target_dir.mkdir(parents=True, exist_ok=True) for source in sorted(_SKILLS_DIR.iterdir()): if not source.is_dir(): continue link = target_dir / source.name if link.is_symlink(): if link.resolve() == source.resolve(): print(f" ✓ {source.name} (already installed)") continue else: print( f" ⚠ {source.name}: symlink exists but points to " f"{link.resolve()}, expected {source.resolve()}. Skipping." ) continue elif link.exists(): print( f" ⚠ {source.name}: {link} already exists and is not a " f"symlink. Skipping." ) continue link.symlink_to(source.resolve()) print(f" ✓ {source.name} -> {source.resolve()}") print(f"\nSkills installed to {target_dir}")
[docs] def uninstall_skills(*, target_dir=None): """Remove ge skill symlinks from ~/.claude/skills/. Only removes symlinks that point back into the ge package's skill directory. Non-symlinks and symlinks owned by other packages are left untouched. Args: target_dir: Directory to remove from. Defaults to ``~/.claude/skills/``. """ if target_dir is None: target_dir = _Path.home() / ".claude" / "skills" else: target_dir = _Path(target_dir) if not target_dir.exists(): print("Nothing to uninstall (target directory does not exist).") return removed = 0 for source in sorted(_SKILLS_DIR.iterdir()): if not source.is_dir(): continue link = target_dir / source.name if link.is_symlink() and link.resolve() == source.resolve(): link.unlink() print(f" ✓ Removed {source.name}") removed += 1 elif link.is_symlink(): print( f" · Skipped {source.name} (symlink points to " f"{link.resolve()}, not owned by ge)" ) elif link.exists(): print(f" · Skipped {source.name} (not a symlink, not owned by ge)") if removed: print(f"\n{removed} skill(s) uninstalled from {target_dir}") else: print("\nNo ge skills found to uninstall.")
__all__ = [ "prepare", "prepare_issue", "prepare_pr", "prepare_discussion", "analyze_issue", "analyze_pr", "get_issue", "get_pr", "get_comments", "get_pr_diff", "get_discussion", "find_related_prs", "find_related_commits", "process_all_media", "extract_video_frames", "describe_images", "copy_images_to_clipboard", "resolve_target", "install_skills", "uninstall_skills", ]