"""
Claude Code integration - AI-powered coding using Anthropic API.
Uses GitHub API directly (no git binary required).
"""
import os
import re
import tempfile
from typing import Optional, Dict, Any, List, Tuple
from anthropic import Anthropic
import requests
import logging
REQUEST_TIMEOUT = (5, 30)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def generate_code_with_claude(feature_description: str, repo_context: str, anthropic_key: str) -> str:
"""
Generate code using Claude API.
Args:
feature_description: What the user wants to implement
repo_context: Context about the repository (structure, files, etc.)
anthropic_key: User's Anthropic API key
Returns:
Generated code/changes as a string
"""
client = Anthropic(api_key=anthropic_key)
prompt = f"""You are an expert software engineer. Generate code to implement the following feature:
Feature Request: {feature_description}
Repository Context:
{repo_context}
Please provide the complete code changes needed. Format your response as:
FILE: path/to/file.py
```python
# complete file contents here
```
FILE: path/to/another/file.js
```javascript
// complete file contents here
```
EXPLANATION:
What this does and why
Be specific about which files to create or modify. Include the full file contents for each file.
If the feature is very simple (like adding a test file), just create that one file without overthinking it.
"""
logger.info(f"Generating code with Claude for: {feature_description}")
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8000,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
def parse_code_changes(changes: str) -> List[Tuple[str, str]]:
"""
Parse Claude's response to extract file paths and their contents.
Args:
changes: Raw text from Claude with FILE: markers and code blocks
Returns:
List of (file_path, file_content) tuples
"""
files = []
pattern = r'FILE:\s*([^\n]+)\s*```(?:\w+)?\s*\n(.*?)```'
matches = re.finditer(pattern, changes, re.DOTALL)
for match in matches:
file_path = match.group(1).strip()
file_content = match.group(2)
files.append((file_path, file_content))
logger.info(f"Parsed file: {file_path} ({len(file_content)} chars)")
if not files:
logger.warning("No structured file changes found, creating CLAUDE_CHANGES.md")
files.append(('CLAUDE_CHANGES.md', f"# Changes by Claude AI\n\n{changes}"))
return files
def get_default_branch(owner: str, repo: str, github_token: str) -> str:
"""
Get the default branch of a GitHub repository.
Args:
owner: Repository owner
repo: Repository name
github_token: GitHub access token
Returns:
Default branch name (e.g., 'main', 'master', 'flutterflow')
"""
url = f'https://api.github.com/repos/{owner}/{repo}'
headers = {
'Authorization': f'token {github_token}',
'Accept': 'application/vnd.github.v3+json'
}
try:
response = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
if response.status_code == 200:
default_branch = response.json().get('default_branch', 'main')
logger.info(f"Default branch for {owner}/{repo}: {default_branch}")
return default_branch
except Exception as e:
logger.error(f"Failed to get default branch: {e}")
return 'main'
def get_repo_context_via_api(owner: str, repo: str, github_token: str, path: str = "") -> str:
"""
Get repository context via GitHub API (no git clone needed).
Args:
owner: Repository owner
repo: Repository name
github_token: GitHub access token
path: Path within repo to explore (default: root)
Returns:
String describing the repo structure
"""
url = f'https://api.github.com/repos/{owner}/{repo}/contents/{path}'
headers = {
'Authorization': f'token {github_token}',
'Accept': 'application/vnd.github.v3+json'
}
try:
response = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
if response.status_code == 200:
items = response.json()
context = ["Repository Structure:"]
for item in items[:20]:
if item['type'] == 'dir':
context.append(f" 📁 {item['name']}/")
else:
context.append(f" 📄 {item['name']}")
if len(items) > 20:
context.append(f" ... and {len(items) - 20} more items")
return '\n'.join(context)
except Exception as e:
logger.error(f"Failed to get repo context: {e}")
return "Repository: Unable to fetch structure"
def create_or_update_files_via_api(
owner: str,
repo: str,
branch_name: str,
files: List[Tuple[str, str]],
commit_message: str,
github_token: str,
base_branch: str
) -> Dict[str, Any]:
"""
Create a new branch and commit files using GitHub API.
Args:
owner: Repository owner
repo: Repository name
branch_name: New branch name
files: List of (file_path, content) tuples
commit_message: Commit message
github_token: GitHub access token
base_branch: Base branch to branch from
Returns:
Dict with success status and branch name
"""
headers = {
'Authorization': f'token {github_token}',
'Accept': 'application/vnd.github.v3+json'
}
try:
logger.info(f"Getting reference for base branch: {base_branch}")
ref_url = f'https://api.github.com/repos/{owner}/{repo}/git/ref/heads/{base_branch}'
response = requests.get(ref_url, headers=headers, timeout=REQUEST_TIMEOUT)
if response.status_code != 200:
return {
'success': False,
'message': f'Failed to get base branch: {response.text}'
}
base_sha = response.json()['object']['sha']
logger.info(f"Base branch SHA: {base_sha}")
logger.info(f"Getting base tree...")
commit_url = f'https://api.github.com/repos/{owner}/{repo}/git/commits/{base_sha}'
response = requests.get(commit_url, headers=headers, timeout=REQUEST_TIMEOUT)
if response.status_code != 200:
return {
'success': False,
'message': f'Failed to get base commit: {response.text}'
}
base_tree_sha = response.json()['tree']['sha']
logger.info(f"Base tree SHA: {base_tree_sha}")
logger.info(f"Creating blobs for {len(files)} files...")
tree_items = []
for file_path, content in files:
blob_url = f'https://api.github.com/repos/{owner}/{repo}/git/blobs'
blob_data = {
'content': content,
'encoding': 'utf-8'
}
response = requests.post(blob_url, headers=headers, json=blob_data, timeout=REQUEST_TIMEOUT)
if response.status_code != 201:
logger.error(f"Failed to create blob for {file_path}: {response.status_code}")
continue
blob_sha = response.json()['sha']
logger.info(f"Created blob for {file_path}: {blob_sha}")
tree_items.append({
'path': file_path,
'mode': '100644',
'type': 'blob',
'sha': blob_sha
})
if not tree_items:
return {
'success': False,
'message': 'No files were successfully processed'
}
logger.info(f"Creating new tree with {len(tree_items)} items...")
tree_url = f'https://api.github.com/repos/{owner}/{repo}/git/trees'
tree_data = {
'base_tree': base_tree_sha,
'tree': tree_items
}
response = requests.post(tree_url, headers=headers, json=tree_data, timeout=REQUEST_TIMEOUT)
if response.status_code != 201:
return {
'success': False,
'message': f'Failed to create tree: {response.text}'
}
new_tree_sha = response.json()['sha']
logger.info(f"New tree SHA: {new_tree_sha}")
logger.info(f"Creating commit...")
commit_url = f'https://api.github.com/repos/{owner}/{repo}/git/commits'
commit_data = {
'message': commit_message,
'tree': new_tree_sha,
'parents': [base_sha]
}
response = requests.post(commit_url, headers=headers, json=commit_data, timeout=REQUEST_TIMEOUT)
if response.status_code != 201:
return {
'success': False,
'message': f'Failed to create commit: {response.text}'
}
new_commit_sha = response.json()['sha']
logger.info(f"New commit SHA: {new_commit_sha}")
logger.info(f"Creating branch: {branch_name}")
ref_url = f'https://api.github.com/repos/{owner}/{repo}/git/refs'
ref_data = {
'ref': f'refs/heads/{branch_name}',
'sha': new_commit_sha
}
response = requests.post(ref_url, headers=headers, json=ref_data, timeout=REQUEST_TIMEOUT)
if response.status_code not in [201, 422]:
return {
'success': False,
'message': f'Failed to create branch: {response.text}'
}
logger.info(f"Branch {branch_name} created successfully!")
return {
'success': True,
'branch': branch_name,
'default_branch': base_branch,
'message': f'Created branch {branch_name} with {len(tree_items)} files'
}
except Exception as e:
logger.error(f"Error creating branch via API: {e}", exc_info=True)
return {
'success': False,
'message': f'Failed to create branch: {str(e)}'
}
def create_pr_with_github_api(
owner: str,
repo: str,
branch: str,
title: str,
body: str,
github_token: str,
base_branch: str = 'main'
) -> Optional[Dict[str, Any]]:
"""
Create a pull request using GitHub API.
Args:
owner: Repository owner
repo: Repository name
branch: Branch with changes
title: PR title
body: PR description
github_token: GitHub access token
base_branch: Base branch to merge into (default: 'main')
Returns:
Dict with pr_url and pr_number if successful, None otherwise
"""
url = f'https://api.github.com/repos/{owner}/{repo}/pulls'
headers = {
'Authorization': f'token {github_token}',
'Accept': 'application/vnd.github.v3+json'
}
data = {
'title': title,
'body': body,
'head': branch,
'base': base_branch
}
logger.info(f"Creating PR: {branch} -> {base_branch}")
try:
response = requests.post(url, headers=headers, json=data, timeout=REQUEST_TIMEOUT)
if response.status_code == 201:
pr_data = response.json()
pr_url = pr_data.get('html_url')
pr_number = pr_data.get('number')
logger.info(f"PR created successfully: {pr_url}")
return {
'pr_url': pr_url,
'pr_number': pr_number
}
else:
error_msg = f"Failed to create PR: {response.status_code} - {response.text}"
logger.error(error_msg)
return None
except Exception as e:
logger.error(f"Exception creating PR: {e}", exc_info=True)
return None
def merge_pr_with_github_api(
owner: str,
repo: str,
pr_number: int,
github_token: str,
merge_method: str = 'merge'
) -> bool:
"""
Merge a pull request using GitHub API.
Args:
owner: Repository owner
repo: Repository name
pr_number: PR number to merge
github_token: GitHub access token
merge_method: Merge method ('merge', 'squash', or 'rebase')
Returns:
True if merged successfully, False otherwise
"""
url = f'https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/merge'
headers = {
'Authorization': f'token {github_token}',
'Accept': 'application/vnd.github.v3+json'
}
data = {
'merge_method': merge_method
}
logger.info(f"Merging PR #{pr_number} using {merge_method} method")
try:
response = requests.put(url, headers=headers, json=data, timeout=REQUEST_TIMEOUT)
if response.status_code == 200:
logger.info(f"PR #{pr_number} merged successfully")
return True
else:
error_msg = f"Failed to merge PR: {response.status_code} - {response.text}"
logger.error(error_msg)
return False
except Exception as e:
logger.error(f"Exception merging PR: {e}", exc_info=True)
return False