glaip-sdk 0.0.15__py3-none-any.whl → 0.0.17__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.
Files changed (43) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/branding.py +28 -2
  3. glaip_sdk/cli/commands/agents.py +36 -27
  4. glaip_sdk/cli/commands/configure.py +46 -52
  5. glaip_sdk/cli/commands/mcps.py +19 -22
  6. glaip_sdk/cli/commands/tools.py +19 -13
  7. glaip_sdk/cli/config.py +42 -0
  8. glaip_sdk/cli/display.py +97 -30
  9. glaip_sdk/cli/main.py +141 -124
  10. glaip_sdk/cli/mcp_validators.py +2 -2
  11. glaip_sdk/cli/pager.py +3 -2
  12. glaip_sdk/cli/parsers/json_input.py +2 -2
  13. glaip_sdk/cli/resolution.py +12 -10
  14. glaip_sdk/cli/rich_helpers.py +29 -0
  15. glaip_sdk/cli/slash/agent_session.py +7 -0
  16. glaip_sdk/cli/slash/prompt.py +21 -2
  17. glaip_sdk/cli/slash/session.py +15 -21
  18. glaip_sdk/cli/update_notifier.py +8 -2
  19. glaip_sdk/cli/utils.py +115 -58
  20. glaip_sdk/client/_agent_payloads.py +504 -0
  21. glaip_sdk/client/agents.py +633 -559
  22. glaip_sdk/client/base.py +92 -20
  23. glaip_sdk/client/main.py +14 -0
  24. glaip_sdk/client/run_rendering.py +275 -0
  25. glaip_sdk/config/constants.py +4 -1
  26. glaip_sdk/exceptions.py +15 -0
  27. glaip_sdk/models.py +5 -0
  28. glaip_sdk/payload_schemas/__init__.py +19 -0
  29. glaip_sdk/payload_schemas/agent.py +87 -0
  30. glaip_sdk/rich_components.py +12 -0
  31. glaip_sdk/utils/client_utils.py +12 -0
  32. glaip_sdk/utils/import_export.py +2 -2
  33. glaip_sdk/utils/rendering/formatting.py +5 -0
  34. glaip_sdk/utils/rendering/models.py +22 -0
  35. glaip_sdk/utils/rendering/renderer/base.py +9 -1
  36. glaip_sdk/utils/rendering/renderer/panels.py +0 -1
  37. glaip_sdk/utils/rendering/steps.py +59 -0
  38. glaip_sdk/utils/serialization.py +24 -3
  39. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/METADATA +2 -2
  40. glaip_sdk-0.0.17.dist-info/RECORD +73 -0
  41. glaip_sdk-0.0.15.dist-info/RECORD +0 -67
  42. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/WHEEL +0 -0
  43. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/entry_points.txt +0 -0
@@ -11,7 +11,6 @@ from typing import Any
11
11
 
12
12
  import click
13
13
  from rich.console import Console
14
- from rich.text import Text
15
14
 
16
15
  from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
