agentic-fabriq-sdk 0.1.5__py3-none-any.whl → 0.1.7__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 agentic-fabriq-sdk might be problematic. Click here for more details.
- af_cli/__init__.py +8 -0
- af_cli/commands/__init__.py +3 -0
- af_cli/commands/agents.py +238 -0
- af_cli/commands/auth.py +390 -0
- af_cli/commands/config.py +102 -0
- af_cli/commands/mcp_servers.py +83 -0
- af_cli/commands/secrets.py +109 -0
- af_cli/commands/tools.py +474 -0
- af_cli/core/__init__.py +3 -0
- af_cli/core/client.py +127 -0
- af_cli/core/config.py +200 -0
- af_cli/core/oauth.py +506 -0
- af_cli/core/output.py +180 -0
- af_cli/core/token_storage.py +263 -0
- af_cli/main.py +187 -0
- {agentic_fabriq_sdk-0.1.5.dist-info → agentic_fabriq_sdk-0.1.7.dist-info}/METADATA +40 -10
- {agentic_fabriq_sdk-0.1.5.dist-info → agentic_fabriq_sdk-0.1.7.dist-info}/RECORD +19 -3
- agentic_fabriq_sdk-0.1.7.dist-info/entry_points.txt +3 -0
- {agentic_fabriq_sdk-0.1.5.dist-info → agentic_fabriq_sdk-0.1.7.dist-info}/WHEEL +0 -0
af_cli/commands/tools.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool management commands for the Agentic Fabric CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from af_cli.core.client import get_client
|
|
9
|
+
from af_cli.core.output import debug, error, info, print_output, success, warning
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Tool management commands")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def list(
|
|
16
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
17
|
+
):
|
|
18
|
+
"""List your tool connections (configured and connected tools)."""
|
|
19
|
+
try:
|
|
20
|
+
with get_client() as client:
|
|
21
|
+
connections = client.get("/api/v1/user-connections")
|
|
22
|
+
|
|
23
|
+
if not connections:
|
|
24
|
+
warning("No tool connections found. Add connections in the dashboard UI.")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
# Format for better display
|
|
28
|
+
display_data = []
|
|
29
|
+
for conn in connections:
|
|
30
|
+
# Format tool name nicely (e.g., "google_docs" -> "Google Docs")
|
|
31
|
+
tool_name = conn.get("tool", "N/A").replace("_", " ").title()
|
|
32
|
+
|
|
33
|
+
# Status indicator
|
|
34
|
+
status = "✓ Connected" if conn.get("connected") else "○ Configured"
|
|
35
|
+
|
|
36
|
+
display_data.append({
|
|
37
|
+
"Tool": tool_name,
|
|
38
|
+
"ID": conn.get("connection_id", "N/A"),
|
|
39
|
+
"Name": conn.get("display_name") or conn.get("connection_id", "N/A"),
|
|
40
|
+
"Status": status,
|
|
41
|
+
"Method": conn.get("method", "oauth"),
|
|
42
|
+
"Added": conn.get("created_at", "N/A")[:10] if conn.get("created_at") else "N/A",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
print_output(
|
|
46
|
+
display_data,
|
|
47
|
+
format_type=format,
|
|
48
|
+
title="Your Tool Connections"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
error(f"Failed to list tool connections: {e}")
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command()
|
|
57
|
+
def get(
|
|
58
|
+
connection_id: str = typer.Argument(..., help="Connection ID (e.g., 'google', 'slack')"),
|
|
59
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
60
|
+
):
|
|
61
|
+
"""Get tool connection details."""
|
|
62
|
+
try:
|
|
63
|
+
with get_client() as client:
|
|
64
|
+
# Get all user connections and find the matching one
|
|
65
|
+
connections = client.get("/api/v1/user-connections")
|
|
66
|
+
|
|
67
|
+
# Find the specific connection
|
|
68
|
+
connection = None
|
|
69
|
+
for conn in connections:
|
|
70
|
+
if conn.get("connection_id") == connection_id or conn.get("tool") == connection_id:
|
|
71
|
+
connection = conn
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if not connection:
|
|
75
|
+
error(f"Connection '{connection_id}' not found")
|
|
76
|
+
info("Available connections:")
|
|
77
|
+
for conn in connections:
|
|
78
|
+
info(f" - {conn.get('tool')} (ID: {conn.get('connection_id')})")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
# Format tool name nicely
|
|
82
|
+
tool_name = connection.get("tool", "N/A").replace("_", " ").title()
|
|
83
|
+
|
|
84
|
+
# Format the connection details for display
|
|
85
|
+
details = {
|
|
86
|
+
"Tool": tool_name,
|
|
87
|
+
"Connection ID": connection.get("connection_id", "N/A"),
|
|
88
|
+
"Display Name": connection.get("display_name") or connection.get("connection_id", "N/A"),
|
|
89
|
+
"Status": "✓ Connected" if connection.get("connected") else "○ Configured",
|
|
90
|
+
"Method": connection.get("method", "oauth"),
|
|
91
|
+
"Created": connection.get("created_at", "N/A"),
|
|
92
|
+
"Updated": connection.get("updated_at", "N/A"),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Add tool-specific fields if present
|
|
96
|
+
if connection.get("team_name"):
|
|
97
|
+
details["Team Name"] = connection.get("team_name")
|
|
98
|
+
if connection.get("team_id"):
|
|
99
|
+
details["Team ID"] = connection.get("team_id")
|
|
100
|
+
if connection.get("bot_user_id"):
|
|
101
|
+
details["Bot User ID"] = connection.get("bot_user_id")
|
|
102
|
+
if connection.get("email"):
|
|
103
|
+
details["Email"] = connection.get("email")
|
|
104
|
+
if connection.get("login"):
|
|
105
|
+
details["GitHub Login"] = connection.get("login")
|
|
106
|
+
if connection.get("workspace_name"):
|
|
107
|
+
details["Workspace Name"] = connection.get("workspace_name")
|
|
108
|
+
if connection.get("scopes"):
|
|
109
|
+
details["Scopes"] = ", ".join(connection.get("scopes", []))
|
|
110
|
+
|
|
111
|
+
print_output(
|
|
112
|
+
details,
|
|
113
|
+
format_type=format,
|
|
114
|
+
title=f"{tool_name} Connection Details"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
error(f"Failed to get tool connection: {e}")
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command()
|
|
123
|
+
def invoke(
|
|
124
|
+
tool_id: str = typer.Argument(..., help="Tool ID"),
|
|
125
|
+
method: str = typer.Option(..., "--method", "-m", help="Tool method to invoke"),
|
|
126
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
127
|
+
):
|
|
128
|
+
"""Invoke a tool."""
|
|
129
|
+
try:
|
|
130
|
+
with get_client() as client:
|
|
131
|
+
data = {
|
|
132
|
+
"method": method,
|
|
133
|
+
"parameters": {},
|
|
134
|
+
"context": {},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
info(f"Invoking tool {tool_id} method {method}...")
|
|
138
|
+
response = client.post(f"/api/v1/tools/{tool_id}/invoke", data)
|
|
139
|
+
|
|
140
|
+
success("Tool invoked successfully")
|
|
141
|
+
print_output(response, format_type=format)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
error(f"Failed to invoke tool: {e}")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command()
|
|
149
|
+
def add(
|
|
150
|
+
tool: str = typer.Argument(..., help="Tool name (google, slack, notion, github, etc.)"),
|
|
151
|
+
connection_id: str = typer.Option(..., "--connection-id", help="Unique connection ID"),
|
|
152
|
+
display_name: str = typer.Option(None, "--display-name", help="Human-readable name"),
|
|
153
|
+
method: str = typer.Option(..., "--method", help="Connection method: 'api' or 'credentials'"),
|
|
154
|
+
|
|
155
|
+
# API method fields
|
|
156
|
+
token: str = typer.Option(None, "--token", help="API token (for api method)"),
|
|
157
|
+
|
|
158
|
+
# Credentials method fields
|
|
159
|
+
client_id: str = typer.Option(None, "--client-id", help="OAuth client ID (for credentials method)"),
|
|
160
|
+
client_secret: str = typer.Option(None, "--client-secret", help="OAuth client secret (for credentials method)"),
|
|
161
|
+
redirect_uri: str = typer.Option(None, "--redirect-uri", help="OAuth redirect URI (optional, auto-generated)"),
|
|
162
|
+
):
|
|
163
|
+
"""
|
|
164
|
+
Add a new tool connection with credentials.
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
# Notion (api method - single token)
|
|
168
|
+
afctl tools add notion --connection-id notion-work --method api --token "secret_abc123"
|
|
169
|
+
|
|
170
|
+
# Google (credentials method - OAuth app)
|
|
171
|
+
afctl tools add google --connection-id google-work --method credentials \\
|
|
172
|
+
--client-id "123.apps.googleusercontent.com" \\
|
|
173
|
+
--client-secret "GOCSPX-abc123"
|
|
174
|
+
|
|
175
|
+
# Slack bot (api method)
|
|
176
|
+
afctl tools add slack --connection-id slack-bot --method api --token "xoxb-123..."
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
from af_cli.core.config import get_config
|
|
180
|
+
|
|
181
|
+
with get_client() as client:
|
|
182
|
+
# Validate method
|
|
183
|
+
if method not in ["api", "credentials"]:
|
|
184
|
+
error("Method must be 'api' or 'credentials'")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
# Validate API method requirements
|
|
188
|
+
if method == "api":
|
|
189
|
+
if not token:
|
|
190
|
+
error("API method requires --token")
|
|
191
|
+
info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method api --token YOUR_TOKEN")
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
# Validate credentials method requirements
|
|
195
|
+
elif method == "credentials":
|
|
196
|
+
if not client_id or not client_secret:
|
|
197
|
+
error("Credentials method requires --client-id and --client-secret")
|
|
198
|
+
info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method credentials \\")
|
|
199
|
+
info(f" --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET")
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
|
|
202
|
+
info(f"Creating connection: {connection_id}")
|
|
203
|
+
info(f"Tool: {tool}")
|
|
204
|
+
info(f"Method: {method}")
|
|
205
|
+
|
|
206
|
+
# Step 1: Create connection metadata
|
|
207
|
+
connection_data = {
|
|
208
|
+
"tool": tool,
|
|
209
|
+
"connection_id": connection_id,
|
|
210
|
+
"display_name": display_name or connection_id,
|
|
211
|
+
"method": method,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
client.post("/api/v1/user-connections", data=connection_data)
|
|
215
|
+
success(f"✅ Connection entry created: {connection_id}")
|
|
216
|
+
|
|
217
|
+
# Step 2: Store credentials based on method
|
|
218
|
+
if method == "credentials":
|
|
219
|
+
# Auto-generate redirect_uri if not provided
|
|
220
|
+
if not redirect_uri:
|
|
221
|
+
config = get_config()
|
|
222
|
+
redirect_uri = f"{config.gateway_url}/api/v1/tools/{tool}/oauth/callback"
|
|
223
|
+
info(f"Using default redirect URI: {redirect_uri}")
|
|
224
|
+
|
|
225
|
+
# Store OAuth app config
|
|
226
|
+
info("Storing OAuth app credentials...")
|
|
227
|
+
config_payload = {
|
|
228
|
+
"client_id": client_id,
|
|
229
|
+
"client_secret": client_secret,
|
|
230
|
+
}
|
|
231
|
+
if redirect_uri:
|
|
232
|
+
config_payload["redirect_uri"] = redirect_uri
|
|
233
|
+
|
|
234
|
+
client.post(
|
|
235
|
+
f"/api/v1/tools/{tool}/config?connection_id={connection_id}",
|
|
236
|
+
data=config_payload
|
|
237
|
+
)
|
|
238
|
+
success("✅ OAuth app credentials stored")
|
|
239
|
+
info("")
|
|
240
|
+
info(f"Next: Run 'afctl tools connect {connection_id}' to complete OAuth setup")
|
|
241
|
+
|
|
242
|
+
elif method == "api":
|
|
243
|
+
# Store API token directly
|
|
244
|
+
info("Storing API credentials...")
|
|
245
|
+
|
|
246
|
+
# Tool-specific endpoint and payload mappings
|
|
247
|
+
if tool == "notion":
|
|
248
|
+
# Notion uses /config endpoint with integration_token field
|
|
249
|
+
endpoint = f"/api/v1/tools/{tool}/config?connection_id={connection_id}"
|
|
250
|
+
cred_payload = {"integration_token": token}
|
|
251
|
+
else:
|
|
252
|
+
# Generic tools use /connection endpoint with api_token field
|
|
253
|
+
endpoint = f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
|
|
254
|
+
cred_payload = {"api_token": token}
|
|
255
|
+
|
|
256
|
+
client.post(endpoint, data=cred_payload)
|
|
257
|
+
success("✅ API credentials stored")
|
|
258
|
+
success(f"✅ Connection '{connection_id}' is ready to use!")
|
|
259
|
+
|
|
260
|
+
# Show helpful info
|
|
261
|
+
info("")
|
|
262
|
+
info("View your connections:")
|
|
263
|
+
info(f" • List all: afctl tools list")
|
|
264
|
+
info(f" • View details: afctl tools get {connection_id}")
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
error(f"Failed to add connection: {e}")
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@app.command()
|
|
272
|
+
def connect(
|
|
273
|
+
connection_id: str = typer.Argument(..., help="Connection ID to connect"),
|
|
274
|
+
):
|
|
275
|
+
"""Complete OAuth connection (open browser for authorization)."""
|
|
276
|
+
try:
|
|
277
|
+
import webbrowser
|
|
278
|
+
import time
|
|
279
|
+
|
|
280
|
+
with get_client() as client:
|
|
281
|
+
# Get connection info
|
|
282
|
+
connections = client.get("/api/v1/user-connections")
|
|
283
|
+
|
|
284
|
+
connection = None
|
|
285
|
+
for conn in connections:
|
|
286
|
+
if conn.get("connection_id") == connection_id:
|
|
287
|
+
connection = conn
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
if not connection:
|
|
291
|
+
error(f"Connection '{connection_id}' not found")
|
|
292
|
+
info("Run 'afctl tools list' to see available connections")
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
|
|
295
|
+
tool = connection["tool"]
|
|
296
|
+
method = connection["method"]
|
|
297
|
+
|
|
298
|
+
# Only credentials method needs OAuth completion
|
|
299
|
+
if method != "credentials":
|
|
300
|
+
error(f"Connection '{connection_id}' uses '{method}' method")
|
|
301
|
+
info("Only 'credentials' method connections need OAuth setup")
|
|
302
|
+
info("API connections are already connected after 'afctl tools add'")
|
|
303
|
+
raise typer.Exit(1)
|
|
304
|
+
|
|
305
|
+
# Check if already connected
|
|
306
|
+
if connection.get("connected"):
|
|
307
|
+
warning(f"Connection '{connection_id}' is already connected")
|
|
308
|
+
confirm = typer.confirm("Do you want to reconnect (re-authorize)?")
|
|
309
|
+
if not confirm:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Initiate OAuth flow
|
|
313
|
+
info(f"Initiating OAuth for {tool}...")
|
|
314
|
+
|
|
315
|
+
result = client.post(
|
|
316
|
+
f"/api/v1/tools/{tool}/connect/initiate?connection_id={connection_id}",
|
|
317
|
+
data={}
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
debug(f"Backend response: {result}")
|
|
321
|
+
|
|
322
|
+
# Different tools use different field names for the auth URL
|
|
323
|
+
auth_url = (
|
|
324
|
+
result.get("authorization_url") or
|
|
325
|
+
result.get("auth_url") or
|
|
326
|
+
result.get("oauth_url")
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if not auth_url:
|
|
330
|
+
error("Failed to get authorization URL from backend")
|
|
331
|
+
error(f"Response keys: {list(result.keys())}")
|
|
332
|
+
debug(f"Full response: {result}")
|
|
333
|
+
raise typer.Exit(1)
|
|
334
|
+
|
|
335
|
+
info("Opening browser for authentication...")
|
|
336
|
+
info("")
|
|
337
|
+
info(f"If browser doesn't open, visit: {auth_url}")
|
|
338
|
+
|
|
339
|
+
# Open browser
|
|
340
|
+
webbrowser.open(auth_url)
|
|
341
|
+
|
|
342
|
+
info("")
|
|
343
|
+
info("Waiting for authorization...")
|
|
344
|
+
info("(Complete the login in your browser)")
|
|
345
|
+
|
|
346
|
+
# Poll for connection completion
|
|
347
|
+
max_attempts = 120 # 2 minutes
|
|
348
|
+
for attempt in range(max_attempts):
|
|
349
|
+
time.sleep(1)
|
|
350
|
+
|
|
351
|
+
# Check connection status
|
|
352
|
+
connections = client.get("/api/v1/user-connections")
|
|
353
|
+
for conn in connections:
|
|
354
|
+
if conn.get("connection_id") == connection_id:
|
|
355
|
+
if conn.get("connected"):
|
|
356
|
+
info("")
|
|
357
|
+
success(f"✅ Successfully connected to {tool}!")
|
|
358
|
+
|
|
359
|
+
# Show connection details
|
|
360
|
+
info(f"Connection ID: {connection_id}")
|
|
361
|
+
if conn.get("email"):
|
|
362
|
+
info(f"Email: {conn['email']}")
|
|
363
|
+
if conn.get("team_name"):
|
|
364
|
+
info(f"Team: {conn['team_name']}")
|
|
365
|
+
if conn.get("login"):
|
|
366
|
+
info(f"GitHub: {conn['login']}")
|
|
367
|
+
|
|
368
|
+
return
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
# Timeout
|
|
372
|
+
error("")
|
|
373
|
+
error("Timeout: Authorization not completed within 2 minutes")
|
|
374
|
+
info("Please try again or check your browser")
|
|
375
|
+
raise typer.Exit(1)
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
error(f"Failed to connect: {e}")
|
|
379
|
+
raise typer.Exit(1)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@app.command()
|
|
383
|
+
def disconnect(
|
|
384
|
+
connection_id: str = typer.Argument(..., help="Connection ID to disconnect"),
|
|
385
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
386
|
+
):
|
|
387
|
+
"""Disconnect a tool (remove credentials but keep connection entry)."""
|
|
388
|
+
try:
|
|
389
|
+
with get_client() as client:
|
|
390
|
+
# Get connection info
|
|
391
|
+
connections = client.get("/api/v1/user-connections")
|
|
392
|
+
|
|
393
|
+
connection = None
|
|
394
|
+
for conn in connections:
|
|
395
|
+
if conn.get("connection_id") == connection_id:
|
|
396
|
+
connection = conn
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
if not connection:
|
|
400
|
+
error(f"Connection '{connection_id}' not found")
|
|
401
|
+
raise typer.Exit(1)
|
|
402
|
+
|
|
403
|
+
tool = connection["tool"]
|
|
404
|
+
tool_display = connection.get("display_name") or connection_id
|
|
405
|
+
|
|
406
|
+
# Check if connected
|
|
407
|
+
if not connection.get("connected"):
|
|
408
|
+
error(f"Connection '{connection_id}' is already disconnected")
|
|
409
|
+
info(f"Use 'afctl tools get {connection_id}' to view status")
|
|
410
|
+
raise typer.Exit(1)
|
|
411
|
+
|
|
412
|
+
# Confirm
|
|
413
|
+
if not force:
|
|
414
|
+
warning(f"This will remove OAuth tokens/credentials for '{tool_display}'")
|
|
415
|
+
info("You can reconnect later with 'afctl tools connect'")
|
|
416
|
+
confirm = typer.confirm(f"Disconnect {tool} connection '{connection_id}'?")
|
|
417
|
+
if not confirm:
|
|
418
|
+
info("Cancelled")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
# Delete connection credentials
|
|
422
|
+
client.delete(
|
|
423
|
+
f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
success(f"✅ Disconnected: {connection_id}")
|
|
427
|
+
info("Connection entry preserved.")
|
|
428
|
+
info(f"Run 'afctl tools connect {connection_id}' to reconnect.")
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
error(f"Failed to disconnect: {e}")
|
|
432
|
+
raise typer.Exit(1)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@app.command()
|
|
436
|
+
def remove(
|
|
437
|
+
connection_id: str = typer.Argument(..., help="Connection ID to remove"),
|
|
438
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
439
|
+
):
|
|
440
|
+
"""Remove a tool connection completely (delete entry and credentials)."""
|
|
441
|
+
try:
|
|
442
|
+
with get_client() as client:
|
|
443
|
+
# Get connection info
|
|
444
|
+
connections = client.get("/api/v1/user-connections")
|
|
445
|
+
|
|
446
|
+
connection = None
|
|
447
|
+
for conn in connections:
|
|
448
|
+
if conn.get("connection_id") == connection_id:
|
|
449
|
+
connection = conn
|
|
450
|
+
break
|
|
451
|
+
|
|
452
|
+
if not connection:
|
|
453
|
+
error(f"Connection '{connection_id}' not found")
|
|
454
|
+
raise typer.Exit(1)
|
|
455
|
+
|
|
456
|
+
tool = connection["tool"]
|
|
457
|
+
tool_display = connection.get("display_name") or connection_id
|
|
458
|
+
|
|
459
|
+
# Confirm
|
|
460
|
+
if not force:
|
|
461
|
+
warning("⚠️ This will permanently delete the connection and credentials")
|
|
462
|
+
confirm = typer.confirm(f"Remove {tool} connection '{tool_display}'?")
|
|
463
|
+
if not confirm:
|
|
464
|
+
info("Cancelled")
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
# Delete connection entry (backend will cascade delete credentials)
|
|
468
|
+
client.delete(f"/api/v1/user-connections/{connection_id}")
|
|
469
|
+
|
|
470
|
+
success(f"✅ Removed: {connection_id}")
|
|
471
|
+
|
|
472
|
+
except Exception as e:
|
|
473
|
+
error(f"Failed to remove: {e}")
|
|
474
|
+
raise typer.Exit(1)
|
af_cli/core/__init__.py
ADDED
af_cli/core/client.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP client for communicating with the Agentic Fabric Gateway.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from urllib.parse import urljoin
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from af_cli.core.config import get_config
|
|
13
|
+
from af_cli.core.output import debug, error
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AFClient:
|
|
17
|
+
"""HTTP client for Agentic Fabric Gateway API."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: Optional[Dict] = None):
|
|
20
|
+
self.config = config or get_config()
|
|
21
|
+
self.client = httpx.Client(
|
|
22
|
+
base_url=self.config.gateway_url,
|
|
23
|
+
timeout=30.0,
|
|
24
|
+
follow_redirects=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
28
|
+
"""Get HTTP headers for requests."""
|
|
29
|
+
return self.config.get_headers()
|
|
30
|
+
|
|
31
|
+
def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
|
32
|
+
"""Handle HTTP response."""
|
|
33
|
+
debug(f"Response: {response.status_code} {response.url}")
|
|
34
|
+
|
|
35
|
+
if response.status_code == 401:
|
|
36
|
+
error("Authentication failed. Please run 'afctl auth login'")
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
if response.status_code == 403:
|
|
40
|
+
error("Access denied. Check your permissions.")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
if response.status_code >= 400:
|
|
44
|
+
try:
|
|
45
|
+
error_data = response.json()
|
|
46
|
+
# Try different error message fields (FastAPI uses "detail")
|
|
47
|
+
error_message = error_data.get("detail") or error_data.get("message") or "Unknown error"
|
|
48
|
+
error(f"API Error: {error_message}")
|
|
49
|
+
# Always show full response for debugging
|
|
50
|
+
debug(f"Response status: {response.status_code}")
|
|
51
|
+
debug(f"Request URL: {response.url}")
|
|
52
|
+
debug(f"Full response: {json.dumps(error_data, indent=2)}")
|
|
53
|
+
except:
|
|
54
|
+
error(f"HTTP Error: {response.status_code}")
|
|
55
|
+
debug(f"Response text: {response.text}")
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
return response.json()
|
|
60
|
+
except:
|
|
61
|
+
return {"message": "Success"}
|
|
62
|
+
|
|
63
|
+
def get(self, path: str, params: Optional[Dict] = None) -> Dict[str, Any]:
|
|
64
|
+
"""Make GET request."""
|
|
65
|
+
url = urljoin(self.config.gateway_url, path)
|
|
66
|
+
debug(f"GET {url}")
|
|
67
|
+
|
|
68
|
+
response = self.client.get(
|
|
69
|
+
path,
|
|
70
|
+
params=params,
|
|
71
|
+
headers=self._get_headers(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return self._handle_response(response)
|
|
75
|
+
|
|
76
|
+
def post(self, path: str, data: Optional[Dict] = None) -> Dict[str, Any]:
|
|
77
|
+
"""Make POST request."""
|
|
78
|
+
url = urljoin(self.config.gateway_url, path)
|
|
79
|
+
debug(f"POST {url}")
|
|
80
|
+
|
|
81
|
+
response = self.client.post(
|
|
82
|
+
path,
|
|
83
|
+
json=data,
|
|
84
|
+
headers=self._get_headers(),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return self._handle_response(response)
|
|
88
|
+
|
|
89
|
+
def put(self, path: str, data: Optional[Dict] = None) -> Dict[str, Any]:
|
|
90
|
+
"""Make PUT request."""
|
|
91
|
+
url = urljoin(self.config.gateway_url, path)
|
|
92
|
+
debug(f"PUT {url}")
|
|
93
|
+
|
|
94
|
+
response = self.client.put(
|
|
95
|
+
path,
|
|
96
|
+
json=data,
|
|
97
|
+
headers=self._get_headers(),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return self._handle_response(response)
|
|
101
|
+
|
|
102
|
+
def delete(self, path: str) -> Dict[str, Any]:
|
|
103
|
+
"""Make DELETE request."""
|
|
104
|
+
url = urljoin(self.config.gateway_url, path)
|
|
105
|
+
debug(f"DELETE {url}")
|
|
106
|
+
|
|
107
|
+
response = self.client.delete(
|
|
108
|
+
path,
|
|
109
|
+
headers=self._get_headers(),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return self._handle_response(response)
|
|
113
|
+
|
|
114
|
+
def close(self) -> None:
|
|
115
|
+
"""Close the HTTP client."""
|
|
116
|
+
self.client.close()
|
|
117
|
+
|
|
118
|
+
def __enter__(self):
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
122
|
+
self.close()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_client() -> AFClient:
|
|
126
|
+
"""Get HTTP client instance."""
|
|
127
|
+
return AFClient()
|