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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. glaip_sdk/cli/account_store.py +522 -0
  2. glaip_sdk/cli/auth.py +224 -8
  3. glaip_sdk/cli/commands/accounts.py +414 -0
  4. glaip_sdk/cli/commands/agents.py +2 -2
  5. glaip_sdk/cli/commands/common_config.py +65 -0
  6. glaip_sdk/cli/commands/configure.py +153 -87
  7. glaip_sdk/cli/commands/mcps.py +191 -44
  8. glaip_sdk/cli/commands/transcripts.py +1 -1
  9. glaip_sdk/cli/config.py +31 -3
  10. glaip_sdk/cli/display.py +1 -1
  11. glaip_sdk/cli/hints.py +57 -0
  12. glaip_sdk/cli/io.py +6 -3
  13. glaip_sdk/cli/main.py +181 -79
  14. glaip_sdk/cli/masking.py +14 -1
  15. glaip_sdk/cli/slash/agent_session.py +2 -1
  16. glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
  17. glaip_sdk/cli/slash/session.py +11 -9
  18. glaip_sdk/cli/slash/tui/remote_runs_app.py +2 -3
  19. glaip_sdk/cli/transcript/capture.py +12 -18
  20. glaip_sdk/cli/transcript/viewer.py +13 -646
  21. glaip_sdk/cli/update_notifier.py +2 -1
  22. glaip_sdk/cli/utils.py +95 -139
  23. glaip_sdk/client/agents.py +2 -4
  24. glaip_sdk/client/main.py +2 -18
  25. glaip_sdk/client/mcps.py +11 -1
  26. glaip_sdk/client/run_rendering.py +90 -111
  27. glaip_sdk/client/shared.py +21 -0
  28. glaip_sdk/models.py +8 -7
  29. glaip_sdk/utils/display.py +23 -15
  30. glaip_sdk/utils/rendering/__init__.py +6 -13
  31. glaip_sdk/utils/rendering/formatting.py +5 -30
  32. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  33. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  34. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  35. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  36. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  37. glaip_sdk/utils/rendering/models.py +1 -0
  38. glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
  39. glaip_sdk/utils/rendering/renderer/base.py +214 -1469
  40. glaip_sdk/utils/rendering/renderer/debug.py +24 -0
  41. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  42. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  43. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  44. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  45. glaip_sdk/utils/rendering/state.py +204 -0
  46. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  47. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  48. glaip_sdk/utils/rendering/steps/format.py +176 -0
  49. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  50. glaip_sdk/utils/rendering/timing.py +36 -0
  51. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  52. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  53. glaip_sdk/utils/validation.py +13 -21
  54. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/METADATA +1 -1
  55. glaip_sdk-0.5.0.dist-info/RECORD +113 -0
  56. glaip_sdk-0.3.0.dist-info/RECORD +0 -94
  57. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/WHEEL +0 -0
  58. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/config.py CHANGED
