agentic-fabriq-sdk 0.1.6__tar.gz → 0.1.8__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.

Potentially problematic release.


This version of agentic-fabriq-sdk might be problematic. Click here for more details.

Files changed (40) hide show
  1. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/PKG-INFO +4 -4
  2. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/README.md +3 -3
  3. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/commands/auth.py +11 -9
  4. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/commands/config.py +1 -1
  5. agentic_fabriq_sdk-0.1.8/af_cli/commands/tools.py +505 -0
  6. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/core/client.py +9 -5
  7. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/core/output.py +4 -4
  8. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/main.py +3 -3
  9. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/pyproject.toml +1 -1
  10. agentic_fabriq_sdk-0.1.6/af_cli/commands/tools.py +0 -83
  11. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/__init__.py +0 -0
  12. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/commands/__init__.py +0 -0
  13. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/commands/agents.py +0 -0
  14. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/commands/mcp_servers.py +0 -0
  15. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/commands/secrets.py +0 -0
  16. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/core/__init__.py +0 -0
  17. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/core/config.py +0 -0
  18. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/core/oauth.py +0 -0
  19. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_cli/core/token_storage.py +0 -0
  20. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/__init__.py +0 -0
  21. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/auth/__init__.py +0 -0
  22. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/auth/dpop.py +0 -0
  23. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/auth/oauth.py +0 -0
  24. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/auth/token_cache.py +0 -0
  25. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/connectors/__init__.py +0 -0
  26. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/connectors/base.py +0 -0
  27. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/connectors/registry.py +0 -0
  28. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/dx/__init__.py +0 -0
  29. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/dx/decorators.py +0 -0
  30. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/dx/runtime.py +0 -0
  31. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/events.py +0 -0
  32. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/exceptions.py +0 -0
  33. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/fabriq_client.py +0 -0
  34. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/models/__init__.py +0 -0
  35. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/models/audit.py +0 -0
  36. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/models/types.py +0 -0
  37. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/py.typed +0 -0
  38. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/transport/__init__.py +0 -0
  39. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/transport/http.py +0 -0
  40. {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.8}/af_sdk/vault.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-fabriq-sdk
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Agentic Fabriq SDK: high-level client, CLI tool, DX helpers, and auth for AI agents
5
5
  License: Apache-2.0
6
6
  Keywords: fabriq,agentic-fabriq,sdk,ai,agents,agentic,fabric,cli
@@ -90,8 +90,8 @@ DX orchestration:
90
90
  ```python
91
91
  from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
92
92
 
93
- slack = ToolFabric(provider="slack", base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
94
- agents = AgentFabric(base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
93
+ slack = ToolFabric(provider="slack", base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
94
+ agents = AgentFabric(base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
95
95
 
96
96
  @tool
97
97
  def echo(x: str) -> str:
@@ -101,7 +101,7 @@ bot = Agent(
101
101
  system_prompt="demo",
102
102
  tools=[echo],
103
103
  agents=agents.get_agents(["summarizer"]),
104
- base_url="http://localhost:8000",
104
+ base_url="https://dashboard.agenticfabriq.com",
105
105
  access_token=TOKEN,
106
106
  tenant_id=TENANT,
107
107
  provider_fabrics={"slack": slack},
@@ -57,8 +57,8 @@ DX orchestration:
57
57
  ```python
58
58
  from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
59
59
 
