glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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 (161) hide show
  1. glaip_sdk/__init__.py +6 -3
  2. glaip_sdk/_version.py +12 -5
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1126 -0
  5. glaip_sdk/branding.py +79 -15
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +699 -0
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents.py +503 -183
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +774 -137
  14. glaip_sdk/cli/commands/mcps.py +1124 -181
  15. glaip_sdk/cli/commands/models.py +25 -10
  16. glaip_sdk/cli/commands/tools.py +144 -92
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +61 -0
  19. glaip_sdk/cli/config.py +95 -0
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +150 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +143 -53
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +24 -18
  30. glaip_sdk/cli/main.py +420 -145
  31. glaip_sdk/cli/masking.py +136 -0
  32. glaip_sdk/cli/mcp_validators.py +287 -0
  33. glaip_sdk/cli/pager.py +266 -0
  34. glaip_sdk/cli/parsers/__init__.py +7 -0
  35. glaip_sdk/cli/parsers/json_input.py +177 -0
  36. glaip_sdk/cli/resolution.py +28 -21
  37. glaip_sdk/cli/rich_helpers.py +27 -0
  38. glaip_sdk/cli/slash/__init__.py +15 -0
  39. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +282 -0
  42. glaip_sdk/cli/slash/prompt.py +245 -0
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +1679 -0
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +31 -0
  52. glaip_sdk/cli/transcript/cache.py +536 -0
  53. glaip_sdk/cli/transcript/capture.py +329 -0
  54. glaip_sdk/cli/transcript/export.py +38 -0
  55. glaip_sdk/cli/transcript/history.py +815 -0
  56. glaip_sdk/cli/transcript/launcher.py +77 -0
  57. glaip_sdk/cli/transcript/viewer.py +372 -0
  58. glaip_sdk/cli/update_notifier.py +290 -0
  59. glaip_sdk/cli/utils.py +247 -1238
  60. glaip_sdk/cli/validators.py +16 -18
  61. glaip_sdk/client/__init__.py +2 -1
  62. glaip_sdk/client/_agent_payloads.py +520 -0
  63. glaip_sdk/client/agent_runs.py +147 -0
  64. glaip_sdk/client/agents.py +940 -574
  65. glaip_sdk/client/base.py +163 -48
  66. glaip_sdk/client/main.py +35 -12
  67. glaip_sdk/client/mcps.py +126 -18
  68. glaip_sdk/client/run_rendering.py +415 -0
  69. glaip_sdk/client/shared.py +21 -0
  70. glaip_sdk/client/tools.py +195 -37
  71. glaip_sdk/client/validators.py +20 -48
  72. glaip_sdk/config/constants.py +15 -5
  73. glaip_sdk/exceptions.py +16 -9
  74. glaip_sdk/icons.py +25 -0
  75. glaip_sdk/mcps/__init__.py +21 -0
  76. glaip_sdk/mcps/base.py +345 -0
  77. glaip_sdk/models/__init__.py +90 -0
  78. glaip_sdk/models/agent.py +47 -0
  79. glaip_sdk/models/agent_runs.py +116 -0
  80. glaip_sdk/models/common.py +42 -0
  81. glaip_sdk/models/mcp.py +33 -0
  82. glaip_sdk/models/tool.py +33 -0
  83. glaip_sdk/payload_schemas/__init__.py +7 -0
  84. glaip_sdk/payload_schemas/agent.py +85 -0
  85. glaip_sdk/registry/__init__.py +55 -0
  86. glaip_sdk/registry/agent.py +164 -0
  87. glaip_sdk/registry/base.py +139 -0
  88. glaip_sdk/registry/mcp.py +253 -0
  89. glaip_sdk/registry/tool.py +231 -0
  90. glaip_sdk/rich_components.py +98 -2
  91. glaip_sdk/runner/__init__.py +59 -0
  92. glaip_sdk/runner/base.py +84 -0
  93. glaip_sdk/runner/deps.py +115 -0
  94. glaip_sdk/runner/langgraph.py +597 -0
  95. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  99. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
  102. glaip_sdk/tools/__init__.py +22 -0
  103. glaip_sdk/tools/base.py +435 -0
  104. glaip_sdk/utils/__init__.py +59 -13
  105. glaip_sdk/utils/a2a/__init__.py +34 -0
  106. glaip_sdk/utils/a2a/event_processor.py +188 -0
  107. glaip_sdk/utils/agent_config.py +53 -40
  108. glaip_sdk/utils/bundler.py +267 -0
  109. glaip_sdk/utils/client.py +111 -0
  110. glaip_sdk/utils/client_utils.py +58 -26
  111. glaip_sdk/utils/datetime_helpers.py +58 -0
  112. glaip_sdk/utils/discovery.py +78 -0
  113. glaip_sdk/utils/display.py +65 -32
  114. glaip_sdk/utils/export.py +143 -0
  115. glaip_sdk/utils/general.py +1 -36
  116. glaip_sdk/utils/import_export.py +20 -25
  117. glaip_sdk/utils/import_resolver.py +492 -0
  118. glaip_sdk/utils/instructions.py +101 -0
  119. glaip_sdk/utils/rendering/__init__.py +115 -1
  120. glaip_sdk/utils/rendering/formatting.py +85 -43
  121. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  122. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
  123. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  124. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  125. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  126. glaip_sdk/utils/rendering/models.py +39 -7
  127. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  128. glaip_sdk/utils/rendering/renderer/base.py +672 -759
  129. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  130. glaip_sdk/utils/rendering/renderer/debug.py +75 -22
  131. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  132. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  133. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  134. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  135. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  136. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  137. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  138. glaip_sdk/utils/rendering/state.py +204 -0
  139. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  140. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  141. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  142. glaip_sdk/utils/rendering/steps/format.py +176 -0
  143. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  144. glaip_sdk/utils/rendering/timing.py +36 -0
  145. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  146. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  147. glaip_sdk/utils/resource_refs.py +29 -26
  148. glaip_sdk/utils/runtime_config.py +422 -0
  149. glaip_sdk/utils/serialization.py +184 -51
  150. glaip_sdk/utils/sync.py +142 -0
  151. glaip_sdk/utils/tool_detection.py +33 -0
  152. glaip_sdk/utils/validation.py +21 -30
  153. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
  154. glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
  155. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
  156. glaip_sdk/models.py +0 -250
  157. glaip_sdk/utils/rendering/renderer/progress.py +0 -118
  158. glaip_sdk/utils/rendering/steps.py +0 -232
  159. glaip_sdk/utils/rich_utils.py +0 -29
  160. glaip_sdk-0.0.7.dist-info/RECORD +0 -55
  161. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,187 @@
