agentic-fabriq-sdk 0.1.4__py3-none-any.whl → 0.1.6__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 +388 -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 +83 -0
- af_cli/core/__init__.py +3 -0
- af_cli/core/client.py +123 -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.4.dist-info → agentic_fabriq_sdk-0.1.6.dist-info}/METADATA +40 -10
- {agentic_fabriq_sdk-0.1.4.dist-info → agentic_fabriq_sdk-0.1.6.dist-info}/RECORD +19 -3
- agentic_fabriq_sdk-0.1.6.dist-info/entry_points.txt +3 -0
- {agentic_fabriq_sdk-0.1.4.dist-info → agentic_fabriq_sdk-0.1.6.dist-info}/WHEEL +0 -0
af_cli/core/output.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatting utilities for the Agentic Fabric CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from af_cli.core.config import get_config
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def success(message: str) -> None:
|
|
18
|
+
"""Print success message."""
|
|
19
|
+
console.print(f"✅ {message}", style="green")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def error(message: str) -> None:
|
|
23
|
+
"""Print error message."""
|
|
24
|
+
console.print(f"❌ {message}", style="red")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def warning(message: str) -> None:
|
|
28
|
+
"""Print warning message."""
|
|
29
|
+
console.print(f"⚠️ {message}", style="yellow")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def info(message: str) -> None:
|
|
33
|
+
"""Print info message."""
|
|
34
|
+
console.print(f"ℹ️ {message}", style="blue")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def debug(message: str) -> None:
|
|
38
|
+
"""Print debug message if verbose mode is enabled."""
|
|
39
|
+
config = get_config()
|
|
40
|
+
if config.verbose:
|
|
41
|
+
console.print(f"🔍 {message}", style="dim")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def print_table(
|
|
45
|
+
data: List[Dict[str, Any]],
|
|
46
|
+
columns: Optional[List[str]] = None,
|
|
47
|
+
title: Optional[str] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Print data as a table."""
|
|
50
|
+
if not data:
|
|
51
|
+
warning("No data to display")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Use provided columns or infer from first row
|
|
55
|
+
if columns is None:
|
|
56
|
+
columns = list(data[0].keys())
|
|
57
|
+
|
|
58
|
+
# Create table
|
|
59
|
+
table = Table(title=title)
|
|
60
|
+
|
|
61
|
+
# Add columns
|
|
62
|
+
for column in columns:
|
|
63
|
+
table.add_column(column.replace("_", " ").title(), style="cyan")
|
|
64
|
+
|
|
65
|
+
# Add rows
|
|
66
|
+
for row in data:
|
|
67
|
+
table.add_row(*[str(row.get(col, "")) for col in columns])
|
|
68
|
+
|
|
69
|
+
console.print(table)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def print_json(data: Any) -> None:
|
|
73
|
+
"""Print data as JSON."""
|
|
74
|
+
console.print_json(json.dumps(data, indent=2, default=str))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def print_yaml(data: Any) -> None:
|
|
78
|
+
"""Print data as YAML."""
|
|
79
|
+
console.print(yaml.dump(data, default_flow_style=False))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def print_output(
|
|
83
|
+
data: Any,
|
|
84
|
+
format_type: Optional[str] = None,
|
|
85
|
+
columns: Optional[List[str]] = None,
|
|
86
|
+
title: Optional[str] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Print output in the specified format."""
|
|
89
|
+
config = get_config()
|
|
90
|
+
format_type = format_type or config.output_format
|
|
91
|
+
|
|
92
|
+
if format_type == "json":
|
|
93
|
+
print_json(data)
|
|
94
|
+
elif format_type == "yaml":
|
|
95
|
+
print_yaml(data)
|
|
96
|
+
elif format_type == "table":
|
|
97
|
+
if isinstance(data, list):
|
|
98
|
+
print_table(data, columns, title)
|
|
99
|
+
else:
|
|
100
|
+
# Convert single item to table format
|
|
101
|
+
if isinstance(data, dict):
|
|
102
|
+
table_data = [{"Field": k, "Value": v} for k, v in data.items()]
|
|
103
|
+
print_table(table_data, ["Field", "Value"], title)
|
|
104
|
+
else:
|
|
105
|
+
console.print(str(data))
|
|
106
|
+
else:
|
|
107
|
+
console.print(str(data))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def print_status(
|
|
111
|
+
resource_type: str,
|
|
112
|
+
resource_id: str,
|
|
113
|
+
status: str,
|
|
114
|
+
details: Optional[Dict[str, Any]] = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Print resource status."""
|
|
117
|
+
status_color = {
|
|
118
|
+
"created": "green",
|
|
119
|
+
"updated": "blue",
|
|
120
|
+
"deleted": "red",
|
|
121
|
+
"error": "red",
|
|
122
|
+
"warning": "yellow",
|
|
123
|
+
}.get(status, "white")
|
|
124
|
+
|
|
125
|
+
message = f"{resource_type} {resource_id} {status}"
|
|
126
|
+
console.print(message, style=status_color)
|
|
127
|
+
|
|
128
|
+
if details and get_config().verbose:
|
|
129
|
+
for key, value in details.items():
|
|
130
|
+
console.print(f" {key}: {value}", style="dim")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def prompt_confirm(message: str, default: bool = False) -> bool:
|
|
134
|
+
"""Prompt for confirmation."""
|
|
135
|
+
default_text = "Y/n" if default else "y/N"
|
|
136
|
+
response = console.input(f"{message} [{default_text}]: ")
|
|
137
|
+
|
|
138
|
+
if not response:
|
|
139
|
+
return default
|
|
140
|
+
|
|
141
|
+
return response.lower() in ["y", "yes"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def prompt_input(message: str, default: Optional[str] = None) -> str:
|
|
145
|
+
"""Prompt for input."""
|
|
146
|
+
if default:
|
|
147
|
+
message = f"{message} [{default}]"
|
|
148
|
+
|
|
149
|
+
response = console.input(f"{message}: ")
|
|
150
|
+
|
|
151
|
+
if not response and default:
|
|
152
|
+
return default
|
|
153
|
+
|
|
154
|
+
return response
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def format_timestamp(timestamp: str) -> str:
|
|
158
|
+
"""Format timestamp for display."""
|
|
159
|
+
try:
|
|
160
|
+
from datetime import datetime
|
|
161
|
+
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
162
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
163
|
+
except:
|
|
164
|
+
return timestamp
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def format_size(size: int) -> str:
|
|
168
|
+
"""Format file size for display."""
|
|
169
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
170
|
+
if size < 1024.0:
|
|
171
|
+
return f"{size:.1f}{unit}"
|
|
172
|
+
size /= 1024.0
|
|
173
|
+
return f"{size:.1f}PB"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def truncate_text(text: str, max_length: int = 50) -> str:
|
|
177
|
+
"""Truncate text for display."""
|
|
178
|
+
if len(text) <= max_length:
|
|
179
|
+
return text
|
|
180
|
+
return text[:max_length - 3] + "..."
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secure token storage for Agentic Fabric CLI.
|
|
3
|
+
|
|
4
|
+
This module provides secure storage for authentication tokens using the system keychain
|
|
5
|
+
(macOS Keychain, Windows Credential Manager, Linux Secret Service) with fallback to
|
|
6
|
+
encrypted file storage.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, Optional
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import keyring
|
|
17
|
+
KEYRING_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
KEYRING_AVAILABLE = False
|
|
20
|
+
keyring = None
|
|
21
|
+
|
|
22
|
+
import jwt
|
|
23
|
+
from cryptography.fernet import Fernet
|
|
24
|
+
from pydantic import BaseModel
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TokenData(BaseModel):
|
|
28
|
+
"""Token data model."""
|
|
29
|
+
|
|
30
|
+
access_token: str
|
|
31
|
+
refresh_token: Optional[str] = None
|
|
32
|
+
expires_at: int # Unix timestamp
|
|
33
|
+
tenant_id: Optional[str] = None
|
|
34
|
+
user_id: Optional[str] = None
|
|
35
|
+
email: Optional[str] = None
|
|
36
|
+
name: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TokenStorage:
|
|
40
|
+
"""Secure token storage using system keychain or encrypted files."""
|
|
41
|
+
|
|
42
|
+
SERVICE_NAME = "agentic-fabriq-cli"
|
|
43
|
+
ACCOUNT_NAME = "default"
|
|
44
|
+
|
|
45
|
+
def __init__(self, use_keyring: bool = True):
|
|
46
|
+
"""
|
|
47
|
+
Initialize token storage.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
use_keyring: Whether to use system keyring (falls back to file if unavailable)
|
|
51
|
+
"""
|
|
52
|
+
self.use_keyring = use_keyring and KEYRING_AVAILABLE
|
|
53
|
+
self.config_dir = Path.home() / ".af"
|
|
54
|
+
self.token_file = self.config_dir / "tokens.enc"
|
|
55
|
+
self.key_file = self.config_dir / ".key"
|
|
56
|
+
|
|
57
|
+
# Ensure config directory exists
|
|
58
|
+
self.config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
59
|
+
|
|
60
|
+
def _get_encryption_key(self) -> bytes:
|
|
61
|
+
"""
|
|
62
|
+
Get or create encryption key for file storage.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Fernet encryption key
|
|
66
|
+
"""
|
|
67
|
+
if self.key_file.exists():
|
|
68
|
+
with open(self.key_file, 'rb') as f:
|
|
69
|
+
return f.read()
|
|
70
|
+
else:
|
|
71
|
+
# Generate new key
|
|
72
|
+
key = Fernet.generate_key()
|
|
73
|
+
with open(self.key_file, 'wb') as f:
|
|
74
|
+
f.write(key)
|
|
75
|
+
# Set restrictive permissions
|
|
76
|
+
os.chmod(self.key_file, 0o600)
|
|
77
|
+
return key
|
|
78
|
+
|
|
79
|
+
def save(self, token_data: TokenData) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Save token data securely.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
token_data: Token data to save
|
|
85
|
+
"""
|
|
86
|
+
# Serialize token data
|
|
87
|
+
token_json = token_data.json()
|
|
88
|
+
|
|
89
|
+
if self.use_keyring:
|
|
90
|
+
# Use system keyring
|
|
91
|
+
try:
|
|
92
|
+
keyring.set_password(
|
|
93
|
+
self.SERVICE_NAME,
|
|
94
|
+
self.ACCOUNT_NAME,
|
|
95
|
+
token_json
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
except Exception as e:
|
|
99
|
+
# Fall back to file storage
|
|
100
|
+
print(f"Warning: Keyring storage failed, using file storage: {e}")
|
|
101
|
+
self.use_keyring = False
|
|
102
|
+
|
|
103
|
+
# Fall back to encrypted file storage
|
|
104
|
+
key = self._get_encryption_key()
|
|
105
|
+
fernet = Fernet(key)
|
|
106
|
+
|
|
107
|
+
encrypted_data = fernet.encrypt(token_json.encode('utf-8'))
|
|
108
|
+
|
|
109
|
+
with open(self.token_file, 'wb') as f:
|
|
110
|
+
f.write(encrypted_data)
|
|
111
|
+
|
|
112
|
+
# Set restrictive permissions
|
|
113
|
+
os.chmod(self.token_file, 0o600)
|
|
114
|
+
|
|
115
|
+
def load(self) -> Optional[TokenData]:
|
|
116
|
+
"""
|
|
117
|
+
Load token data.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Token data if available, None otherwise
|
|
121
|
+
"""
|
|
122
|
+
token_json = None
|
|
123
|
+
|
|
124
|
+
if self.use_keyring:
|
|
125
|
+
# Try system keyring first
|
|
126
|
+
try:
|
|
127
|
+
token_json = keyring.get_password(
|
|
128
|
+
self.SERVICE_NAME,
|
|
129
|
+
self.ACCOUNT_NAME
|
|
130
|
+
)
|
|
131
|
+
except Exception:
|
|
132
|
+
# Fall back to file storage
|
|
133
|
+
self.use_keyring = False
|
|
134
|
+
|
|
135
|
+
if not token_json and self.token_file.exists():
|
|
136
|
+
# Try encrypted file storage
|
|
137
|
+
try:
|
|
138
|
+
key = self._get_encryption_key()
|
|
139
|
+
fernet = Fernet(key)
|
|
140
|
+
|
|
141
|
+
with open(self.token_file, 'rb') as f:
|
|
142
|
+
encrypted_data = f.read()
|
|
143
|
+
|
|
144
|
+
token_json = fernet.decrypt(encrypted_data).decode('utf-8')
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"Warning: Failed to load tokens from file: {e}")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
if token_json:
|
|
150
|
+
try:
|
|
151
|
+
return TokenData.parse_raw(token_json)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"Warning: Failed to parse token data: {e}")
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def delete(self) -> None:
|
|
159
|
+
"""Delete stored token data."""
|
|
160
|
+
if self.use_keyring:
|
|
161
|
+
# Delete from keyring
|
|
162
|
+
try:
|
|
163
|
+
keyring.delete_password(
|
|
164
|
+
self.SERVICE_NAME,
|
|
165
|
+
self.ACCOUNT_NAME
|
|
166
|
+
)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
# Delete file storage
|
|
171
|
+
if self.token_file.exists():
|
|
172
|
+
try:
|
|
173
|
+
self.token_file.unlink()
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def is_token_expired(self, token_data: Optional[TokenData] = None) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Check if token is expired.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
token_data: Token data to check (loads from storage if not provided)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True if token is expired or invalid
|
|
186
|
+
"""
|
|
187
|
+
if token_data is None:
|
|
188
|
+
token_data = self.load()
|
|
189
|
+
|
|
190
|
+
if not token_data:
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
# Add 60 second buffer to account for clock skew
|
|
194
|
+
return time.time() >= (token_data.expires_at - 60)
|
|
195
|
+
|
|
196
|
+
def parse_jwt_claims(self, access_token: str) -> Dict:
|
|
197
|
+
"""
|
|
198
|
+
Parse JWT token claims without validation.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
access_token: JWT access token
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dictionary of claims
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
# Decode without verification (we trust the token from Keycloak)
|
|
208
|
+
claims = jwt.decode(
|
|
209
|
+
access_token,
|
|
210
|
+
options={"verify_signature": False}
|
|
211
|
+
)
|
|
212
|
+
return claims
|
|
213
|
+
except Exception:
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
def extract_token_info(self, tokens: Dict) -> TokenData:
|
|
217
|
+
"""
|
|
218
|
+
Extract and parse token information.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
tokens: Token response from OAuth endpoint
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Structured token data
|
|
225
|
+
"""
|
|
226
|
+
access_token = tokens['access_token']
|
|
227
|
+
refresh_token = tokens.get('refresh_token')
|
|
228
|
+
expires_in = tokens.get('expires_in', 3600)
|
|
229
|
+
|
|
230
|
+
# Calculate expiration time
|
|
231
|
+
expires_at = int(time.time()) + expires_in
|
|
232
|
+
|
|
233
|
+
# Parse JWT claims
|
|
234
|
+
claims = self.parse_jwt_claims(access_token)
|
|
235
|
+
|
|
236
|
+
# Extract user info from claims
|
|
237
|
+
tenant_id = claims.get('tenant_id') or claims.get('tenant')
|
|
238
|
+
user_id = claims.get('sub')
|
|
239
|
+
email = claims.get('email')
|
|
240
|
+
name = claims.get('name') or claims.get('preferred_username')
|
|
241
|
+
|
|
242
|
+
return TokenData(
|
|
243
|
+
access_token=access_token,
|
|
244
|
+
refresh_token=refresh_token,
|
|
245
|
+
expires_at=expires_at,
|
|
246
|
+
tenant_id=tenant_id,
|
|
247
|
+
user_id=user_id,
|
|
248
|
+
email=email,
|
|
249
|
+
name=name
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Global token storage instance
|
|
254
|
+
_token_storage: Optional[TokenStorage] = None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_token_storage() -> TokenStorage:
|
|
258
|
+
"""Get the global token storage instance."""
|
|
259
|
+
global _token_storage
|
|
260
|
+
if _token_storage is None:
|
|
261
|
+
_token_storage = TokenStorage()
|
|
262
|
+
return _token_storage
|
|
263
|
+
|
af_cli/main.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main CLI application for Agentic Fabric.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from af_cli.commands.agents import app as agents_app
|
|
13
|
+
from af_cli.commands.auth import app as auth_app
|
|
14
|
+
from af_cli.commands.config import app as config_app
|
|
15
|
+
from af_cli.commands.mcp_servers import app as mcp_servers_app
|
|
16
|
+
from af_cli.commands.secrets import app as secrets_app
|
|
17
|
+
from af_cli.commands.tools import app as tools_app
|
|
18
|
+
from af_cli.core.config import get_config
|
|
19
|
+
from af_cli.core.output import success, error, info
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(
|
|
22
|
+
name="af",
|
|
23
|
+
help="Agentic Fabric CLI - Manage your connectivity hub",
|
|
24
|
+
add_completion=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
# Add subcommands
|
|
30
|
+
app.add_typer(auth_app, name="auth", help="Authentication commands")
|
|
31
|
+
app.add_typer(config_app, name="config", help="Configuration commands")
|
|
32
|
+
app.add_typer(agents_app, name="agents", help="Agent management commands")
|
|
33
|
+
app.add_typer(tools_app, name="tools", help="Tool management commands")
|
|
34
|
+
app.add_typer(mcp_servers_app, name="mcp-servers", help="MCP server management commands")
|
|
35
|
+
app.add_typer(secrets_app, name="secrets", help="Secret management commands")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def version():
|
|
40
|
+
"""Show version information."""
|
|
41
|
+
from af_cli import __version__
|
|
42
|
+
console.print(f"Agentic Fabric CLI v{__version__}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command()
|
|
46
|
+
def status():
|
|
47
|
+
"""Show system status and configuration."""
|
|
48
|
+
config = get_config()
|
|
49
|
+
|
|
50
|
+
# Create status table
|
|
51
|
+
table = Table(title="Agentic Fabric Status")
|
|
52
|
+
table.add_column("Component", style="cyan")
|
|
53
|
+
table.add_column("Status", style="green")
|
|
54
|
+
table.add_column("Details", style="yellow")
|
|
55
|
+
|
|
56
|
+
# Check gateway connection
|
|
57
|
+
try:
|
|
58
|
+
import httpx
|
|
59
|
+
response = httpx.get(f"{config.gateway_url}/health", timeout=5.0)
|
|
60
|
+
if response.status_code == 200:
|
|
61
|
+
table.add_row("Gateway", "✅ Online", config.gateway_url)
|
|
62
|
+
else:
|
|
63
|
+
table.add_row("Gateway", "❌ Error", f"Status: {response.status_code}")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
table.add_row("Gateway", "❌ Offline", str(e))
|
|
66
|
+
|
|
67
|
+
# Check authentication
|
|
68
|
+
if config.access_token:
|
|
69
|
+
table.add_row("Authentication", "✅ Authenticated", f"Tenant: {config.tenant_id}")
|
|
70
|
+
else:
|
|
71
|
+
table.add_row("Authentication", "❌ Not authenticated", "Run 'af auth login'")
|
|
72
|
+
|
|
73
|
+
# Check configuration
|
|
74
|
+
config_path = config.config_file
|
|
75
|
+
if os.path.exists(config_path):
|
|
76
|
+
table.add_row("Configuration", "✅ Found", config_path)
|
|
77
|
+
else:
|
|
78
|
+
table.add_row("Configuration", "❌ Not found", config_path)
|
|
79
|
+
|
|
80
|
+
console.print(table)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command()
|
|
84
|
+
def init(
|
|
85
|
+
gateway_url: str = typer.Option(
|
|
86
|
+
"http://localhost:8000",
|
|
87
|
+
"--gateway-url",
|
|
88
|
+
"-g",
|
|
89
|
+
help="Gateway URL"
|
|
90
|
+
),
|
|
91
|
+
tenant_id: Optional[str] = typer.Option(
|
|
92
|
+
None,
|
|
93
|
+
"--tenant-id",
|
|
94
|
+
"-t",
|
|
95
|
+
help="Tenant ID"
|
|
96
|
+
),
|
|
97
|
+
force: bool = typer.Option(
|
|
98
|
+
False,
|
|
99
|
+
"--force",
|
|
100
|
+
"-f",
|
|
101
|
+
help="Force initialization, overwrite existing config"
|
|
102
|
+
),
|
|
103
|
+
):
|
|
104
|
+
"""Initialize CLI configuration."""
|
|
105
|
+
config = get_config()
|
|
106
|
+
|
|
107
|
+
# Check if config exists
|
|
108
|
+
if os.path.exists(config.config_file) and not force:
|
|
109
|
+
error(f"Configuration already exists at {config.config_file}")
|
|
110
|
+
error("Use --force to overwrite")
|
|
111
|
+
raise typer.Exit(1)
|
|
112
|
+
|
|
113
|
+
# Create config directory if it doesn't exist
|
|
114
|
+
os.makedirs(os.path.dirname(config.config_file), exist_ok=True)
|
|
115
|
+
|
|
116
|
+
# Save configuration
|
|
117
|
+
config.gateway_url = gateway_url
|
|
118
|
+
if tenant_id:
|
|
119
|
+
config.tenant_id = tenant_id
|
|
120
|
+
|
|
121
|
+
config.save()
|
|
122
|
+
|
|
123
|
+
success(f"Configuration initialized at {config.config_file}")
|
|
124
|
+
info(f"Gateway URL: {gateway_url}")
|
|
125
|
+
if tenant_id:
|
|
126
|
+
info(f"Tenant ID: {tenant_id}")
|
|
127
|
+
else:
|
|
128
|
+
info("Run 'af auth login' to authenticate")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.callback()
|
|
132
|
+
def main(
|
|
133
|
+
ctx: typer.Context,
|
|
134
|
+
config_file: Optional[str] = typer.Option(
|
|
135
|
+
None,
|
|
136
|
+
"--config",
|
|
137
|
+
"-c",
|
|
138
|
+
help="Path to configuration file"
|
|
139
|
+
),
|
|
140
|
+
gateway_url: Optional[str] = typer.Option(
|
|
141
|
+
None,
|
|
142
|
+
"--gateway-url",
|
|
143
|
+
"-g",
|
|
144
|
+
help="Gateway URL"
|
|
145
|
+
),
|
|
146
|
+
tenant_id: Optional[str] = typer.Option(
|
|
147
|
+
None,
|
|
148
|
+
"--tenant-id",
|
|
149
|
+
"-t",
|
|
150
|
+
help="Tenant ID"
|
|
151
|
+
),
|
|
152
|
+
verbose: bool = typer.Option(
|
|
153
|
+
False,
|
|
154
|
+
"--verbose",
|
|
155
|
+
"-v",
|
|
156
|
+
help="Enable verbose output"
|
|
157
|
+
),
|
|
158
|
+
):
|
|
159
|
+
"""
|
|
160
|
+
Agentic Fabric CLI - Manage your connectivity hub.
|
|
161
|
+
|
|
162
|
+
The CLI provides commands for managing agents, tools, MCP servers, and secrets
|
|
163
|
+
in your Agentic Fabric deployment.
|
|
164
|
+
"""
|
|
165
|
+
# Configure global options
|
|
166
|
+
config = get_config()
|
|
167
|
+
|
|
168
|
+
if config_file:
|
|
169
|
+
config.config_file = config_file
|
|
170
|
+
|
|
171
|
+
# Load configuration
|
|
172
|
+
config.load()
|
|
173
|
+
|
|
174
|
+
# Override with command line options
|
|
175
|
+
if gateway_url:
|
|
176
|
+
config.gateway_url = gateway_url
|
|
177
|
+
if tenant_id:
|
|
178
|
+
config.tenant_id = tenant_id
|
|
179
|
+
|
|
180
|
+
config.verbose = verbose
|
|
181
|
+
|
|
182
|
+
# Store config in context
|
|
183
|
+
ctx.obj = config
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
app()
|