glaip-sdk 0.0.20__py3-none-any.whl → 0.1.1__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 (65) hide show
  1. glaip_sdk/_version.py +1 -3
  2. glaip_sdk/branding.py +2 -6
  3. glaip_sdk/cli/agent_config.py +2 -6
  4. glaip_sdk/cli/auth.py +11 -30
  5. glaip_sdk/cli/commands/agents.py +64 -107
  6. glaip_sdk/cli/commands/configure.py +12 -36
  7. glaip_sdk/cli/commands/mcps.py +25 -63
  8. glaip_sdk/cli/commands/models.py +2 -4
  9. glaip_sdk/cli/commands/tools.py +22 -35
  10. glaip_sdk/cli/commands/update.py +3 -8
  11. glaip_sdk/cli/config.py +1 -3
  12. glaip_sdk/cli/display.py +4 -12
  13. glaip_sdk/cli/io.py +8 -14
  14. glaip_sdk/cli/main.py +10 -30
  15. glaip_sdk/cli/mcp_validators.py +5 -15
  16. glaip_sdk/cli/pager.py +3 -9
  17. glaip_sdk/cli/parsers/json_input.py +11 -22
  18. glaip_sdk/cli/resolution.py +3 -9
  19. glaip_sdk/cli/rich_helpers.py +1 -3
  20. glaip_sdk/cli/slash/agent_session.py +5 -10
  21. glaip_sdk/cli/slash/prompt.py +3 -10
  22. glaip_sdk/cli/slash/session.py +46 -98
  23. glaip_sdk/cli/transcript/cache.py +6 -19
  24. glaip_sdk/cli/transcript/capture.py +6 -20
  25. glaip_sdk/cli/transcript/launcher.py +1 -3
  26. glaip_sdk/cli/transcript/viewer.py +187 -46
  27. glaip_sdk/cli/update_notifier.py +165 -21
  28. glaip_sdk/cli/utils.py +33 -85
  29. glaip_sdk/cli/validators.py +11 -12
  30. glaip_sdk/client/_agent_payloads.py +10 -30
  31. glaip_sdk/client/agents.py +33 -63
  32. glaip_sdk/client/base.py +6 -22
  33. glaip_sdk/client/mcps.py +1 -3
  34. glaip_sdk/client/run_rendering.py +121 -24
  35. glaip_sdk/client/tools.py +8 -24
  36. glaip_sdk/client/validators.py +20 -48
  37. glaip_sdk/exceptions.py +1 -3
  38. glaip_sdk/icons.py +9 -3
  39. glaip_sdk/models.py +14 -33
  40. glaip_sdk/payload_schemas/agent.py +1 -3
  41. glaip_sdk/utils/agent_config.py +4 -14
  42. glaip_sdk/utils/client_utils.py +7 -21
  43. glaip_sdk/utils/display.py +2 -6
  44. glaip_sdk/utils/general.py +1 -3
  45. glaip_sdk/utils/import_export.py +3 -9
  46. glaip_sdk/utils/rendering/formatting.py +52 -12
  47. glaip_sdk/utils/rendering/models.py +17 -8
  48. glaip_sdk/utils/rendering/renderer/__init__.py +1 -5
  49. glaip_sdk/utils/rendering/renderer/base.py +1107 -320
  50. glaip_sdk/utils/rendering/renderer/config.py +3 -5
  51. glaip_sdk/utils/rendering/renderer/debug.py +4 -14
  52. glaip_sdk/utils/rendering/renderer/panels.py +1 -3
  53. glaip_sdk/utils/rendering/renderer/progress.py +3 -11
  54. glaip_sdk/utils/rendering/renderer/stream.py +10 -22
  55. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  56. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  57. glaip_sdk/utils/rendering/steps.py +899 -25
  58. glaip_sdk/utils/resource_refs.py +4 -13
  59. glaip_sdk/utils/serialization.py +14 -46
  60. glaip_sdk/utils/validation.py +4 -4
  61. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/METADATA +12 -1
  62. glaip_sdk-0.1.1.dist-info/RECORD +82 -0
  63. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  64. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/WHEEL +0 -0
  65. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/entry_points.txt +0 -0
