Claude Skill

invoking-github

Enables GitHub repository operations (read/write/commit/PR) for Claude.ai chat environments. Use when users request GitHub commits, repository updates, DEVLOG persistence, or cross-session state management via GitHub branches. Not needed in Claude Code (has native git access).

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oaustegard-claude-skills-plugins_github-and-git_skills_invoking-github-e39c726.zip · 19 KB
Part of oaustegard/claude-skills — 39 skills

Install

skills CLI npx skills add https://github.com/oaustegard/claude-skills/tree/main/plugins/github-and-git/skills/invoking-github
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oaustegard-claude-skills@llmmart
Git git clone https://github.com/oaustegard/claude-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oaustegard/claude-skills collection as a plugin from our marketplace. Git is the plain clone.

README

invoking-github

Enables GitHub repository operations (read/write/commit/PR) for Claude.ai chat environments. Use when users request GitHub commits, repository updates, DEVLOG persistence, or cross-session state management via GitHub branches. Not needed in Claude Code (has native git access).

Skill manifest

Invoking GitHub

Programmatically interact with GitHub repositories from Claude.ai chat: read files, commit changes, create PRs, and persist state across sessions.

When to Use This Skill

Not needed for:

  • Claude Code environments (use native git commands)
  • Read-only repository access (use GitHub UI or API directly)

Quick Start

Prerequisites

Create a GitHub Personal Access Token and add to Project Knowledge:

  1. Go to https://github.com/settings/tokens
  2. Create new token (classic or fine-grained)
  3. Required scopes: repo (or public_repo for public repos only)
  4. In Claude.ai, add to Project Knowledge:
    • Title: GITHUB_API_KEY
    • Content: Your token (e.g., ghp_abc123...)

Single File Commit

from invoking_github import commit_file

result = commit_file(
    repo="username/repo-name",
    path="README.md",
    content="# Updated README\n\nNew content here...",
    branch="main",
    message="Update README with new instructions"
)

print(f"Committed: {result['commit_sha']}")

Read File

from invoking_github import read_file

content = read_file(
    repo="username/repo-name",
    path="config.json",
    branch="main"
)

print(content)

Batch Commit (Multiple Files)

from invoking_github import commit_files

files = [
    {"path": "src/main.py", "content": "# Python code..."},
    {"path": "README.md", "content": "# Updated docs..."},
    {"path": "tests/test.py", "content": "# Tests..."}
]

result = commit_files(
    repo="username/repo-name",
    files=files,
    branch="feature-branch",
    message="Add new feature implementation",
    create_branch_from="main"  # Create branch if it doesn't exist
)

print(f"Committed {len(files)} files: {result['commit_sha']}")

Create Pull Request

from invoking_github import create_pull_request

pr = create_pull_request(
    repo="username/repo-name",
    head="feature-branch",
    base="main",
    title="Add new feature",
    body="## Changes\n- Implemented feature X\n- Updated docs\n- Added tests"
)

print(f"PR created: {pr['html_url']}")

Core Functions

read_file()

Read a file from repository:

read_file(
    repo: str,           # "owner/name"
    path: str,           # "path/to/file.py"
    branch: str = "main" # Branch name
) -> str

Returns: File content as string Raises: GitHubAPIError if file not found or access denied

commit_file()

Commit a single file (create or update):

commit_file(
    repo: str,                      # "owner/name"
    path: str,                      # "path/to/file.py"
    content: str,                   # New file content
    branch: str,                    # Target branch
    message: str,                   # Commit message
    create_branch_from: str = None  # Create branch from this if doesn't exist
) -> dict

Returns: Dict with commit_sha, branch, file_path Raises: GitHubAPIError on conflicts or auth failures

commit_files()

Commit multiple files in a single commit:

commit_files(
    repo: str,                      # "owner/name"
    files: list[dict],              # [{"path": "...", "content": "..."}]
    branch: str,                    # Target branch
    message: str,                   # Commit message
    create_branch_from: str = None  # Create branch from this if doesn't exist
) -> dict

Returns: Dict with commit_sha, branch, files_committed Raises: GitHubAPIError on failures

Note: Uses Git Trees API for efficiency - atomic commit of all files.

create_pull_request()

Create a pull request:

create_pull_request(
    repo: str,      # "owner/name"
    head: str,      # Source branch (your changes)
    base: str,      # Target branch (where to merge)
    title: str,     # PR title
    body: str = ""  # PR description (supports markdown)
) -> dict

Returns: Dict with number, html_url, state Raises: GitHubAPIError if branches invalid or PR exists

Credential Configuration

This skill requires a GitHub Personal Access Token. Two configuration methods:

Method 1: Project Knowledge (Recommended)

Best for Claude.ai chat users (mobile/web):

  1. Create token at https://github.com/settings/tokens
  2. In Claude.ai Project settings → Add to Project Knowledge
  3. Create document titled GITHUB_API_KEY
  4. Paste your token as content

Permissions required:

  • Classic token: repo scope
  • Fine-grained token: Repository permissions → Contents (read/write) and Pull requests (read/write)

Method 2: API Credentials Skill (Fallback)

Alternatively, use combined credentials file:

  1. Create API_CREDENTIALS.json in project knowledge
  2. Add: {"github_api_key": "ghp_your-token-here"}

Integration with Iterating Skill

Auto-persist DEVLOG.md to GitHub for cross-session continuity:

# In your DEVLOG update function
from invoking_github import commit_file
from pathlib import Path

def update_devlog_with_sync(data, repo="user/project", branch="devlog"):
    """Update DEVLOG.md locally and sync to GitHub"""

    # Update local DEVLOG
    update_devlog(data)  # Your existing function

    # Auto-sync to GitHub
    try:
        devlog_content = Path("DEVLOG.md").read_text()

        result = commit_file(
            repo=repo,
            path="DEVLOG.md",
            content=devlog_content,
            branch=branch,
            message=f"DEVLOG: {data['title']}",
            create_branch_from="main"
        )

        print(f"✓ DEVLOG synced to GitHub ({repo}:{branch})")
        return result

    except Exception as e:
        print(f"⚠ DEVLOG sync failed: {e}")
        # Continue anyway - local DEVLOG.md still updated
        return None

Benefits:

  • Automatic backup of session progress
  • Cross-device access to development logs
  • Git history of decisions and progress
  • No manual copy/paste to Project Knowledge

See references/iterating-integration.md for complete patterns.

Error Handling

All functions raise GitHubAPIError with descriptive messages:

from invoking_github import commit_file, GitHubAPIError

try:
    result = commit_file(
        repo="user/repo",
        path="file.py",
        content="...",
        branch="main",
        message="Update"
    )
except GitHubAPIError as e:
    if e.status_code == 404:
        print("Repository or branch not found")
    elif e.status_code == 401:
        print("Authentication failed - check your token")
    elif e.status_code == 403:
        print("Access denied - check token permissions")
    elif e.status_code == 409:
        print("Conflict - file was modified since last read")
    else:
        print(f"GitHub API error: {e}")

Common errors:

  • 404: Repository, branch, or file not found
  • 401/403: Invalid token or insufficient permissions
  • 409: Merge conflict (file changed concurrently)
  • 422: Validation error (invalid branch name, etc.)
  • Rate limit: Too many requests (wait before retrying)

Best Practices

  1. Use descriptive commit messages

    • Bad: "Update files"
    • Good: "Add authentication middleware and update user routes"
  2. Batch commits when possible

    • Use commit_files() for related changes
    • Single atomic commit = cleaner git history
  3. Create feature branches

    • Don't commit directly to main
    • Use create_branch_from="main" parameter
    • Create PR for review
  4. Secure token management

    • Use fine-grained tokens with minimal scopes
    • Set expiration dates
    • Rotate regularly
    • Never commit tokens to repositories

Advanced Usage

Conditional Commits

Only commit if file content changed:

from invoking_github import read_file, commit_file, GitHubAPIError

try:
    current_content = read_file(repo, path, branch)
    if current_content != new_content:
        commit_file(repo, path, new_content, branch, "Update file")
    else:
        print("No changes detected, skipping commit")
except GitHubAPIError as e:
    if e.status_code == 404:
        # File doesn't exist, create it
        commit_file(repo, path, new_content, branch, "Create file")
    else:
        raise

Session-Specific Branches

Avoid conflicts with unique branch names:

import datetime

session_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
branch_name = f"devlog-{session_id}"

commit_file(
    repo="user/project",
    path="DEVLOG.md",
    content=devlog_content,
    branch=branch_name,
    message="Session progress",
    create_branch_from="main"
)

Multi-Repository Workflows

Work across multiple repos:

repos = ["user/frontend", "user/backend", "user/docs"]

