glaip-sdk 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. glaip_sdk/_version.py +8 -0
  2. glaip_sdk/branding.py +13 -0
  3. glaip_sdk/cli/commands/agents.py +180 -39
  4. glaip_sdk/cli/commands/mcps.py +44 -18
  5. glaip_sdk/cli/commands/models.py +11 -5
  6. glaip_sdk/cli/commands/tools.py +35 -16
  7. glaip_sdk/cli/commands/transcripts.py +8 -0
  8. glaip_sdk/cli/constants.py +38 -0
  9. glaip_sdk/cli/context.py +8 -0
  10. glaip_sdk/cli/display.py +34 -19
  11. glaip_sdk/cli/main.py +14 -7
  12. glaip_sdk/cli/masking.py +8 -33
  13. glaip_sdk/cli/pager.py +9 -10
  14. glaip_sdk/cli/slash/agent_session.py +57 -20
  15. glaip_sdk/cli/slash/prompt.py +8 -0
  16. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  17. glaip_sdk/cli/slash/session.py +341 -46
  18. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  19. glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
  20. glaip_sdk/cli/transcript/viewer.py +232 -32
  21. glaip_sdk/cli/update_notifier.py +2 -2
  22. glaip_sdk/cli/utils.py +266 -35
  23. glaip_sdk/cli/validators.py +5 -6
  24. glaip_sdk/client/__init__.py +2 -1
  25. glaip_sdk/client/_agent_payloads.py +30 -0
  26. glaip_sdk/client/agent_runs.py +147 -0
  27. glaip_sdk/client/agents.py +186 -22
  28. glaip_sdk/client/main.py +23 -6
  29. glaip_sdk/client/mcps.py +2 -4
  30. glaip_sdk/client/run_rendering.py +66 -0
  31. glaip_sdk/client/tools.py +2 -3
  32. glaip_sdk/config/constants.py +11 -0
  33. glaip_sdk/models/__init__.py +56 -0
  34. glaip_sdk/models/agent_runs.py +117 -0
  35. glaip_sdk/rich_components.py +58 -2
  36. glaip_sdk/utils/client_utils.py +13 -0
  37. glaip_sdk/utils/export.py +143 -0
  38. glaip_sdk/utils/import_export.py +6 -9
  39. glaip_sdk/utils/rendering/__init__.py +122 -1
  40. glaip_sdk/utils/rendering/renderer/base.py +3 -7
  41. glaip_sdk/utils/rendering/renderer/debug.py +0 -1
  42. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  43. glaip_sdk/utils/rendering/steps.py +1 -0
  44. glaip_sdk/utils/resource_refs.py +26 -15
  45. glaip_sdk/utils/serialization.py +16 -0
  46. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
  47. glaip_sdk-0.3.0.dist-info/RECORD +94 -0
  48. glaip_sdk-0.2.1.dist-info/RECORD +0 -86
  49. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/_version.py CHANGED
@@ -14,6 +14,14 @@ except Exception: # pragma: no cover - extremely unlikely
14
14
  PackageNotFoundError = Exception # type: ignore
15
15
 
16
16
  def version(_: str) -> str: # type: ignore
17
+ """Fallback version function when importlib.metadata is unavailable.
18
+
19
+ Args:
20
+ _: Package name (ignored in fallback).
21
+
22
+ Returns:
23
+ Default dev version string.
24
+ """
17
25
  return "0.0.0.dev0"
18
26
 
19
27
 
glaip_sdk/branding.py CHANGED
@@ -90,6 +90,14 @@ GDP Labs AI Agents Package
90
90
  # ---- small helpers --------------------------------------------------------
91
91
  @staticmethod
92
92
  def _auto_version(package_name: str | None) -> str:
93
+ """Auto-detect version from environment, package metadata, or fallback.
94
+
95
+ Args:
96
+ package_name: Optional package name to read version from installed metadata.
97
+
98
+ Returns:
99
+ Version string from AIP_VERSION env var, package metadata, or SDK_VERSION fallback.
100
+ """
93
101
  # Priority: env → package metadata → fallback
94
102
  env_version = os.getenv("AIP_VERSION")
95
103
  if env_version:
@@ -103,6 +111,11 @@ GDP Labs AI Agents Package
103
111
 
104
112
  @staticmethod
105
113
  def _make_console() -> Console:
