github-agent 0.2.46__tar.gz → 0.2.54__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {github_agent-0.2.46 → github_agent-0.2.54}/PKG-INFO +2 -2
- {github_agent-0.2.46 → github_agent-0.2.54}/README.md +1 -1
- github_agent-0.2.54/github_agent/agent_data/A2A_AGENTS.md +12 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/IDENTITY.md +3 -3
- github_agent-0.2.54/github_agent/agent_data/MCP_AGENTS.md +21 -0
- github_agent-0.2.54/github_agent/agent_data/mcp_config.json +18 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/templates.py +5 -6
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_server.py +22 -13
- github_agent-0.2.54/github_agent/api_wrapper.py +276 -0
- github_agent-0.2.54/github_agent/auth.py +89 -0
- github_agent-0.2.54/github_agent/github_input_models.py +207 -0
- github_agent-0.2.54/github_agent/github_response_models.py +185 -0
- github_agent-0.2.54/github_agent/mcp_server.py +279 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent.egg-info/PKG-INFO +2 -2
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent.egg-info/SOURCES.txt +6 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent.egg-info/entry_points.txt +1 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/pyproject.toml +2 -1
- github_agent-0.2.46/github_agent/agent_data/A2A_AGENTS.md +0 -12
- github_agent-0.2.46/github_agent/agent_data/mcp_config.json +0 -3
- {github_agent-0.2.46 → github_agent-0.2.54}/LICENSE +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/__init__.py +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/__main__.py +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/CRON.md +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/CRON_LOG.md +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/HEARTBEAT.md +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/MEMORY.md +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/USER.md +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/chats +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent/agent_data/icon.png +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent.egg-info/dependency_links.txt +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent.egg-info/requires.txt +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/github_agent.egg-info/top_level.txt +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/scripts/validate_a2a_agent.py +0 -0
- {github_agent-0.2.46 → github_agent-0.2.54}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-agent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.54
|
|
4
4
|
Summary: GitHub Agent for MCP
|
|
5
5
|
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|

|
|
39
39
|