1
+ """CLI rendering utilities: Rich console helpers, viewer launchers, renderer builders.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from contextlib import AbstractContextManager, contextmanager, nullcontext
13
+ from typing import Any
14
+
15
+ from rich.console import Console
16
+
17
+ from glaip_sdk.branding import ACCENT_STYLE
18
+ from glaip_sdk.cli.context import _get_view, get_ctx_value
19
+ from glaip_sdk.utils.rendering.renderer import (
20
+ CapturingConsole,
21
+ RendererFactoryOptions,
22
+ RichStreamRenderer,
23
+ make_default_renderer,
24
+ make_verbose_renderer,
25
+ )
26
+
27
+ # Export console for backward compatibility
28
+ console = Console()
29
+
30
+
31
+ def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
32
+ """Check if spinner output is allowed in the current environment."""
33
+ if ctx is not None:
34
+ tty_enabled = bool(get_ctx_value(ctx, "tty", True))
35
+ view = (_get_view(ctx) or "rich").lower()
36
+ if not tty_enabled or view not in {"", "rich"}:
37
+ return False
38
+
39
+ if not active_console.is_terminal:
40
+ return False
41
+
42
+ return _stream_supports_tty(getattr(active_console, "file", None))
43
+
44
+
45
+ def _stream_supports_tty(stream: Any) -> bool:
46
+ """Return True if the provided stream can safely render a spinner."""
47
+ target = stream if hasattr(stream, "isatty") else sys.stdout
48
+ try:
49
+ return bool(target.isatty())
50
+ except Exception:
51
+ return False
52
+
53
+
54
+ def update_spinner(status_indicator: Any | None, message: str) -> None:
55
+ """Update spinner text when a status indicator is active."""
56
+ if status_indicator is None:
57
+ return
58
+
59
+ try:
60
+ status_indicator.update(message)
61
+ except Exception: # pragma: no cover - defensive update
62
+ pass
63
+
64
+
65
+ def stop_spinner(status_indicator: Any | None) -> None:
66
+ """Stop an active spinner safely."""
67
+ if status_indicator is None:
68
+ return
69
+
70
+ try:
71
+ status_indicator.stop()
72
+ except Exception: # pragma: no cover - defensive stop
73
+ pass
74
+
75
+
76
+ # Backwards compatibility aliases for legacy callers
77
+ _spinner_update = update_spinner
78
+ _spinner_stop = stop_spinner
79
+
80
+
81
+ def spinner_context(
82
+ ctx: Any | None,
83
+ message: str,
84
+ *,
85
+ console_override: Console | None = None,
86
+ spinner: str = "dots",
87
+ spinner_style: str = ACCENT_STYLE,
88
+ ) -> AbstractContextManager[Any]:
89
+ """Return a context manager that renders a spinner when appropriate."""
90
+ active_console = console_override or console
91
+ if not _can_use_spinner(ctx, active_console):
92
+ return nullcontext()
93
+
94
+ status = active_console.status(
95
+ message,
96
+ spinner=spinner,
97
+ spinner_style=spinner_style,
98
+ )
99
+
100
+ if not hasattr(status, "__enter__") or not hasattr(status, "__exit__"):
101
+ return nullcontext()
102
+
103
+ return status
104
+
105
+
106
+ def _register_renderer_with_session(ctx: Any, renderer: RichStreamRenderer) -> None:
107
+ """Attach renderer to an active slash session when present."""
108
+ try:
109
+ ctx_obj = getattr(ctx, "obj", None)
110
+ session = ctx_obj.get("_slash_session") if isinstance(ctx_obj, dict) else None
111
+ if session and hasattr(session, "register_active_renderer"):
112
+ session.register_active_renderer(renderer)
113
+ except Exception:
114
+ # Never let session bookkeeping break renderer creation
115
+ pass
116
+
117
+
118
+ def build_renderer(
119
+ _ctx: Any,
120
+ *,
121
+ save_path: str | os.PathLike[str] | None,
122
+ verbose: bool = False,
123
+ _tty_enabled: bool = True,
124
+ live: bool | None = None,
125
+ snapshots: bool | None = None,
126
+ ) -> tuple[RichStreamRenderer, Console | CapturingConsole]:
127
+ """Build renderer and capturing console for CLI commands.
128
+
129
+ Args:
130
+ _ctx: Click context object for CLI operations.
131
+ save_path: Path to save output to (enables capturing console).
132
+ verbose: Whether to enable verbose mode.
133
+ _tty_enabled: Whether TTY is available for interactive features.
134
+ live: Whether to enable live rendering mode (overrides verbose default).
135
+ snapshots: Whether to capture and store snapshots.
136
+
137
+ Returns:
138
+ Tuple of (renderer, capturing_console) for streaming output.
139
+ """
140
+ # Use capturing console if saving output
141
+ working_console = CapturingConsole(console, capture=True) if save_path else console
142
+
143
+ # Configure renderer based on verbose mode and explicit overrides
144
+ live_enabled = bool(live) if live is not None else not verbose
145
+ cfg_overrides = {
146
+ "live": live_enabled,
147
+ "append_finished_snapshots": bool(snapshots) if snapshots is not None else False,
148
+ }
149
+ renderer_console = (
150
+ working_console.original_console if isinstance(working_console, CapturingConsole) else working_console
151
+ )
152
+ factory = make_verbose_renderer if verbose else make_default_renderer
153
+ factory_options = RendererFactoryOptions(
154
+ console=renderer_console,
155
+ cfg_overrides=cfg_overrides,
156
+ verbose=verbose if factory is make_default_renderer else None,
157
+ )
158
+ renderer = factory_options.build(factory)
159
+
160
+ # Link the renderer back to the slash session when running from the palette.
161
+ _register_renderer_with_session(_ctx, renderer)
162
+
163
+ return renderer, working_console
164
+
165
+
166
+ @contextmanager
167
+ def with_client_and_spinner(
168
+ ctx: Any,
169
+ spinner_message: str,
170
+ *,
171
+ console_override: Console | None = None,
172
+ ) -> Any:
173
+ """Context manager for commands that need client and spinner.
174
+
175
+ Args:
176
+ ctx: Click context.
177
+ spinner_message: Message to display in spinner.
178
+ console_override: Optional console override.
179
+
180
+ Yields:
181
+ Client instance.
182
+ """
183
+ from glaip_sdk.cli.core.context import get_client # noqa: PLC0415
184
+
185
+ client = get_client(ctx)
186
+ with spinner_context(ctx, spinner_message, console_override=console_override):
187
+ yield client
glaip_sdk/cli/display.py CHANGED
@@ -15,6 +15,10 @@ 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.branding import ERROR_STYLE, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
19
+ from glaip_sdk.cli.hints import command_hint, format_command_hint, in_slash_mode
20
+ from glaip_sdk.cli.rich_helpers import markup_text
21
+ from glaip_sdk.icons import ICON_AGENT, ICON_TOOL
18
22
  from glaip_sdk.rich_components import AIPPanel
19
23
 
20
24
  console = Console()
@@ -37,15 +41,13 @@ def display_creation_success(
37
41
  # Build additional fields display
38
42
  fields_display = ""
39
43
  if additional_fields:
40
- fields_display = "\n" + "\n".join(
41
- f"{key}: {value}" for key, value in additional_fields.items()
42
- )
44
+ fields_display = "\n" + "\n".join(f"{key}: {value}" for key, value in additional_fields.items())
43
45
 
44
46
  return AIPPanel(
45
- f"[green]✅ {resource_type} '{resource_name}' created successfully![/green]\n\n"
47
+ f"[{SUCCESS_STYLE}]✅ {resource_type} '{resource_name}' created successfully![/]\n\n"
46
48
  f"ID: {resource_id}{fields_display}",
47
- title=f"🤖 {resource_type} Created",
48
- border_style="green",
49
+ title=f"{ICON_AGENT} {resource_type} Created",
50
+ border_style=SUCCESS,
49
51
  padding=(0, 1),
50
52
  )
51
53
 
@@ -60,9 +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(
64
- f"[green]✅ {resource_type} '{resource_name}' updated successfully[/green]"
65
- )
65
+ return markup_text(f"[{SUCCESS_STYLE}]✅ {resource_type} '{resource_name}' updated successfully[/]")
66
66
 
67
67
 
68
68
  def display_deletion_success(resource_type: str, resource_name: str) -> Text:
@@ -75,9 +75,7 @@ def display_deletion_success(resource_type: str, resource_name: str) -> Text:
75
75
  Returns:
76
76
  Rich Text object for display
77
77
  """