for repo in repos:
    commit_file(
        repo=repo,
        path="VERSION",
        content="2.0.0\n",
        branch="release-2.0",
        message="Bump version to 2.0.0"
    )

Limitations

  • Target environment: Claude.ai chat only (not Claude Code)
  • No OAuth: Must use Personal Access Tokens manually
  • No git operations: Pure REST API (no clone, pull, rebase, etc.)
  • File size: GitHub API limits ~100MB per file
  • Rate limits: 5000 requests/hour for authenticated users
  • Network required: All operations require internet access

See Also

Token Efficiency

This skill uses ~800 tokens when loaded but provides essential GitHub operations for claude.ai chat environments where native git access isn't available. Enables persistent state management and cross-session workflows.

Files (claude-skills)
  • references
    • credential-setup.md 6.6 KB
      # Credential Setup Guide
      
      Complete guide to configuring GitHub authentication for the invoking-github skill.
      
      ## Overview
      
      This skill requires a GitHub Personal Access Token (PAT). Two configuration methods are supported:
      
      1. **Project Knowledge** (Primary) - Recommended for claude.ai chat users
      2. **API Credentials Skill** (Fallback) - For power users or local environments
      
      ## Method 1: Project Knowledge (Recommended)
      
      Best for Claude.ai chat (mobile/web/desktop).
      
      ### Step 1: Create GitHub Personal Access Token
      
      #### Option A: Fine-Grained Token (Recommended - More Secure)
      
      1. Go to https://github.com/settings/tokens?type=beta
      2. Click "Generate new token"
      3. Configure:
         - **Token name**: `claude-ai-chat` (or your preference)
         - **Expiration**: 90 days (or your preference)
         - **Repository access**: Select specific repositories you want Claude to access
         - **Permissions**:
           - Repository permissions → **Contents**: Read and write
           - Repository permissions → **Pull requests**: Read and write
      4. Click "Generate token"
      5. **Copy the token immediately** (you won't see it again!)
      
      #### Option B: Classic Token (Easier - Less Secure)
      
      1. Go to https://github.com/settings/tokens
      2. Click "Generate new token (classic)"
      3. Configure:
         - **Note**: `claude-ai-chat`
         - **Expiration**: 90 days
         - **Scopes**:
           - ✓ `repo` (Full control of private repositories)
           - Or just `public_repo` if you only need public repo access
      4. Click "Generate token"
      5. **Copy the token immediately**
      
      ### Step 2: Add Token to Project Knowledge
      
      #### In Claude.ai Web:
      
      1. Open your project in Claude.ai
      2. Click the project name/settings
      3. Click "Add to Project Knowledge"
      4. Create a new document:
         - **Title**: `GITHUB_API_KEY` (exactly this, all caps)
         - **Content**: Paste your token (e.g., `ghp_1234567890abcdefgh...`)
      5. Click "Add" or "Save"
      
      #### In Claude.ai Mobile (iOS/Android):
      
      1. Open your project
      2. Tap the menu icon
      3. Select "Project Knowledge"
      4. Tap "Add document"
      5. Title: `GITHUB_API_KEY`
      6. Content: Paste your token
      7. Save
      
      ### Step 3: Verify Configuration
      
      Ask Claude to run this test:
      
      ```python
      import sys
      sys.path.append('/home/user/claude-skills/invoking-github/scripts')
      from github_client import get_github_token
      
      try:
          token = get_github_token()
          masked = f"{token[:7]}...{token[-4:]}"
          print(f"✓ Token found: {masked}")
          print(f"✓ Token length: {len(token)} characters")
      except ValueError as e:
          print(f"✗ Token not found")
          print(e)
      ```
      
      **Expected output**:
      ```
      ✓ Token found: ghp_123...xyz
      ✓ Token length: 40 characters
      ```
      
      ## Method 2: API Credentials Skill (Fallback)
      
      For power users who prefer file-based configuration.
      
      ### Step 1: Create Token
      
      Follow the same token creation steps as Method 1.
      
      ### Step 2: Add to api-credentials config.json
      
      1. Edit `/home/user/claude-skills/api-credentials/config.json`
      2. Add your token:
         ```json
         {
           "anthropic_api_key": "sk-ant-...",
           "google_api_key": "AIza...",
           "github_api_key": "ghp_your_token_here"
         }
         ```
      3. Save the file
      
      ### Step 3: Verify Configuration
      
      Same test as Method 1 above.
      
      ## Security Best Practices
      
      ### 1. Use Fine-Grained Tokens
      
      Fine-grained tokens are more secure because:
      - Limited to specific repositories
      - Specific permissions (not full repo access)
      - Easier to audit
      
      ### 2. Set Expiration Dates
      
      - Recommended: 90 days
      - Maximum: 1 year (but not recommended)
      - Never select "No expiration" for production use
      
      ### 3. Minimal Permissions
      
      Only grant permissions you actually need:
      
      | Operation | Required Permissions |
      |-----------|---------------------|
      | Read files | Contents: Read |
      | Commit files | Contents: Read and write |
      | Create PRs | Contents: Read and write + Pull requests: Read and write |
      
      ### 4. Token Rotation
      
      Rotate tokens regularly:
      - Set calendar reminder before expiration
      - Create new token
      - Update Project Knowledge or config.json
      - Revoke old token
      
      ### 5. Never Commit Tokens
      
      - **Never** paste tokens in code that gets committed
      - **Never** commit config.json with real tokens
      - Use .gitignore (api-credentials/config.json is already gitignored)
      
      ### 6. Revoke Compromised Tokens
      
      If a token is accidentally exposed:
      1. Go to https://github.com/settings/tokens
      2. Find the token
      3. Click "Delete" or "Revoke"
      4. Create a new token immediately
      5. Update configuration
      
      ## Troubleshooting
      
      ### "No GitHub API token found!"
      
      **Problem**: Token not detected in either location.
      
      **Solutions**:
      - Check Project Knowledge document is titled **exactly** `GITHUB_API_KEY` (all caps)
      - Verify token is the only content in the document (no extra text)
      - Check for trailing whitespace
      - Try Method 2 (api-credentials) as a test
      
      ### "Authentication failed"
      
      **Problem**: Token is invalid or expired.
      
      **Solutions**:
      - Verify token hasn't expired (check GitHub settings)
      - Ensure no extra characters when copying token
      - Generate a new token and update configuration
      
      ### "Access denied"
      
      **Problem**: Token lacks required permissions.
      
      **Solutions**:
      - For fine-grained tokens: Check repository access includes your target repo
      - For fine-grained tokens: Verify Contents and Pull requests permissions
      - For classic tokens: Ensure `repo` scope is selected
      - Generate new token with correct permissions
      
      ### Token works in web but not mobile
      
      **Problem**: Project Knowledge might not sync properly.
      
      **Solutions**:
      - Wait a few minutes for sync
      - Refresh the app
      - Try deleting and re-adding the document
      - Use api-credentials method as workaround (if app supports file access)
      
      ## FAQ
      
      **Q: Can I use the GitHub OAuth connection in claude.ai UI?**
      
      A: No, the OAuth connection in the UI is not accessible to skills. You must create a Personal Access Token manually.
      
      **Q: How long do tokens last?**
      
      A: Based on your selection during creation. Recommended: 90 days with rotation.
      
      **Q: Can I use the same token across multiple Claude projects?**
      
      A: Yes, but for security, consider creating separate tokens per project with limited repo access.
      
      **Q: What happens when my token expires?**
      
      A: The skill will fail with authentication errors. Create a new token and update your configuration.
      
      **Q: Can I use a GitHub App token?**
      
      A: Not directly. This skill is designed for Personal Access Tokens. GitHub App tokens have different authentication flows.
      
      **Q: Is my token secure in Project Knowledge?**
      
      A: Project Knowledge is stored securely, but treat it like a password - use minimal permissions, set expiration, rotate regularly.
      
      ## Next Steps
      
      - [SKILL.md](../SKILL.md) - Core skill documentation
      - [iterating-integration.md](iterating-integration.md) - Auto-sync DEVLOG patterns
      - [troubleshooting.md](troubleshooting.md) - Common issues and solutions
      
    • iterating-integration.md 12.6 KB
      # Iterating Skill Integration
      
      Patterns for integrating invoking-github with the iterating skill to enable automatic DEVLOG persistence to GitHub.
      
      ## Overview
      
      The iterating skill tracks development progress across sessions using DEVLOG.md. By integrating with invoking-github, we can automatically persist this log to a GitHub branch, enabling:
      
      - **Cross-session continuity**: Resume work across different Claude environments
      - **Version control**: Git history of all decisions and progress
      - **Team collaboration**: Share development logs with team members
      - **Backup**: Automatic backup of session progress
      
      ## Basic Integration Pattern
      
      ### Modified Update DEVLOG Function
      
      ```python
      from pathlib import Path
      from datetime import datetime
      
      def update_devlog_with_github_sync(
          data: dict,
          repo: str,
          branch: str = "devlog",
          auto_sync: bool = True
      ):
          """
          Update DEVLOG.md locally and optionally sync to GitHub.
      
          Args:
              data: DEVLOG entry data (title, prev, now, work, etc.)
              repo: GitHub repository ("owner/name")
              branch: Target branch for DEVLOG
              auto_sync: Whether to automatically sync to GitHub
          """
          from iterating import update_devlog  # Your existing function
      
          # Update local DEVLOG
          update_devlog(data)
      
          # Optionally sync to GitHub
          if auto_sync:
              try:
                  import sys
                  sys.path.append('/home/user/claude-skills/invoking-github/scripts')
                  from github_client import commit_file
      
                  devlog_content = Path("DEVLOG.md").read_text()
      
                  result = commit_file(
                      repo=repo,
                      path="DEVLOG.md",
                      content=devlog_content,
                      branch=branch,
                      message=f"DEVLOG: {data['title']}",
                      create_branch_from="main"
                  )
      
                  print(f"✓ DEVLOG synced to GitHub ({repo}:{branch}) - {result['commit_sha'][:7]}")
                  return result
      
              except Exception as e:
                  print(f"⚠ DEVLOG sync failed: {e}")
                  print("  Local DEVLOG.md updated successfully")
                  return None
      ```
      
      ### Usage
      
      ```python
      # At end of session
      data = {
          'title': 'Implemented authentication middleware',
          'prev': 'Designed auth flow',
          'now': 'Built JWT middleware',
          'work': {
              'added': ['src/auth/middleware.py'],
              'changed': ['src/app.py'],
              'fixed': ['Auth validation bug']
          },
          'decisions': [
              {
                  'what': 'Use JWT tokens',
                  'why': 'Stateless, scalable',
                  'alt': 'Session cookies'
              }
          ],
          'next': ['Add token refresh', 'Test edge cases']
      }
      
      update_devlog_with_github_sync(
          data=data,
          repo="username/project-name",
          branch="devlog"
      )
      ```
      
      ## Configuration-Based Pattern
      
      For users who want to configure once and forget:
      
      ### Configuration via Project Knowledge
      
      Create `GITHUB_DEVLOG_CONFIG` in Project Knowledge:
      
      ```json
      {
        "repo": "username/project-name",
        "branch": "devlog",
        "auto_sync": true,
        "create_branch_from": "main"
      }
      ```
      
      ### Configuration-Aware Function
      
      ```python
      import json
      from pathlib import Path
      
      def get_devlog_config():
          """Load DEVLOG GitHub sync configuration"""
          config_path = Path("/mnt/project/GITHUB_DEVLOG_CONFIG")
      
          if config_path.exists():
              try:
                  config = json.loads(config_path.read_text())
                  return config
              except (json.JSONDecodeError, IOError):
                  pass
      
          # Default config (no sync)
          return {"auto_sync": False}
      
      
      def update_devlog_smart(data: dict):
          """
          Update DEVLOG with automatic GitHub sync based on configuration.
          """
          from iterating import update_devlog
      
          # Update local DEVLOG
          update_devlog(data)
      
          # Check if GitHub sync is configured
          config = get_devlog_config()
      
          if config.get("auto_sync"):
              try:
                  import sys
                  sys.path.append('/home/user/claude-skills/invoking-github/scripts')
                  from github_client import commit_file
      
                  devlog_content = Path("DEVLOG.md").read_text()
      
                  result = commit_file(
                      repo=config["repo"],
                      path="DEVLOG.md",
                      content=devlog_content,
                      branch=config.get("branch", "devlog"),
                      message=f"DEVLOG: {data['title']}",
                      create_branch_from=config.get("create_branch_from", "main")
                  )
      
                  print(f"✓ DEVLOG synced to {config['repo']}:{config['branch']}")
      
              except Exception as e:
                  print(f"⚠ DEVLOG sync failed: {e}")
      ```
      
      ## Session-Specific Branch Pattern
      
      Avoid conflicts by using unique branch names per session:
      
      ```python
      from datetime import datetime
      
      def update_devlog_with_session_branch(data: dict, repo: str):
          """
          Update DEVLOG and sync to session-specific branch.
          """
          from iterating import update_devlog
      
          # Update local DEVLOG
          update_devlog(data)
      
          # Generate session-specific branch name
          session_id = datetime.now().strftime("%Y%m%d-%H%M%S")
          branch_name = f"devlog-{session_id}"
      
          try:
              import sys
              sys.path.append('/home/user/claude-skills/invoking-github/scripts')
              from github_client import commit_file
      
              devlog_content = Path("DEVLOG.md").read_text()
      
              result = commit_file(
                  repo=repo,
                  path="DEVLOG.md",
                  content=devlog_content,
                  branch=branch_name,
                  message=f"DEVLOG: {data['title']}",
                  create_branch_from="main"
              )
      
              print(f"✓ DEVLOG synced to {repo}:{branch_name}")
              print(f"  Commit: {result['commit_sha'][:7]}")
              print(f"  To merge: Create PR from {branch_name} to main")
      
          except Exception as e:
              print(f"⚠ DEVLOG sync failed: {e}")
      ```
      
      ## Multi-File Session State Pattern
      
      Persist entire session state (not just DEVLOG):
      
      ```python
      def persist_session_state(
          repo: str,
          branch: str,
          files: dict[str, str],
          message: str
      ):
          """
          Persist multiple files representing session state.
      
          Args:
              repo: GitHub repository
              branch: Target branch
              files: Dict of {file_path: content}
              message: Commit message
          """
          try:
              import sys
              sys.path.append('/home/user/claude-skills/invoking-github/scripts')
              from github_client import commit_files
      
              # Convert dict to list format
              files_list = [
                  {"path": path, "content": content}
                  for path, content in files.items()
              ]
      
              result = commit_files(
                  repo=repo,
                  files=files_list,
                  branch=branch,
                  message=message,
                  create_branch_from="main"
              )
      
              print(f"✓ Session state synced: {result['files_committed']} files")
              return result
      
          except Exception as e:
              print(f"⚠ State sync failed: {e}")
              return None
      
      
      # Usage
      session_files = {
          "DEVLOG.md": Path("DEVLOG.md").read_text(),
          "session_notes.md": "# Session Notes\n...",
          "current_progress.json": json.dumps(progress_data)
      }
      
      persist_session_state(
          repo="user/project",
          branch="session-state",
          files=session_files,
          message="Session checkpoint"
      )
      ```
      
      ## Progressive Summarization Pattern
      
      Maintain both detailed and summarized DEVLOGs:
      
      ```python
      def update_devlog_with_summary(
          data: dict,
          repo: str,
          branch: str = "devlog"
      ):
          """
          Update DEVLOG and maintain a summary file.
          """
          from iterating import update_devlog
      
          # Update local DEVLOG
          update_devlog(data)
      
          # Generate or update summary
          summary = generate_devlog_summary()  # Your summarization logic
      
          try:
              import sys
              sys.path.append('/home/user/claude-skills/invoking-github/scripts')
              from github_client import commit_files
      
              devlog_content = Path("DEVLOG.md").read_text()
      
              files = [
                  {"path": "DEVLOG.md", "content": devlog_content},
                  {"path": "DEVLOG_SUMMARY.md", "content": summary}
              ]
      
              result = commit_files(
                  repo=repo,
                  files=files,
                  branch=branch,
                  message=f"DEVLOG: {data['title']} (with summary)",
                  create_branch_from="main"
              )
      
              print(f"✓ DEVLOG and summary synced")
      
          except Exception as e:
              print(f"⚠ Sync failed: {e}")
      
      
      def generate_devlog_summary():
          """
          Generate a summary of DEVLOG.md.
          Extract key decisions, milestones, open items.
          """
          devlog = Path("DEVLOG.md").read_text()
      
          # Simple summarization (you can enhance this)
          summary_parts = [
              "# DEVLOG Summary",
              "",
              "## Key Decisions",
              "<!-- Extract from DEVLOG -->",
              "",
              "## Open Items",
              "<!-- Extract from DEVLOG -->",
              "",
              "## Next Steps",
              "<!-- Extract from DEVLOG -->"
          ]
      
          return "\n".join(summary_parts)
      ```
      
      ## Cross-Environment Continuity
      
      Resume work from any Claude environment by reading DEVLOG from GitHub:
      
      ```python
      def resume_session_from_github(repo: str, branch: str = "devlog"):
          """
          Resume a session by reading DEVLOG from GitHub.
          """
          try:
              import sys
              sys.path.append('/home/user/claude-skills/invoking-github/scripts')
              from github_client import read_file
      
              devlog_content = read_file(
                  repo=repo,
                  path="DEVLOG.md",
                  branch=branch
              )
      
              # Save to local working directory
              Path("DEVLOG.md").write_text(devlog_content)
      
              print(f"✓ Resumed session from {repo}:{branch}")
              print("\nRecent entries:")
      
              # Show last 20 lines
              recent = "\n".join(devlog_content.split("\n")[-20:])
              print(recent)
      
              return devlog_content
      
          except Exception as e:
              print(f"✗ Could not resume session: {e}")
              return None
      
      
      # Usage at start of new session
      print("Resuming previous session...")
      resume_session_from_github("user/project", "devlog")
      ```
      
      ## Automatic PR Creation Pattern
      
      At session end, automatically create PR with DEVLOG:
      
      ```python
      def finalize_session_with_pr(
          data: dict,
          repo: str,
          session_branch: str,
          message: str = "Session progress"
      ):
          """
          Finalize session by syncing DEVLOG and creating PR.
          """
          from iterating import update_devlog
      
          # Update local DEVLOG
          update_devlog(data)
      
          try:
              import sys
              sys.path.append('/home/user/claude-skills/invoking-github/scripts')
              from github_client import commit_file, create_pull_request
      
              # Commit DEVLOG
              devlog_content = Path("DEVLOG.md").read_text()
      
              commit_result = commit_file(
                  repo=repo,
                  path="DEVLOG.md",
                  content=devlog_content,
                  branch=session_branch,
                  message=message,
                  create_branch_from="main"
              )
      
              print(f"✓ DEVLOG committed: {commit_result['commit_sha'][:7]}")
      
              # Create PR
              pr = create_pull_request(
                  repo=repo,
                  head=session_branch,
                  base="main",
                  title=f"Session: {data['title']}",
                  body=f"## Session Summary\n\n{message}\n\n## DEVLOG\n\nSee DEVLOG.md for details."
              )
      
              print(f"✓ PR created: {pr['html_url']}")
              return pr
      
          except Exception as e:
              print(f"⚠ Finalization failed: {e}")
              return None
      ```
      
      ## Best Practices
      
      ### 1. Graceful Degradation
      
      Always update local DEVLOG first, sync as bonus:
      
      ```python
      # Good
      update_devlog(data)  # Always succeeds
      try:
          sync_to_github()  # Bonus feature
      except:
          pass
      
      # Bad
      try:
          sync_to_github()  # If this fails, local DEVLOG not updated
          update_devlog(data)
      except:
          pass
      ```
      
      ### 2. Informative Messages
      
      Tell user what's happening:
      
      ```python
      print("Updating DEVLOG...")
      update_devlog(data)
      print("✓ DEVLOG updated locally")
      
      print("Syncing to GitHub...")
      sync_to_github()
      print("✓ Synced to GitHub")
      ```
      
      ### 3. Configuration Over Code
      
      Use Project Knowledge for configuration:
      
      ```json
      {
        "repo": "user/project",
        "branch": "devlog",
        "auto_sync": true
      }
      ```
      
      ### 4. Session Identifiers
      
      Use timestamps or UUIDs for unique branches:
      
      ```python
      # Good
      f"devlog-{datetime.now().isoformat()}"
      f"devlog-{uuid.uuid4().hex[:8]}"
      
      # Avoid
      "devlog"  # Can conflict across sessions
      ```
      
      ## Troubleshooting
      
      **DEVLOG syncs but shows conflicts**
      → Use session-specific branches
      
      **Sync is slow**
      → Normal, GitHub API is network-dependent
      
      **Sync fails silently**
      → Check error handling, print exceptions
      
      **DEVLOG not resuming correctly**
      → Verify branch name, check GitHub token permissions
      
      ## Next Steps
      
      - [SKILL.md](../SKILL.md) - Core skill documentation
      - [credential-setup.md](credential-setup.md) - Token configuration
      - [troubleshooting.md](troubleshooting.md) - Common issues
      
    • troubleshooting.md 10.3 KB
      # Troubleshooting Guide
      
      Common issues and solutions when using the invoking-github skill.
      
      ## Credential Issues
      
      ### "No GitHub API token found!"
      
      **Symptoms**:
      - Any operation fails immediately with credential error
      - Self-test shows "Token not configured"
      
      **Causes**:
      1. Token not added to Project Knowledge
      2. Wrong document title in Project Knowledge
      3. api-credentials config.json missing or malformed
      
      **Solutions**:
      
      **Check Project Knowledge**:
      - Document must be titled exactly `GITHUB_API_KEY` (all caps, underscores)
      - Content should be just the token, no extra text
      - No trailing whitespace or line breaks
      
      **Check api-credentials**:
      ```bash
      cat /home/user/claude-skills/api-credentials/config.json
      ```
      
      Should contain:
      ```json
      {
        "github_api_key": "ghp_your_token_here"
      }
      ```
      
      **Test credential detection**:
      ```python
      import sys
      sys.path.append('/home/user/claude-skills/invoking-github/scripts')
      from github_client import get_github_token
      
      try:
          token = get_github_token()
          print(f"✓ Token found: {token[:7]}...{token[-4:]}")
      except ValueError as e:
          print(e)
      ```
      
      ### "Authentication failed" (401)
      
      **Symptoms**:
      - Token is detected but API calls fail with 401
      - "Authentication failed" error message
      
      **Causes**:
      1. Token has expired
      2. Token was revoked
      3. Token contains typos or extra characters
      
      **Solutions**:
      
      **Check token status on GitHub**:
      1. Go to https://github.com/settings/tokens
      2. Find your token
      3. Check if it's expired or revoked
      
      **Generate new token**:
      1. Create new token at https://github.com/settings/tokens
      2. Update Project Knowledge or config.json
      3. Retry operation
      
      **Verify no extra characters**:
      - Token should start with `ghp_` (classic) or `github_pat_` (fine-grained)
      - No spaces, quotes, or line breaks
      - Exact length: 40 characters (classic) or longer (fine-grained)
      
      ### "Access denied" (403)
      
      **Symptoms**:
      - Authentication works but specific operations fail
      - "Access denied" or "insufficient permissions" errors
      
      **Causes**:
      1. Token lacks required permissions
      2. Repository is private and token doesn't have access
      3. Rate limit exceeded
      
      **Solutions**:
      
      **Check token permissions**:
      - For fine-grained tokens:
        - Repository access: Verify target repo is in the list
        - Permissions: Contents (read/write), Pull requests (read/write)
      - For classic tokens:
        - Scope: Must have `repo` (or `public_repo` for public repos only)
      
      **Check rate limits**:
      ```python
      import sys
      sys.path.append('/home/user/claude-skills/invoking-github/scripts')
      from github_client import _make_api_request
      
      response = _make_api_request("/rate_limit")
      print(f"Remaining: {response['rate']['remaining']}/{response['rate']['limit']}")
      print(f"Reset at: {response['rate']['reset']}")
      ```
      
      If rate limited:
      - Wait until reset time
      - Reduce request frequency
      - Consider using different token
      
      ## Repository Issues
      
      ### "Resource not found" (404)
      
      **Symptoms**:
      - Repository, branch, or file not found
      - "404" error message
      
      **Causes**:
      1. Repository name is incorrect
      2. Branch doesn't exist
      3. File path is wrong
      4. Repository is private and token doesn't have access
      
      **Solutions**:
      
      **Verify repository format**:
      - Correct: `username/repo-name` or `organization/repo-name`
      - Incorrect: `https://github.com/username/repo-name`
      - Incorrect: `username-repo-name`
      - Incorrect: `repo-name` (missing owner)
      
      **Check branch exists**:
      ```python
      import sys
      sys.path.append('/home/user/claude-skills/invoking-github/scripts')
      from github_client import _make_api_request
      
      # List all branches
      response = _make_api_request("/repos/username/repo-name/branches")
      branches = [b['name'] for b in response]
      print(f"Available branches: {branches}")
      ```
      
      **Verify file path**:
      - Use forward slashes: `src/main.py` not `src\main.py`
      - No leading slash: `README.md` not `/README.md`
      - Case-sensitive: `readme.md` ≠ `README.md`
      
      ### "Conflict" (409)
      
      **Symptoms**:
      - Commit fails with 409 error
      - "Conflict" or "file was modified" message
      
      **Causes**:
      1. File was modified since last read
      2. Multiple concurrent commits to same file
      3. Branch was force-pushed
      
      **Solutions**:
      
      **Read file again before committing**:
      ```python
      from github_client import read_file, commit_file
      
      # Read current version
      current = read_file(repo, path, branch)
      
      # Make your changes
      new_content = modify(current)
      
      # Commit (will have latest SHA)
      commit_file(repo, path, new_content, branch, "Update")
      ```
      
      **Use unique branches per session**:
      ```python
      from datetime import datetime
      
      branch = f"feature-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
      commit_file(repo, path, content, branch, "Update", create_branch_from="main")
      ```
      
      **Check for concurrent modifications**:
      - Ensure only one Claude session is modifying the file
      - Use session-specific branches
      - Coordinate with team members
      
      ## File Operation Issues
      
      ### File content is corrupted
      
      **Symptoms**:
      - File appears as binary/garbage after commit
      - Encoding errors when reading
      
      **Causes**:
      1. Binary file being treated as text
      2. Encoding issues
      
      **Solutions**:
      
      **This skill is for text files only**:
      - Supports: .txt, .md, .py, .js, .json, .yaml, etc.
      - Not supported: Images, PDFs, executables, archives
      
      **Ensure UTF-8 encoding**:
      ```python
      # Read with explicit encoding
      content = Path("file.txt").read_text(encoding='utf-8')
      
      # Commit
      commit_file(repo, path, content, branch, "Update")
      ```
      
      ### Large file fails to commit
      
      **Symptoms**:
      - Commit fails for large files
      - Timeout errors
      
      **Causes**:
      1. File exceeds GitHub API limits (~100MB)
      2. Network timeout
      
      **Solutions**:
      
      **Check file size**:
      ```python
      size = len(content.encode('utf-8'))
      if size > 100 * 1024 * 1024:  # 100MB
          print(f"File too large: {size / (1024*1024):.1f}MB")
      ```
      
      **Split large files**:
      - Break into smaller chunks
      - Use Git LFS (not supported by this skill)
      - Consider alternative storage
      
      ## Network Issues
      
      ### "Network error" or timeouts
      
      **Symptoms**:
      - Operations fail with network errors
      - Timeouts during API calls
      
      **Causes**:
      1. No internet connection
      2. GitHub API is down
      3. Firewall blocking requests
      
      **Solutions**:
      
      **Check internet connection**:
      ```bash
      curl -I https://api.github.com
      ```
      
      **Check GitHub status**:
      - Visit https://www.githubstatus.com
      - Check for ongoing incidents
      
      **Retry with backoff**:
      ```python
      import time
      
      for attempt in range(3):
          try:
              result = commit_file(...)
              break
          except Exception as e:
              if attempt < 2:
                  wait = 2 ** attempt
                  print(f"Retry in {wait}s...")
                  time.sleep(wait)
              else:
                  raise
      ```
      
      ## Integration Issues
      
      ### DEVLOG sync silently fails
      
      **Symptoms**:
      - Local DEVLOG updates but no GitHub commit
      - No error messages
      
      **Causes**:
      1. Exception is being caught and suppressed
      2. auto_sync is False
      
      **Solutions**:
      
      **Add logging**:
      ```python
      try:
          sync_to_github()
      except Exception as e:
          print(f"⚠ Sync failed: {e}")
          import traceback
          traceback.print_exc()
      ```
      
      **Check configuration**:
      ```python
      config = get_devlog_config()
      print(f"auto_sync: {config.get('auto_sync')}")
      print(f"repo: {config.get('repo')}")
      ```
      
      ### Can't import github_client
      
      **Symptoms**:
      - `ImportError: No module named github_client`
      - `ModuleNotFoundError`
      
      **Causes**:
      1. Path not in sys.path
      2. Skill not installed
      
      **Solutions**:
      
      **Add to path explicitly**:
      ```python
      import sys
      sys.path.insert(0, '/home/user/claude-skills/invoking-github/scripts')
      from github_client import commit_file
      ```
      
      **Verify skill exists**:
      ```bash
      ls -la /home/user/claude-skills/invoking-github/scripts/
      ```
      
      Should show:
      - `github_client.py`
      - `__init__.py`
      
      ## API Behavior Issues
      
      ### Commit succeeds but file doesn't update
      
      **Symptoms**:
      - Commit returns success
      - File on GitHub hasn't changed
      
      **Causes**:
      1. Committing to wrong branch
      2. Looking at different branch on GitHub
      
      **Solutions**:
      
      **Verify branch**:
      ```python
      result = commit_file(repo, path, content, branch, "Update")
      print(f"Committed to branch: {result['branch']}")
      print(f"Commit SHA: {result['commit_sha']}")
      ```
      
      **Check GitHub UI**:
      - Make sure you're viewing the correct branch
      - Click branch dropdown and select your target branch
      
      ### PR creation fails: "A pull request already exists"
      
      **Symptoms**:
      - PR creation fails with "already exists" error
      
      **Causes**:
      1. PR from head to base already exists
      2. Previous PR wasn't closed
      
      **Solutions**:
      
      **List existing PRs**:
      ```python
      from github_client import _make_api_request
      
      response = _make_api_request(f"/repos/{repo}/pulls?state=open")
      for pr in response:
          print(f"PR #{pr['number']}: {pr['head']['ref']} → {pr['base']['ref']}")
      ```
      
      **Close or merge existing PR first**:
      - Go to GitHub and close/merge the existing PR
      - Or use a different head branch
      
      ## Performance Issues
      
      ### Operations are slow
      
      **Symptoms**:
      - Each commit takes several seconds
      - Batch operations take a long time
      
      **Causes**:
      1. Network latency
      2. Large files
      3. Many files in batch
      
      **Solutions**:
      
      **Use batch operations**:
      ```python
      # Slow: Multiple individual commits
      for file in files:
          commit_file(repo, file['path'], file['content'], branch, f"Update {file['path']}")
      
      # Fast: Single batch commit
      commit_files(repo, files, branch, "Update multiple files")
      ```
      
      **Minimize requests**:
      - Don't read file before every commit if you already have content
      - Use commit_files() instead of multiple commit_file() calls
      
      ## Getting Help
      
      If you're still stuck:
      
      1. **Check GitHub API status**: https://www.githubstatus.com
      2. **Verify token permissions**: https://github.com/settings/tokens
      3. **Test with GitHub CLI**: Try same operation with `gh` CLI
      4. **Check GitHub docs**: https://docs.github.com/rest
      
      **Provide this info when asking for help**:
      - Error message (full text)
      - Operation you're trying to perform
      - Repository name (if not sensitive)
      - Token type (classic vs fine-grained)
      - Whether credential test passes
      
      ## Common Error Reference
      
      | Error Code | Meaning | Common Solution |
      |------------|---------|-----------------|
      | 401 | Authentication failed | Check token is valid and not expired |
      | 403 | Access denied | Check token permissions and rate limits |
      | 404 | Not found | Verify repo/branch/file exists and is accessible |
      | 409 | Conflict | Re-read file, use unique branch names |
      | 422 | Validation error | Check branch name format, commit message |
      
      ## Next Steps
      
      - [credential-setup.md](credential-setup.md) - Token configuration guide
      - [SKILL.md](../SKILL.md) - Main skill documentation
      - [GitHub API Docs](https://docs.github.com/rest) - Official API reference
      
  • scripts
    • github_client.py 16.1 KB
      #!/usr/bin/env python3
      """
      GitHub API Client for Claude.ai Chat Environments
      
      Provides programmatic GitHub operations (read/write/commit/PR) using REST API.
      Designed for claude.ai chat where native git access isn't available.
      """
      
      import base64
      import json
      import os
      from pathlib import Path
      from urllib import error, parse, request
      
      
      class GitHubAPIError(Exception):
          """
          Custom exception for GitHub API errors.
      
          Attributes:
              message: Error description
              status_code: HTTP status code (if applicable)
              response: Full response data (if available)
          """
          def __init__(self, message: str, status_code: int | None = None, response: dict | None = None):
              self.message = message
              self.status_code = status_code
              self.response = response
              super().__init__(self.message)
      
      
      def get_github_token() -> str:
          """
          Get GitHub API token from environment or project knowledge files.
      
          Priority order:
          1. GH_TOKEN or GITHUB_TOKEN environment variable
          2. GitHub.env project file (GH_TOKEN=...)
          3. Individual file: /mnt/project/GITHUB_API_KEY.txt
          4. Combined file: /mnt/project/API_CREDENTIALS.json
      
          Returns:
              str: GitHub Personal Access Token
      
          Raises:
              ValueError: If no token found in any source
          """
          # Pattern 0: Environment variables (standard conventions)
          for var in ("GH_TOKEN", "GITHUB_TOKEN", "GITHUB_API_KEY"):
              token = os.environ.get(var, "").strip()
              if token:
                  return token
      
          # Pattern 0b: GitHub.env project file
          gh_env = Path("/mnt/project/GitHub.env")
          if gh_env.exists():
              try:
                  for line in gh_env.read_text().splitlines():
                      line = line.strip()
                      if line.startswith("GH_TOKEN="):
                          token = line[len("GH_TOKEN="):].strip().strip('"').strip("'")
                          if token:
                              return token
              except OSError:
                  pass  # Fall through to other methods
      
          # Pattern 1: Individual key file (recommended)
          key_file = Path("/mnt/project/GITHUB_API_KEY.txt")
          if key_file.exists():
              try:
                  token = key_file.read_text().strip()
                  if token:
                      return token
              except OSError as e:
                  raise ValueError(
                      f"Found GITHUB_API_KEY.txt but couldn't read it: {e}\n"
                      f"Please check file permissions or recreate the file"
                  )
      
          # Pattern 2: Combined credentials file
          creds_file = Path("/mnt/project/API_CREDENTIALS.json")
          if creds_file.exists():
              try:
                  with open(creds_file) as f:
                      config = json.load(f)
                      token = config.get("github_api_key", "").strip()
                      if token:
                          return token
              except (json.JSONDecodeError, OSError) as e:
                  raise ValueError(
                      f"Found API_CREDENTIALS.json but couldn't parse it: {e}\n"
                      f"Please check file format"
                  )
      
          # No token found - provide helpful error message
          raise ValueError(
              "No GitHub API token found!\n\n"
              "Provide a token using one of these methods:\n\n"
              "Option 1: Environment variable\n"
              "  export GH_TOKEN=ghp_...\n\n"
              "Option 2: GitHub.env project file\n"
              "  GH_TOKEN=ghp_...\n\n"
              "Option 3: Individual file\n"
              "  File: GITHUB_API_KEY.txt\n"
              "  Content: ghp_...\n\n"
              "Option 4: Combined file\n"
              "  File: API_CREDENTIALS.json\n"
              "  Content: {\"github_api_key\": \"ghp_...\"}\n\n"
              "Generate token at: https://github.com/settings/tokens\n"
              "Required scopes:\n"
              "  - Classic token: 'repo' scope\n"
              "  - Fine-grained token: Repository permissions → Contents (read/write) + Pull requests (read/write)"
          )
      
      
      def _make_api_request(
          endpoint: str,
          method: str = "GET",
          data: dict | None = None,
          token: str | None = None
      ) -> dict:
          """
          Make authenticated GitHub API request.
      
          Args:
              endpoint: API endpoint (e.g., "/repos/owner/name/contents/file")
              method: HTTP method (GET, POST, PUT, PATCH, DELETE)
              data: Request body (will be JSON-encoded)
              token: GitHub API token (if None, will fetch automatically)
      
          Returns:
              Dict: Parsed JSON response
      
          Raises:
              GitHubAPIError: On API errors
          """
          if token is None:
              token = get_github_token()
      
          url = f"https://api.github.com{endpoint}"
      
          headers = {
              "Authorization": f"Bearer {token}",
              "Accept": "application/vnd.github+json",
              "X-GitHub-Api-Version": "2022-11-28",
              "User-Agent": "Claude-Invoking-GitHub-Skill/1.0"
          }
      
          # Prepare request
          req_data = None
          if data is not None:
              req_data = json.dumps(data).encode('utf-8')
              headers["Content-Type"] = "application/json"
      
          req = request.Request(url, data=req_data, headers=headers, method=method)
      
          try:
              with request.urlopen(req) as response:
                  response_data = response.read().decode('utf-8')
                  if response_data:
                      return json.loads(response_data)
                  return {}
      
          except error.HTTPError as e:
              error_body = e.read().decode('utf-8')
              try:
                  error_json = json.loads(error_body)
                  error_message = error_json.get('message', str(e))
              except json.JSONDecodeError:
                  error_message = error_body or str(e)
      
              # Provide helpful error messages based on status code
              if e.code == 401:
                  raise GitHubAPIError(
                      "Authentication failed. Please check your GitHub token is valid and not expired.",
                      status_code=401,
                      response=error_json if 'error_json' in locals() else None
                  )
              elif e.code == 403:
                  if 'rate limit' in error_message.lower():
                      raise GitHubAPIError(
                          "GitHub API rate limit exceeded. Please wait before making more requests.",
                          status_code=403,
                          response=error_json if 'error_json' in locals() else None
                      )
                  else:
                      raise GitHubAPIError(
                          f"Access denied. Check that your token has required permissions: {error_message}",
                          status_code=403,
                          response=error_json if 'error_json' in locals() else None
                      )
              elif e.code == 404:
                  raise GitHubAPIError(
                      f"Resource not found: {error_message}",
                      status_code=404,
                      response=error_json if 'error_json' in locals() else None
                  )
              elif e.code == 409:
                  raise GitHubAPIError(
                      f"Conflict: {error_message}. The resource may have been modified concurrently.",
                      status_code=409,
                      response=error_json if 'error_json' in locals() else None
                  )
              elif e.code == 422:
                  raise GitHubAPIError(
                      f"Validation failed: {error_message}",
                      status_code=422,
                      response=error_json if 'error_json' in locals() else None
                  )
              else:
                  raise GitHubAPIError(
                      f"GitHub API error ({e.code}): {error_message}",
                      status_code=e.code,
                      response=error_json if 'error_json' in locals() else None
                  )
      
          except error.URLError as e:
              raise GitHubAPIError(f"Network error: {e.reason}")
      
      
      def read_file(repo: str, path: str, branch: str = "main") -> str:
          """
          Read a file from GitHub repository.
      
          Args:
              repo: Repository in format "owner/name"
              path: File path within repository
              branch: Branch name (default: main)
      
          Returns:
              str: File content
      
          Raises:
              GitHubAPIError: If file not found or access denied
      
          Example:
              >>> content = read_file("octocat/Hello-World", "README", "main")
              >>> print(content)
          """
          endpoint = f"/repos/{repo}/contents/{path}"
          params = {"ref": branch}
          endpoint_with_params = f"{endpoint}?{parse.urlencode(params)}"
      
          response = _make_api_request(endpoint_with_params, method="GET")
      
          if response.get("type") != "file":
              raise GitHubAPIError(f"'{path}' is not a file (type: {response.get('type')})")
      
          # GitHub returns content as base64-encoded
          content_b64 = response.get("content", "")
          content = base64.b64decode(content_b64).decode('utf-8')
      
          return content
      
      
      def commit_file(
          repo: str,
          path: str,
          content: str,
          branch: str,
          message: str,
          create_branch_from: str | None = None
      ) -> dict:
          """
          Commit a single file (create or update).
      
          Args:
              repo: Repository in format "owner/name"
              path: File path within repository
              content: New file content
              branch: Target branch
              message: Commit message
              create_branch_from: If branch doesn't exist, create from this branch
      
          Returns:
              Dict with commit_sha, branch, file_path
      
          Raises:
              GitHubAPIError: On conflicts or auth failures
      
          Example:
              >>> result = commit_file(
              ...     repo="user/repo",
              ...     path="README.md",
              ...     content="# Hello World",
              ...     branch="main",
              ...     message="Update README"
              ... )
              >>> print(f"Committed: {result['commit_sha']}")
          """
          # Check if branch exists, create if needed
          if create_branch_from:
              _ensure_branch_exists(repo, branch, create_branch_from)
      
          # Get current file SHA if it exists (needed for updates)
          file_sha = None
          try:
              current_file = _make_api_request(
                  f"/repos/{repo}/contents/{path}?ref={branch}",
                  method="GET"
              )
              file_sha = current_file.get("sha")
          except GitHubAPIError as e:
              if e.status_code != 404:
                  raise  # Re-raise if not a "file not found" error
              # File doesn't exist, will be created
      
          # Encode content as base64
          content_b64 = base64.b64encode(content.encode('utf-8')).decode('utf-8')
      
          # Commit file
          data = {
              "message": message,
              "content": content_b64,
              "branch": branch
          }
      
          if file_sha:
              data["sha"] = file_sha  # Required for updates
      
          response = _make_api_request(
              f"/repos/{repo}/contents/{path}",
              method="PUT",
              data=data
          )
      
          return {
              "commit_sha": response["commit"]["sha"],
              "branch": branch,
              "file_path": path
          }
      
      
      def commit_files(
          repo: str,
          files: list[dict[str, str]],
          branch: str,
          message: str,
          create_branch_from: str | None = None
      ) -> dict:
          """
          Commit multiple files in a single commit using Git Trees API.
      
          Args:
              repo: Repository in format "owner/name"
              files: List of dicts with 'path' and 'content' keys
              branch: Target branch
              message: Commit message
              create_branch_from: If branch doesn't exist, create from this branch
      
          Returns:
              Dict with commit_sha, branch, files_committed (count)
      
          Raises:
              GitHubAPIError: On failures
      
          Example:
              >>> files = [
              ...     {"path": "file1.txt", "content": "Content 1"},
              ...     {"path": "dir/file2.txt", "content": "Content 2"}
              ... ]
              >>> result = commit_files(
              ...     repo="user/repo",
              ...     files=files,
              ...     branch="main",
              ...     message="Add multiple files"
              ... )
              >>> print(f"Committed {result['files_committed']} files")
          """
          # Check if branch exists, create if needed
          if create_branch_from:
              _ensure_branch_exists(repo, branch, create_branch_from)
      
          # Get current branch reference
          ref_data = _make_api_request(f"/repos/{repo}/git/refs/heads/{branch}")
          base_commit_sha = ref_data["object"]["sha"]
      
          # Get base tree SHA
          base_commit = _make_api_request(f"/repos/{repo}/git/commits/{base_commit_sha}")
          base_tree_sha = base_commit["tree"]["sha"]
      
          # Create blobs for each file
          tree_items = []
          for file_info in files:
              # Create blob
              blob_data = {
                  "content": file_info["content"],
                  "encoding": "utf-8"
              }
              blob_response = _make_api_request(
                  f"/repos/{repo}/git/blobs",
                  method="POST",
                  data=blob_data
              )
      
              tree_items.append({
                  "path": file_info["path"],
                  "mode": "100644",  # Regular file
                  "type": "blob",
                  "sha": blob_response["sha"]
              })
      
          # Create tree
          tree_data = {
              "base_tree": base_tree_sha,
              "tree": tree_items
          }
          tree_response = _make_api_request(
              f"/repos/{repo}/git/trees",
              method="POST",
              data=tree_data
          )
      
          # Create commit
          commit_data = {
              "message": message,
              "tree": tree_response["sha"],
              "parents": [base_commit_sha]
          }
          commit_response = _make_api_request(
              f"/repos/{repo}/git/commits",
              method="POST",
              data=commit_data
          )
      
          # Update branch reference
          _make_api_request(
              f"/repos/{repo}/git/refs/heads/{branch}",
              method="PATCH",
              data={"sha": commit_response["sha"]}
          )
      
          return {
              "commit_sha": commit_response["sha"],
              "branch": branch,
              "files_committed": len(files)
          }
      
      
      def create_pull_request(
          repo: str,
          head: str,
          base: str,
          title: str,
          body: str = ""
      ) -> dict:
          """
          Create a pull request.
      
          Args:
              repo: Repository in format "owner/name"
              head: Source branch (where your changes are)
              base: Target branch (where you want to merge)
              title: PR title
              body: PR description (supports markdown)
      
          Returns:
              Dict with number, html_url, state
      
          Raises:
              GitHubAPIError: If branches invalid or PR already exists
      
          Example:
              >>> pr = create_pull_request(
              ...     repo="user/repo",
              ...     head="feature-branch",
              ...     base="main",
              ...     title="Add new feature",
              ...     body="## Changes\\n- Added feature X"
              ... )
              >>> print(f"PR created: {pr['html_url']}")
          """
          data = {
              "title": title,
              "head": head,
              "base": base,
              "body": body
          }
      
          response = _make_api_request(
              f"/repos/{repo}/pulls",
              method="POST",
              data=data
          )
      
          return {
              "number": response["number"],
              "html_url": response["html_url"],
              "state": response["state"]
          }
      
      
      def _ensure_branch_exists(repo: str, branch: str, create_from: str) -> None:
          """
          Ensure a branch exists, creating it if necessary.
      
          Args:
              repo: Repository in format "owner/name"
              branch: Branch name to ensure exists
              create_from: Branch to create from if it doesn't exist
      
          Raises:
              GitHubAPIError: On failures
          """
          try:
              # Check if branch exists
              _make_api_request(f"/repos/{repo}/git/refs/heads/{branch}")
              # Branch exists, nothing to do
          except GitHubAPIError as e:
              if e.status_code == 404:
                  # Branch doesn't exist, create it
                  # Get SHA of source branch
                  source_ref = _make_api_request(f"/repos/{repo}/git/refs/heads/{create_from}")
                  source_sha = source_ref["object"]["sha"]
      
                  # Create new branch
                  _make_api_request(
                      f"/repos/{repo}/git/refs",
                      method="POST",
                      data={
                          "ref": f"refs/heads/{branch}",
                          "sha": source_sha
                      }
                  )
              else:
                  raise  # Re-raise other errors
      
      
      # Export main functions
      __all__ = [
          'GitHubAPIError',
          'commit_file',
          'commit_files',
          'create_pull_request',
          'get_github_token',
          'read_file'
      ]
      
      
      if __name__ == "__main__":
          # Self-test
          print("GitHub Client Self-Test")
          print("=" * 50)
      
          try:
              token = get_github_token()
              masked = f"{token[:7]}...{token[-4:]}" if len(token) > 11 else "***"
              print(f"✓ GitHub token found: {masked}")
              print(f"  Token length: {len(token)} characters")
          except ValueError as e:
              print("✗ GitHub token not configured")
              print(f"\n{e}")
      
    • __init__.py 498 B
      """
      Invoking GitHub Skill - GitHub API Client for Claude.ai Chat
      
      Provides programmatic GitHub operations for claude.ai chat environments
      where native git access isn't available.
      """
      
      from .github_client import (
          GitHubAPIError,
          commit_file,
          commit_files,
          create_pull_request,
          get_github_token,
          read_file,
      )
      
      __all__ = [
          'GitHubAPIError',
          'commit_file',
          'commit_files',
          'create_pull_request',
          'get_github_token',
          'read_file'
      ]
      
      __version__ = "1.0.0"
      
  • CHANGELOG.md 369 B
    # invoking-github - Changelog
    
    All notable changes to the `invoking-github` skill are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
    
    ## [0.1.0] - 2026-09-09
    
    ### Other
    
    - prompt-audit: dated prompting patterns across the skill catalogue (#791)
    - Deprecate mapping-codebases; adopt ruff 0.16.0 baseline (#747)
    
  • README.md 297 B
    # invoking-github
    
    Enables GitHub repository operations (read/write/commit/PR) for Claude.ai chat environments. Use when users request GitHub commits, repository updates, DEVLOG persistence, or cross-session state management via GitHub branches. Not needed in Claude Code (has native git access).
    
  • SKILL.md 10.1 KB
    ---
    name: invoking-github
    description: Enables GitHub repository operations (read/write/commit/PR) for Claude.ai chat environments. Use when users request GitHub commits, repository updates, DEVLOG persistence, or cross-session state management via GitHub branches. Not needed in Claude Code (has native git access).
    metadata:
      version: 0.1.0
    ---
    
    # Invoking GitHub
    
    Programmatically interact with GitHub repositories from Claude.ai chat: read files, commit changes, create PRs, and persist state across sessions.
    
    ## When to Use This Skill
    
    **Not needed for:**
    - Claude Code environments (use native git commands)
    - Read-only repository access (use GitHub UI or API directly)
    
    ## Quick Start
    
    ### Prerequisites
    
    Create a GitHub Personal Access Token and add to Project Knowledge:
    
    1. Go to https://github.com/settings/tokens
    2. Create new token (classic or fine-grained)
    3. Required scopes: `repo` (or `public_repo` for public repos only)
    4. In Claude.ai, add to Project Knowledge:
       - Title: `GITHUB_API_KEY`
       - Content: Your token (e.g., `ghp_abc123...`)
    
    ### Single File Commit
    
    ```python
    from invoking_github import commit_file
    
    result = commit_file(
        repo="username/repo-name",
        path="README.md",
        content="# Updated README\n\nNew content here...",
        branch="main",
        message="Update README with new instructions"
    )
    
    print(f"Committed: {result['commit_sha']}")
    ```
    
    ### Read File
    
    ```python
    from invoking_github import read_file
    
    content = read_file(
        repo="username/repo-name",
        path="config.json",
        branch="main"
    )
    
    print(content)
    ```
    
    ### Batch Commit (Multiple Files)
    
    ```python
    from invoking_github import commit_files
    
    files = [
        {"path": "src/main.py", "content": "# Python code..."},
        {"path": "README.md", "content": "# Updated docs..."},
        {"path": "tests/test.py", "content": "# Tests..."}
    ]
    
    result = commit_files(
        repo="username/repo-name",
        files=files,
        branch="feature-branch",
        message="Add new feature implementation",
        create_branch_from="main"  # Create branch if it doesn't exist
    )
    
    print(f"Committed {len(files)} files: {result['commit_sha']}")
    ```
    
    ### Create Pull Request
    
    ```python
    from invoking_github import create_pull_request
    
    pr = create_pull_request(
        repo="username/repo-name",
        head="feature-branch",
        base="main",
        title="Add new feature",
        body="## Changes\n- Implemented feature X\n- Updated docs\n- Added tests"
    )
    
    print(f"PR created: {pr['html_url']}")
    ```
    
    ## Core Functions
    
    ### `read_file()`
    
    Read a file from repository:
    
    ```python
    read_file(
        repo: str,           # "owner/name"
        path: str,           # "path/to/file.py"
        branch: str = "main" # Branch name
    ) -> str
    ```
    
    **Returns**: File content as string
    **Raises**: `GitHubAPIError` if file not found or access denied
    
    ### `commit_file()`
    
    Commit a single file (create or update):
    
    ```python
    commit_file(
        repo: str,                      # "owner/name"
        path: str,                      # "path/to/file.py"
        content: str,                   # New file content
        branch: str,                    # Target branch
        message: str,                   # Commit message
        create_branch_from: str = None  # Create branch from this if doesn't exist
    ) -> dict
    ```
    
    **Returns**: Dict with `commit_sha`, `branch`, `file_path`
    **Raises**: `GitHubAPIError` on conflicts or auth failures
    
    ### `commit_files()`
    
    Commit multiple files in a single commit:
    
    ```python
    commit_files(
        repo: str,                      # "owner/name"
        files: list[dict],              # [{"path": "...", "content": "..."}]
        branch: str,                    # Target branch
        message: str,                   # Commit message
        create_branch_from: str = None  # Create branch from this if doesn't exist
    ) -> dict
    ```
    
    **Returns**: Dict with `commit_sha`, `branch`, `files_committed`
    **Raises**: `GitHubAPIError` on failures
    
    **Note**: Uses Git Trees API for efficiency - atomic commit of all files.
    
    ### `create_pull_request()`
    
    Create a pull request:
    
    ```python
    create_pull_request(
        repo: str,      # "owner/name"
        head: str,      # Source branch (your changes)
        base: str,      # Target branch (where to merge)
        title: str,     # PR title
        body: str = ""  # PR description (supports markdown)
    ) -> dict
    ```
    
    **Returns**: Dict with `number`, `html_url`, `state`
    **Raises**: `GitHubAPIError` if branches invalid or PR exists
    
    ## Credential Configuration
    
    This skill requires a GitHub Personal Access Token. Two configuration methods:
    
    ### Method 1: Project Knowledge (Recommended)
    
    Best for Claude.ai chat users (mobile/web):
    
    1. Create token at https://github.com/settings/tokens
    2. In Claude.ai Project settings → Add to Project Knowledge
    3. Create document titled `GITHUB_API_KEY`
    4. Paste your token as content
    
    **Permissions required**:
    - Classic token: `repo` scope
    - Fine-grained token: Repository permissions → Contents (read/write) and Pull requests (read/write)
    
    ### Method 2: API Credentials Skill (Fallback)
    
    Alternatively, use combined credentials file:
    
    1. Create `API_CREDENTIALS.json` in project knowledge
    2. Add: `{"github_api_key": "ghp_your-token-here"}`
    
    ## Integration with Iterating Skill
    
    Auto-persist DEVLOG.md to GitHub for cross-session continuity:
    
    ```python
    # In your DEVLOG update function
    from invoking_github import commit_file
    from pathlib import Path
    
    def update_devlog_with_sync(data, repo="user/project", branch="devlog"):
        """Update DEVLOG.md locally and sync to GitHub"""
    
        # Update local DEVLOG
        update_devlog(data)  # Your existing function
    
        # Auto-sync to GitHub
        try:
            devlog_content = Path("DEVLOG.md").read_text()
    
            result = commit_file(
                repo=repo,
                path="DEVLOG.md",
                content=devlog_content,
                branch=branch,
                message=f"DEVLOG: {data['title']}",
                create_branch_from="main"
            )
    
            print(f"✓ DEVLOG synced to GitHub ({repo}:{branch})")
            return result
    
        except Exception as e:
            print(f"⚠ DEVLOG sync failed: {e}")
            # Continue anyway - local DEVLOG.md still updated
            return None
    ```
    
    **Benefits:**
    - Automatic backup of session progress
    - Cross-device access to development logs
    - Git history of decisions and progress
    - No manual copy/paste to Project Knowledge
    
    See [references/iterating-integration.md](references/iterating-integration.md) for complete patterns.
    
    ## Error Handling
    
    All functions raise `GitHubAPIError` with descriptive messages:
    
    ```python
    from invoking_github import commit_file, GitHubAPIError
    
    try:
        result = commit_file(
            repo="user/repo",
            path="file.py",
            content="...",
            branch="main",
            message="Update"
        )
    except GitHubAPIError as e:
        if e.status_code == 404:
            print("Repository or branch not found")
        elif e.status_code == 401:
            print("Authentication failed - check your token")
        elif e.status_code == 403:
            print("Access denied - check token permissions")
        elif e.status_code == 409:
            print("Conflict - file was modified since last read")
        else:
            print(f"GitHub API error: {e}")
    ```
    
    **Common errors:**
    - **404**: Repository, branch, or file not found
    - **401/403**: Invalid token or insufficient permissions
    - **409**: Merge conflict (file changed concurrently)
    - **422**: Validation error (invalid branch name, etc.)
    - **Rate limit**: Too many requests (wait before retrying)
    
    ## Best Practices
    
    1. **Use descriptive commit messages**
       - Bad: "Update files"
       - Good: "Add authentication middleware and update user routes"
    
    2. **Batch commits when possible**
       - Use `commit_files()` for related changes
       - Single atomic commit = cleaner git history
    
    3. **Create feature branches**
       - Don't commit directly to main
       - Use `create_branch_from="main"` parameter
       - Create PR for review
    
    4. **Secure token management**
       - Use fine-grained tokens with minimal scopes
       - Set expiration dates
       - Rotate regularly
       - Never commit tokens to repositories
    
    ## Advanced Usage
    
    ### Conditional Commits
    
    Only commit if file content changed:
    
    ```python
    from invoking_github import read_file, commit_file, GitHubAPIError
    
    try:
        current_content = read_file(repo, path, branch)
        if current_content != new_content:
            commit_file(repo, path, new_content, branch, "Update file")
        else:
            print("No changes detected, skipping commit")
    except GitHubAPIError as e:
        if e.status_code == 404:
            # File doesn't exist, create it
            commit_file(repo, path, new_content, branch, "Create file")
        else:
            raise
    ```
    
    ### Session-Specific Branches
    
    Avoid conflicts with unique branch names:
    
    ```python
    import datetime
    
    session_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
    branch_name = f"devlog-{session_id}"
    
    commit_file(
        repo="user/project",
        path="DEVLOG.md",
        content=devlog_content,
        branch=branch_name,
        message="Session progress",
        create_branch_from="main"
    )
    ```
    
    ### Multi-Repository Workflows
    
    Work across multiple repos:
    
    ```python
    repos = ["user/frontend", "user/backend", "user/docs"]
    
    for repo in repos:
        commit_file(
            repo=repo,
            path="VERSION",
            content="2.0.0\n",
            branch="release-2.0",
            message="Bump version to 2.0.0"
        )
    ```
    
    ## Limitations
    
    - **Target environment**: Claude.ai chat only (not Claude Code)
    - **No OAuth**: Must use Personal Access Tokens manually
    - **No git operations**: Pure REST API (no clone, pull, rebase, etc.)
    - **File size**: GitHub API limits ~100MB per file
    - **Rate limits**: 5000 requests/hour for authenticated users
    - **Network required**: All operations require internet access
    
    ## See Also
    
    - [references/credential-setup.md](references/credential-setup.md) - Step-by-step credential guide
    - [references/iterating-integration.md](references/iterating-integration.md) - DEVLOG auto-sync patterns
    - [references/troubleshooting.md](references/troubleshooting.md) - Common issues and solutions
    - [GitHub REST API Docs](https://docs.github.com/rest) - Official GitHub API reference
    
    ## Token Efficiency
    
    This skill uses ~800 tokens when loaded but provides essential GitHub operations for claude.ai chat environments where native git access isn't available. Enables persistent state management and cross-session workflows.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related