17
16
  from glaip_sdk.cli.display import (
@@ -33,6 +32,7 @@ from glaip_sdk.cli.io import (
33
32
  load_resource_from_file_with_validation as load_resource_from_file,
34
33
  )
35
34
  from glaip_sdk.cli.resolution import resolve_resource_reference
35
+ from glaip_sdk.cli.rich_helpers import markup_text, print_markup
36
36
  from glaip_sdk.cli.utils import (
37
37
  coerce_to_row,
38
38
  get_client,
@@ -349,11 +349,13 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
349
349
  ):
350
350
  tool = client.get_tool_by_id(tool.id)
351
351
  except Exception as e:
352
- console.print(
353
- Text(f"[yellow]⚠️ Could not fetch full tool details: {e}[/yellow]")
352
+ print_markup(
353
+ f"[yellow]⚠️ Could not fetch full tool details: {e}[/yellow]",
354
+ console=console,
354
355
  )
355
- console.print(
356
- Text("[yellow]⚠️ Proceeding with available data[/yellow]")
356
+ print_markup(
357
+ "[yellow]⚠️ Proceeding with available data[/yellow]",
358
+ console=console,
357
359
  )
358
360
 
359
361
  with spinner_context(
@@ -362,10 +364,9 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
362
364
  console_override=console,
363
365
  ):
364
366
  export_resource_to_file(tool, export_path, detected_format)
365
- console.print(
366
- Text(
367
- f"[green]✅ Complete tool configuration exported to: {export_path} (format: {detected_format})[/green]"
368
- )
367
+ print_markup(
368
+ f"[green]✅ Complete tool configuration exported to: {export_path} (format: {detected_format})[/green]",
369
+ console=console,
369
370
  )
370
371
 
371
372
  # Try to fetch raw API data first to preserve ALL fields
@@ -473,7 +474,8 @@ def update(
473
474
  tool.id, file, framework=tool.framework
474
475
  )
475
476
  handle_rich_output(
476
- ctx, Text(f"[green]✓[/green] Tool code updated from {file}")
477
+ ctx,
478
+ markup_text(f"[green]✓[/green] Tool code updated from {file}"),
477
479
  )
478
480
  elif update_data:
479
481
  # Update metadata only (native tools only)
@@ -487,9 +489,13 @@ def update(
487
489
  console_override=console,
488
490
  ):
489
491
  updated_tool = tool.update(**update_data)
490
- handle_rich_output(ctx, Text("[green]✓[/green] Tool metadata updated"))
492
+ handle_rich_output(
493
+ ctx, markup_text("[green]✓[/green] Tool metadata updated")
494
+ )
491
495
  else:
492
- handle_rich_output(ctx, Text("[yellow]No updates specified[/yellow]"))
496
+ handle_rich_output(
497
+ ctx, markup_text("[yellow]No updates specified[/yellow]")
498
+ )
493
499
  return
494
500
 
495
501
  handle_json_output(ctx, updated_tool.model_dump())
@@ -574,5 +580,5 @@ def script(ctx: Any, tool_id: str) -> None:
574
580
  except Exception as e:
575
581
  handle_json_output(ctx, error=e)
576
582
  if get_ctx_value(ctx, "view") != "json":
577
- console.print(Text(f"[red]Error getting tool script: {e}[/red]"))
583
+ print_markup(f"[red]Error getting tool script: {e}[/red]", console=console)
578
584
  raise click.ClickException(str(e))
@@ -0,0 +1,42 @@
1
+ """Configuration management utilities.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ CONFIG_DIR = Path.home() / ".aip"
14
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
+
16
+
17
+ def load_config() -> dict[str, Any]:
18
+ """Load configuration from file."""
19
+ if not CONFIG_FILE.exists():
20
+ return {}
21
+
22
+ try:
23
+ with open(CONFIG_FILE) as f:
24
+ return yaml.safe_load(f) or {}
25
+ except yaml.YAMLError:
26
+ return {}
27
+
28
+
29
+ def save_config(config: dict[str, Any]) -> None:
30
+ """Save configuration to file."""
31
+ CONFIG_DIR.mkdir(exist_ok=True)
32
+
33
+ with open(CONFIG_FILE, "w") as f:
34
+ yaml.dump(config, f, default_flow_style=False)
35
+
36
+ # Set secure file permissions
37
+ try:
38
+ os.chmod(CONFIG_FILE, 0o600)
39
+ except (
40
+ OSError
41
+ ): # pragma: no cover - permission errors are expected in some environments
42
+ pass
glaip_sdk/cli/display.py CHANGED
@@ -15,6 +15,8 @@ from rich.console import Console
15
15
  from rich.panel import Panel
16
16
  from rich.text import Text
17
17
 
18
+ from glaip_sdk.cli.rich_helpers import markup_text, print_markup
19
+ from glaip_sdk.cli.utils import command_hint
18
20
  from glaip_sdk.rich_components import AIPPanel
19
21
 
20
22
  console = Console()
@@ -60,7 +62,7 @@ def display_update_success(resource_type: str, resource_name: str) -> Text:
60
62
  Returns:
61
63
  Rich Text object for display
62
64
  """
63
- return Text(
65
+ return markup_text(
64
66
  f"[green]✅ {resource_type} '{resource_name}' updated successfully[/green]"
65
67
  )
66
68
 
@@ -75,7 +77,7 @@ def display_deletion_success(resource_type: str, resource_name: str) -> Text:
75
77
  Returns:
76
78
  Rich Text object for display
77
79
  """
78
- return Text(
80
+ return markup_text(
79
81
  f"[green]✅ {resource_type} '{resource_name}' deleted successfully[/green]"
80
82
  )
81
83
 
@@ -88,8 +90,8 @@ def display_api_error(error: Exception, operation: str = "operation") -> None:
88
90
  operation: Description of the operation that failed
89
91
  """
90
92
  error_type = type(error).__name__
91
- console.print(Text(f"[red]Error during {operation}: {error}[/red]"))
92
- console.print(Text(f"[dim]Error type: {error_type}[/dim]"))
93
+ print_markup(f"[red]Error during {operation}: {error}[/red]", console=console)
94
+ print_markup(f"[dim]Error type: {error_type}[/dim]", console=console)
93
95
 
94
96
 
95
97
  def print_api_error(e: Exception) -> None:
@@ -102,30 +104,80 @@ def print_api_error(e: Exception) -> None:
102
104
  - Extracts status_code, error_type, and payload from APIError exceptions
103
105
  - Provides consistent error reporting across CLI commands
104
106
  - Handles both JSON and Rich output formats
107
+ - Special handling for validation errors with detailed field-level errors
105
108
  """
106
- if hasattr(e, "__dict__"): # Check if it's an APIError-like object
107
- error_info = {
108
- "error": str(e),
109
- "status_code": getattr(e, "status_code", None),
110
- "error_type": getattr(e, "error_type", None),
111
- "details": getattr(e, "payload", None),
112
- }
113
-
114
- # Filter out None values
115
- error_info = {k: v for k, v in error_info.items() if v is not None}
116
-
117
- # For JSON view, just return the structured error
118
- # (CLI commands handle the JSON formatting)
119
- if hasattr(e, "status_code"):
120
- console.print(f"[red]API Error: {e}[/red]")
121
- if hasattr(e, "status_code"):
122
- console.print(f"[yellow]Status: {e.status_code}[/yellow]")
123
- if hasattr(e, "payload"):
124
- console.print(f"[yellow]Details: {e.payload}[/yellow]")
109
+ if not hasattr(e, "__dict__"):
110
+ console.print(f"[red]Error: {e}[/red]")
111
+ return
112
+
113
+ if not hasattr(e, "status_code"):
114
+ console.print(f"[red]Error: {e}[/red]")
115
+ return
116
+
117
+ console.print(f"[red]API Error: {e}[/red]")
118
+ status_code = getattr(e, "status_code", None)
119
+ if status_code is not None:
120
+ console.print(f"[yellow]Status: {status_code}[/yellow]")
121
+
122
+ payload = getattr(e, "payload", _MISSING)
123
+ if payload is _MISSING:
124
+ return
125
+
126
+ if payload:
127
+ if not _print_structured_payload(payload):
128
+ console.print(f"[yellow]Details: {payload}[/yellow]")
129
+ else:
130
+ console.print(f"[yellow]Details: {payload}[/yellow]")
131
+
132
+
133
+ def _print_structured_payload(payload: Any) -> bool:
134
+ """Print structured payloads with enhanced formatting. Returns True if handled."""
135
+ if not isinstance(payload, dict):
136
+ return False
137
+
138
+ if "detail" in payload and _print_validation_details(payload["detail"]):
139
+ return True
140
+
141
+ if "details" in payload and _print_details_field(payload["details"]):
142
+ return True
143
+
144
+ return False
145
+
146
+
147
+ def _print_validation_details(detail: Any) -> bool:
148
+ """Render FastAPI-style validation errors."""
149
+ if not isinstance(detail, list) or not detail:
150
+ return False
151
+
152
+ console.print("[red]Validation Errors:[/red]")
153
+ for error in detail:
154
+ if isinstance(error, dict):
155
+ loc = " -> ".join(str(x) for x in error.get("loc", []))
156
+ msg = error.get("msg", "Unknown error")
157
+ error_type = error.get("type", "unknown")
158
+ location = loc if loc else "field"
159
+ console.print(f" [yellow]• {location}:[/yellow] {msg}")
160
+ if error_type != "unknown":
161
+ console.print(f" [dim]({error_type})[/dim]")
125
162
  else:
126
- console.print(f"[red]Error: {e}[/red]")
163
+ console.print(f" [yellow]•[/yellow] {error}")
164
+ return True
165
+
166
+
167
+ def _print_details_field(details: Any) -> bool:
168
+ """Render custom error details from API payloads."""
169
+ if not details:
170
+ return False
171
+
172
+ console.print("[red]Error Details:[/red]")
173
+ if isinstance(details, str):
174
+ console.print(f" [yellow]•[/yellow] {details}")
175
+ elif isinstance(details, list):
176
+ for detail in details:
177
+ console.print(f" [yellow]•[/yellow] {detail}")
127
178
  else:
128
- console.print(f"[red]Error: {e}[/red]")
179
+ console.print(f" [yellow]•[/yellow] {details}")
180
+ return True
129
181
 
130
182
 
131
183
  _MISSING = object()
@@ -133,7 +185,6 @@ _MISSING = object()
133
185
 
134
186
  def build_resource_result_data(resource: Any, fields: list[str]) -> dict[str, Any]:
135
187
  """Return a normalized mapping of ``fields`` extracted from ``resource``."""
136
-
137
188
  result: dict[str, Any] = {}
138
189
  for field in fields:
139
190
  try:
@@ -244,14 +295,30 @@ def display_confirmation_prompt(resource_type: str, resource_name: str) -> bool:
244
295
 
245
296
  def display_agent_run_suggestions(agent: Any) -> Panel:
246
297
  """Return a panel with post-creation suggestions for an agent."""
298
+ agent_id = getattr(agent, "id", "")
299
+ agent_name = getattr(agent, "name", "")
300
+ run_hint_id = command_hint(
301
+ f'agents run {agent_id} "Your message here"',
302
+ slash_command=None,
303
+ )
304
+ run_hint_name = command_hint(
305
+ f'agents run "{agent_name}" "Your message here"',
306
+ slash_command=None,
307
+ )
308
+
309
+ cli_section = ""
310
+ if run_hint_id and run_hint_name:
311
+ cli_section = (
312
+ "📋 Prefer the CLI instead?\n"
313
+ f" [green]{run_hint_id}[/green]\n"
314
+ f" [green]{run_hint_name}[/green]\n\n"
315
+ )
247
316
 
248
317
  return AIPPanel(
249
318
  f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
250
- f"🚀 Start chatting with [bold]{agent.name}[/bold] right here:\n"
319
+ f"🚀 Start chatting with [bold]{agent_name}[/bold] right here:\n"
251
320
  f" Type your message below and press Enter to run it immediately.\n\n"
252
- f"📋 Prefer the CLI instead?\n"
253
- f' [green]aip agents run {agent.id} "Your message here"[/green]\n'
254
- f' [green]aip agents run "{agent.name}" "Your message here"[/green]\n\n'
321
+ f"{cli_section}"
255
322
  f"🔧 Available options:\n"
256
323
  f" [dim]--chat-history[/dim] Include previous conversation\n"
257
324
  f" [dim]--file[/dim] Attach files\n"
glaip_sdk/cli/main.py CHANGED
@@ -19,11 +19,11 @@ from glaip_sdk.cli.commands.agents import agents_group
19
19
  from glaip_sdk.cli.commands.configure import (
20
20
  config_group,
21
21
  configure_command,
22
- load_config,
23
22
  )
24
23
  from glaip_sdk.cli.commands.mcps import mcps_group
25
24
  from glaip_sdk.cli.commands.models import models_group
26
25
  from glaip_sdk.cli.commands.tools import tools_group
26
+ from glaip_sdk.cli.config import load_config
27
27
  from glaip_sdk.cli.update_notifier import maybe_notify_update
28
28
  from glaip_sdk.cli.utils import spinner_context, update_spinner
29
29
  from glaip_sdk.config.constants import (
@@ -73,7 +73,6 @@ def main(
73
73
  aip tools create my_tool.py # Create a new tool
74
74
  aip agents run my-agent "Hello world" # Run an agent
75
75
  """
76
-
77
76
  # Store configuration in context
78
77
  ctx.ensure_object(dict)
79
78
  ctx.obj["api_url"] = api_url
@@ -116,7 +115,6 @@ main.add_command(configure_command)
116
115
 
117
116
  def _should_launch_slash(ctx: click.Context) -> bool:
118
117
  """Determine whether to open the command palette automatically."""
119
-
120
118
  ctx_obj = ctx.obj or {}
121
119
  if not bool(ctx_obj.get("tty", True)):
122
120
  return False
@@ -127,6 +125,137 @@ def _should_launch_slash(ctx: click.Context) -> bool:
127
125
  return True
128
126
 
129
127
 
128
+ def _load_and_merge_config(ctx: click.Context) -> dict:
129
+ """Load configuration from multiple sources and merge them."""
130
+ # Load config from file and merge with context
131
+ file_config = load_config()
132
+ context_config = ctx.obj or {}
133
+
134
+ # Load environment variables (middle priority)
135
+ env_config = {}
136
+ if os.getenv("AIP_API_URL"):
137
+ env_config["api_url"] = os.getenv("AIP_API_URL")
138
+ if os.getenv("AIP_API_KEY"):
139
+ env_config["api_key"] = os.getenv("AIP_API_KEY")
140
+
141
+ # Filter out None values from context config to avoid overriding other configs
142
+ filtered_context = {k: v for k, v in context_config.items() if v is not None}
143
+
144
+ # Merge configs: file (low) -> env (mid) -> CLI args (high)
145
+ return {**file_config, **env_config, **filtered_context}
146
+
147
+
148
+ def _validate_config_and_show_error(config: dict, console: Console) -> None:
149
+ """Validate configuration and show error if incomplete."""
150
+ if not config.get("api_url") or not config.get("api_key"):
151
+ console.print(
152
+ AIPPanel(
153
+ "[bold red]❌ Configuration incomplete[/bold red]\n\n"
154
+ f"🔍 Current config:\n"
155
+ f" • API URL: {config.get('api_url', 'Not set')}\n"
156
+ f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
157
+ f"💡 To fix this:\n"
158
+ f" • Run 'aip configure' to set up credentials\n"
159
+ f" • Or run 'aip config list' to see current config",
160
+ title="❌ Configuration Error",
161
+ border_style="red",
162
+ )
163
+ )
164
+ console.print(
165
+ f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION}) - Configure to connect"
166
+ )
167
+ sys.exit(1)
168
+
169
+
170
+ def _create_and_test_client(config: dict, console: Console) -> Client:
171
+ """Create client and test connection by fetching resources."""
172
+ # Try to create client
173
+ client = Client(
174
+ api_url=config["api_url"],
175
+ api_key=config["api_key"],
176
+ timeout=config.get("timeout", 30.0),
177
+ )
178
+
179
+ # Test connection by listing resources
180
+ try:
181
+ with spinner_context(
182
+ None, # We'll pass ctx later
183
+ "[bold blue]Checking GL AIP status…[/bold blue]",
184
+ console_override=console,
185
+ spinner_style="cyan",
186
+ ) as status_indicator:
187
+ update_spinner(status_indicator, "[bold blue]Fetching agents…[/bold blue]")
188
+ agents = client.list_agents()
189
+
190
+ update_spinner(status_indicator, "[bold blue]Fetching tools…[/bold blue]")
191
+ tools = client.list_tools()
192
+
193
+ update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
194
+ mcps = client.list_mcps()
195
+
196
+ # Create status table
197
+ table = AIPTable(title="🔗 GL AIP Status")
198
+ table.add_column("Resource", style="cyan", width=15)
199
+ table.add_column("Count", style="green", width=10)
200
+ table.add_column("Status", style="green", width=15)
201
+
202
+ table.add_row("Agents", str(len(agents)), "✅ Available")
203
+ table.add_row("Tools", str(len(tools)), "✅ Available")
204
+ table.add_row("MCPs", str(len(mcps)), "✅ Available")
205
+
206
+ console.print(
207
+ AIPPanel(
208
+ f"[bold green]✅ Connected to GL AIP[/bold green]\n"
209
+ f"🔗 API URL: {client.api_url}\n"
210
+ f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
211
+ title="🚀 Connection Status",
212
+ border_style="green",
213
+ )
214
+ )
215
+
216
+ console.print(table)
217
+
218
+ except Exception as e:
219
+ # Show AIP Ready status even if connection fails
220
+ console.print(
221
+ f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
222
+ )
223
+
224
+ console.print(
225
+ AIPPanel(
226
+ f"[bold yellow]⚠️ Connection established but API call failed[/bold yellow]\n"
227
+ f"🔗 API URL: {client.api_url}\n"
228
+ f"❌ Error: {e}\n\n"
229
+ f"💡 This usually means:\n"
230
+ f" • Network connectivity issues\n"
231
+ f" • API permissions problems\n"
232
+ f" • Backend service issues",
233
+ title="⚠️ Partial Connection",
234
+ border_style="yellow",
235
+ )
236
+ )
237
+
238
+ return client
239
+
240
+
241
+ def _handle_connection_error(config: dict, console: Console, error: Exception) -> None:
242
+ """Handle connection errors and show troubleshooting information."""
243
+ console.print(
244
+ AIPPanel(
245
+ f"[bold red]❌ Connection failed[/bold red]\n\n"
246
+ f"🔍 Error: {error}\n\n"
247
+ f"💡 Troubleshooting steps:\n"
248
+ f" • Verify your API URL and key are correct\n"
249
+ f" • Check network connectivity to {config.get('api_url', 'your API')}\n"
250
+ f" • Run 'aip configure' to update credentials\n"
251
+ f" • Run 'aip config list' to check configuration",
252
+ title="❌ Connection Error",
253
+ border_style="red",
254
+ )
255
+ )
256
+ sys.exit(1)
257
+
258
+
130
259
  @main.command()
131
260
  @click.pass_context
132
261
  def status(ctx: Any) -> None:
@@ -146,132 +275,20 @@ def status(ctx: Any) -> None:
146
275
  f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
147
276
  )
148
277
 
149
- # Load config from file and merge with context
150
- file_config = load_config()
151
- context_config = ctx.obj or {}
152
-
153
- # Load environment variables (middle priority)
154
-
155
- env_config = {}
156
- if os.getenv("AIP_API_URL"):
157
- env_config["api_url"] = os.getenv("AIP_API_URL")
158
- if os.getenv("AIP_API_KEY"):
159
- env_config["api_key"] = os.getenv("AIP_API_KEY")
160
-
161
- # Filter out None values from context config to avoid overriding other configs
162
- filtered_context = {k: v for k, v in context_config.items() if v is not None}
163
-
164
- # Merge configs: file (low) -> env (mid) -> CLI args (high)
165
- config = {**file_config, **env_config, **filtered_context}
166
-
167
- if not config.get("api_url") or not config.get("api_key"):
168
- console.print(
169
- AIPPanel(
170
- "[bold red]❌ Configuration incomplete[/bold red]\n\n"
171
- f"🔍 Current config:\n"
172
- f" • API URL: {config.get('api_url', 'Not set')}\n"
173
- f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
174
- f"💡 To fix this:\n"
175
- f" • Run 'aip configure' to set up credentials\n"
176
- f" • Or run 'aip config list' to see current config",
177
- title="❌ Configuration Error",
178
- border_style="red",
179
- )
180
- )
181
- console.print(
182
- f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION}) - Configure to connect"
183
- )
184
- sys.exit(1)
185
-
186
- # Try to create client
187
- client = Client(
188
- api_url=config["api_url"],
189
- api_key=config["api_key"],
190
- timeout=config.get("timeout", 30.0),
191
- )
192
-
193
- # Test connection by listing resources
194
- try:
195
- with spinner_context(
196
- ctx,
197
- "[bold blue]Checking GL AIP status…[/bold blue]",
198
- console_override=console,
199
- spinner_style="cyan",
200
- ) as status_indicator:
201
- update_spinner(
202
- status_indicator, "[bold blue]Fetching agents…[/bold blue]"
203
- )
204
- agents = client.list_agents()
278
+ # Load and merge configuration
279
+ config = _load_and_merge_config(ctx)
205
280
 