78
- return Text(
79
- f"[green]✅ {resource_type} '{resource_name}' deleted successfully[/green]"
80
- )
78
+ return markup_text(f"[{SUCCESS_STYLE}]✅ {resource_type} '{resource_name}' deleted successfully[/]")
81
79
 
82
80
 
83
81
  def display_api_error(error: Exception, operation: str = "operation") -> None:
@@ -88,8 +86,15 @@ def display_api_error(error: Exception, operation: str = "operation") -> None:
88
86
  operation: Description of the operation that failed
89
87
  """
90
88
  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]"))
89
+ error_message = markup_text(f"[{ERROR_STYLE}]Error during {operation}: {error}[/]")
90
+ error_message.no_wrap = True
91
+ error_message.overflow = "ignore"
92
+ console.print(error_message)
93
+
94
+ error_type_message = markup_text(f"[dim]Error type: {error_type}[/dim]")
95
+ error_type_message.no_wrap = True
96
+ error_type_message.overflow = "ignore"
97
+ console.print(error_type_message)
93
98
 
94
99
 
95
100
  def print_api_error(e: Exception) -> None:
@@ -102,30 +107,85 @@ def print_api_error(e: Exception) -> None:
102
107
  - Extracts status_code, error_type, and payload from APIError exceptions
103
108
  - Provides consistent error reporting across CLI commands
104
109
  - Handles both JSON and Rich output formats
110
+ - Special handling for validation errors with detailed field-level errors
105
111
  """
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]")
112
+ if not hasattr(e, "__dict__"):
113
+ console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
114
+ return
115
+
116
+ if not hasattr(e, "status_code"):
117
+ console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
118
+ return
119
+
120
+ error_text = str(e).strip()
121
+ if not error_text:
122
+ error_text = "Unknown error"
123
+ if "\n" in error_text:
124
+ error_text = error_text.splitlines()[0]
125
+ console.print(f"[{ERROR_STYLE}]API Error: {error_text}[/]")
126
+ status_code = getattr(e, "status_code", None)
127
+ if status_code is not None:
128
+ console.print(f"[{WARNING_STYLE}]Status: {status_code}[/]")
129
+
130
+ payload = getattr(e, "payload", _MISSING)
131
+ if payload is _MISSING:
132
+ return
133
+
134
+ if payload:
135
+ if not _print_structured_payload(payload):
136
+ console.print(f"[{WARNING_STYLE}]Details: {payload}[/]")
137
+ else:
138
+ console.print(f"[{WARNING_STYLE}]Details: {payload}[/]")
139
+
140
+
141
+ def _print_structured_payload(payload: Any) -> bool:
142
+ """Print structured payloads with enhanced formatting. Returns True if handled."""
143
+ if not isinstance(payload, dict):
144
+ return False
145
+
146
+ if "detail" in payload and _print_validation_details(payload["detail"]):
147
+ return True
148
+
149
+ if "details" in payload and _print_details_field(payload["details"]):
150
+ return True
151
+
152
+ return False
153
+
154
+
155
+ def _print_validation_details(detail: Any) -> bool:
156
+ """Render FastAPI-style validation errors."""
157
+ if not isinstance(detail, list) or not detail:
158
+ return False
159
+
160
+ console.print(f"[{ERROR_STYLE}]Validation Errors:[/]")
161
+ for error in detail:
162
+ if isinstance(error, dict):
163
+ loc = " -> ".join(str(x) for x in error.get("loc", []))
164
+ msg = error.get("msg", "Unknown error")
165
+ error_type = error.get("type", "unknown")
166
+ location = loc if loc else "field"
167
+ console.print(f" [{WARNING_STYLE}]• {location}:[/] {msg}")
168
+ if error_type != "unknown":
169
+ console.print(f" [dim]({error_type})[/dim]")
125
170
  else:
