devduck 0.1.1766644714__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of devduck might be problematic. Click here for more details.
- devduck/__init__.py +591 -1092
- devduck/_version.py +2 -2
- devduck/install.sh +42 -0
- devduck/test_redduck.py +1 -0
- devduck/tools/__init__.py +4 -44
- devduck/tools/install_tools.py +2 -103
- devduck/tools/mcp_server.py +6 -34
- devduck/tools/tcp.py +7 -6
- devduck/tools/websocket.py +2 -8
- devduck-0.2.0.dist-info/METADATA +143 -0
- devduck-0.2.0.dist-info/RECORD +16 -0
- {devduck-0.1.1766644714.dist-info → devduck-0.2.0.dist-info}/entry_points.txt +0 -1
- devduck-0.2.0.dist-info/licenses/LICENSE +21 -0
- devduck/agentcore_handler.py +0 -76
- devduck/tools/_ambient_input.py +0 -423
- devduck/tools/_tray_app.py +0 -530
- devduck/tools/agentcore_agents.py +0 -197
- devduck/tools/agentcore_config.py +0 -441
- devduck/tools/agentcore_invoke.py +0 -423
- devduck/tools/agentcore_logs.py +0 -320
- devduck/tools/ambient.py +0 -157
- devduck/tools/create_subagent.py +0 -659
- devduck/tools/fetch_github_tool.py +0 -201
- devduck/tools/ipc.py +0 -546
- devduck/tools/scraper.py +0 -935
- devduck/tools/speech_to_speech.py +0 -850
- devduck/tools/state_manager.py +0 -292
- devduck/tools/store_in_kb.py +0 -187
- devduck/tools/system_prompt.py +0 -608
- devduck/tools/tray.py +0 -247
- devduck/tools/use_github.py +0 -438
- devduck-0.1.1766644714.dist-info/METADATA +0 -717
- devduck-0.1.1766644714.dist-info/RECORD +0 -33
- devduck-0.1.1766644714.dist-info/licenses/LICENSE +0 -201
- {devduck-0.1.1766644714.dist-info → devduck-0.2.0.dist-info}/WHEEL +0 -0
- {devduck-0.1.1766644714.dist-info → devduck-0.2.0.dist-info}/top_level.txt +0 -0
devduck/tools/tray.py
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tray app control tool - integrated with devduck
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from strands import tool
|
|
6
|
-
from typing import Dict, Any, List
|
|
7
|
-
import subprocess
|
|
8
|
-
import socket
|
|
9
|
-
import json
|
|
10
|
-
import tempfile
|
|
11
|
-
import os
|
|
12
|
-
import sys
|
|
13
|
-
import time
|
|
14
|
-
import signal
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
# Global state
|
|
18
|
-
_tray_process = None
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _send_ipc_command(socket_path: str, command: Dict) -> Dict:
|
|
22
|
-
"""Send IPC command to tray app"""
|
|
23
|
-
try:
|
|
24
|
-
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
25
|
-
client.settimeout(10.0)
|
|
26
|
-
client.connect(socket_path)
|
|
27
|
-
|
|
28
|
-
# Send command
|
|
29
|
-
message = json.dumps(command).encode("utf-8")
|
|
30
|
-
client.sendall(message)
|
|
31
|
-
|
|
32
|
-
# Receive response
|
|
33
|
-
response_data = b""
|
|
34
|
-
while True:
|
|
35
|
-
chunk = client.recv(4096)
|
|
36
|
-
if not chunk:
|
|
37
|
-
break
|
|
38
|
-
response_data += chunk
|
|
39
|
-
# Check if we have complete JSON
|
|
40
|
-
try:
|
|
41
|
-
json.loads(response_data.decode("utf-8"))
|
|
42
|
-
break
|
|
43
|
-
except:
|
|
44
|
-
continue
|
|
45
|
-
|
|
46
|
-
client.close()
|
|
47
|
-
|
|
48
|
-
if not response_data:
|
|
49
|
-
return {"status": "error", "message": "Empty response"}
|
|
50
|
-
|
|
51
|
-
return json.loads(response_data.decode("utf-8"))
|
|
52
|
-
except socket.timeout:
|
|
53
|
-
return {"status": "error", "message": "Timeout"}
|
|
54
|
-
except Exception as e:
|
|
55
|
-
return {"status": "error", "message": str(e)}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@tool
|
|
59
|
-
def tray(
|
|
60
|
-
action: str,
|
|
61
|
-
items: List[Dict[str, Any]] = None,
|
|
62
|
-
title: str = None,
|
|
63
|
-
message: Dict[str, Any] = None,
|
|
64
|
-
text: str = None,
|
|
65
|
-
) -> Dict[str, Any]:
|
|
66
|
-
"""Control system tray app with devduck integration.
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
Dict with status and content
|
|
70
|
-
"""
|
|
71
|
-
global _tray_process
|
|
72
|
-
|
|
73
|
-
socket_path = os.path.join(tempfile.gettempdir(), "devduck_tray.sock")
|
|
74
|
-
|
|
75
|
-
if action == "start":
|
|
76
|
-
if _tray_process and _tray_process.poll() is None:
|
|
77
|
-
return {"status": "success", "content": [{"text": "✓ Already running"}]}
|
|
78
|
-
|
|
79
|
-
# Get tray script path
|
|
80
|
-
tools_dir = Path(__file__).parent
|
|
81
|
-
tray_script = tools_dir / "_tray_app.py"
|
|
82
|
-
|
|
83
|
-
if not tray_script.exists():
|
|
84
|
-
return {
|
|
85
|
-
"status": "error",
|
|
86
|
-
"content": [{"text": f"❌ Tray app not found: {tray_script}"}],
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
_tray_process = subprocess.Popen(
|
|
90
|
-
[sys.executable, str(tray_script)],
|
|
91
|
-
stdout=subprocess.DEVNULL,
|
|
92
|
-
stderr=subprocess.DEVNULL,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
time.sleep(1.5)
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
"status": "success",
|
|
99
|
-
"content": [{"text": f"✓ Tray app started (PID: {_tray_process.pid})"}],
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
elif action == "stop":
|
|
103
|
-
if _tray_process:
|
|
104
|
-
try:
|
|
105
|
-
os.kill(_tray_process.pid, signal.SIGTERM)
|
|
106
|
-
_tray_process.wait(timeout=3)
|
|
107
|
-
except:
|
|
108
|
-
pass
|
|
109
|
-
_tray_process = None
|
|
110
|
-
|
|
111
|
-
return {"status": "success", "content": [{"text": "✓ Stopped"}]}
|
|
112
|
-
|
|
113
|
-
elif action == "status":
|
|
114
|
-
is_running = _tray_process and _tray_process.poll() is None
|
|
115
|
-
return {"status": "success", "content": [{"text": f"Running: {is_running}"}]}
|
|
116
|
-
|
|
117
|
-
elif action == "update_menu":
|
|
118
|
-
if not items:
|
|
119
|
-
return {
|
|
120
|
-
"status": "error",
|
|
121
|
-
"content": [{"text": "items parameter required"}],
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
result = _send_ipc_command(
|
|
125
|
-
socket_path, {"action": "update_menu", "items": items}
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
if result.get("status") == "success":
|
|
129
|
-
return {
|
|
130
|
-
"status": "success",
|
|
131
|
-
"content": [{"text": f"✓ Menu updated ({len(items)} items)"}],
|
|
132
|
-
}
|
|
133
|
-
else:
|
|
134
|
-
return {
|
|
135
|
-
"status": "error",
|
|
136
|
-
"content": [
|
|
137
|
-
{"text": f"Failed: {result.get('message', 'Unknown error')}"}
|
|
138
|
-
],
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
elif action == "update_title":
|
|
142
|
-
if not title:
|
|
143
|
-
return {
|
|
144
|
-
"status": "error",
|
|
145
|
-
"content": [{"text": "title parameter required"}],
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
result = _send_ipc_command(
|
|
149
|
-
socket_path, {"action": "update_title", "title": title}
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
if result.get("status") == "success":
|
|
153
|
-
return {"status": "success", "content": [{"text": f"✓ Title: {title}"}]}
|
|
154
|
-
else:
|
|
155
|
-
return {
|
|
156
|
-
"status": "error",
|
|
157
|
-
"content": [{"text": f"Failed: {result.get('message')}"}],
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
elif action == "set_progress":
|
|
161
|
-
"""Set progress indicator: idle, thinking, processing, complete, error"""
|
|
162
|
-
if not text:
|
|
163
|
-
return {
|
|
164
|
-
"status": "error",
|
|
165
|
-
"content": [
|
|
166
|
-
{
|
|
167
|
-
"text": "text parameter required (idle/thinking/processing/complete/error)"
|
|
168
|
-
}
|
|
169
|
-
],
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
result = _send_ipc_command(
|
|
173
|
-
socket_path, {"action": "set_progress", "progress": text}
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
if result.get("status") == "success":
|
|
177
|
-
return {"status": "success", "content": [{"text": f"✓ Progress: {text}"}]}
|
|
178
|
-
else:
|
|
179
|
-
return {
|
|
180
|
-
"status": "error",
|
|
181
|
-
"content": [{"text": f"Failed: {result.get('message')}"}],
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
elif action == "notify":
|
|
185
|
-
if not message:
|
|
186
|
-
return {
|
|
187
|
-
"status": "error",
|
|
188
|
-
"content": [{"text": "message parameter required"}],
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
result = _send_ipc_command(
|
|
192
|
-
socket_path, {"action": "notify", "message": message}
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
if result.get("status") == "success":
|
|
196
|
-
return {"status": "success", "content": [{"text": "✓ Notification sent"}]}
|
|
197
|
-
else:
|
|
198
|
-
return {
|
|
199
|
-
"status": "error",
|
|
200
|
-
"content": [{"text": f"Failed: {result.get('message')}"}],
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
elif action == "show_input":
|
|
204
|
-
result = _send_ipc_command(socket_path, {"action": "show_input"})
|
|
205
|
-
|
|
206
|
-
if result.get("status") == "success":
|
|
207
|
-
return {"status": "success", "content": [{"text": "✓ Input shown"}]}
|
|
208
|
-
else:
|
|
209
|
-
return {
|
|
210
|
-
"status": "error",
|
|
211
|
-
"content": [{"text": f"Failed: {result.get('message')}"}],
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
elif action == "stream_text":
|
|
215
|
-
if not text:
|
|
216
|
-
return {"status": "error", "content": [{"text": "text parameter required"}]}
|
|
217
|
-
|
|
218
|
-
result = _send_ipc_command(socket_path, {"action": "stream_text", "text": text})
|
|
219
|
-
|
|
220
|
-
if result.get("status") == "success":
|
|
221
|
-
return {"status": "success", "content": [{"text": "✓ Text streamed"}]}
|
|
222
|
-
else:
|
|
223
|
-
return {
|
|
224
|
-
"status": "error",
|
|
225
|
-
"content": [{"text": f"Failed: {result.get('message')}"}],
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
elif action in ["toggle_tcp", "toggle_ws", "toggle_mcp"]:
|
|
229
|
-
result = _send_ipc_command(socket_path, {"action": action})
|
|
230
|
-
|
|
231
|
-
if result.get("status") == "success":
|
|
232
|
-
return {"status": "success", "content": [{"text": f"✓ {action} executed"}]}
|
|
233
|
-
else:
|
|
234
|
-
return {
|
|
235
|
-
"status": "error",
|
|
236
|
-
"content": [{"text": f"Failed: {result.get('message')}"}],
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
else:
|
|
240
|
-
return {
|
|
241
|
-
"status": "error",
|
|
242
|
-
"content": [
|
|
243
|
-
{
|
|
244
|
-
"text": f"Unknown action: {action}. Available: start, stop, status, update_menu, update_title, set_progress, notify, show_input, stream_text, toggle_tcp, toggle_ws, toggle_mcp"
|
|
245
|
-
}
|
|
246
|
-
],
|
|
247
|
-
}
|
devduck/tools/use_github.py
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
"""GitHub GraphQL API integration tool for Strands Agents.
|
|
2
|
-
|
|
3
|
-
This module provides a comprehensive interface to GitHub's v4 GraphQL API,
|
|
4
|
-
allowing you to execute any GitHub GraphQL query or mutation directly from your Strands Agent.
|
|
5
|
-
The tool handles authentication, parameter validation, response formatting,
|
|
6
|
-
and provides user-friendly error messages with schema recommendations.
|
|
7
|
-
|
|
8
|
-
Key Features:
|
|
9
|
-
|
|
10
|
-
1. Universal GitHub GraphQL Access:
|
|
11
|
-
• Access to GitHub's full GraphQL API (v4)
|
|
12
|
-
• Support for both queries and mutations
|
|
13
|
-
• Authentication via GITHUB_TOKEN environment variable
|
|
14
|
-
• Rate limit awareness and error handling
|
|
15
|
-
|
|
16
|
-
2. Safety Features:
|
|
17
|
-
• Confirmation prompts for mutative operations (mutations)
|
|
18
|
-
• Parameter validation with helpful error messages
|
|
19
|
-
• Error handling with detailed feedback
|
|
20
|
-
• Query complexity analysis
|
|
21
|
-
|
|
22
|
-
3. Response Handling:
|
|
23
|
-
• JSON formatting of responses
|
|
24
|
-
• Error message extraction from GraphQL responses
|
|
25
|
-
• Rate limit information display
|
|
26
|
-
• Pretty printing of operation details
|
|
27
|
-
|
|
28
|
-
4. Usage Examples:
|
|
29
|
-
```python
|
|
30
|
-
from strands import Agent
|
|
31
|
-
from tools.use_github import use_github
|
|
32
|
-
|
|
33
|
-
agent = Agent(tools=[use_github])
|
|
34
|
-
|
|
35
|
-
# Get repository information
|
|
36
|
-
result = agent.tool.use_github(
|
|
37
|
-
query_type="query",
|
|
38
|
-
query='''
|
|
39
|
-
query($owner: String!, $name: String!) {
|
|
40
|
-
repository(owner: $owner, name: $name) {
|
|
41
|
-
name
|
|
42
|
-
description
|
|
43
|
-
stargazerCount
|
|
44
|
-
forkCount
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
''',
|
|
48
|
-
variables={"owner": "octocat", "name": "Hello-World"},
|
|
49
|
-
label="Get repository information",
|
|
50
|
-
)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
See the use_github function docstring for more details on parameters and usage.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
import json
|
|
57
|
-
import logging
|
|
58
|
-
import os
|
|
59
|
-
from typing import Any
|
|
60
|
-
|
|
61
|
-
import requests
|
|
62
|
-
from colorama import Fore, Style, init
|
|
63
|
-
from rich.console import Console
|
|
64
|
-
from rich.panel import Panel
|
|
65
|
-
from strands import tool
|
|
66
|
-
|
|
67
|
-
# Initialize colorama
|
|
68
|
-
init(autoreset=True)
|
|
69
|
-
|
|
70
|
-
logger = logging.getLogger(__name__)
|
|
71
|
-
|
|
72
|
-
# GitHub GraphQL API endpoint
|
|
73
|
-
GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def create_console() -> Console:
|
|
77
|
-
"""Create a Rich console instance."""
|
|
78
|
-
return Console()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def get_user_input(prompt: str) -> str:
|
|
82
|
-
"""Simple user input function with styled prompt."""
|
|
83
|
-
# Remove Rich markup for simple input
|
|
84
|
-
clean_prompt = prompt.replace("<yellow><bold>", "").replace(
|
|
85
|
-
"</bold> [y/*]</yellow>", " [y/*] "
|
|
86
|
-
)
|
|
87
|
-
return input(clean_prompt)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# Common mutation keywords that indicate potentially destructive operations
|
|
91
|
-
MUTATIVE_KEYWORDS = [
|
|
92
|
-
"create",
|
|
93
|
-
"update",
|
|
94
|
-
"delete",
|
|
95
|
-
"add",
|
|
96
|
-
"remove",
|
|
97
|
-
"merge",
|
|
98
|
-
"close",
|
|
99
|
-
"reopen",
|
|
100
|
-
"lock",
|
|
101
|
-
"unlock",
|
|
102
|
-
"pin",
|
|
103
|
-
"unpin",
|
|
104
|
-
"transfer",
|
|
105
|
-
"archive",
|
|
106
|
-
"unarchive",
|
|
107
|
-
"enable",
|
|
108
|
-
"disable",
|
|
109
|
-
"accept",
|
|
110
|
-
"decline",
|
|
111
|
-
"dismiss",
|
|
112
|
-
"submit",
|
|
113
|
-
"request",
|
|
114
|
-
"cancel",
|
|
115
|
-
"convert",
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def get_github_token() -> str | None:
|
|
120
|
-
"""Get GitHub token from environment variables.
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
GitHub token string or None if not found
|
|
124
|
-
"""
|
|
125
|
-
return os.environ.get("GITHUB_TOKEN", "")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def is_mutation_query(query: str) -> bool:
|
|
129
|
-
"""Check if a GraphQL query is a mutation based on keywords and structure.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
query: GraphQL query string
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
True if the query appears to be a mutation
|
|
136
|
-
"""
|
|
137
|
-
query_lower = query.lower().strip()
|
|
138
|
-
|
|
139
|
-
# Check if query starts with "mutation"
|
|
140
|
-
if query_lower.startswith("mutation"):
|
|
141
|
-
return True
|
|
142
|
-
|
|
143
|
-
# Check for mutative keywords in the query
|
|
144
|
-
return any(keyword in query_lower for keyword in MUTATIVE_KEYWORDS)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def execute_github_graphql(
|
|
148
|
-
query: str, variables: dict[str, Any] | None = None, token: str | None = None
|
|
149
|
-
) -> dict[str, Any]:
|
|
150
|
-
"""Execute a GraphQL query against GitHub's API.
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
query: GraphQL query string
|
|
154
|
-
variables: Optional variables for the query
|
|
155
|
-
token: GitHub authentication token
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
Dictionary containing the GraphQL response
|
|
159
|
-
|
|
160
|
-
Raises:
|
|
161
|
-
requests.RequestException: If the request fails
|
|
162
|
-
ValueError: If authentication fails
|
|
163
|
-
"""
|
|
164
|
-
if not token:
|
|
165
|
-
raise ValueError(
|
|
166
|
-
"GitHub token is required. Set GITHUB_TOKEN environment variable."
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
headers = {
|
|
170
|
-
"Authorization": f"Bearer {token}",
|
|
171
|
-
"Content-Type": "application/json",
|
|
172
|
-
"Accept": "application/vnd.github.v4+json",
|
|
173
|
-
"User-Agent": "Strands-Agent-GitHub-Tool/1.0",
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
payload = {"query": query, "variables": variables or {}}
|
|
177
|
-
|
|
178
|
-
response = requests.post(
|
|
179
|
-
GITHUB_GRAPHQL_URL, headers=headers, json=payload, timeout=30
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
response.raise_for_status()
|
|
183
|
-
response_data: dict[str, Any] = response.json()
|
|
184
|
-
return response_data
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def format_github_response(response: dict[str, Any]) -> str:
|
|
188
|
-
"""Format GitHub GraphQL response for display.
|
|
189
|
-
|
|
190
|
-
Args:
|
|
191
|
-
response: GitHub GraphQL response dictionary
|
|
192
|
-
|
|
193
|
-
Returns:
|
|
194
|
-
Formatted string representation of the response
|
|
195
|
-
"""
|
|
196
|
-
formatted_parts = []
|
|
197
|
-
|
|
198
|
-
# Handle errors
|
|
199
|
-
if "errors" in response:
|
|
200
|
-
formatted_parts.append(f"{Fore.RED}Errors:{Style.RESET_ALL}")
|
|
201
|
-
for error in response["errors"]:
|
|
202
|
-
formatted_parts.append(f" - {error.get('message', 'Unknown error')}")
|
|
203
|
-
if "locations" in error:
|
|
204
|
-
locations = error["locations"]
|
|
205
|
-
formatted_parts.append(f" Locations: {locations}")
|
|
206
|
-
|
|
207
|
-
# Handle data
|
|
208
|
-
if "data" in response:
|
|
209
|
-
formatted_parts.append(f"{Fore.GREEN}Data:{Style.RESET_ALL}")
|
|
210
|
-
formatted_parts.append(json.dumps(response["data"], indent=2))
|
|
211
|
-
|
|
212
|
-
# Handle rate limit info
|
|
213
|
-
if "extensions" in response and "cost" in response["extensions"]:
|
|
214
|
-
cost_info = response["extensions"]["cost"]
|
|
215
|
-
formatted_parts.append(f"{Fore.YELLOW}Rate Limit Info:{Style.RESET_ALL}")
|
|
216
|
-
formatted_parts.append(
|
|
217
|
-
f" - Query Cost: {cost_info.get('requestedQueryCost', 'N/A')}"
|
|
218
|
-
)
|
|
219
|
-
formatted_parts.append(f" - Node Count: {cost_info.get('nodeCount', 'N/A')}")
|
|
220
|
-
if "rateLimit" in cost_info:
|
|
221
|
-
rate_limit = cost_info["rateLimit"]
|
|
222
|
-
formatted_parts.append(
|
|
223
|
-
f" - Remaining: {rate_limit.get('remaining', 'N/A')}"
|
|
224
|
-
)
|
|
225
|
-
formatted_parts.append(f" - Reset At: {rate_limit.get('resetAt', 'N/A')}")
|
|
226
|
-
|
|
227
|
-
return "\n".join(formatted_parts)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@tool
|
|
231
|
-
def use_github(
|
|
232
|
-
query_type: str,
|
|
233
|
-
query: str,
|
|
234
|
-
label: str,
|
|
235
|
-
variables: dict[str, Any] | None = None,
|
|
236
|
-
) -> dict[str, Any]:
|
|
237
|
-
"""Execute GitHub GraphQL API operations with comprehensive error handling and validation.
|
|
238
|
-
|
|
239
|
-
This tool provides a universal interface to GitHub's GraphQL API (v4), allowing you to execute
|
|
240
|
-
any query or mutation supported by GitHub's GraphQL schema. It handles authentication via
|
|
241
|
-
GITHUB_TOKEN, parameter validation, response formatting, and provides helpful error messages.
|
|
242
|
-
|
|
243
|
-
How It Works:
|
|
244
|
-
------------
|
|
245
|
-
1. The tool validates the GitHub token from environment variables
|
|
246
|
-
2. For mutations or potentially destructive operations, it prompts for confirmation
|
|
247
|
-
3. It executes the GraphQL query/mutation against GitHub's API
|
|
248
|
-
4. Responses are processed and formatted with proper error handling
|
|
249
|
-
5. Rate limit information is displayed when available
|
|
250
|
-
|
|
251
|
-
Common Usage Scenarios:
|
|
252
|
-
---------------------
|
|
253
|
-
- Repository Management: Get repository info, create/update repositories
|
|
254
|
-
- Issue & PR Operations: Create, update, close issues and pull requests
|
|
255
|
-
- User & Organization Data: Retrieve user profiles, organization details
|
|
256
|
-
- Project Management: Manage GitHub Projects, milestones, and labels
|
|
257
|
-
- Git Operations: Access commit history, branches, and tags
|
|
258
|
-
- Security: Manage webhooks, deploy keys, and security settings
|
|
259
|
-
|
|
260
|
-
Example Queries:
|
|
261
|
-
---------------
|
|
262
|
-
Repository Information:
|
|
263
|
-
```graphql
|
|
264
|
-
query($owner: String!, $name: String!) {
|
|
265
|
-
repository(owner: $owner, name: $name) {
|
|
266
|
-
name
|
|
267
|
-
description
|
|
268
|
-
stargazerCount
|
|
269
|
-
forkCount
|
|
270
|
-
issues(states: OPEN) {
|
|
271
|
-
totalCount
|
|
272
|
-
}
|
|
273
|
-
pullRequests(states: OPEN) {
|
|
274
|
-
totalCount
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
Create Issue Mutation:
|
|
281
|
-
```graphql
|
|
282
|
-
mutation($repositoryId: ID!, $title: String!, $body: String) {
|
|
283
|
-
createIssue(input: {repositoryId: $repositoryId, title: $title, body: $body}) {
|
|
284
|
-
issue {
|
|
285
|
-
number
|
|
286
|
-
title
|
|
287
|
-
url
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
query_type: Type of GraphQL operation ("query" or "mutation")
|
|
295
|
-
query: The GraphQL query or mutation string
|
|
296
|
-
label: Human-readable description of the GitHub operation
|
|
297
|
-
variables: Optional dictionary of variables for the query
|
|
298
|
-
|
|
299
|
-
Returns:
|
|
300
|
-
Dict containing status and response content:
|
|
301
|
-
{
|
|
302
|
-
"status": "success|error",
|
|
303
|
-
"content": [{"text": "Response message"}]
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
Notes:
|
|
307
|
-
- Requires GITHUB_TOKEN environment variable to be set
|
|
308
|
-
- Mutations require user confirmation in non-dev environments
|
|
309
|
-
- You can disable confirmation by setting BYPASS_TOOL_CONSENT=true
|
|
310
|
-
- The tool automatically handles rate limiting information
|
|
311
|
-
- GraphQL errors are formatted and displayed clearly
|
|
312
|
-
- All responses are JSON formatted for easy parsing
|
|
313
|
-
|
|
314
|
-
Environment Variables:
|
|
315
|
-
- GITHUB_TOKEN: Required GitHub personal access token or app token
|
|
316
|
-
- BYPASS_TOOL_CONSENT: Set to "true" to skip confirmation prompts
|
|
317
|
-
"""
|
|
318
|
-
console = create_console()
|
|
319
|
-
|
|
320
|
-
# Set default for variables if None
|
|
321
|
-
if variables is None:
|
|
322
|
-
variables = {}
|
|
323
|
-
|
|
324
|
-
STRANDS_BYPASS_TOOL_CONSENT = (
|
|
325
|
-
os.environ.get("BYPASS_TOOL_CONSENT", "").lower() == "true"
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
# Create a panel for GitHub Operation Details
|
|
329
|
-
operation_details = f"{Fore.CYAN}Type:{Style.RESET_ALL} {query_type}\n"
|
|
330
|
-
operation_details += f"{Fore.CYAN}Query:{Style.RESET_ALL}\n{query}\n"
|
|
331
|
-
if variables:
|
|
332
|
-
operation_details += f"{Fore.CYAN}Variables:{Style.RESET_ALL}\n"
|
|
333
|
-
for key, value in variables.items():
|
|
334
|
-
operation_details += f" - {key}: {value}\n"
|
|
335
|
-
|
|
336
|
-
console.print(Panel(operation_details, title=label, expand=False))
|
|
337
|
-
|
|
338
|
-
logger.debug(
|
|
339
|
-
f"Invoking GitHub GraphQL: query_type = {query_type}, variables = {variables}"
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
# Get GitHub token
|
|
343
|
-
github_token = get_github_token()
|
|
344
|
-
if not github_token:
|
|
345
|
-
return {
|
|
346
|
-
"status": "error",
|
|
347
|
-
"content": [
|
|
348
|
-
{
|
|
349
|
-
"text": "GitHub token not found. Please set the GITHUB_TOKEN environment variable.\n"
|
|
350
|
-
"You can create a token at: https://github.com/settings/tokens"
|
|
351
|
-
}
|
|
352
|
-
],
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
# Check if the operation is potentially mutative
|
|
356
|
-
is_mutative = query_type.lower() == "mutation" or is_mutation_query(query)
|
|
357
|
-
|
|
358
|
-
if is_mutative and not STRANDS_BYPASS_TOOL_CONSENT:
|
|
359
|
-
# Prompt for confirmation before executing the operation
|
|
360
|
-
confirm = get_user_input(
|
|
361
|
-
f"<yellow><bold>This appears to be a mutative operation ({query_type}). "
|
|
362
|
-
f"Do you want to proceed?</bold> [y/*]</yellow>"
|
|
363
|
-
)
|
|
364
|
-
if confirm.lower() != "y":
|
|
365
|
-
return {
|
|
366
|
-
"status": "error",
|
|
367
|
-
"content": [
|
|
368
|
-
{"text": f"Operation canceled by user. Reason: {confirm}."}
|
|
369
|
-
],
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
try:
|
|
373
|
-
# Execute the GraphQL query
|
|
374
|
-
response = execute_github_graphql(query, variables, github_token)
|
|
375
|
-
|
|
376
|
-
# Format the response
|
|
377
|
-
formatted_response = format_github_response(response)
|
|
378
|
-
|
|
379
|
-
# Check if there were GraphQL errors
|
|
380
|
-
if "errors" in response:
|
|
381
|
-
return {
|
|
382
|
-
"status": "error",
|
|
383
|
-
"content": [
|
|
384
|
-
{"text": "GraphQL query completed with errors:"},
|
|
385
|
-
{"text": formatted_response},
|
|
386
|
-
],
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
"status": "success",
|
|
391
|
-
"content": [{"text": formatted_response}],
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
except requests.exceptions.HTTPError as http_err:
|
|
395
|
-
if http_err.response.status_code == 401:
|
|
396
|
-
return {
|
|
397
|
-
"status": "error",
|
|
398
|
-
"content": [
|
|
399
|
-
{
|
|
400
|
-
"text": "Authentication failed. Please check your GITHUB_TOKEN.\n"
|
|
401
|
-
"Make sure the token has the required permissions for this operation."
|
|
402
|
-
}
|
|
403
|
-
],
|
|
404
|
-
}
|
|
405
|
-
elif http_err.response.status_code == 403:
|
|
406
|
-
return {
|
|
407
|
-
"status": "error",
|
|
408
|
-
"content": [
|
|
409
|
-
{
|
|
410
|
-
"text": "Forbidden. Your token may not have sufficient permissions for this operation.\n"
|
|
411
|
-
f"HTTP Error: {http_err}"
|
|
412
|
-
}
|
|
413
|
-
],
|
|
414
|
-
}
|
|
415
|
-
else:
|
|
416
|
-
return {
|
|
417
|
-
"status": "error",
|
|
418
|
-
"content": [{"text": f"HTTP Error: {http_err}"}],
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
except requests.exceptions.RequestException as req_err:
|
|
422
|
-
return {
|
|
423
|
-
"status": "error",
|
|
424
|
-
"content": [{"text": f"Request Error: {req_err}"}],
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
except ValueError as val_err:
|
|
428
|
-
return {
|
|
429
|
-
"status": "error",
|
|
430
|
-
"content": [{"text": f"Configuration Error: {val_err}"}],
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
except Exception as ex:
|
|
434
|
-
logger.warning(f"GitHub GraphQL call threw exception: {type(ex).__name__}")
|
|
435
|
-
return {
|
|
436
|
-
"status": "error",
|
|
437
|
-
"content": [{"text": f"GitHub GraphQL call threw exception: {ex!s}"}],
|
|
438
|
-
}
|