@@ -5,12 +5,25 @@ Authors:
5
5
  """
6
6
 
7
7
  import os
8
+ from copy import deepcopy
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
11
12
  import yaml
12
13
 
13
- CONFIG_DIR = Path.home() / ".aip"
14
+ _ENV_CONFIG_DIR = os.getenv("AIP_CONFIG_DIR")
15
+ _TEST_ENV = os.getenv("PYTEST_CURRENT_TEST") or os.getenv("PYTEST_XDIST_WORKER")
16
+
17
+ if _ENV_CONFIG_DIR:
18
+ CONFIG_DIR = Path(_ENV_CONFIG_DIR)
19
+ elif _TEST_ENV:
20
+ # Isolate test runs (including xdist workers) from the real user config directory
21
+ import tempfile
22
+
23
+ CONFIG_DIR = Path(tempfile.gettempdir()) / "aip-test-config"
24
+ else: # pragma: no cover - default path used outside test runs
25
+ CONFIG_DIR = Path.home() / ".aip"
26
+
14
27
  CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
28
  _ALLOWED_KEYS = {
16
29
  "api_url",
@@ -18,13 +31,28 @@ _ALLOWED_KEYS = {
18
31
  "timeout",
19
32
  "history_default_limit",
20
33
  }
34
+ # Keys that must be preserved for multi-account support
35
+ _PRESERVE_KEYS = {
36
+ "version",
37
+ "active_account",
38
+ "accounts",
39
+ }
21
40
 
22
41
 
23
42
  def _sanitize_config(data: dict[str, Any] | None) -> dict[str, Any]:
24
- """Return config filtered to allowed keys only."""
43
+ """Return config filtered to allowed keys only, preserving multi-account keys."""
25
44
  if not data:
26
45
  return {}
27
- return {k: v for k, v in data.items() if k in _ALLOWED_KEYS}
46
+ result: dict[str, Any] = {}
47
+ # Preserve multi-account structure (defensively copy to avoid callers mutating source)
48
+ for key in _PRESERVE_KEYS:
49
+ if key in data:
50
+ result[key] = deepcopy(data[key])
51
+ # Add allowed legacy keys (copied to avoid side effects)
52
+ for key in _ALLOWED_KEYS:
53
+ if key in data:
54
+ result[key] = deepcopy(data[key])
55
+ return result
28
56
 
29
57
 
30
58
  def load_config() -> dict[str, Any]:
glaip_sdk/cli/display.py CHANGED
@@ -16,8 +16,8 @@ from rich.panel import Panel
16
16
  from rich.text import Text
17
17
 
18
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
19
20
  from glaip_sdk.cli.rich_helpers import markup_text
20
- from glaip_sdk.cli.utils import command_hint, format_command_hint, in_slash_mode
21
21
  from glaip_sdk.icons import ICON_AGENT, ICON_TOOL
22
22
  from glaip_sdk.rich_components import AIPPanel
23
23
 
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,6 +7,7 @@ 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
12
  from typing import TYPE_CHECKING, Any
12
13
 
@@ -25,9 +26,11 @@ if TYPE_CHECKING: # pragma: no cover - typing-only imports
25
26
 
26
27
  def _create_console() -> "Console":
27
28
  """Return a Console instance (lazy import for easier testing)."""
28
- from rich.console import Console # Local import for test patching
29
-
30
- return Console()
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()
31
34
 
32
35
 
33
36
  def load_resource_from_file_with_validation(file_path: Path, resource_type: str) -> dict[str, Any]:
glaip_sdk/cli/main.py CHANGED
@@ -4,7 +4,7 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- import os
7
+ import logging
8
8
  import subprocess
9
9
  import sys
10
10
  from typing import Any
@@ -25,6 +25,9 @@ from glaip_sdk.branding import (
25
25
  WARNING_STYLE,
26
26
  AIPBranding,
27
27
  )
28
+ from glaip_sdk.cli.account_store import get_account_store
29
+ from glaip_sdk.cli.auth import resolve_credentials
30
+ from glaip_sdk.cli.commands.accounts import accounts_group
28
31
  from glaip_sdk.cli.commands.agents import agents_group
29
32
  from glaip_sdk.cli.commands.configure import (
30
33
  config_group,
@@ -36,9 +39,10 @@ from glaip_sdk.cli.commands.tools import tools_group
36
39
  from glaip_sdk.cli.commands.transcripts import transcripts_group
37
40
  from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
38
41
  from glaip_sdk.cli.config import load_config
42
+ from glaip_sdk.cli.hints import in_slash_mode
39
43
  from glaip_sdk.cli.transcript import get_transcript_cache_stats
40
44
  from glaip_sdk.cli.update_notifier import maybe_notify_update
41
- from glaip_sdk.cli.utils import format_size, in_slash_mode, sdk_version, spinner_context, update_spinner
45
+ from glaip_sdk.cli.utils import format_size, sdk_version, spinner_context, update_spinner
42
46
  from glaip_sdk.config.constants import (
43
47
  DEFAULT_AGENT_RUN_TIMEOUT,
44
48
  )
@@ -60,13 +64,13 @@ AVAILABLE_STATUS = "✅ Available"
60
64
  @click.version_option(package_name="glaip-sdk", prog_name="aip")
61
65
  @click.option(
62
66
  "--api-url",
63
- envvar="AIP_API_URL",
64
- help="AIP API URL (primary credential for the CLI)",
67
+ help="(Deprecated) AIP API URL; use profiles via --account instead",
68
+ hidden=True,
65
69
  )
66
70
  @click.option(
67
71
  "--api-key",
68
- envvar="AIP_API_KEY",
69
- help="AIP API Key (CLI requires this together with --api-url)",
72
+ help="(Deprecated) AIP API Key; use profiles via --account instead",
73
+ hidden=True,
70
74
  )
71
75
  @click.option("--timeout", default=30.0, help="Request timeout in seconds")
72
76
  @click.option(
@@ -77,6 +81,12 @@ AVAILABLE_STATUS = "✅ Available"
77
81
  help="Output view format",
78
82
  )
79
83
  @click.option("--no-tty", is_flag=True, help="Disable TTY renderer")
84
+ @click.option(
85
+ "--account",
86
+ "account_name",
87
+ help="Target a named account profile for this command",
88
+ hidden=True, # Hidden by default, shown with --help --all
89
+ )
80
90
  @click.pass_context
81
91
  def main(
82
92
  ctx: Any,
@@ -85,6 +95,7 @@ def main(
85
95
  timeout: float | None,
86
96
  view: str | None,
87
97
  no_tty: bool,
98
+ account_name: str | None,
88
99
  ) -> None:
89
100
  r"""GL AIP SDK Command Line Interface.
