glaip-sdk 0.0.15__py3-none-any.whl → 0.0.16__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 (40) hide show
  1. glaip_sdk/branding.py +27 -1
  2. glaip_sdk/cli/commands/agents.py +26 -17
  3. glaip_sdk/cli/commands/configure.py +39 -50
  4. glaip_sdk/cli/commands/mcps.py +1 -3
  5. glaip_sdk/cli/config.py +42 -0
  6. glaip_sdk/cli/display.py +92 -26
  7. glaip_sdk/cli/main.py +141 -124
  8. glaip_sdk/cli/mcp_validators.py +2 -2
  9. glaip_sdk/cli/pager.py +3 -2
  10. glaip_sdk/cli/parsers/json_input.py +2 -2
  11. glaip_sdk/cli/resolution.py +12 -10
  12. glaip_sdk/cli/slash/agent_session.py +7 -0
  13. glaip_sdk/cli/slash/prompt.py +21 -2
  14. glaip_sdk/cli/slash/session.py +15 -21
  15. glaip_sdk/cli/update_notifier.py +8 -2
  16. glaip_sdk/cli/utils.py +110 -53
  17. glaip_sdk/client/_agent_payloads.py +504 -0
  18. glaip_sdk/client/agents.py +194 -551
  19. glaip_sdk/client/base.py +92 -20
  20. glaip_sdk/client/main.py +6 -0
  21. glaip_sdk/client/run_rendering.py +275 -0
  22. glaip_sdk/config/constants.py +3 -0
  23. glaip_sdk/exceptions.py +15 -0
  24. glaip_sdk/models.py +5 -0
  25. glaip_sdk/payload_schemas/__init__.py +19 -0
  26. glaip_sdk/payload_schemas/agent.py +87 -0
  27. glaip_sdk/rich_components.py +12 -0
  28. glaip_sdk/utils/client_utils.py +12 -0
  29. glaip_sdk/utils/import_export.py +2 -2
  30. glaip_sdk/utils/rendering/formatting.py +5 -0
  31. glaip_sdk/utils/rendering/models.py +22 -0
  32. glaip_sdk/utils/rendering/renderer/base.py +9 -1
  33. glaip_sdk/utils/rendering/renderer/panels.py +0 -1
  34. glaip_sdk/utils/rendering/steps.py +59 -0
  35. glaip_sdk/utils/serialization.py +24 -3
  36. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/METADATA +1 -1
  37. glaip_sdk-0.0.16.dist-info/RECORD +72 -0
  38. glaip_sdk-0.0.15.dist-info/RECORD +0 -67
  39. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/WHEEL +0 -0
  40. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/main.py CHANGED
@@ -19,11 +19,11 @@ from glaip_sdk.cli.commands.agents import agents_group
19
19
  from glaip_sdk.cli.commands.configure import (
20
20
  config_group,
21
21
  configure_command,
22
- load_config,
23
22
  )
24
23
  from glaip_sdk.cli.commands.mcps import mcps_group
25
24
  from glaip_sdk.cli.commands.models import models_group
26
25
  from glaip_sdk.cli.commands.tools import tools_group
26
+ from glaip_sdk.cli.config import load_config
27
27
  from glaip_sdk.cli.update_notifier import maybe_notify_update
28
28
  from glaip_sdk.cli.utils import spinner_context, update_spinner