@@ -6,10 +6,14 @@ Author:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import importlib
10
+ import logging
9
11
  import os
10
- from collections.abc import Callable
12
+ from collections.abc import Callable, Iterable, Iterator
13
+ from contextlib import contextmanager
11
14
  from typing import Any, Literal
12
15
 
16
+ import click
13
17
  import httpx
14
18
  from packaging.version import InvalidVersion, Version
15
19
  from rich import box
@@ -17,9 +21,11 @@ from rich.console import Console
17
21
 
18
22
  from glaip_sdk.branding import (
19
23
  ACCENT_STYLE,
24
+ ERROR_STYLE,
20
25
  SUCCESS_STYLE,
21
26
  WARNING_STYLE,
22
27
  )
28
+ from glaip_sdk.cli.commands.update import update_command
23
29
  from glaip_sdk.cli.utils import command_hint, format_command_hint
24
30
  from glaip_sdk.rich_components import AIPPanel
25
31
 
@@ -28,6 +34,8 @@ FetchLatestVersion = Callable[[], str | None]
28
34
  PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
29
35
  DEFAULT_TIMEOUT = 1.5 # seconds
30
36
 
37
+ _LOGGER = logging.getLogger(__name__)
38
+
31
39
 
32
40
  def _parse_version(value: str) -> Version | None:
33
41
  """Parse a version string into a `Version`, returning None on failure."""
@@ -43,13 +51,16 @@ def _fetch_latest_version(package_name: str) -> str | None:
43
51
  timeout = httpx.Timeout(DEFAULT_TIMEOUT)
44
52
 
45
53
  try:
46
- with httpx.Client(timeout=timeout) as client:
47
- response = client.get(url, headers={"Accept": "application/json"})
48
- response.raise_for_status()
49
- payload = response.json()
50
- except httpx.HTTPError:
54
+ with _suppress_library_logging():
55
+ with httpx.Client(timeout=timeout) as client:
56
+ response = client.get(url, headers={"Accept": "application/json"})
57
+ response.raise_for_status()
58
+ payload = response.json()
59
+ except httpx.HTTPError as exc:
60
+ _LOGGER.debug("Update check failed: %s", exc, exc_info=True)
51
61
  return None
52
- except ValueError:
62
+ except ValueError as exc:
63
+ _LOGGER.debug("Invalid JSON while checking for updates: %s", exc, exc_info=True)
53
64
  return None
54
65
 
55
66
  info = payload.get("info") if isinstance(payload, dict) else None
@@ -68,6 +79,8 @@ def _build_update_panel(
68
79
  current_version: str,
69
80
  latest_version: str,
70
81
  command_text: str,
82
+ *,
83
+ show_command_hint: bool,
71
84
  ) -> AIPPanel:
72
85
  """Create a Rich panel that prompts the user to update."""
73
86
  command_markup = format_command_hint(command_text) or command_text
@@ -75,9 +88,10 @@ def _build_update_panel(
75
88
  f"[{WARNING_STYLE}]✨ Update available![/] "
76
89
  f"{current_version} → {latest_version}\n\n"
77
90
  "See the latest release notes:\n"
78
- f"https://pypi.org/project/glaip-sdk/{latest_version}/\n\n"
79
- f"[{ACCENT_STYLE}]Run[/] {command_markup} to install."
91
+ f"https://pypi.org/project/glaip-sdk/{latest_version}/"
80
92
  )