206
- update_spinner(
207
- status_indicator, "[bold blue]Fetching tools…[/bold blue]"
208
- )
209
- tools = client.list_tools()
210
-
211
- update_spinner(
212
- status_indicator, "[bold blue]Fetching MCPs…[/bold blue]"
213
- )
214
- mcps = client.list_mcps()
215
-
216
- # Create status table
217
- table = AIPTable(title="🔗 GL AIP Status")
218
- table.add_column("Resource", style="cyan", width=15)
219
- table.add_column("Count", style="green", width=10)
220
- table.add_column("Status", style="green", width=15)
221
-
222
- table.add_row("Agents", str(len(agents)), "✅ Available")
223
- table.add_row("Tools", str(len(tools)), "✅ Available")
224
- table.add_row("MCPs", str(len(mcps)), "✅ Available")
225
-
226
- console.print(
227
- AIPPanel(
228
- f"[bold green]✅ Connected to GL AIP[/bold green]\n"
229
- f"🔗 API URL: {client.api_url}\n"
230
- f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
231
- title="🚀 Connection Status",
232
- border_style="green",
233
- )
234
- )
235
-
236
- console.print(table)
237
-
238
- except Exception as e:
239
- # Show AIP Ready status even if connection fails
240
- console.print(
241
- f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
242
- )
243
-
244
- console.print(
245
- AIPPanel(
246
- f"[bold yellow]⚠️ Connection established but API call failed[/bold yellow]\n"
247
- f"🔗 API URL: {client.api_url}\n"
248
- f"❌ Error: {e}\n\n"
249
- f"💡 This usually means:\n"
250
- f" • Network connectivity issues\n"
251
- f" • API permissions problems\n"
252
- f" • Backend service issues",
253
- title="⚠️ Partial Connection",
254
- border_style="yellow",
255
- )
256
- )
281
+ # Validate configuration
282
+ _validate_config_and_show_error(config, console)
257
283
 