29
29
  from glaip_sdk.config.constants import (
@@ -73,7 +73,6 @@ def main(
73
73
  aip tools create my_tool.py # Create a new tool
74
74
  aip agents run my-agent "Hello world" # Run an agent
75
75
  """
76
-
77
76
  # Store configuration in context
78
77
  ctx.ensure_object(dict)
79
78
  ctx.obj["api_url"] = api_url
@@ -116,7 +115,6 @@ main.add_command(configure_command)
116
115
 
117
116
  def _should_launch_slash(ctx: click.Context) -> bool:
118
117
  """Determine whether to open the command palette automatically."""
119
-
120
118
  ctx_obj = ctx.obj or {}
121
119
  if not bool(ctx_obj.get("tty", True)):
122
120
  return False
@@ -127,6 +125,137 @@ def _should_launch_slash(ctx: click.Context) -> bool:
127
125
  return True
128
126
 
129
127
 
128
+ def _load_and_merge_config(ctx: click.Context) -> dict:
129
+ """Load configuration from multiple sources and merge them."""
130
+ # Load config from file and merge with context
131
+ file_config = load_config()
132
+ context_config = ctx.obj or {}
133
+
134
+ # Load environment variables (middle priority)
135
+ env_config = {}
136
+ if os.getenv("AIP_API_URL"):
137
+ env_config["api_url"] = os.getenv("AIP_API_URL")
138
+ if os.getenv("AIP_API_KEY"):
139
+ env_config["api_key"] = os.getenv("AIP_API_KEY")
140
+
141
+ # Filter out None values from context config to avoid overriding other configs
142
+ filtered_context = {k: v for k, v in context_config.items() if v is not None}
143
+
144
+ # Merge configs: file (low) -> env (mid) -> CLI args (high)
145
+ return {**file_config, **env_config, **filtered_context}
146
+
147
+
148
+ def _validate_config_and_show_error(config: dict, console: Console) -> None:
149
+ """Validate configuration and show error if incomplete."""
150
+ if not config.get("api_url") or not config.get("api_key"):
151
+ console.print(
152
+ AIPPanel(
153
+ "[bold red]❌ Configuration incomplete[/bold red]\n\n"
154
+ f"🔍 Current config:\n"
155
+ f" • API URL: {config.get('api_url', 'Not set')}\n"
156
+ f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
157
+ f"💡 To fix this:\n"
158
+ f" • Run 'aip configure' to set up credentials\n"
159
+ f" • Or run 'aip config list' to see current config",
160
+ title="❌ Configuration Error",
161
+ border_style="red",
162
+ )
163
+ )
164
+ console.print(
165
+ f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION}) - Configure to connect"
166
+ )
167
+ sys.exit(1)
168
+
169
+
170
+ def _create_and_test_client(config: dict, console: Console) -> Client:
171
+ """Create client and test connection by fetching resources."""
172
+ # Try to create client
173
+ client = Client(
174
+ api_url=config["api_url"],
175
+ api_key=config["api_key"],
176
+ timeout=config.get("timeout", 30.0),
177
+ )
178
+
179
+ # Test connection by listing resources
180
+ try:
181
+ with spinner_context(
182
+ None, # We'll pass ctx later
183
+ "[bold blue]Checking GL AIP status…[/bold blue]",
184
+ console_override=console,
185
+ spinner_style="cyan",
186
+ ) as status_indicator:
187
+ update_spinner(status_indicator, "[bold blue]Fetching agents…[/bold blue]")
188
+ agents = client.list_agents()
189
+
190
+ update_spinner(status_indicator, "[bold blue]Fetching tools…[/bold blue]")
191
+ tools = client.list_tools()
192
+
193
+ update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
194
+ mcps = client.list_mcps()
195
+
196
+ # Create status table
197
+ table = AIPTable(title="🔗 GL AIP Status")
198
+ table.add_column("Resource", style="cyan", width=15)
199
+ table.add_column("Count", style="green", width=10)
200
+ table.add_column("Status", style="green", width=15)
201
+
202
+ table.add_row("Agents", str(len(agents)), "✅ Available")
203
+ table.add_row("Tools", str(len(tools)), "✅ Available")
204
+ table.add_row("MCPs", str(len(mcps)), "✅ Available")
205
+
206
+ console.print(
207
+ AIPPanel(
208
+ f"[bold green]✅ Connected to GL AIP[/bold green]\n"
209
+ f"🔗 API URL: {client.api_url}\n"
210
+ f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
211
+ title="🚀 Connection Status",
212
+ border_style="green",
213
+ )
214
+ )
215
+
216
+ console.print(table)
217
+
218
+ except Exception as e:
219
+ # Show AIP Ready status even if connection fails
220
+ console.print(
221
+ f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
222
+ )
223
+
224
+ console.print(
225
+ AIPPanel(
226
+ f"[bold yellow]⚠️ Connection established but API call failed[/bold yellow]\n"
227
+ f"🔗 API URL: {client.api_url}\n"
228
+ f"❌ Error: {e}\n\n"
229
+ f"💡 This usually means:\n"
230
+ f" • Network connectivity issues\n"
231
+ f" • API permissions problems\n"
232
+ f" • Backend service issues",
233
+ title="⚠️ Partial Connection",
234
+ border_style="yellow",
235
+ )
236
+ )
237
+
238
+ return client
239
+
240
+
241
+ def _handle_connection_error(config: dict, console: Console, error: Exception) -> None:
242
+ """Handle connection errors and show troubleshooting information."""
243
+ console.print(
244
+ AIPPanel(
245
+ f"[bold red]❌ Connection failed[/bold red]\n\n"
246
+ f"🔍 Error: {error}\n\n"
247
+ f"💡 Troubleshooting steps:\n"
248
+ f" • Verify your API URL and key are correct\n"
249
+ f" • Check network connectivity to {config.get('api_url', 'your API')}\n"
250
+ f" • Run 'aip configure' to update credentials\n"
251
+ f" • Run 'aip config list' to check configuration",
252
+ title="❌ Connection Error",
253
+ border_style="red",
254
+ )
255
+ )
256
+ sys.exit(1)
257
+
258
+
130
259
  @main.command()
131
260
  @click.pass_context
132
261
  def status(ctx: Any) -> None:
@@ -146,132 +275,20 @@ def status(ctx: Any) -> None:
146
275
  f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
147
276
  )
148
277
 
149
- # Load config from file and merge with context
150
- file_config = load_config()
151
- context_config = ctx.obj or {}
152
-
153
- # Load environment variables (middle priority)
154
-
155
- env_config = {}
156
- if os.getenv("AIP_API_URL"):
157
- env_config["api_url"] = os.getenv("AIP_API_URL")
158
- if os.getenv("AIP_API_KEY"):
159
- env_config["api_key"] = os.getenv("AIP_API_KEY")
160
-
161
- # Filter out None values from context config to avoid overriding other configs
162
- filtered_context = {k: v for k, v in context_config.items() if v is not None}
163
-
164
- # Merge configs: file (low) -> env (mid) -> CLI args (high)
165
- config = {**file_config, **env_config, **filtered_context}
166
-
167
- if not config.get("api_url") or not config.get("api_key"):
168
- console.print(
169
- AIPPanel(
170
- "[bold red]❌ Configuration incomplete[/bold red]\n\n"
171
- f"🔍 Current config:\n"
172
- f" • API URL: {config.get('api_url', 'Not set')}\n"
173
- f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
174
- f"💡 To fix this:\n"
175
- f" • Run 'aip configure' to set up credentials\n"
176
- f" • Or run 'aip config list' to see current config",
177
- title="❌ Configuration Error",
178
- border_style="red",
179
- )
180
- )
181
- console.print(
182
- f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION}) - Configure to connect"
183
- )
184
- sys.exit(1)
185
-
186
- # Try to create client
187
- client = Client(
188
- api_url=config["api_url"],
189
- api_key=config["api_key"],
190
- timeout=config.get("timeout", 30.0),
191
- )
192
-
193
- # Test connection by listing resources
194
- try:
195
- with spinner_context(
196
- ctx,
197
- "[bold blue]Checking GL AIP status…[/bold blue]",
198
- console_override=console,
199
- spinner_style="cyan",
200
- ) as status_indicator:
201
- update_spinner(
202
- status_indicator, "[bold blue]Fetching agents…[/bold blue]"
203
- )
204
- agents = client.list_agents()
278
+ # Load and merge configuration
279
+ config = _load_and_merge_config(ctx)
205
280
 
206
- update_spinner(
207
- status_indicator, "[bold blue]Fetching tools…[/bold blue]"
208
- )
209
- tools = client.list_tools()
210
-
211
- update_spinner(
212
- status_indicator, "[bold blue]Fetching MCPs…[/bold blue]"
213
- )
214
- mcps = client.list_mcps()
215
-
216
- # Create status table
217
- table = AIPTable(title="🔗 GL AIP Status")
218
- table.add_column("Resource", style="cyan", width=15)
219
- table.add_column("Count", style="green", width=10)
220
- table.add_column("Status", style="green", width=15)
221
-
222
- table.add_row("Agents", str(len(agents)), "✅ Available")
223
- table.add_row("Tools", str(len(tools)), "✅ Available")
224
- table.add_row("MCPs", str(len(mcps)), "✅ Available")
225
-
226
- console.print(
227
- AIPPanel(
228
- f"[bold green]✅ Connected to GL AIP[/bold green]\n"
229
- f"🔗 API URL: {client.api_url}\n"
230
- f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
231
- title="🚀 Connection Status",
232
- border_style="green",
233
- )
234
- )
235
-
236
- console.print(table)
237
-
238
- except Exception as e:
239
- # Show AIP Ready status even if connection fails
240
- console.print(
241
- f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
242
- )
243
-
244
- console.print(
245
- AIPPanel(
246
- f"[bold yellow]⚠️ Connection established but API call failed[/bold yellow]\n"
247
- f"🔗 API URL: {client.api_url}\n"
248
- f"❌ Error: {e}\n\n"
249
- f"💡 This usually means:\n"
250
- f" • Network connectivity issues\n"
251
- f" • API permissions problems\n"
252
- f" • Backend service issues",
253
- title="⚠️ Partial Connection",
254
- border_style="yellow",
255
- )
256
- )
281
+ # Validate configuration
282
+ _validate_config_and_show_error(config, console)
257
283
 
284
+ # Create and test client connection
285
+ client = _create_and_test_client(config, console)
258
286
  client.close()
259
287
 
260
288
  except Exception as e:
261
- console.print(
262
- AIPPanel(
263
- f"[bold red]❌ Connection failed[/bold red]\n\n"
264
- f"🔍 Error: {e}\n\n"
265
- f"💡 Troubleshooting steps:\n"
266
- f" • Run 'aip config list' to check configuration\n"
267
- f" • Run 'aip configure' to update credentials\n"
268
- f" • Verify your API URL and key are correct\n"
269
- f" • Check network connectivity to {config.get('api_url', 'your API')}",
270
- title="❌ Connection Error",
271
- border_style="red",
272
- )
273
- )
274
- sys.exit(1)
289
+ # Handle any unexpected errors during the process
290
+ console = Console()
291
+ _handle_connection_error(config if "config" in locals() else {}, console, e)
275
292
 
276
293
 
277
294
  @main.command()
@@ -15,7 +15,7 @@ import click
15
15
 
16
16
 
17
17
  def format_validation_error(prefix: str, detail: str | None = None) -> str:
18
- """Format a validation error message with optional detail.
18
+ r"""Format a validation error message with optional detail.
19
19
 
20
20
  Args:
21
21
  prefix: Main error message
@@ -26,7 +26,7 @@ def format_validation_error(prefix: str, detail: str | None = None) -> str:
26
26
 
27
27
  Examples:
28
28
  >>> format_validation_error("Invalid config", "Missing 'url' field")
29
- "Invalid config\\nMissing 'url' field"
29
+ "Invalid config\nMissing 'url' field"
30
30
  """
31
31
  parts = [prefix]
32
32
  if detail:
glaip_sdk/cli/pager.py CHANGED
@@ -57,8 +57,9 @@ def _get_console() -> Console:
57
57
 
58
58
 
59
59
  def _prepare_pager_env(clear_on_exit: bool = True) -> None:
60
- """
61
- Configure LESS flags for a predictable, high-quality UX:
60
+ """Configure LESS flags for a predictable, high-quality UX.
61
+
62
+ Sets sensible defaults for the system pager:
62
63
  -R : pass ANSI color escapes
63
64
  -S : chop long lines (horizontal scroll with ←/→)
64
65
  (No -F, no -X) so we open a full-screen pager and clear on exit.
@@ -17,7 +17,7 @@ import click
17
17
  def _format_file_error(
18
18
  prefix: str, file_path_str: str, resolved_path: Path, *, detail: str | None = None
19
19
  ) -> str:
20
- """Format a file-related error message with path context.
20
+ r"""Format a file-related error message with path context.
21
21
 
22
22
  Args:
23
23
  prefix: Main error message
@@ -31,7 +31,7 @@ def _format_file_error(
31
31
  Examples:
32
32
  >>> from pathlib import Path
33
33
  >>> _format_file_error("File not found", "config.json", Path("/abs/config.json"))
34
- 'File not found: config.json\\nResolved path: /abs/config.json'
34
+ 'File not found: config.json\nResolved path: /abs/config.json'
35
35
  """
36
36
  parts = [f"{prefix}: {file_path_str}", f"Resolved path: {resolved_path}"]
37
37
  if detail:
@@ -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 = (
@@ -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