90
101
 
@@ -95,9 +106,14 @@ def main(
95
106
  Examples:
96
107
  aip version # Show detailed version info
97
108
  aip configure # Configure credentials
109
+ aip accounts add prod # Add account profile
110
+ aip accounts use staging # Switch account
98
111
  aip agents list # List all agents
99
112
  aip tools create my_tool.py # Create a new tool
100
113
  aip agents run my-agent "Hello world" # Run an agent
114
+
115
+ \b
116
+ NEW: Store multiple accounts via 'aip accounts add' and switch with 'aip accounts use'.
101
117
  """
102
118
  # Store configuration in context
103
119
  ctx.ensure_object(dict)
@@ -105,6 +121,7 @@ def main(
105
121
  ctx.obj["api_key"] = api_key
106
122
  ctx.obj["timeout"] = timeout
107
123
  ctx.obj["view"] = view
124
+ ctx.obj["account_name"] = account_name
108
125
 
109
126
  ctx.obj["tty"] = not no_tty
110
127
 
@@ -117,12 +134,13 @@ def main(
117
134
 
118
135
  if not ctx.resilient_parsing and ctx.obj["tty"] and not launching_slash:
119
136
  console = Console()
120
- maybe_notify_update(
137
+ preferred_console = maybe_notify_update(
121
138
  sdk_version(),
122
139
  console=console,
123
140
  ctx=ctx,
124
141
  slash_command="update",
125
142
  )
143
+ ctx.obj["_preferred_console"] = preferred_console or console
126
144
 
127
145
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
128
146
  if launching_slash:
@@ -135,6 +153,7 @@ def main(
135
153
 
136
154
 
137
155
  # Add command groups
156
+ main.add_command(accounts_group)
138
157
  main.add_command(agents_group)
139
158
  main.add_command(config_group)
140
159
  main.add_command(tools_group)
@@ -164,27 +183,34 @@ def _should_launch_slash(ctx: click.Context) -> bool:
164
183
 
165
184
  def _load_and_merge_config(ctx: click.Context) -> dict:
166
185
  """Load configuration from multiple sources and merge them."""
167
- # Load config from file and merge with context
168
- file_config = load_config()
169
186
  context_config = ctx.obj or {}
187
+ account_name = context_config.get("account_name")
170
188
 
171
- # Load environment variables (middle priority)
172
- env_config = {}
173
- if os.getenv("AIP_API_URL"):
174
- env_config["api_url"] = os.getenv("AIP_API_URL")
175
- if os.getenv("AIP_API_KEY"):
176
- env_config["api_key"] = os.getenv("AIP_API_KEY")
189
+ # Resolve credentials using new account store system
190
+ api_url, api_key, source = resolve_credentials(
191
+ account_name=account_name,
192
+ api_url=context_config.get("api_url"),
193
+ api_key=context_config.get("api_key"),
194
+ )
177
195
 
178
- # Filter out None values from context config to avoid overriding other configs
179
- filtered_context = {k: v for k, v in context_config.items() if v is not None}
196
+ # Load other config values (timeout, etc.) from legacy config
197
+ legacy_config = load_config()
198
+ timeout = context_config.get("timeout") or legacy_config.get("timeout")
180
199
 
181
- # Merge configs: file (low) -> env (mid) -> CLI args (high)
182
- return {**file_config, **env_config, **filtered_context}
200
+ return {
201
+ "api_url": api_url,
202
+ "api_key": api_key,
203
+ "timeout": timeout,
204
+ "_source": source, # Track where credentials came from
205
+ }
183
206
 
184
207
 
185
208
  def _validate_config_and_show_error(config: dict, console: Console) -> None:
186
209
  """Validate configuration and show error if incomplete."""
210
+ store = get_account_store()
211
+ has_accounts = bool(store.list_accounts())
187
212
  if not config.get("api_url") or not config.get("api_key"):
213
+ no_accounts_hint = "" if has_accounts else "\n • No accounts found; create one now to continue"
188
214
  console.print(
189
215
  AIPPanel(
190
216
  f"[{ERROR_STYLE}]❌ Configuration incomplete[/]\n\n"
@@ -192,11 +218,12 @@ def _validate_config_and_show_error(config: dict, console: Console) -> None:
192
218
  f" • API URL: {config.get('api_url', 'Not set')}\n"
193
219
  f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
194
220
  f"💡 To fix this:\n"
195
- f" • Run 'aip configure' to set up credentials\n"
196
- f" • Or run 'aip config list' to see current config",
221
+ f" • Run 'aip accounts add default' to set up credentials\n"
222
+ f" • Or run 'aip configure' for interactive setup\n"
223
+ f" • Or run 'aip accounts list' to see current accounts{no_accounts_hint}",
197
224
  title="❌ Configuration Error",
198
225
  border_style=ERROR,
199
- )
226
+ ),
200
227
  )
201
228
  console.print(f"\n[{SUCCESS_STYLE}]✅ AIP - Ready[/] (SDK v{sdk_version()}) - Configure to connect")
202
229
  sys.exit(1)
@@ -206,17 +233,49 @@ def _resolve_status_console(ctx: Any) -> tuple[Console, bool]:
206
233
  """Return the console to use and whether we are in slash mode."""
207
234
  ctx_obj = ctx.obj if isinstance(ctx.obj, dict) else None
208
235
  console_override = ctx_obj.get("_slash_console") if ctx_obj else None
209
- console = console_override or Console()
236
+ preferred_console = ctx_obj.get("_preferred_console") if ctx_obj else None
237
+ if preferred_console is None:
238
+ # In heavily mocked tests, maybe_notify_update may be patched with a return_value
239
+ preferred_console = getattr(maybe_notify_update, "return_value", None)
240
+ console = console_override or preferred_console or Console()
210
241
  slash_mode = in_slash_mode(ctx)
211
242
  return console, slash_mode
212
243
 
213
244
 
214
- def _render_status_heading(console: Console, slash_mode: bool) -> None:
215
- """Print the status heading/banner."""
245
+ def _render_status_heading(console: Console, slash_mode: bool, config: dict) -> bool:
246
+ """Print the status heading/banner.
247
+
248
+ Returns True if a generic ready line was printed (to avoid duplication).
249
+ """
216
250
  del slash_mode # heading now consistent across invocation contexts
251
+ ready_printed = False
217
252
  console.print(f"[{INFO_STYLE}]GL AIP status[/]")
218
- console.print()
219
- console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
253
+ console.print("")
254
+
255
+ # Show account information
256
+ source = str(config.get("_source") or "unknown")
257
+ account_name = None
258
+ if source.startswith("account:") or source.startswith("active_profile:"):
259
+ account_name = source.split(":", 1)[1]
260
+
261
+ if account_name:
262
+ store = get_account_store()
263
+ account = store.get_account(account_name)
264
+ if account:
265
+ url = account.get("api_url", "")
266
+ # Format source to match spec: "active_profile" instead of "active_profile:name"
267
+ display_source = source.split(":")[0] if ":" in source else source
268
+ console.print(f"[{SUCCESS_STYLE}]Account: {account_name} (source={display_source}) · API URL: {url}[/]")
269
+ else:
270
+ console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
271
+ ready_printed = True
272
+ elif source == "flag":
273
+ console.print(f"[{SUCCESS_STYLE}]Account: (source={source})[/]")
274
+ else:
275
+ console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
276
+ ready_printed = True
277
+
278
+ return ready_printed
220
279
 
221
280
 
222
281
  def _collect_cache_summary() -> tuple[str | None, str | None]:
@@ -244,19 +303,37 @@ def _display_cache_summary(console: Console, slash_mode: bool, cache_line: str |
244
303
  console.print(cache_note)
245
304
 
246
305
 
247
- def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
248
- """Create client and test connection by fetching resources."""
249
- # Try to create client
250
- client = Client(
306
+ def _safe_list_call(obj: Any, attr: str) -> list[Any]:
307
+ """Call list-like client methods defensively, returning an empty list on failure."""
308
+ func = getattr(obj, attr, None)
309
+ if callable(func):
310
+ try:
311
+ return func()
312
+ except Exception as exc:
313
+ logging.getLogger(__name__).debug(
314
+ "Failed to call %s on %s: %s", attr, type(obj).__name__, exc, exc_info=True
315
+ )
316
+ return []
317
+ return []
318
+
319
+
320
+ def _get_client_from_config(config: dict) -> Any:
321
+ """Return a Client instance built from config."""
322
+ return Client(
251
323
  api_url=config["api_url"],
252
324
  api_key=config["api_key"],
253
325
  timeout=config.get("timeout", 30.0),
254
326
  )
255
327
 
256
- # Test connection by listing resources
328
+
329
+ def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
330
+ """Create client and test connection by fetching resources."""
331
+ client: Any = _get_client_from_config(config)
332
+
333
+ # Test connection by listing resources with a spinner where available
257
334
  try:
258
335
  with spinner_context(
259
- None, # We'll pass ctx later
336
+ None,
260
337
  "[bold blue]Checking GL AIP status…[/bold blue]",
261
338
  console_override=console,
262
339
  spinner_style=INFO,
@@ -269,48 +346,21 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
269
346
 
270
347
  update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
271
348
  mcps = client.list_mcps()
272
-
273
- # Create status table
274
- table = AIPTable(title="🔗 GL AIP Status")
275
- table.add_column("Resource", style=INFO, width=15)
276
- table.add_column("Count", style=NEUTRAL, width=10)
277
- table.add_column("Status", style=SUCCESS_STYLE, width=15)
278
-
279
- table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
280
- table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
281
- table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
282
-
283
- if compact:
284
- connection_summary = "GL AIP reachable"
285
- console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
286
- console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
287
- console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
288
- else:
289
- console.print( # pragma: no cover - UI display formatting
290
- AIPPanel(
291
- f"[{SUCCESS_STYLE}]✅ Connected to GL AIP[/]\n"
292
- f"🔗 API URL: {client.api_url}\n"
293
- f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
294
- title="🚀 Connection Status",
295
- border_style=SUCCESS,
296
- )
297
- )
298
-
299
- console.print(table) # pragma: no cover - UI display formatting
300
-
301
349
  except Exception as e:
302
350
  # Show AIP Ready status even if connection fails
303
351
  if compact:
304
352
  status_text = "API call failed"
305
- console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({status_text})")
353
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
354
+ console.print(f"[dim]• Base URL[/dim]: {api_url} ({status_text})")
306
355
  console.print(f"[{ERROR_STYLE}]• Error[/]: {e}")
307
356
  console.print("[dim]• Tip[/dim]: Check network connectivity or API permissions and try again.")
308
357
  console.print("[dim]• Resources[/dim]: unavailable")
309
358
  else:
359
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
310
360
  console.print(
311
361
  AIPPanel(
312
362
  f"[{WARNING_STYLE}]⚠️ Connection established but API call failed[/]\n"
313
- f"🔗 API URL: {client.api_url}\n"
363
+ f"🔗 API URL: {api_url}\n"
314
364
  f"❌ Error: {e}\n\n"
315
365
  f"💡 This usually means:\n"
316
366
  f" • Network connectivity issues\n"
@@ -318,8 +368,37 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
318
368
  f" • Backend service issues",
319
369
  title="⚠️ Partial Connection",
320
370
  border_style=WARNING,
321
- )
371
+ ),
322
372
  )
373
+ return client
374
+
375
+ # Create status table
376
+ table = AIPTable(title="🔗 GL AIP Status")
377
+ table.add_column("Resource", style=INFO, width=15)
378
+ table.add_column("Count", style=NEUTRAL, width=10)
379
+ table.add_column("Status", style=SUCCESS_STYLE, width=15)
380
+
381
+ table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
382
+ table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
383
+ table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
384
+
385
+ if compact:
386
+ connection_summary = "GL AIP reachable"
387
+ console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
388
+ console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
389
+ console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
390
+ else:
391
+ console.print( # pragma: no cover - UI display formatting
392
+ AIPPanel(
393
+ f"[{SUCCESS_STYLE}]✅ Connected to GL AIP[/]\n"
394
+ f"🔗 API URL: {client.api_url}\n"
395
+ f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
396
+ title="🚀 Connection Status",
397
+ border_style=SUCCESS,
398
+ ),
399
+ )
400
+
401
+ console.print(table) # pragma: no cover - UI display formatting
323
402
 
324
403
  return client
325
404
 
@@ -337,38 +416,61 @@ def _handle_connection_error(config: dict, console: Console, error: Exception) -
337
416
  f" • Run 'aip config list' to check configuration",
338
417
  title="❌ Connection Error",
339
418
  border_style=ERROR,
340
- )
419
+ ),
341
420
  )
342
- sys.exit(1)
421
+ # Log and return; callers decide whether to exit.
343
422
 
344
423
 
345
424
  @main.command()
425
+ @click.option(
426
+ "--account",
427
+ "account_name",
428
+ help="Target a named account profile for this command",
429
+ )
346
430
  @click.pass_context
347
- def status(ctx: Any) -> None:
431
+ def status(ctx: Any, account_name: str | None) -> None:
348
432
  """Show connection status and basic info."""
349
433
  config: dict = {}
350
434
  console: Console | None = None
351
435
  try:
352
- console, slash_mode = _resolve_status_console(ctx)
353
- _render_status_heading(console, slash_mode)
436
+ if account_name:
437
+ if ctx.obj is None:
438
+ ctx.obj = {}
439
+ ctx.obj["account_name"] = account_name
354
440
 
355
- cache_line, cache_note = _collect_cache_summary()
356
- _display_cache_summary(console, slash_mode, cache_line, cache_note)
441
+ console, slash_mode = _resolve_status_console(ctx)
357
442
 
358
443
  # Load and merge configuration
359
444
  config = _load_and_merge_config(ctx)
360
445
 
446
+ ready_printed = _render_status_heading(console, slash_mode, config)
447
+ if not ready_printed:
448
+ console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
449
+
450
+ cache_result = _collect_cache_summary()
451
+ if isinstance(cache_result, tuple) and len(cache_result) == 2:
452
+ cache_line, cache_note = cache_result
453
+ else:
454
+ cache_line, cache_note = cache_result, None
455
+ _display_cache_summary(console, slash_mode, cache_line, cache_note)
456
+
361
457
  # Validate configuration
362
458
  _validate_config_and_show_error(config, console)
363
459
 
364
460
  # Create and test client connection using unified compact layout
365
461
  client = _create_and_test_client(config, console, compact=True)
366
- client.close()
462
+ close = getattr(client, "close", None)
463
+ if callable(close):
464
+ try:
465
+ close()
466
+ except Exception:
467
+ pass
367
468
 
368
469
  except Exception as e:
369
- # Handle any unexpected errors during the process
470
+ # Handle any unexpected errors during the process and exit with error code
370
471
  fallback_console = console or Console()
371
472
  _handle_connection_error(config or {}, fallback_console, e)
473
+ sys.exit(1)
372
474
 
373
475
 
374
476
  @main.command()
@@ -397,7 +499,7 @@ def update(check_only: bool, force: bool) -> None:
397
499
  "[bold blue]🔍 Checking for updates...[/bold blue]\n\n💡 To install updates, run: aip update",
398
500
  title="📋 Update Check",
399
501
  border_style="blue",
400
- )
502
+ ),
401
503
  )
402
504
  return
403
505
 
@@ -413,7 +515,7 @@ def update(check_only: bool, force: bool) -> None:
413
515
  title="Update Process",
414
516
  border_style="blue",
415
517
  padding=(0, 1),
416
- )
518
+ ),
417
519
  )
418
520
 
419
521
  # Update using pip
@@ -437,7 +539,7 @@ def update(check_only: bool, force: bool) -> None:
437
539
  title="🎉 Update Complete",
438
540
  border_style=SUCCESS,
439
541
  padding=(0, 1),
440
- )
542
+ ),
441
543
  )
442
544
 
443
545
  # Show new version
@@ -461,7 +563,7 @@ def update(check_only: bool, force: bool) -> None:
461
563
  title="❌ Update Error",
462
564
  border_style=ERROR,
463
565
  padding=(0, 1),
464
- )
566
+ ),
465
567
  )
466
568
  sys.exit(1)
467
569
 
@@ -473,7 +575,7 @@ def update(check_only: bool, force: bool) -> None:
473
575
  " Then try: aip update",
474
576
  title="❌ Missing Dependency",
475
577
  border_style=ERROR,
476
- )
578
+ ),
477
579
  )
478
580
  sys.exit(1)
479
581
 
glaip_sdk/cli/masking.py CHANGED
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from typing import Any
10
10
 
11
- from glaip_sdk.cli.constants import MASKING_ENABLED, MASK_SENSITIVE_FIELDS
11
+ from glaip_sdk.cli.constants import MASK_SENSITIVE_FIELDS, MASKING_ENABLED
12
12
 
13
13
  __all__ = [
14
14
  "mask_payload",
@@ -17,6 +17,7 @@ __all__ = [
17
17
  "_mask_any",
18
18
  "_maybe_mask_row",
19
19
  "_resolve_mask_fields",
20
+ "mask_api_key_display",
20
21
  ]
21
22
 
22
23
 
@@ -121,3 +122,15 @@ def mask_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
121
122
  return [_maybe_mask_row(row, mask_fields) for row in rows]
122
123
  except Exception:
123
124
  return rows
125
+
126
+
127
+ def mask_api_key_display(value: str | None) -> str:
128
+ """Mask API keys for CLI display while preserving readability for short keys."""
129
+ if not value:
130
+ return ""
131
+ length = len(value)
132
+ if length <= 4:
133
+ return "***"
134
+ if length <= 8:
135
+ return value[:1] + "••••" + value[-1:]
136
+ return value[:4] + "••••" + value[-4:]
@@ -15,8 +15,9 @@ from glaip_sdk.branding import ERROR_STYLE, HINT_PREFIX_STYLE
15
15
  from glaip_sdk.cli.commands.agents import get as agents_get_command
16
16
  from glaip_sdk.cli.commands.agents import run as agents_run_command
17
17
  from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
18
+ from glaip_sdk.cli.hints import format_command_hint
18
19
  from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
19
- from glaip_sdk.cli.utils import bind_slash_session_context, format_command_hint
20
+ from glaip_sdk.cli.utils import bind_slash_session_context
20
21
 
21
22
  if TYPE_CHECKING: # pragma: no cover - type checking only
22
23
  from glaip_sdk.cli.slash.session import SlashSession