Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ instruments/**
# for browser-use
agent_history.gif

.agent/**
.claude/**
7 changes: 7 additions & 0 deletions agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ class UserMessage:
message: str
attachments: list[str] = field(default_factory=list[str])
system_message: list[str] = field(default_factory=list[str])
repo_mentions: list[dict] = field(default_factory=list)


class LoopData:
Expand Down Expand Up @@ -675,6 +676,12 @@ def hist_add_user_message(self, message: UserMessage, intervention: bool = False
if isinstance(content, dict):
content = {k: v for k, v in content.items() if v}

# Store repo_mentions for extensions to access
if hasattr(message, 'repo_mentions') and message.repo_mentions:
self.data['repo_mentions'] = message.repo_mentions
else:
self.data['repo_mentions'] = []

# add to history
msg = self.hist_add_message(False, content=content) # type: ignore
self.last_user_message = msg
Expand Down
9 changes: 9 additions & 0 deletions prompts/agent.extras.github_status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# GitHub Integration
The user is connected to GitHub as **{{username}}**{{if name}} ({{name}}){{/if}}.

You have access to their GitHub account and can:
- Browse their repositories
- Access file contents from their repos
- Help with GitHub-related tasks (repos, issues, PRs, etc.)

When the user references a GitHub repository or asks about their code on GitHub, you can use the GitHub integration to help them.
10 changes: 10 additions & 0 deletions prompts/agent.extras.repo_mention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# GitHub Repository: {{full_name}}
{{if description}}{{description}}{{/if}}

## Repository Info
- **Language:** {{language}}
- **Stars:** {{stars}}
- **Default Branch:** {{default_branch}}

## File Structure
{{file_tree}}
52 changes: 52 additions & 0 deletions prompts/agent.system.tool.github.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{{if github_connected}}
### github:
Interact with user's GitHub repositories. User is connected as **{{github_username}}**.
Available actions:
- `list_repos`: List user's repositories (optional: page, per_page)
- `get_repo`: Get repo details (required: owner, repo)
- `get_contents`: List directory contents (required: owner, repo; optional: path)
- `get_file`: Read file contents (required: owner, repo, path)
- `search_repos`: Search GitHub repos (required: query)

**Example - List repos:**
~~~json
{
"thoughts": ["User wants to see their repositories"],
"headline": "Listing GitHub repositories",
"tool_name": "github",
"tool_args": {
"action": "list_repos"
}
}
~~~

**Example - Get file contents:**
~~~json
{
"thoughts": ["Need to read the README from user's repo"],
"headline": "Reading README.md",
"tool_name": "github",
"tool_args": {
"action": "get_file",
"owner": "username",
"repo": "repo-name",
"path": "README.md"
}
}
~~~

**Example - Browse directory:**
~~~json
{
"thoughts": ["User wants to see what's in the src folder"],
"headline": "Browsing src directory",
"tool_name": "github",
"tool_args": {
"action": "get_contents",
"owner": "username",
"repo": "repo-name",
"path": "src"
}
}
~~~
{{/if}}
28 changes: 28 additions & 0 deletions prompts/agent.system.tool.github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import Any
from python.helpers.files import VariablesPlugin


class GithubToolVariables(VariablesPlugin):
"""Provide variables for the GitHub tool prompt."""

def get_variables(
self, file: str, backup_dirs: list[str] | None = None, **kwargs
) -> dict[str, Any]:
# Import here to avoid circular imports
from python.api.github_callback import get_github_auth

auth_data = get_github_auth()

if not auth_data:
return {"github_connected": False, "github_username": ""}

access_token = auth_data.get("access_token")
user = auth_data.get("user")

if not access_token or not user:
return {"github_connected": False, "github_username": ""}

return {
"github_connected": True,
"github_username": user.get("login", "unknown"),
}
2 changes: 2 additions & 0 deletions python/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# GitHub API handlers are auto-discovered from this folder
# See run_ui.py:register_api_handler() and load_classes_from_folder()
123 changes: 123 additions & 0 deletions python/api/github_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from python.helpers.api import ApiHandler
from python.helpers.print_style import PrintStyle
from python.helpers import files
from flask import Request, Response, session, redirect
import os
import json
import httpx
import urllib.parse

GITHUB_AUTH_FILE = files.get_abs_path("tmp/github_auth.json")


def save_github_auth(data: dict):
"""Save GitHub auth data to file"""
content = json.dumps(data, indent=2)
files.write_file(GITHUB_AUTH_FILE, content)


def get_github_auth() -> dict | None:
"""Load GitHub auth data from file"""
if os.path.exists(GITHUB_AUTH_FILE):
content = files.read_file(GITHUB_AUTH_FILE)
return json.loads(content)
return None


def clear_github_auth():
"""Remove GitHub auth file"""
if os.path.exists(GITHUB_AUTH_FILE):
os.remove(GITHUB_AUTH_FILE)


class GithubCallback(ApiHandler):
"""Handle GitHub OAuth callback"""

@staticmethod
def requires_csrf() -> bool:
return False # OAuth callback comes from GitHub, not our frontend

@staticmethod
def get_methods() -> list[str]:
return ["GET"]

async def process(self, input: dict, request: Request) -> Response:
code = request.args.get("code")
state = request.args.get("state")
error = request.args.get("error")

# Handle user denial
if error:
return redirect(f"/?github_error={error}")

if not code:
return redirect("/?github_error=no_code")

# Validate state for CSRF protection
stored_state = session.pop("github_oauth_state", None)
if not state or state != stored_state:
return redirect("/?github_error=invalid_state")

client_id = os.getenv("GITHUB_CLIENT_ID")
client_secret = os.getenv("GITHUB_CLIENT_SECRET")

if not client_id or not client_secret:
return redirect("/?github_error=not_configured")

try:
async with httpx.AsyncClient() as client:
# Exchange code for access token
token_response = await client.post(
"https://github.com/login/oauth/access_token",
data={
"client_id": client_id,
"client_secret": client_secret,
"code": code,
},
headers={"Accept": "application/json"},
)

if token_response.status_code != 200:
return redirect("/?github_error=token_exchange_failed")

token_data = token_response.json()

if "error" in token_data:
error_desc = token_data.get("error_description", token_data["error"])
return redirect(f"/?github_error={error_desc}")

access_token = token_data.get("access_token")
if not access_token:
return redirect("/?github_error=no_access_token")

# Fetch user info
user_response = await client.get(
"https://api.github.com/user",
headers={
"Authorization": f"Bearer {access_token}",
"Accept": "application/vnd.github+json",
},
)

if user_response.status_code != 200:
return redirect("/?github_error=user_fetch_failed")

user_data = user_response.json()

# Store in separate auth file (not main settings)
save_github_auth({
"access_token": access_token,
"user": {
"login": user_data.get("login"),
"name": user_data.get("name"),
"avatar_url": user_data.get("avatar_url"),
"html_url": user_data.get("html_url"),
},
})

return redirect("/?github_connected=true")

except Exception as e:
PrintStyle.error(f"GitHub OAuth callback error: {e}")
error_msg = urllib.parse.quote(str(e)[:100])
return redirect(f"/?github_error={error_msg}")
78 changes: 78 additions & 0 deletions python/api/github_contents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from python.helpers.api import ApiHandler
from python.api.github_callback import get_github_auth
from flask import Request
import httpx


class GithubContents(ApiHandler):
"""Get repository contents (file browser)"""

async def process(self, input: dict, request: Request) -> dict:
auth_data = get_github_auth()
access_token = auth_data.get("access_token") if auth_data else None

if not access_token:
return {"error": "Not connected to GitHub", "connected": False}

# Get params from input or query string
owner = input.get("owner") or request.args.get("owner")
repo = input.get("repo") or request.args.get("repo")
path = input.get("path") or request.args.get("path", "")
ref = input.get("ref") or request.args.get("ref", "")

if not owner or not repo:
return {"error": "owner and repo parameters required"}

headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/vnd.github+json",
}

params = {}
if ref:
params["ref"] = ref

async with httpx.AsyncClient() as client:
url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}"
response = await client.get(url, params=params, headers=headers)

if response.status_code != 200:
return {"error": "Failed to fetch contents", "status": response.status_code}

data = response.json()

# Handle single file vs directory
if isinstance(data, dict):
# Single file
return {
"type": "file",
"content": {
"name": data["name"],
"path": data["path"],
"sha": data["sha"],
"size": data["size"],
"type": data["type"],
"content": data.get("content"), # Base64 encoded
"encoding": data.get("encoding"),
"html_url": data["html_url"],
"download_url": data.get("download_url"),
},
}
else:
# Directory listing
return {
"type": "directory",
"path": path,
"contents": [
{
"name": item["name"],
"path": item["path"],
"sha": item["sha"],
"size": item.get("size", 0),
"type": item["type"],
"html_url": item["html_url"],
"download_url": item.get("download_url"),
}
for item in sorted(data, key=lambda x: (x["type"] != "dir", x["name"].lower()))
],
}
11 changes: 11 additions & 0 deletions python/api/github_disconnect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from python.helpers.api import ApiHandler
from python.api.github_callback import clear_github_auth
from flask import Request


class GithubDisconnect(ApiHandler):
"""Disconnect GitHub integration"""

async def process(self, input: dict, request: Request) -> dict:
clear_github_auth()
return {"success": True, "message": "GitHub disconnected"}
Loading