114
+ """Create a Rich Console instance respecting NO_COLOR environment variables.
115
+
116
+ Returns:
117
+ Console instance with color system configured based on environment.
118
+ """
106
119
  # Respect NO_COLOR/AIP_NO_COLOR environment variables
107
120
  no_color_env = os.getenv("NO_COLOR") is not None or os.getenv("AIP_NO_COLOR") is not None
108
121
  if no_color_env:
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import json
10
10
  import os
11
11
  from collections.abc import Mapping
12
+ from copy import deepcopy
12
13
  from pathlib import Path
13
14
  from typing import Any
14
15
 
@@ -18,6 +19,7 @@ from rich.console import Console
18
19
  from glaip_sdk.branding import (
19
20
  ACCENT_STYLE,
20
21
  ERROR_STYLE,
22
+ HINT_PREFIX_STYLE,
21
23
  INFO,
22
24
  SUCCESS,
23
25
  SUCCESS_STYLE,
@@ -33,6 +35,7 @@ from glaip_sdk.cli.agent_config import (
33
35
  sanitize_agent_config_for_cli as sanitize_agent_config,
34
36
  )
35
37
  from glaip_sdk.cli.context import get_ctx_value, output_flags
38
+ from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
36
39
  from glaip_sdk.cli.display import (
37
40
  build_resource_result_data,
38
41
  display_agent_run_suggestions,
@@ -61,9 +64,12 @@ from glaip_sdk.cli.utils import (
61
64
  build_renderer,
62
65
  coerce_to_row,
63
66
  get_client,
67
+ handle_resource_export,
68
+ in_slash_mode,
64
69
  output_list,
65
70
  output_result,
66
71
  spinner_context,
72
+ with_client_and_spinner,
67
73
  )
68
74
  from glaip_sdk.cli.validators import (
69
75
  validate_agent_instruction_cli as validate_agent_instruction,
@@ -74,7 +80,7 @@ from glaip_sdk.cli.validators import (
74
80
  from glaip_sdk.cli.validators import (
75
81
  validate_timeout_cli as validate_timeout,
76
82
  )
77
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
83
+ from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS, DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
78
84
  from glaip_sdk.exceptions import AgentTimeoutError
79
85
  from glaip_sdk.icons import ICON_AGENT
80
86
  from glaip_sdk.utils import format_datetime, is_uuid
@@ -88,6 +94,8 @@ console = Console()
88
94
  # Error message constants
89
95
  AGENT_NOT_FOUND_ERROR = "Agent not found"
90
96
 
97
+ # Instruction preview controls
98
+
91
99
 
92
100
  def _safe_agent_attribute(agent: Any, name: str) -> Any:
93
101
  """Return attribute value for ``name`` while filtering Mock sentinels."""
@@ -140,10 +148,7 @@ def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
140
148
  "description",
141
149
  "model",
142
150
  "agent_config",
143
- "tools",
144
- "agents",
145
- "mcps",
146
- "timeout",
151
+ *[field for field in AGENT_CONFIG_FIELDS if field not in ("name", "instruction", "model")],
147
152
  "tool_configs",
148
153
  )
149
154
 
@@ -307,12 +312,92 @@ def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
307
312
  return result_data
308
313
 
309
314
 
310
- def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
315
+ def _clamp_instruction_preview_limit(limit: int | None) -> int:
316
+ """Normalise preview limit; 0 disables trimming."""
317
+ default = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
318
+ if limit is None: # pragma: no cover
319
+ return default
320
+ try:
321
+ limit_value = int(limit)
322
+ except (TypeError, ValueError): # pragma: no cover - defensive parsing
323
+ return default
324
+
325
+ if limit_value <= 0:
326
+ return 0
327
+
328
+ return limit_value
329
+
330
+
331
+ def _build_instruction_preview(value: Any, limit: int) -> tuple[Any, bool]:
332
+ """Return a trimmed preview for long instruction strings."""
333
+ if not isinstance(value, str) or limit <= 0: # pragma: no cover
334
+ return value, False
335
+
336
+ if len(value) <= limit:
337
+ return value, False
338
+
339
+ trimmed_value = value[:limit].rstrip()
340
+ preview = f"{trimmed_value}\n\n... (preview trimmed)"
341
+ return preview, True
342
+
343
+
344
+ def _prepare_agent_details_payload(
345
+ data: dict[str, Any],
346
+ *,
347
+ instruction_preview_limit: int,
348
+ ) -> tuple[dict[str, Any], bool]:
349
+ """Return payload ready for rendering plus trim indicator."""
350
+ payload = deepcopy(data)
351
+ trimmed = False
352
+ if instruction_preview_limit > 0:
353
+ preview, trimmed = _build_instruction_preview(payload.get("instruction"), instruction_preview_limit)
354
+ if trimmed:
355
+ payload["instruction"] = preview
356
+ return payload, trimmed
357
+
358
+
359
+ def _show_instruction_trim_hint(
360
+ ctx: Any,
361
+ *,
362
+ trimmed: bool,
363
+ preview_limit: int,
364
+ ) -> None:
365
+ """Render hint describing how to expand or collapse the instruction preview."""
366
+ if not trimmed or preview_limit <= 0:
367
+ return
368
+
369
+ view = get_ctx_value(ctx, "view", "rich") if ctx is not None else "rich"
370
+ if view != "rich": # pragma: no cover - non-rich view handling
371
+ return
372
+
373
+ suffix = f"[dim](preview: {preview_limit:,} chars)[/]"
374
+ if in_slash_mode(ctx):
375
+ console.print(
376
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Use '/details' again to toggle between trimmed and full prompts {suffix}"
377
+ )
378
+ return
379
+
380
+ console.print( # pragma: no cover - fallback hint rendering
381
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Run 'aip agents get <agent> --instruction-preview <n>' "
382
+ f"to control prompt preview length {suffix}"
383
+ )
384
+
385
+
386
+ def _display_agent_details(
387
+ ctx: Any,
388
+ client: Any,
389
+ agent: Any,
390
+ *,
391
+ instruction_preview_limit: int | None = None,
392
+ ) -> None:
311
393
  """Display full agent details using raw API data to preserve ALL fields."""
312
394
  if agent is None:
313
395
  handle_rich_output(ctx, markup_text(f"[{ERROR_STYLE}]❌ No agent provided[/]"))
314
396
  return
315
397
 
398
+ preview_limit = _clamp_instruction_preview_limit(instruction_preview_limit)
399
+ trimmed_instruction = False
400
+
316
401
  # Try to fetch and format raw agent data first
317
402
  with spinner_context(
318
403
  ctx,
@@ -324,9 +409,13 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
324
409
  if formatted_data:
325
410
  # Use raw API data - this preserves ALL fields including account_id
326
411
  panel_title = f"{ICON_AGENT} {formatted_data.get('name', 'Unknown')}"
412
+ payload, trimmed_instruction = _prepare_agent_details_payload(
413
+ formatted_data,
414
+ instruction_preview_limit=preview_limit,
415
+ )
327
416
  output_result(
328
417
  ctx,
329
- formatted_data,
418
+ payload,
330
419
  title=panel_title,
331
420
  )
332
421
  else:
@@ -344,12 +433,22 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
344
433
  result_data = _format_fallback_agent_data(client, agent)
345
434
 
346
435
  # Display using output_result
436
+ payload, trimmed_instruction = _prepare_agent_details_payload(
437
+ result_data,
438
+ instruction_preview_limit=preview_limit,
439
+ )
347
440
  output_result(
348
441
  ctx,
349
- result_data,
442
+ payload,
350
443
  title="Agent Details",
351
444
  )
352
445
 
446
+ _show_instruction_trim_hint(
447
+ ctx,
448
+ trimmed=trimmed_instruction,
449
+ preview_limit=preview_limit,
450
+ )
451
+
353
452
 
354
453
  @click.group(name="agents", no_args_is_help=True)
355
454
  def agents_group() -> None:
@@ -357,7 +456,6 @@ def agents_group() -> None:
357
456
  pass
358
457
 
359
458
 
360
- # pylint: disable=duplicate-code
361
459
  def _resolve_agent(
362
460
  ctx: Any,
363
461
  client: Any,
@@ -365,26 +463,38 @@ def _resolve_agent(
365
463
  select: int | None = None,
366
464
  interface_preference: str = "fuzzy",
367
465
  ) -> Any | None:
368
- """Resolve agent reference (ID or name) with ambiguity handling.
466
+ """Resolve an agent by ID or name, supporting fuzzy and questionary interfaces.
467
+
468
+ This function provides agent-specific resolution with flexible UI options.
469
+ It wraps resolve_resource_reference with agent-specific configuration, allowing
470
+ users to choose between fuzzy search and traditional questionary selection.
369
471
 
370
472
  Args:
371
- ctx: Click context object for CLI operations.
372
- client: AIP client instance for API operations.
373
- ref: Agent reference (ID or name) to resolve.
374
- select: Pre-selected agent index for non-interactive mode.
375
- interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list.
473
+ ctx: Click context for CLI command execution.
474
+ client: AIP SDK client instance.
475
+ ref: Agent identifier (UUID or name string).
476
+ select: Pre-selected index for non-interactive resolution (1-based).
477
+ interface_preference: UI preference - "fuzzy" for search or "questionary" for list.
376
478
 
377
479
  Returns:
378
- Resolved agent object or None if not found.
480
+ Agent object when found, None when resolution fails.
379
481
  """
482
+ # Configure agent-specific resolution parameters
483
+ resolution_config = {
484
+ "resource_type": "agent",
485
+ "get_by_id": client.agents.get_agent_by_id,
486
+ "find_by_name": client.agents.find_agents,
487
+ "label": "Agent",
488
+ }
489
+ # Use agent-specific resolution with flexible interface preference
380
490
  return resolve_resource_reference(
381
491
  ctx,
382
492
  client,
383
493
  ref,
384
- "agent",
385
- client.agents.get_agent_by_id,
386
- client.agents.find_agents,
387
- "Agent",
494
+ resolution_config["resource_type"],
495
+ resolution_config["get_by_id"],
496
+ resolution_config["find_by_name"],
497
+ resolution_config["label"],
388
498
  select=select,
389
499
  interface_preference=interface_preference,
390
500
  )
@@ -414,19 +524,20 @@ def list_agents(
414
524
  ) -> None:
415
525
  """List agents with optional filtering."""
416
526
  try:
417
- client = get_client(ctx)
418
- with spinner_context(
527
+ with with_client_and_spinner(
419
528
  ctx,
420
529
  "[bold blue]Fetching agents…[/bold blue]",
421
530
  console_override=console,
422
- ):
423
- agents = client.agents.list_agents(
424
- agent_type=agent_type,
425
- framework=framework,
426
- name=name,
427
- version=version,
428
- sync_langflow_agents=sync_langflow,
429
- )
531
+ ) as client:
532
+ # Query agents with specified filters
533
+ filter_params = {
534
+ "agent_type": agent_type,
535
+ "framework": framework,
536
+ "name": name,
537
+ "version": version,
538
+ "sync_langflow_agents": sync_langflow,
539
+ }
540
+ agents = client.agents.list_agents(**filter_params)
430
541
 
431
542
  # Define table columns: (data_key, header, style, width)
432
543
  columns = [
@@ -439,6 +550,14 @@ def list_agents(
439
550
 
440
551
  # Transform function for safe attribute access
441
552
  def transform_agent(agent: Any) -> dict[str, Any]:
553
+ """Transform an agent object to a display row dictionary.
554
+
555
+ Args:
556
+ agent: Agent object to transform.
557
+
558
+ Returns:
559
+ Dictionary with id, name, type, framework, and version fields.
560
+ """
442
561
  row = coerce_to_row(agent, ["id", "name", "type", "framework", "version"])
443
562
  # Ensure id is always a string
444
563
  row["id"] = str(row["id"])
@@ -488,9 +607,22 @@ def list_agents(
488
607
  type=click.Path(dir_okay=False, writable=True),
489
608
  help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
490
609
  )
610
+ @click.option(
611
+ "--instruction-preview",
612
+ type=int,
613
+ default=0,
614
+ show_default=True,
615
+ help="Instruction preview length when printing instructions (0 shows full prompt).",
616
+ )
491
617
  @output_flags()
492
618
  @click.pass_context
493
- def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> None:
619
+ def get(
620
+ ctx: Any,
621
+ agent_ref: str,
622
+ select: int | None,
623
+ export: str | None,
624
+ instruction_preview: int,
625
+ ) -> None:
494
626
  r"""Get agent details.
495
627
 
496
628
  \b
@@ -500,30 +632,39 @@ def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> Non
500
632
  aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
501
633
  """
502
634
  try:
503
- client = get_client(ctx)
635
+ # Initialize API client for agent retrieval
636
+ api_client = get_client(ctx)
504
637
 
505
- # Resolve agent with ambiguity handling - use questionary interface for traditional UX
506
- agent = _resolve_agent(ctx, client, agent_ref, select, interface_preference="questionary")
638
+ # Resolve agent reference using questionary interface for better UX
639
+ agent = _resolve_agent(ctx, api_client, agent_ref, select, interface_preference="questionary")
507
640
 
508
- # Handle export option
509
- if export:
510
- from glaip_sdk.cli.utils import handle_resource_export
641
+ if not agent:
642
+ raise click.ClickException(f"Agent '{agent_ref}' not found")
511
643
 
644
+ # Handle export option if requested
645
+ if export:
512
646
  handle_resource_export(
513
647
  ctx,
514
648
  agent,
515
649
  Path(export),
516
650
  resource_type="agent",
517
- get_by_id_func=client.agents.get_agent_by_id,
651
+ get_by_id_func=api_client.agents.get_agent_by_id,
518
652
  console_override=console,
519
653
  )
520
654
 
521
655
  # Display full agent details using the standardized helper
522
- _display_agent_details(ctx, client, agent)
656
+ _display_agent_details(
657
+ ctx,
658
+ api_client,
659
+ agent,
660
+ instruction_preview_limit=instruction_preview,
661
+ )
523
662
 
524
663
  # Show run suggestions via centralized display helper
525
664
  handle_rich_output(ctx, display_agent_run_suggestions(agent))
526
665
 
666
+ except click.ClickException:
667
+ raise
527
668
  except Exception as e:
528
669
  raise click.ClickException(str(e)) from e
529
670
 
@@ -43,10 +43,13 @@ from glaip_sdk.cli.resolution import resolve_resource_reference
43
43
  from glaip_sdk.cli.rich_helpers import print_markup
44
44
  from glaip_sdk.cli.utils import (
45
45
  coerce_to_row,
46
+ fetch_resource_for_export,
47
+ format_datetime_fields,
46
48
  get_client,
47
49
  output_list,
48
50
  output_result,
49
51
  spinner_context,
52
+ with_client_and_spinner,
50
53
  )
51
54
  from glaip_sdk.config.constants import (
52
55
  DEFAULT_MCP_TYPE,
@@ -298,28 +301,37 @@ def mcps_group() -> None:
298
301
  pass
299
302
 
300
303
 
301
- def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None: # pylint: disable=duplicate-code
302
- """Resolve MCP reference (ID or name) with ambiguity handling.
304
+ def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
305
+ """Resolve an MCP server by ID or name, with interactive selection support.
306
+
307
+ This function provides MCP-specific resolution logic. It delegates to
308
+ resolve_resource_reference for MCP-specific resolution, supporting UUID
309
+ lookups and name-based fuzzy matching.
303
310
 
304
311
  Args:
305
- ctx: Click context object
306
- client: API client instance
307
- ref: MCP reference (ID or name)
308
- select: Index to select when multiple matches found
312
+ ctx: Click context for command execution.
313
+ client: API client for backend operations.
314
+ ref: MCP identifier (UUID or name string).
315
+ select: Optional selection index when multiple MCPs match (1-based).
309
316
 
310
317
  Returns:
311
- MCP object if found, None otherwise
318
+ MCP instance if resolution succeeds, None if not found.
312
319
 
313
320
  Raises:
314
- ClickException: If MCP not found or selection invalid
321
+ click.ClickException: When resolution fails or selection is invalid.
315
322
  """
323
+ # Configure MCP-specific resolution functions
324
+ mcp_client = client.mcps
325
+ get_by_id_func = mcp_client.get_mcp_by_id
326
+ find_by_name_func = mcp_client.find_mcps
327
+ # Use MCP-specific resolution with standard fuzzy matching
316
328
  return resolve_resource_reference(
317
329
  ctx,
318
330
  client,
319
331
  ref,
320
332
  "mcp",
321
- client.mcps.get_mcp_by_id,
322
- client.mcps.find_mcps,
333
+ get_by_id_func,
334
+ find_by_name_func,
323
335
  "MCP",
324
336
  select=select,
325
337
  )
@@ -512,12 +524,11 @@ def list_mcps(ctx: Any) -> None:
512
524
  ClickException: If API request fails
513
525
  """
514
526
  try:
515
- client = get_client(ctx)
516
- with spinner_context(
527
+ with with_client_and_spinner(
517
528
  ctx,
518
529
  "[bold blue]Fetching MCPs…[/bold blue]",
519
530
  console_override=console,
520
- ):
531
+ ) as client:
521
532
  mcps = client.mcps.list_mcps()
522
533
 
523
534
  # Define table columns: (data_key, header, style, width)
@@ -529,6 +540,14 @@ def list_mcps(ctx: Any) -> None:
529
540
 
530
541
  # Transform function for safe dictionary access
531
542
  def transform_mcp(mcp: Any) -> dict[str, Any]:
543
+ """Transform an MCP object to a display row dictionary.
544
+
545
+ Args:
546
+ mcp: MCP object to transform.
547
+
548
+ Returns:
549
+ Dictionary with id, name, and config fields.
550
+ """
532
551
  row = coerce_to_row(mcp, ["id", "name", "config"])
533
552
  # Ensure id is always a string
534
553
  row["id"] = str(row["id"])
@@ -604,8 +623,10 @@ def create(
604
623
  aip mcps create --import mcp-export.json --name new-name --transport sse
605
624
  """
606
625
  try:
607
- client = get_client(ctx)
626
+ # Get API client instance for MCP operations
627
+ api_client = get_client(ctx)
608
628
 
629
+ # Process import file if specified, otherwise use None
609
630
  import_payload = _load_import_ready_payload(import_file) if import_file is not None else None
610
631
 
611
632
  merged_payload, missing_fields = _merge_import_payload(
@@ -649,7 +670,7 @@ def create(
649
670
  if mcp_metadata is not None:
650
671
  create_kwargs["mcp_metadata"] = mcp_metadata
651
672
 
652
- mcp = client.mcps.create_mcp(**create_kwargs)
673
+ mcp = api_client.mcps.create_mcp(**create_kwargs)
653
674
 
654
675
  # Handle JSON output
655
676
  handle_json_output(ctx, mcp.model_dump())
@@ -696,7 +717,6 @@ def _handle_mcp_export(
696
717
  detected_format = detect_export_format(export_path)
697
718
 
698
719
  # Always export comprehensive data - re-fetch with full details
699
- from glaip_sdk.cli.utils import fetch_resource_for_export
700
720
 
701
721
  mcp = fetch_resource_for_export(
702
722
  ctx,
@@ -775,8 +795,6 @@ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
775
795
 
776
796
  if raw_mcp_data:
777
797
  # Use raw API data - this preserves ALL fields
778
- from glaip_sdk.cli.utils import format_datetime_fields
779
-
780
798
  formatted_data = format_datetime_fields(raw_mcp_data)
781
799
 
782
800
  output_result(
@@ -897,6 +915,14 @@ def list_tools(ctx: Any, mcp_ref: str) -> None:
897
915
 
898
916
  # Transform function for safe dictionary access
899
917
  def transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
918
+ """Transform a tool dictionary to a display row dictionary.
919
+
920
+ Args:
921
+ tool: Tool dictionary to transform.
922
+
923
+ Returns:
924
+ Dictionary with name and description fields.
925
+ """
900
926
  return {
901
927
  "name": tool.get("name", "N/A"),
902
928
  "description": tool.get("description", "N/A")[:47] + "..."
@@ -12,9 +12,8 @@ from rich.console import Console
12
12
  from glaip_sdk.branding import ACCENT_STYLE, INFO, SUCCESS
13
13
  from glaip_sdk.cli.context import output_flags
14
14
  from glaip_sdk.cli.utils import (
15
- get_client,
16
15
  output_list,
17
- spinner_context,
16
+ with_client_and_spinner,
18
17
  )
19
18
 
20
19
  console = Console()
@@ -32,12 +31,11 @@ def models_group() -> None:
32
31
  def list_models(ctx: Any) -> None:
33
32
  """List available language models."""
34
33
  try:
35
- client = get_client(ctx)
36
- with spinner_context(
34
+ with with_client_and_spinner(
37
35
  ctx,
38
36
  "[bold blue]Fetching language models…[/bold blue]",
39
37
  console_override=console,
40
- ):
38
+ ) as client:
41
39
  models = client.list_language_models()
42
40
 
43
41
  # Define table columns: (data_key, header, style, width)
@@ -50,6 +48,14 @@ def list_models(ctx: Any) -> None:
50
48
 
51
49
  # Transform function for safe dictionary access
52
50
  def transform_model(model: dict[str, Any]) -> dict[str, Any]:
51
+ """Transform a model dictionary to a display row dictionary.
52
+
53
+ Args:
54
+ model: Model dictionary to transform.
55
+
56
+ Returns:
57
+ Dictionary with id, provider, name, and base_url fields.
58
+ """
53
59
  return {
54
60
  "id": str(model.get("id", "N/A")),
55
61
  "provider": model.get("provider", "N/A"),