|
|
40
40
|
|
|
41
|
-
*Version: 0.2.
|
|
41
|
+
*Version: 0.2.54*
|
|
42
42
|
|
|
43
43
|
## Overview
|
|
44
44
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# AGENTS.md - Known A2A Peer Agents
|
|
2
|
+
Last updated: 2026-02-21 01:19
|
|
3
|
+
|
|
4
|
+
This file is the local registry of other A2A agents this agent can discover and call.
|
|
5
|
+
|
|
6
|
+
## Registered A2A Peers
|
|
7
|
+
|
|
8
|
+
| Name | Endpoint URL | Description | Capabilities | Auth | Notes / Last Connected |
|
|
9
|
+
|------|--------------|-------------|--------------|------|------------------------|
|
|
10
|
+
| | | | | | |
|
|
11
|
+
|
|
12
|
+
*Add new rows manually or let the agent call `register_a2a_peer(...)`.*
|
|
@@ -18,9 +18,9 @@ You have three primary operational modes:
|
|
|
18
18
|
|
|
19
19
|
#### 1. Context-Aware Delegation
|
|
20
20
|
When dealing with complex Github workflows, optimize your context by spawning specialized versions of yourself:
|
|
21
|
-
- **PR Review**: Call `spawn_agent(
|
|
22
|
-
- **Issue Management**: Call `spawn_agent(
|
|
23
|
-
- **Discovery**: Always use `get_mcp_reference(
|
|
21
|
+
- **PR Review**: Call `spawn_agent(agent_name="github", prompt="Review and summarize PR #456...", enabled_tools=["PULL_REQUESTSTOOL", "REPOSTOOL"])`.
|
|
22
|
+
- **Issue Management**: Call `spawn_agent(agent_name="github", prompt="Triage and label new issues with 'bug'...", enabled_tools=["ISSUESTOOL"])`.
|
|
23
|
+
- **Discovery**: Always use `get_mcp_reference(agent_name="github")` to verify available tool tags before spawning.
|
|
24
24
|
|
|
25
25
|
#### 2. Workflow for Meta-Tasks
|
|
26
26
|
- **Memory Management**:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MCP_AGENTS.md - Dynamic Agent Registry
|
|
2
|
+
|
|
3
|
+
This file tracks the generated agents from MCP servers. You can manually modify the 'Tools' list to customize agent expertise.
|
|
4
|
+
|
|
5
|
+
## Agent Mapping Table
|
|
6
|
+
|
|
7
|
+
| Name | Description | System Prompt | Tools | Tag | Source MCP |
|
|
8
|
+
|------|-------------|---------------|-------|-----|------------|
|
|
9
|
+
| Github Contents Specialist | Expert specialist for contents domain tasks. | You are a Github Contents specialist. Help users manage and interact with Contents functionality using the available tools. | github-mcp_contents_toolset | contents | github-mcp |
|
|
10
|
+
| Github Issue Specialist | Expert specialist for issue domain tasks. | You are a Github Issue specialist. Help users manage and interact with Issue functionality using the available tools. | github-mcp_issue_toolset | issue | github-mcp |
|
|
11
|
+
| Github Pulls Specialist | Expert specialist for pulls domain tasks. | You are a Github Pulls specialist. Help users manage and interact with Pulls functionality using the available tools. | github-mcp_pulls_toolset | pulls | github-mcp |
|
|
12
|
+
| Github Repos Specialist | Expert specialist for repos domain tasks. | You are a Github Repos specialist. Help users manage and interact with Repos functionality using the available tools. | github-mcp_repos_toolset | repos | github-mcp |
|
|
13
|
+
|
|
14
|
+
## Tool Inventory Table
|
|
15
|
+
|
|
16
|
+
| Tool Name | Description | Tag | Source |
|
|
17
|
+
|-----------|-------------|-----|--------|
|
|
18
|
+
| github-mcp_contents_toolset | Static hint toolset for contents based on config env. | contents | github-mcp |
|
|
19
|
+
| github-mcp_issue_toolset | Static hint toolset for issue based on config env. | issue | github-mcp |
|
|
20
|
+
| github-mcp_pulls_toolset | Static hint toolset for pulls based on config env. | pulls | github-mcp |
|
|
21
|
+
| github-mcp_repos_toolset | Static hint toolset for repos based on config env. | repos | github-mcp |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"github-mcp": {
|
|
4
|
+
"command": "github-mcp",
|
|
5
|
+
"args": [
|
|
6
|
+
"--transport",
|
|
7
|
+
"stdio"
|
|
8
|
+
],
|
|
9
|
+
"env": {
|
|
10
|
+
"GITHUB_TOKEN": "${GITHUB_TOKEN}",
|
|
11
|
+
"CONTENTSTOOL": "${ CONTENTSTOOL:-True }",
|
|
12
|
+
"ISSUETOOL": "${ ISSUETOOL:-True }",
|
|
13
|
+
"PULLSTOOL": "${ PULLSTOOL:-True }",
|
|
14
|
+
"REPOSTOOL": "${ REPOSTOOL:-True }"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -2,7 +2,6 @@ import asyncio
|
|
|
2
2
|
from typing import Dict
|
|
3
3
|
from github_agent.models import PeriodicTask
|
|
4
4
|
|
|
5
|
-
# Core files we care about most
|
|
6
5
|
CORE_FILES = {
|
|
7
6
|
"IDENTITY": "IDENTITY.md",
|
|
8
7
|
"USER": "USER.md",
|
|
@@ -13,11 +12,11 @@ CORE_FILES = {
|
|
|
13
12
|
"MCP_CONFIG": "mcp_config.json",
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
tasks: list[PeriodicTask] = []
|
|
18
17
|
lock = asyncio.Lock()
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
|
|
21
20
|
TEMPLATES: Dict[str, str] = {
|
|
22
21
|
"IDENTITY": """# IDENTITY.md - Who I Am, Core Personality, & Boundaries
|
|
23
22
|
|
|
@@ -66,9 +65,9 @@ This file is the local registry of other A2A agents this agent can discover and
|
|
|
66
65
|
|
|
67
66
|
## Registered A2A Peers
|
|
68
67
|
|
|
69
|
-
| Name
|
|
70
|
-
|
|
71
|
-
|
|
|
68
|
+
| Name | Endpoint URL | Description | Capabilities | Auth | Notes / Last Connected |
|
|
69
|
+
|------|--------------|-------------|--------------|------|------------------------|
|
|
70
|
+
| | | | | | |
|
|
72
71
|
|
|
73
72
|
*Add new rows manually or let the agent call `register_a2a_peer(...)`.*
|
|
74
73
|
""",
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/python
|
|
2
2
|
# coding: utf-8
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
import logging
|
|
6
|
+
import warnings
|
|
5
7
|
|
|
6
8
|
from agent_utilities import (
|
|
7
9
|
build_system_prompt_from_workspace,
|
|
8
10
|
create_agent_parser,
|
|
9
|
-
|
|
11
|
+
create_graph_agent_server,
|
|
10
12
|
initialize_workspace,
|
|
11
13
|
load_identity,
|
|
12
14
|
)
|
|
13
15
|
|
|
14
|
-
__version__ = "0.2.
|
|
16
|
+
__version__ = "0.2.54"
|
|
15
17
|
|
|
16
18
|
logging.basicConfig(
|
|
17
19
|
level=logging.INFO,
|
|
@@ -20,13 +22,16 @@ logging.basicConfig(
|
|
|
20
22
|
)
|
|
21
23
|
logger = logging.getLogger(__name__)
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
initialize_workspace()
|
|
25
27
|
meta = load_identity()
|
|
26
28
|
DEFAULT_AGENT_NAME = os.getenv("DEFAULT_AGENT_NAME", meta.get("name", "Github Agent"))
|
|
27
29
|
DEFAULT_AGENT_DESCRIPTION = os.getenv(
|
|
28
30
|
"AGENT_DESCRIPTION",
|
|
29
|
-
meta.get(
|
|
31
|
+
meta.get(
|
|
32
|
+
"description",
|
|
33
|
+
"AI agent for GitHub Agent management.",
|
|
34
|
+
),
|
|
30
35
|
)
|
|
31
36
|
DEFAULT_AGENT_SYSTEM_PROMPT = os.getenv(
|
|
32
37
|
"AGENT_SYSTEM_PROMPT",
|
|
@@ -35,34 +40,38 @@ DEFAULT_AGENT_SYSTEM_PROMPT = os.getenv(
|
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def agent_server():
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
warnings.filterwarnings("ignore", message=".*urllib3.*or chardet.*")
|
|
44
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="fastmcp")
|
|
40
45
|
|
|
46
|
+
print(f"{DEFAULT_AGENT_NAME} v{__version__}", file=sys.stderr)
|
|
47
|
+
parser = create_agent_parser()
|
|
41
48
|
args = parser.parse_args()
|
|
42
49
|
|
|
43
50
|
if args.debug:
|
|
44
51
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
45
52
|
logger.debug("Debug mode enabled")
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
# Start server using the auto-discovery pattern (from mcp_config.json)
|
|
55
|
+
create_graph_agent_server(
|
|
56
|
+
mcp_url=args.mcp_url,
|
|
57
|
+
mcp_config=args.mcp_config or "mcp_config.json",
|
|
58
|
+
host=args.host,
|
|
59
|
+
port=args.port,
|
|
48
60
|
provider=args.provider,
|
|
49
61
|
model_id=args.model_id,
|
|
62
|
+
router_model=args.model_id,
|
|
63
|
+
agent_model=args.model_id,
|
|
50
64
|
base_url=args.base_url,
|
|
51
65
|
api_key=args.api_key,
|
|
52
66
|
custom_skills_directory=args.custom_skills_directory,
|
|
53
|
-
debug=args.debug,
|
|
54
|
-
host=args.host,
|
|
55
|
-
port=args.port,
|
|
56
67
|
enable_web_ui=args.web,
|
|
57
|
-
ssl_verify=not args.insecure,
|
|
58
|
-
name=DEFAULT_AGENT_NAME,
|
|
59
|
-
system_prompt=DEFAULT_AGENT_SYSTEM_PROMPT,
|
|
60
68
|
enable_otel=args.otel,
|
|
61
69
|
otel_endpoint=args.otel_endpoint,
|
|
62
70
|
otel_headers=args.otel_headers,
|
|
63
71
|
otel_public_key=args.otel_public_key,
|
|
64
72
|
otel_secret_key=args.otel_secret_key,
|
|
65
73
|
otel_protocol=args.otel_protocol,
|
|
74
|
+
debug=args.debug,
|
|
66
75
|
)
|
|
67
76
|
|
|
68
77
|
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import requests
|
|
5
|
+
import urllib3
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, TypeVar, Tuple
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
|
+
from agent_utilities.base_utilities import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
from github_agent.github_input_models import (
|
|
15
|
+
RepoModel,
|
|
16
|
+
IssueModel,
|
|
17
|
+
PullRequestModel,
|
|
18
|
+
ContentModel,
|
|
19
|
+
BranchModel,
|
|
20
|
+
CommitModel,
|
|
21
|
+
)
|
|
22
|
+
from github_agent.github_response_models import (
|
|
23
|
+
Repository,
|
|
24
|
+
Issue,
|
|
25
|
+
PullRequest,
|
|
26
|
+
Content,
|
|
27
|
+
Branch,
|
|
28
|
+
Commit,
|
|
29
|
+
Response,
|
|
30
|
+
)
|
|
31
|
+
from agent_utilities.decorators import require_auth
|
|
32
|
+
from agent_utilities.exceptions import (
|
|
33
|
+
AuthError,
|
|
34
|
+
UnauthorizedError,
|
|
35
|
+
ParameterError,
|
|
36
|
+
MissingParameterError,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Api(object):
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
url: str = "https://api.github.com",
|
|
47
|
+
token: str = None,
|
|
48
|
+
proxies: dict = None,
|
|
49
|
+
verify: bool = True,
|
|
50
|
+
debug: bool = False,
|
|
51
|
+
):
|
|
52
|
+
if debug:
|
|
53
|
+
logger.setLevel(logging.DEBUG)
|
|
54
|
+
else:
|
|
55
|
+
logger.setLevel(logging.ERROR)
|
|
56
|
+
|
|
57
|
+
if url is None:
|
|
58
|
+
raise MissingParameterError
|
|
59
|
+
|
|
60
|
+
self._session = requests.Session()
|
|
61
|
+
self.url = url.rstrip("/")
|
|
62
|
+
self.headers = {
|
|
63
|
+
"Accept": "application/vnd.github+json",
|
|
64
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
65
|
+
}
|
|
66
|
+
self.verify = verify
|
|
67
|
+
self.proxies = proxies
|
|
68
|
+
self.debug = debug
|
|
69
|
+
|
|
70
|
+
if self.verify is False:
|
|
71
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
72
|
+
|
|
73
|
+
if token:
|
|
74
|
+
self.headers["Authorization"] = f"Bearer {token}"
|
|
75
|
+
else:
|
|
76
|
+
|
|
77
|
+
logger.warning("No token provided for GitHub API")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
response = self._session.get(
|
|
81
|
+
url=f"{self.url}/user",
|
|
82
|
+
headers=self.headers,
|
|
83
|
+
verify=self.verify,
|
|
84
|
+
proxies=self.proxies,
|
|
85
|
+
)
|
|
86
|
+
if response.status_code in (401, 403):
|
|
87
|
+
logger.error(f"Authentication Error: {response.content}")
|
|
88
|
+
raise AuthError if response.status_code == 401 else UnauthorizedError
|
|
89
|
+
except requests.exceptions.RequestException as e:
|
|
90
|
+
logger.error(f"Connection Error: {str(e)}")
|
|
91
|
+
|
|
92
|
+
def _fetch_next_page(
|
|
93
|
+
self, endpoint: str, model: T, header: dict, page: int
|
|
94
|
+
) -> List[dict]:
|
|
95
|
+
"""Fetch a single page of data from the specified endpoint"""
|
|
96
|
+
model.page = page
|
|
97
|
+
model.model_post_init(None)
|
|
98
|
+
response = self._session.get(
|
|
99
|
+
url=f"{self.url}{endpoint}" if endpoint.startswith("/") else endpoint,
|
|
100
|
+
params=model.api_parameters,
|
|
101
|
+
headers=header,
|
|
102
|
+
verify=self.verify,
|
|
103
|
+
proxies=self.proxies,
|
|
104
|
+
)
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
page_data = response.json()
|
|
107
|
+
return page_data if isinstance(page_data, list) else []
|
|
108
|
+
|
|
109
|
+
def _get_total_pages(self, response: requests.Response) -> int:
|
|
110
|
+
"""Extract total pages from GitHub Link header"""
|
|
111
|
+
link = response.headers.get("Link")
|
|
112
|
+
if not link:
|
|
113
|
+
return 1
|
|
114
|
+
|
|
115
|
+
last_match = re.search(r'page=(\d+)>; rel="last"', link)
|
|
116
|
+
if last_match:
|
|
117
|
+
return int(last_match.group(1))
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
def _fetch_all_pages(
|
|
121
|
+
self, endpoint: str, model: T
|
|
122
|
+
) -> Tuple[requests.Response, List[dict]]:
|
|
123
|
+
"""Generic method to fetch all pages with parallelization if possible"""
|
|
124
|
+
all_data = []
|
|
125
|
+
|
|
126
|
+
initial_url = f"{self.url}{endpoint}" if endpoint.startswith("/") else endpoint
|
|
127
|
+
|
|
128
|
+
response = self._session.get(
|
|
129
|
+
url=initial_url,
|
|
130
|
+
params=model.api_parameters,
|
|
131
|
+
headers=self.headers,
|
|
132
|
+
verify=self.verify,
|
|
133
|
+
proxies=self.proxies,
|
|
134
|
+
)
|
|
135
|
+
response.raise_for_status()
|
|
136
|
+
initial_data = response.json()
|
|
137
|
+
|
|
138
|
+
if isinstance(initial_data, list):
|
|
139
|
+
all_data.extend(initial_data)
|
|
140
|
+
else:
|
|
141
|
+
return response, [initial_data]
|
|
142
|
+
|
|
143
|
+
total_pages = self._get_total_pages(response)
|
|
144
|
+
|
|
145
|
+
if not model.max_pages or model.max_pages == 0 or model.max_pages > total_pages:
|
|
146
|
+
model.max_pages = total_pages
|
|
147
|
+
|
|
148
|
+
if model.max_pages > 1:
|
|
149
|
+
|
|
150
|
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
|
151
|
+
futures = []
|
|
152
|
+
for page in range(2, model.max_pages + 1):
|
|
153
|
+
futures.append(
|
|
154
|
+
executor.submit(
|
|
155
|
+
self._fetch_next_page,
|
|
156
|
+
initial_url,
|
|
157
|
+
model,
|
|
158
|
+
self.headers,
|
|
159
|
+
page,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
for future in as_completed(futures):
|
|
164
|
+
try:
|
|
165
|
+
all_data.extend(future.result())
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Error fetching page: {str(e)}")
|
|
168
|
+
|
|
169
|
+
return response, all_data
|
|
170
|
+
|
|
171
|
+
@require_auth
|
|
172
|
+
def get_repositories(self, **kwargs) -> Response:
|
|
173
|
+
"""List repositories for the authenticated user."""
|
|
174
|
+
model = RepoModel(**kwargs)
|
|
175
|
+
try:
|
|
176
|
+
response, data = self._fetch_all_pages("/user/repos", model)
|
|
177
|
+
parsed_data = [Repository(**item) for item in data]
|
|
178
|
+
return Response(response=response, data=parsed_data)
|
|
179
|
+
except ValidationError as e:
|
|
180
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}")
|
|
181
|
+
|
|
182
|
+
@require_auth
|
|
183
|
+
def get_repository(self, owner: str, repo: str) -> Response:
|
|
184
|
+
"""Get a specific repository."""
|
|
185
|
+
try:
|
|
186
|
+
response = self._session.get(
|
|
187
|
+
url=f"{self.url}/repos/{owner}/{repo}",
|
|
188
|
+
headers=self.headers,
|
|
189
|
+
verify=self.verify,
|
|
190
|
+
proxies=self.proxies,
|
|
191
|
+
)
|
|
192
|
+
response.raise_for_status()
|
|
193
|
+
parsed_data = Repository(**response.json())
|
|
194
|
+
return Response(response=response, data=parsed_data)
|
|
195
|
+
except requests.exceptions.HTTPError as e:
|
|
196
|
+
if e.response.status_code == 404:
|
|
197
|
+
raise ParameterError(f"Repository {owner}/{repo} not found")
|
|
198
|
+
raise e
|
|
199
|
+
|
|
200
|
+
@require_auth
|
|
201
|
+
def get_issues(self, **kwargs) -> Response:
|
|
202
|
+
"""List issues for a repository."""
|
|
203
|
+
model = IssueModel(**kwargs)
|
|
204
|
+
if not model.owner or not model.repo:
|
|
205
|
+
raise MissingParameterError("owner and repo are required")
|
|
206
|
+
try:
|
|
207
|
+
response, data = self._fetch_all_pages(
|
|
208
|
+
f"/repos/{model.owner}/{model.repo}/issues", model
|
|
209
|
+
)
|
|
210
|
+
parsed_data = [Issue(**item) for item in data]
|
|
211
|
+
return Response(response=response, data=parsed_data)
|
|
212
|
+
except ValidationError as e:
|
|
213
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}")
|
|
214
|
+
|
|
215
|
+
@require_auth
|
|
216
|
+
def get_pull_requests(self, **kwargs) -> Response:
|
|
217
|
+
"""List pull requests for a repository."""
|
|
218
|
+
model = PullRequestModel(**kwargs)
|
|
219
|
+
if not model.owner or not model.repo:
|
|
220
|
+
raise MissingParameterError("owner and repo are required")
|
|
221
|
+
try:
|
|
222
|
+
response, data = self._fetch_all_pages(
|
|
223
|
+
f"/repos/{model.owner}/{model.repo}/pulls", model
|
|
224
|
+
)
|
|
225
|
+
parsed_data = [PullRequest(**item) for item in data]
|
|
226
|
+
return Response(response=response, data=parsed_data)
|
|
227
|
+
except ValidationError as e:
|
|
228
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}")
|
|
229
|
+
|
|
230
|
+
@require_auth
|
|
231
|
+
def get_contents(self, **kwargs) -> Response:
|
|
232
|
+
"""Get contents of a file or directory in a repository."""
|
|
233
|
+
model = ContentModel(**kwargs)
|
|
234
|
+
try:
|
|
235
|
+
response = self._session.get(
|
|
236
|
+
url=f"{self.url}/repos/{model.owner}/{model.repo}/contents/{model.path}",
|
|
237
|
+
params=model.api_parameters,
|
|
238
|
+
headers=self.headers,
|
|
239
|
+
verify=self.verify,
|
|
240
|
+
proxies=self.proxies,
|
|
241
|
+
)
|
|
242
|
+
response.raise_for_status()
|
|
243
|
+
data = response.json()
|
|
244
|
+
if isinstance(data, list):
|
|
245
|
+
parsed_data = [Content(**item) for item in data]
|
|
246
|
+
else:
|
|
247
|
+
parsed_data = Content(**data)
|
|
248
|
+
return Response(response=response, data=parsed_data)
|
|
249
|
+
except ValidationError as e:
|
|
250
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}")
|
|
251
|
+
|
|
252
|
+
@require_auth
|
|
253
|
+
def get_branches(self, **kwargs) -> Response:
|
|
254
|
+
"""List branches for a repository."""
|
|
255
|
+
model = BranchModel(**kwargs)
|
|
256
|
+
try:
|
|
257
|
+
response, data = self._fetch_all_pages(
|
|
258
|
+
f"/repos/{model.owner}/{model.repo}/branches", model
|
|
259
|
+
)
|
|
260
|
+
parsed_data = [Branch(**item) for item in data]
|
|
261
|
+
return Response(response=response, data=parsed_data)
|
|
262
|
+
except ValidationError as e:
|
|
263
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}")
|
|
264
|
+
|
|
265
|
+
@require_auth
|
|
266
|
+
def get_commits(self, **kwargs) -> Response:
|
|
267
|
+
"""List commits for a repository."""
|
|
268
|
+
model = CommitModel(**kwargs)
|
|
269
|
+
try:
|
|
270
|
+
response, data = self._fetch_all_pages(
|
|
271
|
+
f"/repos/{model.owner}/{model.repo}/commits", model
|
|
272
|
+
)
|
|
273
|
+
parsed_data = [Commit(**item) for item in data]
|
|
274
|
+
return Response(response=response, data=parsed_data)
|
|
275
|
+
except ValidationError as e:
|
|
276
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from agent_utilities.base_utilities import to_boolean, get_logger
|
|
9
|
+
from github_agent.api_wrapper import Api
|
|
10
|
+
from agent_utilities.exceptions import AuthError, UnauthorizedError
|
|
11
|
+
|
|
12
|
+
local = threading.local()
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_client(
|
|
17
|
+
instance: str = os.getenv("GITHUB_URL", "https://api.github.com"),
|
|
18
|
+
token: Optional[str] = os.getenv("GITHUB_TOKEN", None),
|
|
19
|
+
verify: bool = to_boolean(string=os.getenv("GITHUB_VERIFY", "True")),
|
|
20
|
+
config: Optional[dict] = None,
|
|
21
|
+
) -> Api:
|
|
22
|
+
"""
|
|
23
|
+
Factory function to create the GitHub Api client.
|
|
24
|
+
Supports fixed credentials (token) and delegation (OAuth exchange).
|
|
25
|
+
"""
|
|
26
|
+
if config is None:
|
|
27
|
+
from agent_utilities.mcp_utilities import config as default_config
|
|
28
|
+
|
|
29
|
+
config = default_config
|
|
30
|
+
|
|
31
|
+
if config.get("enable_delegation"):
|
|
32
|
+
user_token = getattr(local, "user_token", None)
|
|
33
|
+
if not user_token:
|
|
34
|
+
logger.error("No user token available for delegation")
|
|
35
|
+
raise ValueError("No user token available for delegation")
|
|
36
|
+
|
|
37
|
+
logger.info(
|
|
38
|
+
"Initiating OAuth token exchange for GitHub",
|
|
39
|
+
extra={
|
|
40
|
+
"audience": config["audience"],
|
|
41
|
+
"scopes": config["delegated_scopes"],
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
exchange_data = {
|
|
46
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
47
|
+
"subject_token": user_token,
|
|
48
|
+
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
49
|
+
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
50
|
+
"audience": config["audience"],
|
|
51
|
+
"scope": config["delegated_scopes"],
|
|
52
|
+
}
|
|
53
|
+
auth = (config["oidc_client_id"], config["oidc_client_secret"])
|
|
54
|
+
try:
|
|
55
|
+
response = requests.post(
|
|
56
|
+
config["token_endpoint"], data=exchange_data, auth=auth
|
|
57
|
+
)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
new_token = response.json()["access_token"]
|
|
60
|
+
logger.info("Token exchange successful")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"Token exchange failed: {str(e)}")
|
|
63
|
+
raise RuntimeError(f"Token exchange failed: {str(e)}")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
return Api(
|
|
67
|
+
url=instance,
|
|
68
|
+
token=new_token,
|
|
69
|
+
verify=verify,
|
|
70
|
+
)
|
|
71
|
+
except (AuthError, UnauthorizedError) as e:
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
f"AUTHENTICATION ERROR: The delegated GitHub credentials are not valid for '{instance}'."
|
|
74
|
+
f"Error details: {str(e)}"
|
|
75
|
+
) from e
|
|
76
|
+
else:
|
|
77
|
+
logger.info("Using fixed credentials for GitHub API")
|
|
78
|
+
try:
|
|
79
|
+
return Api(
|
|
80
|
+
url=instance,
|
|
81
|
+
token=token,
|
|
82
|
+
verify=verify,
|
|
83
|
+
)
|
|
84
|
+
except (AuthError, UnauthorizedError) as e:
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
f"AUTHENTICATION ERROR: The GitHub credentials provided are not valid for '{instance}'. "
|
|
87
|
+
f"Please check your GITHUB_TOKEN and GITHUB_URL environment variables. "
|
|
88
|
+
f"Error details: {str(e)}"
|
|
89
|
+
) from e
|