93
+ if show_command_hint:
94
+ message += f"\n\n[{ACCENT_STYLE}]Run[/] {command_markup} to install."
81
95
  return AIPPanel(
82
96
  message,
83
97
  title=f"[{SUCCESS_STYLE}]AIP SDK Update[/]",
@@ -97,11 +111,7 @@ def maybe_notify_update(
97
111
  slash_command: str | None = None,
98
112
  style: Literal["panel", "inline"] = "panel",
99
113
  ) -> None:
100
- """Check PyPI for a newer version and display a prompt if one exists.
101
-
102
- This function deliberately swallows network errors to avoid impacting CLI
103
- startup time when offline or when PyPI is unavailable.
104
- """
114
+ """Check PyPI for a newer version and display a prompt if one exists."""
105
115
  if not _should_check_for_updates():
106
116
  return
107
117
 
@@ -120,18 +130,152 @@ def maybe_notify_update(
120
130
  return
121
131
 
122
132
  active_console = console or Console()
133
+ should_prompt = _should_prompt_for_action(active_console, ctx)
134
+
123
135
  if style == "inline":
136
+ if should_prompt:
137
+ message = (
138
+ f"[{WARNING_STYLE}]✨ Update[/] "
139
+ f"{current_version} → {latest_version} "
140
+ "- choose Update now or Skip to continue."
141
+ )
142
+ active_console.print(message)
143
+ _handle_update_decision(active_console, ctx)
144
+ return
145
+
124
146
  command_markup = format_command_hint(command_text) or command_text
125
- message = (
126
- f"[{WARNING_STYLE}]✨ Update[/] "
127
- f"{current_version} → {latest_version} "
128
- f"- {command_markup}"
129
- )
130
- active_console.print(message)
147
+ active_console.print(f"[{WARNING_STYLE}]✨ Update[/] {current_version} → {latest_version} - {command_markup}")
131
148
  return
132
149
 
133
- panel = _build_update_panel(current_version, latest_version, command_text)
150
+ panel = _build_update_panel(
151
+ current_version,
152
+ latest_version,
153
+ command_text,
154
+ show_command_hint=not should_prompt,
155
+ )
134
156
  active_console.print(panel)
157
+ if should_prompt:
158
+ _handle_update_decision(active_console, ctx)
159
+
160
+
161
+ def _handle_update_decision(console: Console, ctx: Any) -> None:
162
+ """Prompt the user to take action on the available update."""
163
+ choice = _prompt_update_decision(console)
164
+ if choice == "skip":
165
+ return
166
+
167
+ _run_update_command(console, ctx)
168
+
169
+
170
+ def _should_prompt_for_action(console: Console, ctx: Any | None) -> bool:
171
+ """Return True when we can safely block for interactive input."""
172
+ if ctx is None or not hasattr(ctx, "invoke"):
173
+ return False
174
+
175
+ is_interactive = getattr(console, "is_interactive", False)
176
+ if not isinstance(is_interactive, bool) or not is_interactive:
177
+ return False
178
+
179
+ is_terminal = getattr(console, "is_terminal", False)
180
+ if not isinstance(is_terminal, bool) or not is_terminal:
181
+ return False
182
+
183
+ input_method = getattr(console, "input", None)
184
+ return callable(input_method)
185
+
186
+
187
+ def _prompt_update_decision(console: Console) -> Literal["update", "skip"]:
188
+ """Ask the user to choose between updating now or skipping."""
189
+ console.print(
190
+ f"[{ACCENT_STYLE}]Select an option to continue:[/]\n"
191
+ f" [{SUCCESS_STYLE}]1.[/] Update now\n"
192
+ f" [{WARNING_STYLE}]2.[/] Skip\n"
193
+ )
194
+ console.print("[dim]Press Enter after typing your choice.[/]")
195
+
196
+ while True:
197
+ try:
198
+ response = console.input("Choice [1/2]: ").strip().lower()
199
+ except (KeyboardInterrupt, EOFError):
200
+ console.print(f"\n[{WARNING_STYLE}]Update skipped.[/]")
201
+ return "skip"
202
+
203
+ if response in {"1", "update", "u"}:
204
+ return "update"
205
+ if response in {"2", "skip", "s"}:
206
+ return "skip"
207
+
208
+ console.print(f"[{ERROR_STYLE}]Please enter 1 to update now or 2 to skip.[/]")
209
+
210
+
211
+ def _run_update_command(console: Console, ctx: Any) -> None:
212
+ """Invoke the built-in update command and surface any errors."""
213
+ try:
214
+ ctx.invoke(update_command)
215
+ except click.ClickException as exc:
216
+ exc.show()
217
+ console.print(f"[{ERROR_STYLE}]Update command exited with an error.[/]")
218
+ except click.Abort:
219
+ console.print(f"[{WARNING_STYLE}]Update aborted by user.[/]")
220
+ except Exception as exc: # pragma: no cover - defensive guard
221
+ console.print(f"[{ERROR_STYLE}]Unexpected error while running update: {exc}[/]")
222
+ else:
223
+ _refresh_installed_version(console, ctx)
224
+
225
+
226
+ @contextmanager
227
+ def _suppress_library_logging(
228
+ logger_names: Iterable[str] | None = None, *, level: int = logging.WARNING
229
+ ) -> Iterator[None]:
230
+ """Temporarily raise log level for selected libraries during update checks."""
231
+ names = tuple(logger_names) if logger_names is not None else ("httpx",)
232
+ captured: list[tuple[logging.Logger, int]] = []
233
+ try:
234
+ for name in names:
235
+ logger = logging.getLogger(name)
236
+ captured.append((logger, logger.level))
237
+ logger.setLevel(level)
238
+ yield
239
+ finally:
240
+ for logger, previous_level in captured:
241
+ logger.setLevel(previous_level)
242
+
243
+
244
+ def _refresh_installed_version(console: Console, ctx: Any) -> None:
245
+ """Reload runtime metadata after an in-process upgrade."""
246
+ new_version: str | None = None
247
+
248
+ try:
249
+ version_module = importlib.reload(importlib.import_module("glaip_sdk._version"))
250
+ new_version = getattr(version_module, "__version__", None)
251
+ except Exception as exc: # pragma: no cover - defensive guard
252
+ _LOGGER.debug("Failed to reload glaip_sdk._version: %s", exc, exc_info=True)
253
+
254
+ try:
255
+ branding_module = importlib.import_module("glaip_sdk.branding")
256
+ if new_version:
257
+ branding_module.SDK_VERSION = new_version
258
+ except Exception as exc: # pragma: no cover - defensive guard
259
+ _LOGGER.debug("Failed to update branding metadata: %s", exc, exc_info=True)
260
+
261
+ session = _get_slash_session(ctx)
262
+ if session and hasattr(session, "refresh_branding"):
263
+ try:
264
+ session.refresh_branding(new_version)
265
+ return
266
+ except Exception as exc: # pragma: no cover - defensive guard
267
+ _LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
268
+
269
+ if new_version:
270
+ console.print(f"[{SUCCESS_STYLE}]CLI now running glaip-sdk {new_version}.[/]")
271
+
272
+
273
+ def _get_slash_session(ctx: Any) -> Any | None:
274
+ """Return active slash session from the Click context if present."""
275
+ ctx_obj = getattr(ctx, "obj", None)
276
+ if isinstance(ctx_obj, dict):
277
+ return ctx_obj.get("_slash_session")
278
+ return None
135
279
 
136
280
 
137
281
  __all__ = ["maybe_notify_update"]
glaip_sdk/cli/utils.py CHANGED
@@ -147,9 +147,7 @@ def format_command_hint(
147
147
 
148
148
  highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
149
149
  if description:
150
- highlighted += (
151
- f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
152
- )
150
+ highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
153
151
  return highlighted
154
152
 
155
153
 
@@ -234,7 +232,7 @@ _spinner_stop = stop_spinner
234
232
  def get_client(ctx: Any) -> Client: # pragma: no cover
235
233
  """Get configured client from context, env, and config file (ctx > env > file)."""
236
234
  module = importlib.import_module("glaip_sdk")
237
- client_class = cast("type[Client]", getattr(module, "Client"))
235
+ client_class = cast("type[Client]", module.Client)
238
236
  file_config = load_config() or {}
239
237
  context_config_obj = getattr(ctx, "obj", None)
240
238
  context_config = context_config_obj or {}
@@ -419,9 +417,7 @@ def _prompt_with_auto_select(
419
417
  reserve_space_for_menu=8,
420
418
  )
421
419
  except Exception as exc: # pragma: no cover - depends on prompt_toolkit
422
- logger.debug(
423
- "PromptSession init failed (%s); falling back to basic prompt.", exc
424
- )
420
+ logger.debug("PromptSession init failed (%s); falling back to basic prompt.", exc)
425
421
  return _basic_prompt(message, completer)
426
422
 
427
423
  buffer = session.default_buffer
@@ -447,9 +443,7 @@ def _prompt_with_auto_select(
447
443
  except (KeyboardInterrupt, EOFError):
448
444
  return None
449
445
  except Exception as exc: # pragma: no cover - defensive
450
- logger.debug(
451
- "PromptSession prompt failed (%s); falling back to basic prompt.", exc
452
- )
446
+ logger.debug("PromptSession prompt failed (%s); falling back to basic prompt.", exc)
453
447
  return _basic_prompt(message, completer)
454
448
  finally:
455
449
  if handler_attached:
@@ -465,9 +459,7 @@ class _FuzzyCompleter:
465
459
  def __init__(self, words: list[str]) -> None:
466
460
  self.words = words
467
461
 
468
- def get_completions(
469
- self, document: Any, _complete_event: Any
470
- ) -> Any: # pragma: no cover
462
+ def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
471
463
  word = document.get_word_before_cursor()
472
464
  if not word:
473
465
  return
@@ -492,9 +484,7 @@ class _FuzzyCompleter:
492
484
  return False
493
485
 
494
486
 
495
- def _perform_fuzzy_search(
496
- answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]
497
- ) -> dict[str, Any] | None:
487
+ def _perform_fuzzy_search(answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]) -> dict[str, Any] | None:
498
488
  """Perform fuzzy search fallback and return best match."""
499
489
  # Exact label match
500
490
  if answer in by_label:
@@ -615,7 +605,7 @@ def _coerce_result_payload(result: Any) -> Any:
615
605
 
616
606
 
617
607
  def _ensure_displayable(payload: Any) -> Any:
618
- if isinstance(payload, dict | list | str | int | float | bool) or payload is None:
608
+ if isinstance(payload, (dict, list, str, int, float, bool)) or payload is None:
619
609
  return payload
620
610
 
621
611
  if hasattr(payload, "__dict__"):
@@ -686,9 +676,7 @@ def output_result(
686
676
  # _PICK_THRESHOLD = int(os.getenv("AIP_PICK_THRESHOLD", "5") or "5")
687
677
 
688
678
 
689
- def _normalise_rows(
690
- items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None
691
- ) -> list[dict[str, Any]]:
679
+ def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None) -> list[dict[str, Any]]:
692
680
  try:
693
681
  rows: list[dict[str, Any]] = []
694
682
  for item in items:
@@ -707,9 +695,7 @@ def _normalise_rows(
707
695
  return []
708
696
 
709
697
 
710
- def _render_plain_list(
711
- rows: list[dict[str, Any]], title: str, columns: list[tuple]
712
- ) -> None:
698
+ def _render_plain_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
713
699
  if not rows:
714
700
  click.echo(f"No {title.lower()} found.")
715
701
  return
@@ -718,9 +704,7 @@ def _render_plain_list(
718
704
  click.echo(row_str)
719
705
 
720
706
 
721
- def _render_markdown_list(
722
- rows: list[dict[str, Any]], title: str, columns: list[tuple]
723
- ) -> None:
707
+ def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
724
708
  if not rows:
725
709
  click.echo(f"No {title.lower()} found.")
726
710
  return
@@ -748,9 +732,7 @@ def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -
748
732
  return table
749
733
 
750
734
 
751
- def _build_table_group(
752
- rows: list[dict[str, Any]], columns: list[tuple], title: str
753
- ) -> Group:
735
+ def _build_table_group(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> Group:
754
736
  table = _create_table(columns, title)
755
737
  for row in rows:
756
738
  table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
@@ -760,24 +742,16 @@ def _build_table_group(
760
742
 
761
743
  def _handle_json_output(items: list[Any], rows: list[dict[str, Any]]) -> None:
762
744
  """Handle JSON output format."""
763
- data = (
764
- rows
765
- if rows
766
- else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
767
- )
745
+ data = rows if rows else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
768
746
  click.echo(json.dumps(data, indent=2, default=str))
769
747
 
770
748
 
771
- def _handle_plain_output(
772
- rows: list[dict[str, Any]], title: str, columns: list[tuple]
773
- ) -> None:
749
+ def _handle_plain_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
774
750
  """Handle plain text output format."""
775
751
  _render_plain_list(rows, title, columns)
776
752
 
777
753
 
778
- def _handle_markdown_output(
779
- rows: list[dict[str, Any]], title: str, columns: list[tuple]
780
- ) -> None:
754
+ def _handle_markdown_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
781
755
  """Handle markdown output format."""
782
756
  _render_markdown_list(rows, title, columns)
783
757
 
@@ -792,9 +766,7 @@ def _should_use_fuzzy_picker() -> bool:
792
766
  return console.is_terminal and os.isatty(1)
793
767
 
794
768
 
795
- def _try_fuzzy_pick(
796
- rows: list[dict[str, Any]], columns: list[tuple], title: str
797
- ) -> dict[str, Any] | None:
769
+ def _try_fuzzy_pick(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> dict[str, Any] | None:
798
770
  """Best-effort fuzzy selection; returns None if the picker fails."""
799
771
  if not _should_use_fuzzy_picker():
800
772
  return None
@@ -825,14 +797,10 @@ def _print_selection_tip(title: str) -> None:
825
797
  """Print the contextual follow-up tip after a fuzzy selection."""
826
798
  tip_cmd = _resource_tip_command(title)
827
799
  if tip_cmd:
828
- console.print(
829
- markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]")
830
- )
800
+ console.print(markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]"))
831
801
 
832
802
 
833
- def _handle_fuzzy_pick_selection(
834
- rows: list[dict[str, Any]], columns: list[tuple], title: str
835
- ) -> bool:
803
+ def _handle_fuzzy_pick_selection(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> bool:
836
804
  """Handle fuzzy picker selection, returns True if selection was made."""
837
805
  picked = _try_fuzzy_pick(rows, columns, title)
838
806
  if picked is None:
@@ -855,9 +823,7 @@ def _handle_table_output(
855
823
  """Handle table output with paging."""
856
824
  content = _build_table_group(rows, columns, title)
857
825
  should_page = (
858
- pager._should_page_output(len(rows), console.is_terminal and os.isatty(1))
859
- if use_pager is None
860
- else use_pager
826
+ pager._should_page_output(len(rows), console.is_terminal and os.isatty(1)) if use_pager is None else use_pager
861
827
  )
862
828
 
863
829
  if should_page:
@@ -982,7 +948,6 @@ def build_renderer(
982
948
  theme=theme,
983
949
  style=style,
984
950
  live=live_enabled,
985
- show_delegate_tool_panels=False,
986
951
  append_finished_snapshots=bool(snapshots)
987
952
  if snapshots is not None
988
953
  else RendererConfig.append_finished_snapshots,
@@ -990,9 +955,7 @@ def build_renderer(
990
955
 
991
956
  # Create the renderer instance
992
957
  renderer = RichStreamRenderer(
993
- working_console.original_console
994
- if isinstance(working_console, CapturingConsole)
995
- else working_console,
958
+ working_console.original_console if isinstance(working_console, CapturingConsole) else working_console,
996
959
  cfg=renderer_cfg,
997
960
  verbose=verbose,
998
961
  )
@@ -1080,22 +1043,14 @@ def _resolve_by_name_multiple_with_select(matches: list[Any], select: int) -> An
1080
1043
  return matches[idx]
1081
1044
 
1082
1045
 
1083
- def _resolve_by_name_multiple_fuzzy(
1084
- ctx: Any, ref: str, matches: list[Any], label: str
1085
- ) -> Any:
1046
+ def _resolve_by_name_multiple_fuzzy(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
1086
1047
  """Resolve multiple matches preferring the fuzzy picker interface."""
1087
- return handle_ambiguous_resource(
1088
- ctx, label.lower(), ref, matches, interface_preference="fuzzy"
1089
- )
1048
+ return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="fuzzy")
1090
1049
 
1091
1050
 
1092
- def _resolve_by_name_multiple_questionary(
1093
- ctx: Any, ref: str, matches: list[Any], label: str
1094
- ) -> Any:
1051
+ def _resolve_by_name_multiple_questionary(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
1095
1052
  """Resolve multiple matches preferring the questionary interface."""
1096
- return handle_ambiguous_resource(
1097
- ctx, label.lower(), ref, matches, interface_preference="questionary"
1098
- )
1053
+ return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="questionary")
1099
1054
 
1100
1055
 
1101
1056
  def resolve_resource(
@@ -1140,9 +1095,7 @@ def resolve_resource(
1140
1095
  raise click.ClickException(f"{label} '{ref}' not found")
1141
1096
 
1142
1097
  # Find resources by name
1143
- _spinner_update(
1144
- spinner, f"[bold blue]Searching {label}s matching '{ref}'…[/bold blue]"
1145
- )
1098
+ _spinner_update(spinner, f"[bold blue]Searching {label}s matching '{ref}'…[/bold blue]")
1146
1099
  matches = find_by_name(name=ref)
1147
1100
  if not matches:
1148
1101
  _spinner_stop(spinner)
@@ -1173,9 +1126,7 @@ def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
1173
1126
  return matches[0]
1174
1127
 
1175
1128
 
1176
- def _handle_questionary_ambiguity(
1177
- resource_type: str, ref: str, matches: list[Any]
1178
- ) -> Any:
1129
+ def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
1179
1130
  """Handle ambiguity using questionary interactive interface."""
1180
1131
  if not (questionary and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
1181
1132
  raise click.ClickException("Interactive selection not available")
@@ -1188,7 +1139,10 @@ def _handle_questionary_ambiguity(
1188
1139
  f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
1189
1140
  choices=[
1190
1141
  questionary.Choice(
1191
- title=f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — {getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}",
1142
+ title=(
1143
+ f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
1144
+ f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
1145
+ ),
1192
1146
  value=i,
1193
1147
  )
1194
1148
  for i, m in enumerate(matches)
@@ -1202,19 +1156,13 @@ def _handle_questionary_ambiguity(
1202
1156
  return matches[picked_idx]
1203
1157
 
1204
1158
 
1205
- def _handle_fallback_numeric_ambiguity(
1206
- resource_type: str, ref: str, matches: list[Any]
1207
- ) -> Any:
1159
+ def _handle_fallback_numeric_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
1208
1160
  """Handle ambiguity using numeric prompt fallback."""
1209
1161
  # Escape special characters for display
1210
1162
  safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
1211
1163
  safe_ref = ref.replace("{", "{{").replace("}", "}}")
1212
1164
 
1213
- console.print(
1214
- markup_text(
1215
- f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"
1216
- )
1217
- )
1165
+ console.print(markup_text(f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"))
1218
1166
  table = AIPTable(
1219
1167
  title=f"Select {safe_resource_type.title()}",
1220
1168
  )
@@ -1229,8 +1177,8 @@ def _handle_fallback_numeric_ambiguity(
1229
1177
  )
1230
1178
  try:
1231
1179
  choice = int(choice_str)
1232
- except ValueError:
1233
- raise click.ClickException("Invalid selection")
1180
+ except ValueError as err:
1181
+ raise click.ClickException("Invalid selection") from err
1234
1182
  if 1 <= choice <= len(matches):
1235
1183
  return matches[choice - 1]
1236
1184
  raise click.ClickException("Invalid selection")
@@ -42,7 +42,7 @@ def validate_agent_name_cli(name: str) -> str:
42
42
  try:
43
43
  return validate_agent_name(name)
44
44
  except ValueError as e:
45
- raise click.ClickException(str(e))
45
+ raise click.ClickException(str(e)) from e
46
46
 
47
47
 
48
48
  def validate_agent_instruction_cli(instruction: str) -> str:
@@ -60,7 +60,7 @@ def validate_agent_instruction_cli(instruction: str) -> str:
60
60
  try:
61
61
  return validate_agent_instruction(instruction)
62
62
  except ValueError as e:
63
- raise click.ClickException(str(e))
63
+ raise click.ClickException(str(e)) from e
64
64
 
65
65
 
66
66
  def validate_timeout_cli(timeout: int) -> int:
@@ -78,7 +78,7 @@ def validate_timeout_cli(timeout: int) -> int:
78
78
  try:
79
79
  return validate_timeout(timeout)
80
80
  except ValueError as e:
81
- raise click.ClickException(str(e))
81
+ raise click.ClickException(str(e)) from e
82
82
 
83
83
 
84
84
  def validate_tool_name_cli(name: str) -> str:
@@ -96,7 +96,7 @@ def validate_tool_name_cli(name: str) -> str:
96
96
  try:
97
97
  return validate_tool_name(name)
98
98
  except ValueError as e:
99
- raise click.ClickException(str(e))
99
+ raise click.ClickException(str(e)) from e
100
100
 
101
101
 
102
102
  def validate_mcp_name_cli(name: str) -> str:
@@ -114,7 +114,7 @@ def validate_mcp_name_cli(name: str) -> str:
114
114
  try:
115
115
  return validate_mcp_name(name)
116
116
  except ValueError as e:
117
- raise click.ClickException(str(e))
117
+ raise click.ClickException(str(e)) from e
118
118
 
119
119
 
120
120
  def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Path:
@@ -133,7 +133,7 @@ def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Pa
133
133
  try:
134
134
  return validate_file_path(file_path, must_exist)
135
135
  except ValueError as e:
136
- raise click.ClickException(str(e))
136
+ raise click.ClickException(str(e)) from e
137
137
 
138
138
 
139
139
  def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -> Path:
@@ -152,7 +152,7 @@ def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -
152
152
  try:
153
153
  return validate_directory_path(dir_path, must_exist)
154
154
  except ValueError as e:
155
- raise click.ClickException(str(e))
155
+ raise click.ClickException(str(e)) from e
156
156
 
157
157
 
158
158
  def validate_url_cli(url: str) -> str:
@@ -170,7 +170,7 @@ def validate_url_cli(url: str) -> str:
170
170
  try:
171
171
  return validate_url(url)
172
172
  except ValueError as e:
173
- raise click.ClickException(str(e))
173
+ raise click.ClickException(str(e)) from e
174
174
 
175
175
 
176
176
  def validate_api_key_cli(api_key: str) -> str:
@@ -188,7 +188,7 @@ def validate_api_key_cli(api_key: str) -> str:
188
188
  try:
189
189
  return validate_api_key(api_key)
190
190
  except ValueError as e:
191
- raise click.ClickException(str(e))
191
+ raise click.ClickException(str(e)) from e
192
192
 
193
193
 
194
194
  def coerce_timeout_cli(value: int | float | str) -> int:
@@ -206,7 +206,7 @@ def coerce_timeout_cli(value: int | float | str) -> int:
206
206
  try:
207
207
  return coerce_timeout(value)
208
208
  except ValueError as e:
209
- raise click.ClickException(str(e))
209
+ raise click.ClickException(str(e)) from e
210
210
 
211
211
 
212
212
  def validate_name_uniqueness_cli(
@@ -230,8 +230,7 @@ def validate_name_uniqueness_cli(
230
230
  existing = finder_func(name=name)
231
231
  if existing:
232
232
  raise click.ClickException(
233
- f"A {resource_type.lower()} named '{name}' already exists. "
234
- "Please choose a unique name."
233
+ f"A {resource_type.lower()} named '{name}' already exists. Please choose a unique name."
235
234
  )
236
235
  except click.ClickException:
237
236
  raise