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
@@ -32,20 +32,22 @@ def resolve_resource_reference(
32
32
  This is a common pattern used across all resource types.
33
33
 
34
34
  Args:
35
- ctx: Click context
36
- client: API client
37
- reference: Resource ID or name
38
- resource_type: Type of resource
39
- get_by_id_func: Function to get resource by ID
40
- find_by_name_func: Function to find resources by name
41
- label: Label for error messages
42
- select: Selection index for ambiguous matches
35
+ ctx: Click context for CLI operations.
36
+ _client: API client instance for backend operations.
37
+ reference: Resource ID or name to resolve.
38
+ resource_type: Type of resource being resolved.
39
+ get_by_id_func: Function to get resource by ID.
40
+ find_by_name_func: Function to find resources by name.
41
+ label: Label for error messages and user feedback.
42
+ select: Selection index for ambiguous matches in non-interactive mode.
43
+ interface_preference: Interface preference for user interaction ("fuzzy" or "questionary").
44
+ spinner_message: Custom message to show during resolution process.
43
45
 
44
46
  Returns:
45
- Resolved resource object
47
+ Resolved resource object or None if not found.
46
48
 
47
49
  Raises:
48
- click.ClickException: If resolution fails
50
+ click.ClickException: If resolution fails.
49
51
  """
50
52
  try:
51
53
  message = (
@@ -0,0 +1,29 @@
1
+ """Shared helpers for creating and printing Rich markup content.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from rich.console import Console
12
+ from rich.markup import MarkupError
13
+ from rich.text import Text
14
+
15
+
16
+ def markup_text(message: str, **kwargs: Any) -> Text:
17
+ """Create a Rich Text instance from markup with graceful fallback."""
18
+ try:
19
+ return Text.from_markup(message, **kwargs)
20
+ except MarkupError:
21
+ return Text(message, **kwargs)
22
+
23
+
24
+ def print_markup(
25
+ message: str, *, console: Console | None = None, **kwargs: Any
26
+ ) -> None:
27
+ """Print markup-aware text to the provided console (default: new Console)."""
28
+ target_console = console or Console()
29
+ target_console.print(markup_text(message, **kwargs))
@@ -22,6 +22,12 @@ class AgentRunSession:
22
22
  """Per-agent execution context for the command palette."""
23
23
 
24
24
  def __init__(self, session: SlashSession, agent: Any) -> None:
25
+ """Initialize the agent run session.
26
+
27
+ Args:
28
+ session: The slash session context
29
+ agent: The agent to interact with
30
+ """
25
31
  self.session = session
26
32
  self.agent = agent
27
33
  self.console = session.console
@@ -39,6 +45,7 @@ class AgentRunSession:
39
45
  }
40
46
 
41
47
  def run(self) -> None:
48
+ """Run the interactive agent session loop."""
42
49
  self.session.set_contextual_commands(
43
50
  self._contextual_completion_help, include_global=False
44
51
  )
@@ -46,6 +46,11 @@ if _HAS_PROMPT_TOOLKIT:
46
46
  """Provide slash command completions inside the prompt."""
47
47
 
48
48
  def __init__(self, session: SlashSession) -> None:
49
+ """Initialize the slash completer.
50
+
51
+ Args:
52
+ session: The slash session context
53
+ """
49
54
  self._session = session
50
55
 
51
56
  def get_completions(
@@ -53,6 +58,15 @@ if _HAS_PROMPT_TOOLKIT:
53
58
  document: Any,
54
59
  _complete_event: Any, # type: ignore[no-any-return]
55
60
  ) -> Iterable[Completion]: # pragma: no cover - UI
61
+ """Get completions for slash commands.
62
+
63
+ Args:
64
+ document: The document being edited
65
+ _complete_event: The completion event
66
+
67
+ Yields:
68
+ Completion objects for matching commands
69
+ """
56
70
  if Completion is None:
57
71
  return
58
72
 
@@ -66,7 +80,14 @@ if _HAS_PROMPT_TOOLKIT:
66
80
  else: # pragma: no cover - fallback when prompt_toolkit is missing
