PYAE1994 commited on
Commit
e2cfcd7
Β·
verified Β·
1 Parent(s): 9f5c2db

feat: GOD MODE+ v4.0 - tools/github_tool.py

Browse files
Files changed (1) hide show
  1. tools/github_tool.py +310 -0
tools/github_tool.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GitHub Tool β€” Real Autonomous GitHub Operations
3
+ cloneRepo / createRepo / createBranch / commitChanges / pushChanges / openPR / readIssues
4
+ """
5
+
6
+ import asyncio
7
+ import os
8
+ import subprocess
9
+ from typing import Dict, List, Optional
10
+
11
+ import httpx
12
+ import structlog
13
+
14
+ log = structlog.get_logger()
15
+
16
+ GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
17
+ WORKSPACE = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace")
18
+
19
+
20
+ class GitHubTool:
21
+ def __init__(self):
22
+ self.token = GITHUB_TOKEN or os.environ.get("GITHUB_TOKEN", "")
23
+ self.api = "https://api.github.com"
24
+ self.headers = {
25
+ "Authorization": f"token {self.token}",
26
+ "Accept": "application/vnd.github.v3+json",
27
+ "Content-Type": "application/json",
28
+ }
29
+
30
+ def _refresh_token(self):
31
+ self.token = os.environ.get("GITHUB_TOKEN", "")
32
+ self.headers["Authorization"] = f"token {self.token}"
33
+
34
+ async def _api(self, method: str, path: str, **kwargs) -> Dict:
35
+ self._refresh_token()
36
+ url = f"{self.api}{path}" if not path.startswith("http") else path
37
+ async with httpx.AsyncClient(timeout=30) as client:
38
+ resp = await client.request(method, url, headers=self.headers, **kwargs)
39
+ if resp.status_code >= 400:
40
+ return {"success": False, "error": resp.text, "status": resp.status_code}
41
+ try:
42
+ return {"success": True, "data": resp.json()}
43
+ except Exception:
44
+ return {"success": True, "data": resp.text}
45
+
46
+ # ─── Run Git Command ───────────────────────────────────────────────────────
47
+
48
+ async def _git(self, cmd: str, cwd: str = WORKSPACE, timeout: int = 60) -> Dict:
49
+ try:
50
+ env = os.environ.copy()
51
+ env["GIT_ASKPASS"] = "echo"
52
+ env["GIT_TERMINAL_PROMPT"] = "0"
53
+ if self.token:
54
+ env["GIT_TOKEN"] = self.token
55
+ proc = await asyncio.create_subprocess_shell(
56
+ cmd,
57
+ stdout=asyncio.subprocess.PIPE,
58
+ stderr=asyncio.subprocess.PIPE,
59
+ cwd=cwd,
60
+ env=env,
61
+ )
62
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
63
+ out = stdout.decode("utf-8", errors="replace")
64
+ err = stderr.decode("utf-8", errors="replace")
65
+ return {
66
+ "success": proc.returncode == 0,
67
+ "stdout": out,
68
+ "stderr": err,
69
+ "returncode": proc.returncode,
70
+ "output": out or err,
71
+ }
72
+ except asyncio.TimeoutError:
73
+ return {"success": False, "error": f"Git command timed out: {cmd[:60]}"}
74
+ except Exception as e:
75
+ return {"success": False, "error": str(e)}
76
+
77
+ # ─── Clone Repo ───────────────────────────────────────────────────────────
78
+
79
+ async def clone_repo(self, repo_url: str, dest: str = "", branch: str = "") -> Dict:
80
+ """Clone a GitHub repository into workspace."""
81
+ if self.token and "github.com" in repo_url:
82
+ if repo_url.startswith("https://github.com/"):
83
+ repo_url = repo_url.replace(
84
+ "https://github.com/",
85
+ f"https://{self.token}@github.com/",
86
+ )
87
+ dest_path = dest or os.path.join(WORKSPACE, repo_url.split("/")[-1].replace(".git", ""))
88
+ os.makedirs(dest_path, exist_ok=True)
89
+ branch_flag = f"--branch {branch}" if branch else ""
90
+ result = await self._git(f"git clone --depth 1 {branch_flag} {repo_url} .", cwd=dest_path, timeout=120)
91
+ if result["success"]:
92
+ return {
93
+ "success": True,
94
+ "repo_url": repo_url,
95
+ "local_path": dest_path,
96
+ "action": "cloned",
97
+ }
98
+ return result
99
+
100
+ # ─── Create Repo ──────────────────────────────────────────────────────────
101
+
102
+ async def create_repo(
103
+ self,
104
+ name: str,
105
+ description: str = "",
106
+ private: bool = False,
107
+ auto_init: bool = True,
108
+ ) -> Dict:
109
+ """Create a new GitHub repo via API."""
110
+ result = await self._api("POST", "/user/repos", json={
111
+ "name": name,
112
+ "description": description,
113
+ "private": private,
114
+ "auto_init": auto_init,
115
+ })
116
+ if result["success"]:
117
+ data = result["data"]
118
+ return {
119
+ "success": True,
120
+ "repo_name": name,
121
+ "url": data.get("html_url", ""),
122
+ "clone_url": data.get("clone_url", ""),
123
+ "action": "created",
124
+ }
125
+ return result
126
+
127
+ # ─── Create Branch ────────────────────────────────────────────────────────
128
+
129
+ async def create_branch(self, branch: str, cwd: str = WORKSPACE, from_branch: str = "main") -> Dict:
130
+ """Create and checkout a new git branch."""
131
+ # First fetch to ensure we have latest
132
+ await self._git(f"git fetch origin {from_branch} --quiet", cwd=cwd)
133
+ result = await self._git(f"git checkout -b {branch}", cwd=cwd)
134
+ if result["success"]:
135
+ return {"success": True, "branch": branch, "action": "created_and_checked_out"}
136
+ # Try checkout if branch exists
137
+ result2 = await self._git(f"git checkout {branch}", cwd=cwd)
138
+ return result2 if result2["success"] else result
139
+
140
+ # ─── Commit Changes ───────────────────────────────────────────────────────
141
+
142
+ async def commit_changes(
143
+ self,
144
+ message: str,
145
+ cwd: str = WORKSPACE,
146
+ files: Optional[List[str]] = None,
147
+ ) -> Dict:
148
+ """Stage and commit changes."""
149
+ if files:
150
+ for f in files:
151
+ await self._git(f"git add {f}", cwd=cwd)
152
+ else:
153
+ await self._git("git add -A", cwd=cwd)
154
+
155
+ result = await self._git(f'git commit -m "{message}"', cwd=cwd)
156
+ return {
157
+ "success": result["success"],
158
+ "message": message,
159
+ "output": result.get("output", ""),
160
+ "action": "committed",
161
+ }
162
+
163
+ # ─── Push Changes ─────────────────────────────────────────────────────────
164
+
165
+ async def push_changes(self, branch: str = "", cwd: str = WORKSPACE, force: bool = False) -> Dict:
166
+ """Push commits to remote."""
167
+ self._refresh_token()
168
+ branch_arg = branch or ""
169
+ force_flag = "--force" if force else ""
170
+ if branch_arg:
171
+ cmd = f"git push origin {branch_arg} {force_flag}".strip()
172
+ else:
173
+ cmd = f"git push {force_flag}".strip()
174
+ result = await self._git(cmd, cwd=cwd, timeout=60)
175
+ return {
176
+ "success": result["success"],
177
+ "branch": branch_arg,
178
+ "output": result.get("output", ""),
179
+ "action": "pushed",
180
+ }
181
+
182
+ # ─── Open PR ──────────────────────────────────────────────────────────────
183
+
184
+ async def open_pr(
185
+ self,
186
+ owner: str,
187
+ repo: str,
188
+ title: str,
189
+ body: str = "",
190
+ head: str = "genspark_ai_developer",
191
+ base: str = "main",
192
+ ) -> Dict:
193
+ """Create a pull request via GitHub API."""
194
+ result = await self._api("POST", f"/repos/{owner}/{repo}/pulls", json={
195
+ "title": title,
196
+ "body": body,
197
+ "head": head,
198
+ "base": base,
199
+ })
200
+ if result["success"]:
201
+ data = result["data"]
202
+ return {
203
+ "success": True,
204
+ "pr_url": data.get("html_url", ""),
205
+ "pr_number": data.get("number"),
206
+ "title": title,
207
+ "action": "pr_opened",
208
+ }
209
+ return result
210
+
211
+ # ─── Read Issues ──────────────────────────────────────────────────────────
212
+
213
+ async def read_issues(self, owner: str, repo: str, state: str = "open", limit: int = 10) -> Dict:
214
+ result = await self._api("GET", f"/repos/{owner}/{repo}/issues?state={state}&per_page={limit}")
215
+ if result["success"]:
216
+ issues = result["data"]
217
+ return {
218
+ "success": True,
219
+ "issues": [
220
+ {
221
+ "number": i.get("number"),
222
+ "title": i.get("title"),
223
+ "state": i.get("state"),
224
+ "body": (i.get("body") or "")[:500],
225
+ "url": i.get("html_url"),
226
+ "labels": [l["name"] for l in i.get("labels", [])],
227
+ }
228
+ for i in issues
229
+ ],
230
+ }
231
+ return result
232
+
233
+ # ─── Get Repo Info ────────────────────────────────────────────────────────
234
+
235
+ async def get_repo_info(self, owner: str, repo: str) -> Dict:
236
+ result = await self._api("GET", f"/repos/{owner}/{repo}")
237
+ if result["success"]:
238
+ d = result["data"]
239
+ return {
240
+ "success": True,
241
+ "name": d.get("name"),
242
+ "full_name": d.get("full_name"),
243
+ "description": d.get("description"),
244
+ "url": d.get("html_url"),
245
+ "clone_url": d.get("clone_url"),
246
+ "default_branch": d.get("default_branch"),
247
+ "stars": d.get("stargazers_count"),
248
+ "language": d.get("language"),
249
+ "private": d.get("private"),
250
+ }
251
+ return result
252
+
253
+ # ─── Get File from GitHub ─────────────────────────────────────────────────
254
+
255
+ async def get_file(self, owner: str, repo: str, path: str, branch: str = "main") -> Dict:
256
+ import base64
257
+ result = await self._api("GET", f"/repos/{owner}/{repo}/contents/{path}?ref={branch}")
258
+ if result["success"]:
259
+ d = result["data"]
260
+ content = base64.b64decode(d.get("content", "")).decode("utf-8", errors="replace")
261
+ return {"success": True, "path": path, "content": content, "sha": d.get("sha")}
262
+ return result
263
+
264
+ # ─── Setup Git Config ─────────────────────────────────────────────────────
265
+
266
+ async def setup_git_config(self, cwd: str = WORKSPACE, name: str = "God Agent", email: str = "god-agent@ai.com") -> Dict:
267
+ await self._git(f'git config user.name "{name}"', cwd=cwd)
268
+ await self._git(f'git config user.email "{email}"', cwd=cwd)
269
+ return {"success": True, "action": "git_config_set"}
270
+
271
+ # ─── Get Status ───────────────────────────────────────────────────────────
272
+
273
+ async def status(self, cwd: str = WORKSPACE) -> Dict:
274
+ result = await self._git("git status --short", cwd=cwd)
275
+ log_result = await self._git("git log --oneline -5", cwd=cwd)
276
+ branch_result = await self._git("git branch --show-current", cwd=cwd)
277
+ return {
278
+ "success": True,
279
+ "status": result.get("stdout", ""),
280
+ "recent_commits": log_result.get("stdout", ""),
281
+ "current_branch": branch_result.get("stdout", "").strip(),
282
+ }
283
+
284
+ # ─── Init + Setup Repo ────────────────────────────────────────────────────
285
+
286
+ async def init_repo(self, cwd: str = WORKSPACE, remote_url: str = "") -> Dict:
287
+ await self._git("git init", cwd=cwd)
288
+ await self.setup_git_config(cwd=cwd)
289
+ if remote_url:
290
+ await self._git(f"git remote add origin {remote_url}", cwd=cwd)
291
+ return {"success": True, "cwd": cwd, "action": "initialized"}
292
+
293
+ # ─── Full Autonomous Commit + Push ────────────────────────────────────────
294
+
295
+ async def auto_commit_push(
296
+ self,
297
+ message: str,
298
+ branch: str = "main",
299
+ cwd: str = WORKSPACE,
300
+ ) -> Dict:
301
+ """All-in-one: add β†’ commit β†’ push."""
302
+ await self.setup_git_config(cwd=cwd)
303
+ commit_r = await self.commit_changes(message, cwd=cwd)
304
+ push_r = await self.push_changes(branch, cwd=cwd)
305
+ return {
306
+ "success": push_r.get("success", False),
307
+ "commit": commit_r,
308
+ "push": push_r,
309
+ "action": "committed_and_pushed",
310
+ }