60
- slack = ToolFabric(provider="slack", base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
61
- agents = AgentFabric(base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
60
+ slack = ToolFabric(provider="slack", base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
61
+ agents = AgentFabric(base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
62
62
 
63
63
  @tool
64
64
  def echo(x: str) -> str:
@@ -68,7 +68,7 @@ bot = Agent(
68
68
  system_prompt="demo",
69
69
  tools=[echo],
70
70
  agents=agents.get_agents(["summarizer"]),
71
- base_url="http://localhost:8000",
71
+ base_url="https://dashboard.agenticfabriq.com",
72
72
  access_token=TOKEN,
73
73
  tenant_id=TENANT,
74
74
  provider_fabrics={"slack": slack},
@@ -98,9 +98,12 @@ def login(
98
98
  # Extract and save token data
99
99
  token_data = token_storage.extract_token_info(tokens)
100
100
 
101
- # Override tenant_id if provided
101
+ # Override tenant_id if provided, otherwise use default for Keycloak JWTs
102
102
  if tenant_id:
103
103
  token_data.tenant_id = tenant_id
104
+ elif not token_data.tenant_id:
105
+ # Keycloak JWTs don't have tenant_id, use the same default as the backend
106
+ token_data.tenant_id = "550e8400-e29b-41d4-a716-446655440000"
104
107
 
105
108
  # Save tokens
106
109
  token_storage.save(token_data)
@@ -109,8 +112,7 @@ def login(
109
112
  config.access_token = token_data.access_token
110
113
  config.refresh_token = token_data.refresh_token
111
114
  config.token_expires_at = token_data.expires_at
112
- if token_data.tenant_id:
113
- config.tenant_id = token_data.tenant_id
115
+ config.tenant_id = token_data.tenant_id # Always set tenant_id
114
116
  config.save()
115
117
 
116
118
  # Display success message
@@ -190,7 +192,7 @@ def status():
190
192
 
191
193
  if not token_data:
192
194
  warning("Not authenticated")
193
- info("Run 'af auth login' to authenticate")
195
+ info("Run 'afctl auth login' to authenticate")
194
196
  return
195
197
 
196
198
  # Create status table
@@ -242,9 +244,9 @@ def status():
242
244
  # Show recommendations
243
245
  if is_expired:
244
246
  if token_data.refresh_token:
245
- info("Token has expired. Run 'af auth refresh' to get a new token")
247
+ info("Token has expired. Run 'afctl auth refresh' to get a new token")
246
248
  else:
247
- info("Token has expired. Run 'af auth login' to re-authenticate")
249
+ info("Token has expired. Run 'afctl auth login' to re-authenticate")
248
250
 
249
251
 
250
252
  @app.command()
@@ -270,7 +272,7 @@ def refresh(
270
272
 
271
273
  if not token_data or not token_data.refresh_token:
272
274
  error("No refresh token available")
273
- info("Run 'af auth login' to authenticate")
275
+ info("Run 'afctl auth login' to authenticate")
274
276
  raise typer.Exit(1)
275
277
 
276
278
  try:
@@ -304,7 +306,7 @@ def refresh(
304
306
 
305
307
  except Exception as e:
306
308
  error(f"Token refresh failed: {e}")
307
- error("Please run 'af auth login' to re-authenticate")
309
+ error("Please run 'afctl auth login' to re-authenticate")
308
310
 
309
311
  # Clear invalid tokens
310
312
  token_storage.delete()
@@ -366,7 +368,7 @@ def whoami():
366
368
 
367
369
  if not token_data:
368
370
  warning("Not authenticated")
369
- info("Run 'af auth login' to authenticate")
371
+ info("Run 'afctl auth login' to authenticate")
370
372
  return
371
373
 
372
374
  # Create user info table
@@ -93,7 +93,7 @@ def reset():
93
93
  config.clear_auth()
94
94
 
95
95
  # Reset to defaults
96
- config.gateway_url = "http://localhost:8000"
96
+ config.gateway_url = "https://dashboard.agenticfabriq.com"
97
97
  config.tenant_id = None
98
98
  config.output_format = "table"
99
99
 
@@ -0,0 +1,505 @@
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_drive, google_slides, 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', 'credentials', or 'oauth'"),
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 tool name - check for common mistakes
183
+ if tool.lower() == "google":
184
+ error("❌ Invalid tool name: 'google'")
185
+ info("")
186
+ info("Please specify the exact Google Workspace tool:")
187
+ info(" • google_drive - Google Drive")
188
+ info(" • google_docs - Google Docs")
189
+ info(" • google_sheets - Google Sheets")
190
+ info(" • google_slides - Google Slides")
191
+ info(" • gmail - Gmail")
192
+ info(" • google_calendar - Google Calendar")
193
+ info(" • google_meet - Google Meet")
194
+ info(" • google_forms - Google Forms")
195
+ info(" • google_classroom - Google Classroom")
196
+ info(" • google_people - Google People (Contacts)")
197
+ info(" • google_chat - Google Chat")
198
+ info(" • google_tasks - Google Tasks")
199
+ info("")
200
+ info("Example:")
201
+ info(f" afctl tools add google_drive --connection-id {connection_id} --method {method}")
202
+ raise typer.Exit(1)
203
+
204
+ # Validate method
205
+ if method not in ["api", "credentials", "oauth"]:
206
+ error("Method must be 'api', 'credentials', or 'oauth'")
207
+ raise typer.Exit(1)
208
+
209
+ # Validate API method requirements
210
+ if method == "api":
211
+ if not token:
212
+ error("API method requires --token")
213
+ info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method api --token YOUR_TOKEN")
214
+ raise typer.Exit(1)
215
+
216
+ # Validate credentials method requirements
217
+ elif method == "credentials":
218
+ if not client_id or not client_secret:
219
+ error("Credentials method requires --client-id and --client-secret")
220
+ info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method credentials \\")
221
+ info(f" --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET")
222
+ raise typer.Exit(1)
223
+
224
+ info(f"Creating connection: {connection_id}")
225
+ info(f"Tool: {tool}")
226
+ info(f"Method: {method}")
227
+
228
+ # Step 1: Create connection metadata
229
+ connection_data = {
230
+ "tool": tool,
231
+ "connection_id": connection_id,
232
+ "display_name": display_name or connection_id,
233
+ "method": method,
234
+ }
235
+
236
+ client.post("/api/v1/user-connections", data=connection_data)
237
+ success(f"✅ Connection entry created: {connection_id}")
238
+
239
+ # Step 2: Store credentials based on method
240
+ if method == "credentials" or method == "oauth":
241
+ # Determine the API base tool name (Google tools all use "google")
242
+ api_tool_name = "google" if (tool.startswith("google_") or tool == "gmail") else tool
243
+
244
+ # Auto-generate redirect_uri if not provided
245
+ if not redirect_uri:
246
+ config = get_config()
247
+ redirect_uri = f"{config.gateway_url}/api/v1/tools/{api_tool_name}/oauth/callback"
248
+ info(f"Using default redirect URI: {redirect_uri}")
249
+
250
+ # Store OAuth app config
251
+ info("Storing OAuth app credentials...")
252
+ config_payload = {
253
+ "client_id": client_id,
254
+ "client_secret": client_secret,
255
+ }
256
+ if redirect_uri:
257
+ config_payload["redirect_uri"] = redirect_uri
258
+
259
+ client.post(
260
+ f"/api/v1/tools/{api_tool_name}/config?connection_id={connection_id}",
261
+ data=config_payload
262
+ )
263
+ success("✅ OAuth app credentials stored")
264
+ info("")
265
+ info(f"Next: Run 'afctl tools connect {connection_id}' to complete OAuth setup")
266
+
267
+ elif method == "api":
268
+ # Store API token directly
269
+ info("Storing API credentials...")
270
+
271
+ # Tool-specific endpoint and payload mappings
272
+ if tool == "notion":
273
+ # Notion uses /config endpoint with integration_token field
274
+ endpoint = f"/api/v1/tools/{tool}/config?connection_id={connection_id}"
275
+ cred_payload = {"integration_token": token}
276
+ else:
277
+ # Generic tools use /connection endpoint with api_token field
278
+ endpoint = f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
279
+ cred_payload = {"api_token": token}
280
+
281
+ client.post(endpoint, data=cred_payload)
282
+ success("✅ API credentials stored")
283
+ success(f"✅ Connection '{connection_id}' is ready to use!")
284
+
285
+ # Show helpful info
286
+ info("")
287
+ info("View your connections:")
288
+ info(f" • List all: afctl tools list")
289
+ info(f" • View details: afctl tools get {connection_id}")
290
+
291
+ except Exception as e:
292
+ error(f"Failed to add connection: {e}")
293
+ raise typer.Exit(1)
294
+
295
+
296
+ @app.command()
297
+ def connect(
298
+ connection_id: str = typer.Argument(..., help="Connection ID to connect"),
299
+ ):
300
+ """Complete OAuth connection (open browser for authorization)."""
301
+ try:
302
+ import webbrowser
303
+ import time
304
+
305
+ with get_client() as client:
306
+ # Get connection info
307
+ connections = client.get("/api/v1/user-connections")
308
+
309
+ connection = None
310
+ for conn in connections:
311
+ if conn.get("connection_id") == connection_id:
312
+ connection = conn
313
+ break
314
+
315
+ if not connection:
316
+ error(f"Connection '{connection_id}' not found")
317
+ info("Run 'afctl tools list' to see available connections")
318
+ raise typer.Exit(1)
319
+
320
+ tool = connection["tool"]
321
+ method = connection["method"]
322
+
323
+ # Only credentials/oauth method needs OAuth completion
324
+ if method not in ["credentials", "oauth"]:
325
+ error(f"Connection '{connection_id}' uses '{method}' method")
326
+ info("Only 'credentials' or 'oauth' method connections need OAuth setup")
327
+ info("API connections are already connected after 'afctl tools add'")
328
+ raise typer.Exit(1)
329
+
330
+ # Check if already connected
331
+ if connection.get("connected"):
332
+ warning(f"Connection '{connection_id}' is already connected")
333
+ confirm = typer.confirm("Do you want to reconnect (re-authorize)?")
334
+ if not confirm:
335
+ return
336
+
337
+ # Determine the API base tool name (Google tools all use "google")
338
+ api_tool_name = "google" if (tool.startswith("google_") or tool == "gmail") else tool
339
+
340
+ # Initiate OAuth flow
341
+ info(f"Initiating OAuth for {tool}...")
342
+
343
+ # For Google tools, pass the specific tool_type parameter
344
+ tool_type_param = f"&tool_type={tool}" if tool != api_tool_name else ""
345
+
346
+ result = client.post(
347
+ f"/api/v1/tools/{api_tool_name}/connect/initiate?connection_id={connection_id}{tool_type_param}",
348
+ data={}
349
+ )
350
+
351
+ debug(f"Backend response: {result}")
352
+
353
+ # Different tools use different field names for the auth URL
354
+ auth_url = (
355
+ result.get("authorization_url") or
356
+ result.get("auth_url") or
357
+ result.get("oauth_url")
358
+ )
359
+
360
+ if not auth_url:
361
+ error("Failed to get authorization URL from backend")
362
+ error(f"Response keys: {list(result.keys())}")
363
+ debug(f"Full response: {result}")
364
+ raise typer.Exit(1)
365
+
366
+ info("Opening browser for authentication...")
367
+ info("")
368
+ info(f"If browser doesn't open, visit: {auth_url}")
369
+
370
+ # Open browser
371
+ webbrowser.open(auth_url)
372
+
373
+ info("")
374
+ info("Waiting for authorization...")
375
+ info("(Complete the login in your browser)")
376
+
377
+ # Poll for connection completion
378
+ max_attempts = 120 # 2 minutes
379
+ for attempt in range(max_attempts):
380
+ time.sleep(1)
381
+
382
+ # Check connection status
383
+ connections = client.get("/api/v1/user-connections")
384
+ for conn in connections:
385
+ if conn.get("connection_id") == connection_id:
386
+ if conn.get("connected"):
387
+ info("")
388
+ success(f"✅ Successfully connected to {tool}!")
389
+
390
+ # Show connection details
391
+ info(f"Connection ID: {connection_id}")
392
+ if conn.get("email"):
393
+ info(f"Email: {conn['email']}")
394
+ if conn.get("team_name"):
395
+ info(f"Team: {conn['team_name']}")
396
+ if conn.get("login"):
397
+ info(f"GitHub: {conn['login']}")
398
+
399
+ return
400
+ break
401
+
402
+ # Timeout
403
+ error("")
404
+ error("Timeout: Authorization not completed within 2 minutes")
405
+ info("Please try again or check your browser")
406
+ raise typer.Exit(1)
407
+
408
+ except Exception as e:
409
+ error(f"Failed to connect: {e}")
410
+ raise typer.Exit(1)
411
+
412
+
413
+ @app.command()
414
+ def disconnect(
415
+ connection_id: str = typer.Argument(..., help="Connection ID to disconnect"),
416
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
417
+ ):
418
+ """Disconnect a tool (remove credentials but keep connection entry)."""
419
+ try:
420
+ with get_client() as client:
421
+ # Get connection info
422
+ connections = client.get("/api/v1/user-connections")
423
+
424
+ connection = None
425
+ for conn in connections:
426
+ if conn.get("connection_id") == connection_id:
427
+ connection = conn
428
+ break
429
+
430
+ if not connection:
431
+ error(f"Connection '{connection_id}' not found")
432
+ raise typer.Exit(1)
433
+
434
+ tool = connection["tool"]
435
+ tool_display = connection.get("display_name") or connection_id
436
+
437
+ # Check if connected
438
+ if not connection.get("connected"):
439
+ error(f"Connection '{connection_id}' is already disconnected")
440
+ info(f"Use 'afctl tools get {connection_id}' to view status")
441
+ raise typer.Exit(1)
442
+
443
+ # Confirm
444
+ if not force:
445
+ warning(f"This will remove OAuth tokens/credentials for '{tool_display}'")
446
+ info("You can reconnect later with 'afctl tools connect'")
447
+ confirm = typer.confirm(f"Disconnect {tool} connection '{connection_id}'?")
448
+ if not confirm:
449
+ info("Cancelled")
450
+ return
451
+
452
+ # Delete connection credentials
453
+ client.delete(
454
+ f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
455
+ )
456
+
457
+ success(f"✅ Disconnected: {connection_id}")
458
+ info("Connection entry preserved.")
459
+ info(f"Run 'afctl tools connect {connection_id}' to reconnect.")
460
+
461
+ except Exception as e:
462
+ error(f"Failed to disconnect: {e}")
463
+ raise typer.Exit(1)
464
+
465
+
466
+ @app.command()
467
+ def remove(
468
+ connection_id: str = typer.Argument(..., help="Connection ID to remove"),
469
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
470
+ ):
471
+ """Remove a tool connection completely (delete entry and credentials)."""
472
+ try:
473
+ with get_client() as client:
474
+ # Get connection info
475
+ connections = client.get("/api/v1/user-connections")
476
+
477
+ connection = None
478
+ for conn in connections:
479
+ if conn.get("connection_id") == connection_id:
480
+ connection = conn
481
+ break
482
+
483
+ if not connection:
484
+ error(f"Connection '{connection_id}' not found")
485
+ raise typer.Exit(1)
486
+
487
+ tool = connection["tool"]
488
+ tool_display = connection.get("display_name") or connection_id
489
+
490
+ # Confirm
491
+ if not force:
492
+ warning("⚠️ This will permanently delete the connection and credentials")
493
+ confirm = typer.confirm(f"Remove {tool} connection '{tool_display}'?")
494
+ if not confirm:
495
+ info("Cancelled")
496
+ return
497
+
498
+ # Delete connection entry (backend will cascade delete credentials)
499
+ client.delete(f"/api/v1/user-connections/{connection_id}")
500
+
501
+ success(f"✅ Removed: {connection_id}")
502
+
503
+ except Exception as e:
504
+ error(f"Failed to remove: {e}")
505
+ raise typer.Exit(1)
@@ -33,7 +33,7 @@ class AFClient:
33
33
  debug(f"Response: {response.status_code} {response.url}")
34
34
 
35
35
  if response.status_code == 401:
36
- error("Authentication failed. Please run 'af auth login'")
36
+ error("Authentication failed. Please run 'afctl auth login'")
37
37
  raise typer.Exit(1)
38
38
 
39
39
  if response.status_code == 403:
@@ -43,12 +43,16 @@ class AFClient:
43
43
  if response.status_code >= 400:
44
44
  try:
45
45
  error_data = response.json()
46
- error_message = error_data.get("message", "Unknown error")
46
+ # Try different error message fields (FastAPI uses "detail")
47
+ error_message = error_data.get("detail") or error_data.get("message") or "Unknown error"
47
48
  error(f"API Error: {error_message}")
48
- if self.config.verbose and "details" in error_data:
49
- debug(f"Error details: {json.dumps(error_data['details'], indent=2)}")
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)}")
50
53
  except:
51
- error(f"HTTP Error: {response.status_code} {response.text}")
54
+ error(f"HTTP Error: {response.status_code}")
55
+ debug(f"Response text: {response.text}")
52
56
  raise typer.Exit(1)
53
57
 
54
58
  try:
@@ -55,12 +55,12 @@ def print_table(
55
55
  if columns is None:
56
56
  columns = list(data[0].keys())
57
57
 
58
- # Create table
59
- table = Table(title=title)
58
+ # Create table with expand to fill terminal width and show grid lines
59
+ table = Table(title=title, expand=True, show_lines=True)
60
60
 
61
- # Add columns
61
+ # Add columns with no_wrap to prevent text wrapping
62
62
  for column in columns:
63
- table.add_column(column.replace("_", " ").title(), style="cyan")
63
+ table.add_column(column.replace("_", " ").title(), style="cyan", no_wrap=True)
64
64
 
65
65
  # Add rows
66
66
  for row in data:
@@ -68,7 +68,7 @@ def status():
68
68
  if config.access_token:
69
69
  table.add_row("Authentication", "✅ Authenticated", f"Tenant: {config.tenant_id}")
70
70
  else:
71
- table.add_row("Authentication", "❌ Not authenticated", "Run 'af auth login'")
71
+ table.add_row("Authentication", "❌ Not authenticated", "Run 'afctl auth login'")
72
72
 
73
73
  # Check configuration
74
74
  config_path = config.config_file
@@ -83,7 +83,7 @@ def status():
83
83
  @app.command()
84
84
  def init(
85
85
  gateway_url: str = typer.Option(
86
- "http://localhost:8000",
86
+ "https://dashboard.agenticfabriq.com",
87
87
  "--gateway-url",
88
88
  "-g",
89
89
  help="Gateway URL"
@@ -125,7 +125,7 @@ def init(
125
125
  if tenant_id:
126
126
  info(f"Tenant ID: {tenant_id}")
127
127
  else:
128
- info("Run 'af auth login' to authenticate")
128
+ info("Run 'afctl auth login' to authenticate")
129
129
 
130
130
 
131
131
  @app.callback()
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
5
5
 
6
6
  [tool.poetry]
7
7
  name = "agentic-fabriq-sdk"
8
- version = "0.1.6"
8
+ version = "0.1.8"
9
9
  description = "Agentic Fabriq SDK: high-level client, CLI tool, DX helpers, and auth for AI agents"
10
10
  readme = "README.md"
11
11
  license = "Apache-2.0"
@@ -1,83 +0,0 @@
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 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 tools."""
19
- try:
20
- with get_client() as client:
21
- response = client.get("/api/v1/tools")
22
- tools = response["tools"]
23
-
24
- if not tools:
25
- warning("No tools found")
26
- return
27
-
28
- print_output(
29
- tools,
30
- format_type=format,
31
- title="Tools"
32
- )
33
-
34
- except Exception as e:
35
- error(f"Failed to list tools: {e}")
36
- raise typer.Exit(1)
37
-
38
-
39
- @app.command()
40
- def get(
41
- tool_id: str = typer.Argument(..., help="Tool ID"),
42
- format: str = typer.Option("table", "--format", "-f", help="Output format"),
43
- ):
44
- """Get tool details."""
45
- try:
46
- with get_client() as client:
47
- tool = client.get(f"/api/v1/tools/{tool_id}")
48
-
49
- print_output(
50
- tool,
51
- format_type=format,
52
- title=f"Tool {tool_id}"
53
- )
54
-
55
- except Exception as e:
56
- error(f"Failed to get tool: {e}")
57
- raise typer.Exit(1)
58
-
59
-
60
- @app.command()
61
- def invoke(
62
- tool_id: str = typer.Argument(..., help="Tool ID"),
63
- method: str = typer.Option(..., "--method", "-m", help="Tool method to invoke"),
64
- format: str = typer.Option("table", "--format", "-f", help="Output format"),
65
- ):
66
- """Invoke a tool."""
67
- try:
68
- with get_client() as client:
69
- data = {
70
- "method": method,
71
- "parameters": {},
72
- "context": {},
73
- }
74
-
75
- info(f"Invoking tool {tool_id} method {method}...")
76
- response = client.post(f"/api/v1/tools/{tool_id}/invoke", data)
77
-
78
- success("Tool invoked successfully")
79
- print_output(response, format_type=format)
80
-
81
- except Exception as e:
82
- error(f"Failed to invoke tool: {e}")
83
- raise typer.Exit(1)