67
81
 
68
82
  class SlashCompleter: # type: ignore[too-many-ancestors]
83
+ """Fallback slash completer when prompt_toolkit is not available."""
84
+
69
85
  def __init__(self, session: SlashSession) -> None: # noqa: D401 - stub
86
+ """Initialize the fallback slash completer.
87
+
88
+ Args:
89
+ session: The slash session context
90
+ """
70
91
  self._session = session
71
92
 
72
93
 
@@ -76,7 +97,6 @@ def setup_prompt_toolkit(
76
97
  interactive: bool,
77
98
  ) -> tuple[Any | None, Any | None]:
78
99
  """Configure prompt_toolkit session and style for interactive mode."""
79
-
80
100
  if not (interactive and _HAS_PROMPT_TOOLKIT):
81
101
  return None, None
82
102
 
@@ -105,7 +125,6 @@ def setup_prompt_toolkit(
105
125
 
106
126
  def _create_key_bindings(session: SlashSession) -> Any:
107
127
  """Create prompt_toolkit key bindings for the command palette."""
108
-
109
128
  if KeyBindings is None:
110
129
  return None
111
130
 
@@ -19,7 +19,7 @@ from rich.table import Table
19
19
 
20
20
  from glaip_sdk.branding import AIPBranding
21
21
  from glaip_sdk.cli.commands.configure import configure_command, load_config
22
- from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, get_client
22
+ from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, command_hint, get_client
23
23
  from glaip_sdk.rich_components import AIPPanel
24
24
 
25
25
  from .agent_session import AgentRunSession
@@ -49,6 +49,12 @@ class SlashSession:
49
49
  """Interactive command palette controller."""
50
50
 
51
51
  def __init__(self, ctx: click.Context, *, console: Console | None = None) -> None:
52
+ """Initialize the slash session.
53
+
54
+ Args:
55
+ ctx: The Click context
56
+ console: Optional console instance, creates default if None
57
+ """
52
58
  self.ctx = ctx
53
59
  self.console = console or Console()
54
60
  self._commands: dict[str, SlashCommand] = {}
@@ -90,7 +96,6 @@ class SlashSession:
90
96
 
91
97
  def run(self, initial_commands: Iterable[str] | None = None) -> None:
92
98
  """Start the command palette session loop."""
93
-
94
99
  if not self._interactive:
95
100
  self._run_non_interactive(initial_commands)
96
101
  return
@@ -153,7 +158,6 @@ class SlashSession:
153
158
 
154
159
  def _ensure_configuration(self) -> bool:
155
160
  """Ensure the CLI has both API URL and credentials before continuing."""
156
-
157
161
  while not self._configuration_ready():
158
162
  self.console.print(
159
163
  "[yellow]Configuration required.[/] Launching `/login` wizard..."
@@ -173,7 +177,6 @@ class SlashSession:
173
177
 
174
178
  def _configuration_ready(self) -> bool:
175
179
  """Check whether API URL and credentials are available."""
176
-
177
180
  config = self._load_config()
178
181
  api_url = self._get_api_url(config)
179
182
  if not api_url:
@@ -188,7 +191,6 @@ class SlashSession:
188
191
 
189
192
  def handle_command(self, raw: str, *, invoked_from_agent: bool = False) -> bool:
190
193
  """Parse and execute a single slash command string."""
191
-
192
194
  verb, args = self._parse(raw)
193
195
  if not verb:
194
196
  self.console.print("[red]Unrecognised command[/red]")
@@ -308,9 +310,13 @@ class SlashSession:
308
310
  return True
309
311
 
310
312
  if not agents:
311
- self.console.print(
312
- "[yellow]No agents available. Use `aip agents create` to add one.[/yellow]"
313
- )
313
+ hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
314
+ if hint:
315
+ self.console.print(
316
+ f"[yellow]No agents available. Use `{hint}` to add one.[/yellow]"
317
+ )
318
+ else:
319
+ self.console.print("[yellow]No agents available.[/yellow]")
314
320
  return True
315
321
 
316
322
  if args:
@@ -373,7 +379,7 @@ class SlashSession:
373
379
  self._register(
374
380
  SlashCommand(
375
381
  name="login",
376
- help="Run `aip configure` to set credentials.",
382
+ help="Run `/login` (alias `/configure`) to set credentials.",
377
383
  handler=SlashSession._cmd_login,
378
384
  aliases=("configure",),
379
385
  )
@@ -419,12 +425,10 @@ class SlashSession:
419
425
  @property
420
426
  def verbose_enabled(self) -> bool:
421
427
  """Return whether verbose agent runs are enabled."""
422
-
423
428
  return self._verbose_enabled
424
429
 
425
430
  def set_verbose(self, enabled: bool, *, announce: bool = True) -> None:
426
431
  """Enable or disable verbose mode with optional announcement."""
427
-
428
432
  if self._verbose_enabled == enabled:
429
433
  if announce:
430
434
  self._print_verbose_status(context="already")
@@ -437,12 +441,10 @@ class SlashSession:
437
441
 
438
442
  def toggle_verbose(self, *, announce: bool = True) -> None:
439
443
  """Flip verbose mode state."""
440
-
441
444
  self.set_verbose(not self._verbose_enabled, announce=announce)
442
445
 
443
446
  def _cmd_verbose(self, args: list[str], _invoked_from_agent: bool) -> bool:
444
447
  """Slash handler for `/verbose` command."""
445
-
446
448
  if args:
447
449
  self.console.print(
448
450
  "Usage: `/verbose` toggles verbose streaming output. Press Ctrl+T as a shortcut."
@@ -470,30 +472,25 @@ class SlashSession:
470
472
  # ------------------------------------------------------------------
471
473
  def register_active_renderer(self, renderer: Any) -> None:
472
474
  """Register the renderer currently streaming an agent run."""
473
-
474
475
  self._active_renderer = renderer
475
476
  self._sync_active_renderer()
476
477
 
477
478
  def clear_active_renderer(self, renderer: Any | None = None) -> None:
478
479
  """Clear the active renderer if it matches the provided instance."""
479
-
480
480
  if renderer is not None and renderer is not self._active_renderer:
481
481
  return
482
482
  self._active_renderer = None
483
483
 
484
484
  def notify_agent_run_started(self) -> None:
485
485
  """Mark that an agent run is in progress."""
486
-
487
486
  self.clear_active_renderer()
488
487
 
489
488
  def notify_agent_run_finished(self) -> None:
490
489
  """Mark that the active agent run has completed."""
491
-
492
490
  self.clear_active_renderer()
493
491
 
494
492
  def _sync_active_renderer(self) -> None:
495
493
  """Ensure the active renderer reflects the current verbose state."""
496
-
497
494
  renderer = self._active_renderer
498
495
  if renderer is None:
499
496
  return
@@ -622,18 +619,15 @@ class SlashSession:
622
619
  self, commands: dict[str, str] | None, *, include_global: bool = True
623
620
  ) -> None:
624
621
  """Set context-specific commands that should appear in completions."""
625
-
626
622
  self._contextual_commands = dict(commands or {})
627
623
  self._contextual_include_global = include_global if commands else True
628
624
 
629
625
  def get_contextual_commands(self) -> dict[str, str]: # type: ignore[no-any-return]
630
626
  """Return a copy of the currently active contextual commands."""
631
-
632
627
  return dict(self._contextual_commands)
633
628
 
634
629
  def should_include_global_commands(self) -> bool:
635
630
  """Return whether global slash commands should appear in completions."""
636
-
637
631
  return self._contextual_include_global
638
632
 
639
633
  def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
@@ -13,6 +13,7 @@ import httpx
13
13
  from packaging.version import InvalidVersion, Version
14
14
  from rich.console import Console
15
15
 
16
+ from glaip_sdk.cli.utils import command_hint
16
17
  from glaip_sdk.rich_components import AIPPanel
17
18
 
18
19
  FetchLatestVersion = Callable[[], str | None]
@@ -59,6 +60,7 @@ def _should_check_for_updates() -> bool:
59
60
  def _build_update_panel(
60
61
  current_version: str,
61
62
  latest_version: str,
63
+ command_text: str,
62
64
  ) -> AIPPanel:
63
65
  """Create a Rich panel that prompts the user to update."""
64
66
  message = (
@@ -66,7 +68,7 @@ def _build_update_panel(
66
68
  f"{current_version} → {latest_version}\n\n"
67
69
  "See the latest release notes:\n"
68
70
  f"https://pypi.org/project/glaip-sdk/{latest_version}/\n\n"
69
- "[cyan]Run[/cyan] [bold]aip update[/bold] to install."
71
+ f"[cyan]Run[/cyan] [bold]{command_text}[/bold] to install."
70
72
  )
71
73
  return AIPPanel(
72
74
  message,
@@ -99,8 +101,12 @@ def maybe_notify_update(
99
101
  if current is None or latest is None or latest <= current:
100
102
  return
101
103
 
104
+ command_text = command_hint("update")
105
+ if command_text is None:
106
+ return
107
+
102
108
  active_console = console or Console()
103
- panel = _build_update_panel(current_version, latest_version)
109
+ panel = _build_update_panel(current_version, latest_version, command_text)
104
110
  active_console.print(panel)
105
111
 
106
112
 
glaip_sdk/cli/utils.py CHANGED
@@ -19,7 +19,9 @@ import click
19
19
  from rich.console import Console, Group
20
20
  from rich.markdown import Markdown
21
21
  from rich.pretty import Pretty
22
- from rich.text import Text
22
+
23
+ from glaip_sdk.cli.rich_helpers import markup_text
24
+ from glaip_sdk.rich_components import AIPPanel
23
25
 
24
26
  # Optional interactive deps (fuzzy palette)
25
27
  try:
@@ -38,9 +40,15 @@ except Exception: # pragma: no cover - optional dependency
38
40
  if TYPE_CHECKING: # pragma: no cover - import-only during type checking
39
41
  from glaip_sdk import Client
40
42
  from glaip_sdk.cli import masking, pager
41
- from glaip_sdk.cli.commands.configure import load_config
42
- from glaip_sdk.cli.context import _get_view, get_ctx_value
43
- from glaip_sdk.rich_components import AIPPanel, AIPTable
43
+ from glaip_sdk.cli.config import load_config
44
+ from glaip_sdk.cli.context import (
45
+ _get_view,
46
+ get_ctx_value,
47
+ )
48
+ from glaip_sdk.cli.context import (
49
+ detect_export_format as _detect_export_format,
50
+ )
51
+ from glaip_sdk.rich_components import AIPTable
44
52
  from glaip_sdk.utils import is_uuid
45
53
  from glaip_sdk.utils.rendering.renderer import (
46
54
  CapturingConsole,
@@ -53,6 +61,59 @@ pager.console = console
53
61
  logger = logging.getLogger("glaip_sdk.cli.utils")
54
62
 
55
63
 
64
+ # ----------------------------- Context helpers ---------------------------- #
65
+
66
+
67
+ def detect_export_format(file_path: str | os.PathLike[str]) -> str:
68
+ """Backward-compatible proxy to `glaip_sdk.cli.context.detect_export_format`."""
69
+ return _detect_export_format(file_path)
70
+
71
+
72
+ def in_slash_mode(ctx: click.Context | None = None) -> bool:
73
+ """Return True when running inside the slash command palette."""
74
+ if ctx is None:
75
+ try:
76
+ ctx = click.get_current_context(silent=True)
77
+ except RuntimeError:
78
+ ctx = None
79
+
80
+ if ctx is None:
81
+ return False
82
+
83
+ obj = getattr(ctx, "obj", None)
84
+ if isinstance(obj, dict):
85
+ return bool(obj.get("_slash_session"))
86
+
87
+ return bool(getattr(obj, "_slash_session", False))
88
+
89
+
90
+ def command_hint(
91
+ cli_command: str | None,
92
+ slash_command: str | None = None,
93
+ *,
94
+ ctx: click.Context | None = None,
95
+ ) -> str | None:
96
+ """Return the appropriate command string for the current mode.
97
+
98
+ Args:
99
+ cli_command: Command string without the ``aip`` prefix (e.g., ``"status"``).
100
+ slash_command: Slash command counterpart (e.g., ``"status"`` or ``"/status"``).
101
+ ctx: Optional Click context override.
102
+
103
+ Returns:
104
+ The formatted command string for the active mode, or ``None`` when no
105
+ equivalent command exists in that mode.
106
+ """
107
+ if in_slash_mode(ctx):
108
+ if not slash_command:
109
+ return None
110
+ return slash_command if slash_command.startswith("/") else f"/{slash_command}"
111
+
112
+ if not cli_command:
113
+ return None
114
+ return f"aip {cli_command}"
115
+
116
+
56
117
  def spinner_context(
57
118
  ctx: Any | None,
58
119
  message: str,
@@ -62,7 +123,6 @@ def spinner_context(
62
123
  spinner_style: str = "cyan",
63
124
  ) -> AbstractContextManager[Any]:
64
125
  """Return a context manager that renders a spinner when appropriate."""
65
-
66
126
  active_console = console_override or console
67
127
  if not _can_use_spinner(ctx, active_console):
68
128
  return nullcontext()
@@ -76,7 +136,6 @@ def spinner_context(
76
136
 
77
137
  def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
78
138
  """Check if spinner output is allowed in the current environment."""
79
-
80
139
  if ctx is not None:
81
140
  tty_enabled = bool(get_ctx_value(ctx, "tty", True))
82
141
  view = (_get_view(ctx) or "rich").lower()
@@ -91,7 +150,6 @@ def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
91
150
 
92
151
  def _stream_supports_tty(stream: Any) -> bool:
93
152
  """Return True if the provided stream can safely render a spinner."""
94
-
95
153
  target = stream if hasattr(stream, "isatty") else sys.stdout
96
154
  try:
97
155
  return bool(target.isatty())
@@ -101,7 +159,6 @@ def _stream_supports_tty(stream: Any) -> bool:
101
159
 
102
160
  def update_spinner(status_indicator: Any | None, message: str) -> None:
103
161
  """Update spinner text when a status indicator is active."""
104
-
105
162
  if status_indicator is None:
106
163
  return
107
164
 
@@ -113,7 +170,6 @@ def update_spinner(status_indicator: Any | None, message: str) -> None:
113
170
 
114
171
  def stop_spinner(status_indicator: Any | None) -> None:
115
172
  """Stop an active spinner safely."""
116
-
117
173
  if status_indicator is None:
118
174
  return
119
175
 
@@ -160,9 +216,12 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
160
216
  }
161
217
 
162
218
  if not config.get("api_url") or not config.get("api_key"):
163
- raise click.ClickException(
164
- "Missing api_url/api_key. Run `aip configure` or set AIP_* env vars."
165
- )
219
+ configure_hint = command_hint("configure", slash_command="login", ctx=ctx)
220
+ actions = []
221
+ if configure_hint:
222
+ actions.append(f"Run `{configure_hint}`")
223
+ actions.append("set AIP_* env vars")
224
+ raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
166
225
 
167
226
  return Client(
168
227
  api_url=config.get("api_url"),
@@ -171,6 +230,8 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
171
230
  )
172
231
 
173
232
 
233
+ # ----------------------------- Secret masking ---------------------------- #
234
+
174
235
  # ----------------------------- Fuzzy palette ----------------------------- #
175
236
 
176
237
 
@@ -237,8 +298,8 @@ def _build_display_parts(
237
298
 
238
299
 
239
300
  def _row_display(row: dict[str, Any], columns: list[tuple]) -> str:
240
- """
241
- Build a compact text label for the palette.
301
+ """Build a compact text label for the palette.
302
+
242
303
  Prefers: name • type • framework • [id] (when available)
243
304
  Falls back to first 2 columns + [id].
244
305
  """
@@ -331,8 +392,8 @@ def _perform_fuzzy_search(
331
392
  def _fuzzy_pick(
332
393
  rows: list[dict[str, Any]], columns: list[tuple], title: str
333
394
  ) -> dict[str, Any] | None: # pragma: no cover - requires interactive prompt toolkit
334
- """
335
- Open a minimal fuzzy palette using prompt_toolkit.
395
+ """Open a minimal fuzzy palette using prompt_toolkit.
396
+
336
397
  Returns the selected row (dict) or None if cancelled/missing deps.
337
398
  """
338
399
  if not _check_fuzzy_pick_requirements():
@@ -401,8 +462,8 @@ def _calculate_length_bonus(search: str, target: str) -> int:
401
462
 
402
463
 
403
464
  def _fuzzy_score(search: str, target: str) -> int:
404
- """
405
- Calculate fuzzy match score.
465
+ """Calculate fuzzy match score.
466
+
406
467
  Higher score = better match.
407
468
  Returns -1 if no match possible.
408
469
  """
@@ -465,8 +526,15 @@ def output_result(
465
526
  result: Any,
466
527
  title: str = "Result",
467
528
  panel_title: str | None = None,
468
- success_message: str | None = None,
469
529
  ) -> None:
530
+ """Output a result to the console with optional title.
531
+
532
+ Args:
533
+ ctx: Click context
534
+ result: Result data to output
535
+ title: Optional title for the output
536
+ panel_title: Optional Rich panel title for structured output
537
+ """
470
538
  fmt = _get_view(ctx)
471
539
 
472
540
  data = _coerce_result_payload(result)
@@ -485,20 +553,12 @@ def output_result(
485
553
  _render_markdown_output(data)
486
554
  return
487
555
 
488
- if success_message:
489
- console.print(Text(f"[green]✅ {success_message}[/green]"))
490
-
556
+ renderable = Pretty(data)
491
557
  if panel_title:
492
- console.print(
493
- AIPPanel(
494
- Pretty(data),
495
- title=panel_title,
496
- border_style="blue",
497
- )
498
- )
558
+ console.print(AIPPanel(renderable, title=panel_title))
499
559
  else:
500
- console.print(Text(f"[cyan]{title}:[/cyan]"))
501
- console.print(Pretty(data))
560
+ console.print(markup_text(f"[cyan]{title}:[/cyan]"))
561
+ console.print(renderable)
502
562
 
503
563
 
504
564
  # ----------------------------- List rendering ---------------------------- #
@@ -575,7 +635,7 @@ def _build_table_group(
575
635
  table = _create_table(columns, title)
576
636
  for row in rows:
577
637
  table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
578
- footer = Text.from_markup(f"\n[dim]Total {len(rows)} items[/dim]")
638
+ footer = markup_text(f"\n[dim]Total {len(rows)} items[/dim]")
579
639
  return Group(table, footer)
580
640
 
581
641
 
@@ -605,12 +665,11 @@ def _handle_markdown_output(
605
665
 
606
666
  def _handle_empty_items(title: str) -> None:
607
667
  """Handle case when no items are found."""
608
- console.print(Text.from_markup(f"[yellow]No {title.lower()} found.[/yellow]"))
668
+ console.print(markup_text(f"[yellow]No {title.lower()} found.[/yellow]"))
609
669
 
610
670
 
611
671
  def _should_use_fuzzy_picker() -> bool:
612
672
  """Return True when the interactive fuzzy picker can be shown."""
613
-
614
673
  return console.is_terminal and os.isatty(1)
615
674
 
616
675
 
@@ -618,7 +677,6 @@ def _try_fuzzy_pick(
618
677
  rows: list[dict[str, Any]], columns: list[tuple], title: str
619
678
  ) -> dict[str, Any] | None:
620
679
  """Best-effort fuzzy selection; returns None if the picker fails."""
621
-
622
680
  if not _should_use_fuzzy_picker():
623
681
  return None
624
682
 
@@ -629,36 +687,34 @@ def _try_fuzzy_pick(
629
687
  return None
630
688
 
631
689
 
632
- def _resource_tip_command(title: str) -> str:
690
+ def _resource_tip_command(title: str) -> str | None:
633
691
  """Resolve the follow-up command hint for the given table title."""
634
-
635
692
  title_lower = title.lower()
636
693
  mapping = {
637
- "agent": "aip agents get",
638
- "tool": "aip tools get",
639
- "mcp": "aip mcps get",
640
- "model": "aip models list", # models only ship a list command
694
+ "agent": ("agents get", "agents"),
695
+ "tool": ("tools get", None),
696
+ "mcp": ("mcps get", None),
697
+ "model": ("models list", None), # models only ship a list command
641
698
  }
642
- for keyword, command in mapping.items():
699
+ for keyword, (cli_command, slash_command) in mapping.items():
643
700
  if keyword in title_lower:
644
- return command
645
- return "aip agents get"
701
+ return command_hint(cli_command, slash_command=slash_command)
702
+ return command_hint("agents get", slash_command="agents")
646
703
 
647
704
 
648
705
  def _print_selection_tip(title: str) -> None:
649
706
  """Print the contextual follow-up tip after a fuzzy selection."""
650
-
651
707
  tip_cmd = _resource_tip_command(title)
652
- console.print(
653
- Text.from_markup(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]")
654
- )
708
+ if tip_cmd:
709
+ console.print(
710
+ markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]")
711
+ )
655
712
 
656
713
 
657
714
  def _handle_fuzzy_pick_selection(
658
715
  rows: list[dict[str, Any]], columns: list[tuple], title: str
659
716
  ) -> bool:
660
717
  """Handle fuzzy picker selection, returns True if selection was made."""
661
-
662
718
  picked = _try_fuzzy_pick(rows, columns, title)
663
719
  if picked is None:
664
720
  return False
@@ -773,14 +829,16 @@ def build_renderer(
773
829
  """Build renderer and capturing console for CLI commands.
774
830
 
775
831
  Args:
776
- ctx: Click context
777
- save_path: Path to save output to (enables capturing)
778
- theme: Color theme ("dark" or "light")
779
- verbose: Whether to enable verbose mode
780
- tty_enabled: Whether TTY is available
832
+ _ctx: Click context object for CLI operations.
833
+ save_path: Path to save output to (enables capturing console).
834
+ theme: Color theme ("dark" or "light").
835
+ verbose: Whether to enable verbose mode.
836
+ _tty_enabled: Whether TTY is available for interactive features.
837
+ live: Whether to enable live rendering mode (overrides verbose default).
838
+ snapshots: Whether to capture and store snapshots.
781
839
 
782
840
  Returns:
783
- Tuple of (renderer, capturing_console)
841
+ Tuple of (renderer, capturing_console) for streaming output.
784
842
  """
785
843
  # Use capturing console if saving output
786
844
  working_console = console
@@ -859,8 +917,7 @@ def _build_resource_labels(resources: list[Any]) -> tuple[list[str], dict[str, A
859
917
  def _fuzzy_pick_for_resources(
860
918
  resources: list[Any], resource_type: str, _search_term: str
861
919
  ) -> Any | None: # pragma: no cover - interactive selection helper
862
- """
863
- Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
920
+ """Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
864
921
 
865
922
  Args:
866
923
  resources: List of resource objects to choose from
@@ -1035,7 +1092,7 @@ def _handle_fallback_numeric_ambiguity(
1035
1092
  safe_ref = ref.replace("{", "{{").replace("}", "}}")
1036
1093
 
1037
1094
  console.print(
1038
- Text(
1095
+ markup_text(
1039
1096
  f"[yellow]Multiple {safe_resource_type}s found matching '{safe_ref}':[/yellow]"
1040
1097
  )
1041
1098
  )