284
+ # Create and test client connection
285
+ client = _create_and_test_client(config, console)
258
286
  client.close()
259
287
 
260
288
  except Exception as e:
261
- console.print(
262
- AIPPanel(
263
- f"[bold red]❌ Connection failed[/bold red]\n\n"
264
- f"🔍 Error: {e}\n\n"
265
- f"💡 Troubleshooting steps:\n"
266
- f" • Run 'aip config list' to check configuration\n"
267
- f" • Run 'aip configure' to update credentials\n"
268
- f" • Verify your API URL and key are correct\n"
269
- f" • Check network connectivity to {config.get('api_url', 'your API')}",
270
- title="❌ Connection Error",
271
- border_style="red",
272
- )
273
- )
274
- sys.exit(1)
289
+ # Handle any unexpected errors during the process
290
+ console = Console()
291
+ _handle_connection_error(config if "config" in locals() else {}, console, e)
275
292
 
276
293
 
277
294
  @main.command()
@@ -15,7 +15,7 @@ import click
15
15
 
16
16
 
17
17
  def format_validation_error(prefix: str, detail: str | None = None) -> str:
18
- """Format a validation error message with optional detail.
18
+ r"""Format a validation error message with optional detail.
19
19
 
20
20
  Args:
21
21
  prefix: Main error message