126
- console.print(f"[red]Error: {e}[/red]")
171
+ console.print(f" [{WARNING_STYLE}]•[/] {error}")
172
+ return True
173
+
174
+
175
+ def _print_details_field(details: Any) -> bool:
176
+ """Render custom error details from API payloads."""
177
+ if not details:
178
+ return False
179
+
180
+ console.print(f"[{ERROR_STYLE}]Error Details:[/]")
181
+ if isinstance(details, str):
182
+ console.print(f" [{WARNING_STYLE}]•[/] {details}")
183
+ elif isinstance(details, list):
184
+ for detail in details:
185
+ console.print(f" [{WARNING_STYLE}]•[/] {detail}")
127
186
  else:
128
- console.print(f"[red]Error: {e}[/red]")
187
+ console.print(f" [{WARNING_STYLE}]•[/] {details}")
188
+ return True
129
189
 
130
190
 
131
191
  _MISSING = object()
@@ -133,7 +193,6 @@ _MISSING = object()
133
193
 
134
194
  def build_resource_result_data(resource: Any, fields: list[str]) -> dict[str, Any]:
135
195
  """Return a normalized mapping of ``fields`` extracted from ``resource``."""
136
-
137
196
  result: dict[str, Any] = {}
138
197
  for field in fields:
139
198
  try:
@@ -149,6 +208,7 @@ def build_resource_result_data(resource: Any, fields: list[str]) -> dict[str, An
149
208
 
150
209
 
151
210
  def _normalise_field_value(field: str, value: Any) -> Any:
211
+ """Convert special sentinel values into display-friendly text."""
152
212
  if value is _MISSING:
153
213
  return "N/A"
154
214
  if hasattr(value, "_mock_name"):
@@ -233,9 +293,7 @@ def display_confirmation_prompt(resource_type: str, resource_name: str) -> bool:
233
293
  Returns:
234
294
  True if user confirms, False otherwise
235
295
  """
236
- if not click.confirm(
237
- f"Are you sure you want to delete {resource_type.lower()} '{resource_name}'?"
238
- ):
296
+ if not click.confirm(f"Are you sure you want to delete {resource_type.lower()} '{resource_name}'?"):
239
297
  if console.is_terminal:
240
298
  console.print(Text("Deletion cancelled."))
241
299
  return False
@@ -244,22 +302,54 @@ def display_confirmation_prompt(resource_type: str, resource_name: str) -> bool:
244
302
 
245
303
  def display_agent_run_suggestions(agent: Any) -> Panel:
246
304
  """Return a panel with post-creation suggestions for an agent."""
305
+ agent_id = getattr(agent, "id", "")
306
+ agent_name = getattr(agent, "name", "")
307
+ slash_mode = in_slash_mode()
308
+ run_hint_id = command_hint(
309
+ f'agents run {agent_id} "Your message here"',
310
+ slash_command=None,
311
+ )
312
+ run_hint_name = command_hint(
313
+ f'agents run "{agent_name}" "Your message here"',
314
+ slash_command=None,
315
+ )
316
+
317
+ content_parts: list[str] = ["[bold blue]💡 Next Steps:[/bold blue]\n\n"]
318
+
319
+ if slash_mode:
320
+ slash_shortcuts = "\n".join(
321
+ f" {format_command_hint(command, description) or command}"
322
+ for command, description in (
323
+ ("/details", "Show configuration (toggle preview)"),
324
+ ("/help", "Show command palette menu"),
325
+ ("/exit", "Return to the palette"),
326
+ )
327
+ )
328
+ content_parts.append(
329
+ f"🚀 Start chatting with [bold]{agent_name}[/bold] right here:\n"
330
+ f" Type your message below and press Enter to run it immediately.\n\n"
331
+ f"{ICON_TOOL} Slash shortcuts:\n"
332
+ f"{slash_shortcuts}"
333
+ )
334
+ else:
335
+ cli_hint_lines = [format_command_hint(hint) or hint for hint in (run_hint_id, run_hint_name) if hint]
336
+ if cli_hint_lines:
337
+ joined_hints = "\n".join(f" {hint}" for hint in cli_hint_lines)
338
+ content_parts.append(f"🚀 Run this agent from the CLI:\n{joined_hints}\n\n")
339
+ content_parts.append(
340
+ f"{ICON_TOOL} Available options:\n"
341
+ f" [dim]--chat-history[/dim] Include previous conversation\n"
342
+ f" [dim]--file[/dim] Attach files\n"
343
+ f" [dim]--input[/dim] Alternative input method\n"
344
+ f" [dim]--timeout[/dim] Set execution timeout\n"
345
+ f" [dim]--save[/dim] Save transcript to file\n"
346
+ f" [dim]--verbose[/dim] Show detailed execution\n\n"
347
+ f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]"
348
+ )
247
349
 
248
350
  return AIPPanel(
249
- f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
250
- f"🚀 Run this agent:\n"
251
- f' [green]aip agents run {agent.id} "Your message here"[/green]\n\n'
252
- f"📋 Or use the agent name:\n"
253
- f' [green]aip agents run "{agent.name}" "Your message here"[/green]\n\n'
254
- f"🔧 Available options:\n"
255
- f" [dim]--chat-history[/dim] Include previous conversation\n"
256
- f" [dim]--file[/dim] Attach files\n"
257
- f" [dim]--input[/dim] Alternative input method\n"
258
- f" [dim]--timeout[/dim] Set execution timeout\n"
259
- f" [dim]--save[/dim] Save transcript to file\n"
260
- f" [dim]--verbose[/dim] Show detailed execution\n\n"
261
- f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]",
262
- title="🤖 Ready to Run Agent",
351
+ "".join(content_parts),
352
+ title=f"{ICON_AGENT} Ready to Run Agent",
263
353
  border_style="blue",
264
354
  padding=(0, 1),
265
355
  )
glaip_sdk/cli/hints.py ADDED
@@ -0,0 +1,57 @@
1
+ """Helpers for formatting CLI/slash command hints.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import click
10
+
11
+ from glaip_sdk.branding import HINT_COMMAND_STYLE, HINT_DESCRIPTION_COLOR
12
+
13
+
14
+ def in_slash_mode(ctx: click.Context | None = None) -> bool:
15
+ """Return True when running inside the slash command palette."""
16
+ if ctx is None:
17
+ try:
18
+ ctx = click.get_current_context(silent=True)
19
+ except RuntimeError:
20
+ ctx = None
21
+
22
+ if ctx is None:
23
+ return False
24
+
25
+ obj = getattr(ctx, "obj", None)
26
+ if isinstance(obj, dict):
27
+ return bool(obj.get("_slash_session"))
28
+
29
+ return bool(getattr(obj, "_slash_session", False))
30
+
31
+
32
+ def command_hint(
33
+ cli_command: str | None,
34
+ slash_command: str | None = None,
35
+ *,
36
+ ctx: click.Context | None = None,
37
+ ) -> str | None:
38
+ """Return the appropriate command string for the current mode."""
39
+ if in_slash_mode(ctx):
40
+ if not slash_command:
41
+ return None
42
+ return slash_command if slash_command.startswith("/") else f"/{slash_command}"
43
+
44
+ if not cli_command:
45
+ return None
46
+ return f"aip {cli_command}"
47
+
48
+
49
+ def format_command_hint(command: str | None, description: str | None = None) -> str | None:
50
+ """Return a Rich markup string that highlights a command hint."""
51
+ if not command:
52
+ return None
53
+
54
+ highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
55
+ if description:
56
+ highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
57
+ return highlighted
glaip_sdk/cli/io.py CHANGED
@@ -7,21 +7,33 @@ Authors:
7
7
  Raymond Christopher (raymond.christopher@gdplabs.id)
8
8
  """
9
9
 
10
+ from importlib import import_module
10
11
  from pathlib import Path
11
- from typing import Any
12
+ from typing import TYPE_CHECKING, Any
12
13
 
13
14
  import click
14
15
 
16
+ from glaip_sdk.branding import WARNING_STYLE
15
17
  from glaip_sdk.utils.serialization import (
16
18
  collect_attributes_for_export,
17
19
  load_resource_from_file,
18
20
  write_resource_export,
19
21
  )
20
22
 
23
+ if TYPE_CHECKING: # pragma: no cover - typing-only imports
24
+ from rich.console import Console
25
+
26
+
27
+ def _create_console() -> "Console":
28
+ """Return a Console instance (lazy import for easier testing)."""
29
+ try:
30
+ console_module = import_module("rich.console")
31
+ except ImportError as exc: # pragma: no cover - optional dependency missing
32
+ raise RuntimeError("Rich Console is not available") from exc
33
+ return console_module.Console()
21
34
 
22
- def load_resource_from_file_with_validation(
23
- file_path: Path, resource_type: str
24
- ) -> dict[str, Any]:
35
+
36
+ def load_resource_from_file_with_validation(file_path: Path, resource_type: str) -> dict[str, Any]:
25
37
  """Load resource data from JSON or YAML file with CLI-friendly error handling.
26
38
 
27
39
  Args:
@@ -36,17 +48,15 @@ def load_resource_from_file_with_validation(
36
48
  """
37
49
  try:
38
50
  return load_resource_from_file(file_path)
39
- except FileNotFoundError:
40
- raise click.ClickException(f"File not found: {file_path}")
51
+ except FileNotFoundError as err:
52
+ raise click.ClickException(f"File not found: {file_path}") from err
41
53
  except ValueError as e:
42
- raise click.ClickException(f"Invalid {resource_type.lower()} file format: {e}")
54
+ raise click.ClickException(f"Invalid {resource_type.lower()} file format: {e}") from e
43
55
  except Exception as e:
44
- raise click.ClickException(f"Failed to load {resource_type.lower()} file: {e}")
56
+ raise click.ClickException(f"Failed to load {resource_type.lower()} file: {e}") from e
45
57
 
46
58
 
47
- def export_resource_to_file_with_validation(
48
- resource: Any, file_path: Path, format: str = "json"
49
- ) -> None:
59
+ def export_resource_to_file_with_validation(resource: Any, file_path: Path, format: str = "json") -> None:
50
60
  """Export resource to file with CLI-friendly error handling.
51
61
 
52
62
  Args:
@@ -62,7 +72,7 @@ def export_resource_to_file_with_validation(
62
72
  export_data = collect_attributes_for_export(resource)
63
73
  write_resource_export(file_path, export_data, format)
64
74
  except Exception as e:
65
- raise click.ClickException(f"Failed to export resource: {e}")
75
+ raise click.ClickException(f"Failed to export resource: {e}") from e
66
76
 
67
77
 
68
78
  def fetch_raw_resource_details(client: Any, resource: Any, resource_type: str) -> Any:
@@ -79,9 +89,7 @@ def fetch_raw_resource_details(client: Any, resource: Any, resource_type: str) -
79
89
  Notes:
80
90
  This is CLI-specific functionality for displaying comprehensive resource details.
81
91
  """
82
- from rich.console import Console
83
-
84
- console = Console()
92
+ console = _create_console()
85
93
 
86
94
  try:
87
95
  resource_id = str(getattr(resource, "id", "")).strip()
@@ -98,9 +106,7 @@ def fetch_raw_resource_details(client: Any, resource: Any, resource_type: str) -
98
106
  # Direct response
99
107
  return raw_response
100
108
  except Exception as e:
101
- console.print(
102
- f"[yellow]Failed to fetch raw {resource_type} details: {e}[/yellow]"
103
- )
109
+ console.print(f"[{WARNING_STYLE}]Failed to fetch raw {resource_type} details: {e}[/]")
104
110
  # Fall back to regular method
105
111
  return None
106
112
  return None