@@ -26,7 +26,7 @@ def format_validation_error(prefix: str, detail: str | None = None) -> str:
26
26
 
27
27
  Examples:
28
28
  >>> format_validation_error("Invalid config", "Missing 'url' field")
29
- "Invalid config\\nMissing 'url' field"
29
+ "Invalid config\nMissing 'url' field"
30
30
  """
31
31
  parts = [prefix]
32
32
  if detail:
glaip_sdk/cli/pager.py CHANGED
@@ -57,8 +57,9 @@ def _get_console() -> Console:
57
57
 
58
58
 
59
59
  def _prepare_pager_env(clear_on_exit: bool = True) -> None:
60
- """
61
- Configure LESS flags for a predictable, high-quality UX:
60
+ """Configure LESS flags for a predictable, high-quality UX.
61
+
62
+ Sets sensible defaults for the system pager:
62
63
  -R : pass ANSI color escapes
63
64
  -S : chop long lines (horizontal scroll with ←/→)
64
65
  (No -F, no -X) so we open a full-screen pager and clear on exit.
@@ -17,7 +17,7 @@ import click
17
17
  def _format_file_error(
18
18
  prefix: str, file_path_str: str, resolved_path: Path, *, detail: str | None = None
19
19
  ) -> str:
20
- """Format a file-related error message with path context.
20
+ r"""Format a file-related error message with path context.
21
21
 
22
22
  Args:
23
23
  prefix: Main error message
@@ -31,7 +31,7 @@ def _format_file_error(
31
31
  Examples:
32
32
  >>> from pathlib import Path
33
33
  >>> _format_file_error("File not found", "config.json", Path("/abs/config.json"))
34
- 'File not found: config.json\\nResolved path: /abs/config.json'
34
+ 'File not found: config.json\nResolved path: /abs/config.json'
35
35
  """
36
36
  parts = [f"{prefix}: {file_path_str}", f"Resolved path: {resolved_path}"]
37
37
  if detail: