glaip-sdk 0.1.3__py3-none-any.whl → 0.6.19__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 (146) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1196 -0
  5. glaip_sdk/branding.py +13 -0
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/auth.py +254 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents.py +213 -73
  11. glaip_sdk/cli/commands/common_config.py +104 -0
  12. glaip_sdk/cli/commands/configure.py +729 -113
  13. glaip_sdk/cli/commands/mcps.py +241 -72
  14. glaip_sdk/cli/commands/models.py +11 -5
  15. glaip_sdk/cli/commands/tools.py +49 -57
  16. glaip_sdk/cli/commands/transcripts.py +755 -0
  17. glaip_sdk/cli/config.py +48 -4
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +8 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +851 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +35 -19
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +6 -3
  28. glaip_sdk/cli/main.py +241 -121
  29. glaip_sdk/cli/masking.py +21 -33
  30. glaip_sdk/cli/pager.py +9 -10
  31. glaip_sdk/cli/parsers/__init__.py +1 -3
  32. glaip_sdk/cli/slash/__init__.py +0 -9
  33. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  34. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  35. glaip_sdk/cli/slash/agent_session.py +62 -21
  36. glaip_sdk/cli/slash/prompt.py +21 -0
  37. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  38. glaip_sdk/cli/slash/session.py +771 -140
  39. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  40. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  41. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  42. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  43. glaip_sdk/cli/slash/tui/loading.py +58 -0
  44. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  45. glaip_sdk/cli/transcript/__init__.py +12 -52
  46. glaip_sdk/cli/transcript/cache.py +255 -44
  47. glaip_sdk/cli/transcript/capture.py +27 -1
  48. glaip_sdk/cli/transcript/history.py +815 -0
  49. glaip_sdk/cli/transcript/viewer.py +72 -499
  50. glaip_sdk/cli/update_notifier.py +14 -5
  51. glaip_sdk/cli/utils.py +243 -1252
  52. glaip_sdk/cli/validators.py +5 -6
  53. glaip_sdk/client/__init__.py +2 -1
  54. glaip_sdk/client/_agent_payloads.py +45 -9
  55. glaip_sdk/client/agent_runs.py +147 -0
  56. glaip_sdk/client/agents.py +291 -35
  57. glaip_sdk/client/base.py +1 -0
  58. glaip_sdk/client/main.py +19 -10
  59. glaip_sdk/client/mcps.py +122 -12
  60. glaip_sdk/client/run_rendering.py +466 -89
  61. glaip_sdk/client/shared.py +21 -0
  62. glaip_sdk/client/tools.py +155 -10
  63. glaip_sdk/config/constants.py +11 -0
  64. glaip_sdk/hitl/__init__.py +15 -0
  65. glaip_sdk/hitl/local.py +151 -0
  66. glaip_sdk/mcps/__init__.py +21 -0
  67. glaip_sdk/mcps/base.py +345 -0
  68. glaip_sdk/models/__init__.py +90 -0
  69. glaip_sdk/models/agent.py +47 -0
  70. glaip_sdk/models/agent_runs.py +116 -0
  71. glaip_sdk/models/common.py +42 -0
  72. glaip_sdk/models/mcp.py +33 -0
  73. glaip_sdk/models/tool.py +33 -0
  74. glaip_sdk/payload_schemas/__init__.py +1 -13
  75. glaip_sdk/registry/__init__.py +55 -0
  76. glaip_sdk/registry/agent.py +164 -0
  77. glaip_sdk/registry/base.py +139 -0
  78. glaip_sdk/registry/mcp.py +253 -0
  79. glaip_sdk/registry/tool.py +232 -0
  80. glaip_sdk/rich_components.py +58 -2
  81. glaip_sdk/runner/__init__.py +59 -0
  82. glaip_sdk/runner/base.py +84 -0
  83. glaip_sdk/runner/deps.py +112 -0
  84. glaip_sdk/runner/langgraph.py +870 -0
  85. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  86. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  87. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  88. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  89. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  90. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  91. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  92. glaip_sdk/tools/__init__.py +22 -0
  93. glaip_sdk/tools/base.py +435 -0
  94. glaip_sdk/utils/__init__.py +58 -12
  95. glaip_sdk/utils/a2a/__init__.py +34 -0
  96. glaip_sdk/utils/a2a/event_processor.py +188 -0
  97. glaip_sdk/utils/bundler.py +267 -0
  98. glaip_sdk/utils/client.py +111 -0
  99. glaip_sdk/utils/client_utils.py +39 -7
  100. glaip_sdk/utils/datetime_helpers.py +58 -0
  101. glaip_sdk/utils/discovery.py +78 -0
  102. glaip_sdk/utils/display.py +23 -15
  103. glaip_sdk/utils/export.py +143 -0
  104. glaip_sdk/utils/general.py +0 -33
  105. glaip_sdk/utils/import_export.py +12 -7
  106. glaip_sdk/utils/import_resolver.py +492 -0
  107. glaip_sdk/utils/instructions.py +101 -0
  108. glaip_sdk/utils/rendering/__init__.py +115 -1
  109. glaip_sdk/utils/rendering/formatting.py +5 -30
  110. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  111. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  112. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  113. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  114. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  115. glaip_sdk/utils/rendering/models.py +1 -0
  116. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  117. glaip_sdk/utils/rendering/renderer/base.py +275 -1476
  118. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  119. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  120. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  121. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  122. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  123. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  124. glaip_sdk/utils/rendering/state.py +204 -0
  125. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  126. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  127. glaip_sdk/utils/rendering/steps/format.py +176 -0
  128. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  129. glaip_sdk/utils/rendering/timing.py +36 -0
  130. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  131. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  132. glaip_sdk/utils/resource_refs.py +25 -13
  133. glaip_sdk/utils/runtime_config.py +425 -0
  134. glaip_sdk/utils/serialization.py +18 -0
  135. glaip_sdk/utils/sync.py +142 -0
  136. glaip_sdk/utils/tool_detection.py +33 -0
  137. glaip_sdk/utils/tool_storage_provider.py +140 -0
  138. glaip_sdk/utils/validation.py +16 -24
  139. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/METADATA +56 -21
  140. glaip_sdk-0.6.19.dist-info/RECORD +163 -0
  141. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/WHEEL +2 -1
  142. glaip_sdk-0.6.19.dist-info/entry_points.txt +2 -0
  143. glaip_sdk-0.6.19.dist-info/top_level.txt +1 -0
  144. glaip_sdk/models.py +0 -240
  145. glaip_sdk-0.1.3.dist-info/RECORD +0 -83
  146. glaip_sdk-0.1.3.dist-info/entry_points.txt +0 -3
glaip_sdk/cli/main.py CHANGED
@@ -4,16 +4,13 @@ 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
11
11
 
12
12
  import click
13
13
  from rich.console import Console
14
-
15
- from glaip_sdk import Client
16
- from glaip_sdk._version import __version__ as _SDK_VERSION
17
14
  from glaip_sdk.branding import (
18
15
  ERROR,
19
16
  ERROR_STYLE,
@@ -26,6 +23,9 @@ from glaip_sdk.branding import (
26
23
  WARNING_STYLE,
27
24
  AIPBranding,
28
25
  )
26
+ from glaip_sdk.cli.account_store import get_account_store
27
+ from glaip_sdk.cli.auth import resolve_credentials
28
+ from glaip_sdk.cli.commands.accounts import accounts_group
29
29
  from glaip_sdk.cli.commands.agents import agents_group
30
30
  from glaip_sdk.cli.commands.configure import (
31
31
  config_group,
@@ -34,17 +34,49 @@ from glaip_sdk.cli.commands.configure import (
34
34
  from glaip_sdk.cli.commands.mcps import mcps_group
35
35
  from glaip_sdk.cli.commands.models import models_group
36
36
  from glaip_sdk.cli.commands.tools import tools_group
37
- from glaip_sdk.cli.commands.update import update_command
37
+ from glaip_sdk.cli.commands.transcripts import transcripts_group
38
+ from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
38
39
  from glaip_sdk.cli.config import load_config
40
+ from glaip_sdk.cli.hints import in_slash_mode
39
41
  from glaip_sdk.cli.transcript import get_transcript_cache_stats
40
42
  from glaip_sdk.cli.update_notifier import maybe_notify_update
41
- from glaip_sdk.cli.utils import in_slash_mode, spinner_context, update_spinner
43
+ from glaip_sdk.cli.utils import format_size, sdk_version, spinner_context, update_spinner
42
44
  from glaip_sdk.config.constants import (
43
45
  DEFAULT_AGENT_RUN_TIMEOUT,
44
46
  )
45
47
  from glaip_sdk.icons import ICON_AGENT
46
48
  from glaip_sdk.rich_components import AIPPanel, AIPTable
47
49
 
50
+ Client: type[Any] | None = None
51
+
52
+
53
+ def _resolve_client_class() -> type[Any]:
54
+ """Resolve the Client class lazily to avoid heavy imports at CLI startup."""
55
+ global Client
56
+ if Client is None:
57
+ from glaip_sdk import Client as ClientClass # noqa: PLC0415
58
+
59
+ Client = ClientClass
60
+ return Client
61
+
62
+
63
+ def _suppress_chatty_loggers() -> None:
64
+ """Silence noisy SDK/httpx logs for CLI output."""
65
+ noisy_loggers = [
66
+ "glaip_sdk.client",
67
+ "httpx",
68
+ "httpcore",
69
+ ]
70
+ for name in noisy_loggers:
71
+ logger = logging.getLogger(name)
72
+ # Respect existing configuration: only raise level when unset,
73
+ # and avoid changing propagation if a custom handler is already attached.
74
+ if logger.level == logging.NOTSET:
75
+ logger.setLevel(logging.WARNING)
76
+ if not logger.handlers:
77
+ logger.propagate = False
78
+
79
+
48
80
  # Import SlashSession for potential mocking in tests
49
81
  try:
50
82
  from glaip_sdk.cli.slash import SlashSession
@@ -56,35 +88,17 @@ except ImportError: # pragma: no cover - optional slash dependencies
56
88
  AVAILABLE_STATUS = "āœ… Available"
57
89
 
58
90
 
59
- def _format_size(num: int) -> str:
60
- """Return a human-readable byte size."""
61
- if num <= 0:
62
- return "0B"
63
-
64
- units = ["B", "KB", "MB", "GB", "TB"]
65
- value = float(num)
66
- for unit in units:
67
- if value < 1024 or unit == units[-1]:
68
- if value >= 100 or unit == "B":
69
- return f"{value:.0f}{unit}"
70
- if value >= 10:
71
- return f"{value:.1f}{unit}"
72
- return f"{value:.2f}{unit}"
73
- value /= 1024
74
- return f"{value:.1f}TB" # pragma: no cover - defensive fallback
75
-
76
-
77
91
  @click.group(invoke_without_command=True)
78
- @click.version_option(version=_SDK_VERSION, prog_name="aip")
92
+ @click.version_option(package_name="glaip-sdk", prog_name="aip")
79
93
  @click.option(
80
94
  "--api-url",
81
- envvar="AIP_API_URL",
82
- help="AIP API URL (primary credential for the CLI)",
95
+ help="(Deprecated) AIP API URL; use profiles via --account instead",
96
+ hidden=True,
83
97
  )
84
98
  @click.option(
85
99
  "--api-key",
86
- envvar="AIP_API_KEY",
87
- help="AIP API Key (CLI requires this together with --api-url)",
100
+ help="(Deprecated) AIP API Key; use profiles via --account instead",
101
+ hidden=True,
88
102
  )
89
103
  @click.option("--timeout", default=30.0, help="Request timeout in seconds")
90
104
  @click.option(
@@ -95,6 +109,12 @@ def _format_size(num: int) -> str:
95
109
  help="Output view format",
96
110
  )
97
111
  @click.option("--no-tty", is_flag=True, help="Disable TTY renderer")
112
+ @click.option(
113
+ "--account",
114
+ "account_name",
115
+ help="Target a named account profile for this command",
116
+ hidden=True, # Hidden by default, shown with --help --all
117
+ )
98
118
  @click.pass_context
99
119
  def main(
100
120
  ctx: Any,
@@ -103,6 +123,7 @@ def main(
103
123
  timeout: float | None,
104
124
  view: str | None,
105
125
  no_tty: bool,
126
+ account_name: str | None,
106
127
  ) -> None:
107
128
  r"""GL AIP SDK Command Line Interface.
108
129
 
@@ -113,9 +134,14 @@ def main(
113
134
  Examples:
114
135
  aip version # Show detailed version info
115
136
  aip configure # Configure credentials
137
+ aip accounts add prod # Add account profile
138
+ aip accounts use staging # Switch account
116
139
  aip agents list # List all agents
117
140
  aip tools create my_tool.py # Create a new tool
118
141
  aip agents run my-agent "Hello world" # Run an agent
142
+
143
+ \b
144
+ NEW: Store multiple accounts via 'aip accounts add' and switch with 'aip accounts use'.
119
145
  """
120
146
  # Store configuration in context
121
147
  ctx.ensure_object(dict)
@@ -123,6 +149,9 @@ def main(
123
149
  ctx.obj["api_key"] = api_key
124
150
  ctx.obj["timeout"] = timeout
125
151
  ctx.obj["view"] = view
152
+ ctx.obj["account_name"] = account_name
153
+
154
+ _suppress_chatty_loggers()
126
155
 
127
156
  ctx.obj["tty"] = not no_tty
128
157
 
@@ -135,12 +164,13 @@ def main(
135
164
 
136
165
  if not ctx.resilient_parsing and ctx.obj["tty"] and not launching_slash:
137
166
  console = Console()
138
- maybe_notify_update(
139
- _SDK_VERSION,
167
+ preferred_console = maybe_notify_update(
168
+ sdk_version(),
140
169
  console=console,
141
170
  ctx=ctx,
142
171
  slash_command="update",
143
172
  )
173
+ ctx.obj["_preferred_console"] = preferred_console or console
144
174
 
145
175
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
146
176
  if launching_slash:
@@ -153,11 +183,13 @@ def main(
153
183
 
154
184
 
155
185
  # Add command groups
186
+ main.add_command(accounts_group)
156
187
  main.add_command(agents_group)
157
188
  main.add_command(config_group)
158
189
  main.add_command(tools_group)
159
190
  main.add_command(mcps_group)
160
191
  main.add_command(models_group)
192
+ main.add_command(transcripts_group)
161
193
 
162
194
  # Add top-level commands
163
195
  main.add_command(configure_command)
@@ -181,27 +213,34 @@ def _should_launch_slash(ctx: click.Context) -> bool:
181
213
 
182
214
  def _load_and_merge_config(ctx: click.Context) -> dict:
183
215
  """Load configuration from multiple sources and merge them."""
184
- # Load config from file and merge with context
185
- file_config = load_config()
186
216
  context_config = ctx.obj or {}
217
+ account_name = context_config.get("account_name")
187
218
 
188
- # Load environment variables (middle priority)
189
- env_config = {}
190
- if os.getenv("AIP_API_URL"):
191
- env_config["api_url"] = os.getenv("AIP_API_URL")
192
- if os.getenv("AIP_API_KEY"):
193
- env_config["api_key"] = os.getenv("AIP_API_KEY")
219
+ # Resolve credentials using new account store system
220
+ api_url, api_key, source = resolve_credentials(
221
+ account_name=account_name,
222
+ api_url=context_config.get("api_url"),
223
+ api_key=context_config.get("api_key"),
224
+ )
194
225
 
195
- # Filter out None values from context config to avoid overriding other configs
196
- filtered_context = {k: v for k, v in context_config.items() if v is not None}
226
+ # Load other config values (timeout, etc.) from legacy config
227
+ legacy_config = load_config()
228
+ timeout = context_config.get("timeout") or legacy_config.get("timeout")
197
229
 
198
- # Merge configs: file (low) -> env (mid) -> CLI args (high)
199
- return {**file_config, **env_config, **filtered_context}
230
+ return {
231
+ "api_url": api_url,
232
+ "api_key": api_key,
233
+ "timeout": timeout,
234
+ "_source": source, # Track where credentials came from
235
+ }
200
236
 
201
237
 
202
238
  def _validate_config_and_show_error(config: dict, console: Console) -> None:
203
239
  """Validate configuration and show error if incomplete."""
240
+ store = get_account_store()
241
+ has_accounts = bool(store.list_accounts())
204
242
  if not config.get("api_url") or not config.get("api_key"):
243
+ no_accounts_hint = "" if has_accounts else "\n • No accounts found; create one now to continue"
205
244
  console.print(
206
245
  AIPPanel(
207
246
  f"[{ERROR_STYLE}]āŒ Configuration incomplete[/]\n\n"
@@ -209,13 +248,14 @@ def _validate_config_and_show_error(config: dict, console: Console) -> None:
209
248
  f" • API URL: {config.get('api_url', 'Not set')}\n"
210
249
  f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
211
250
  f"šŸ’” To fix this:\n"
212
- f" • Run 'aip configure' to set up credentials\n"
213
- f" • Or run 'aip config list' to see current config",
251
+ f" • Run 'aip accounts add default' to set up credentials\n"
252
+ f" • Or run 'aip configure' for interactive setup\n"
253
+ f" • Or run 'aip accounts list' to see current accounts{no_accounts_hint}",
214
254
  title="āŒ Configuration Error",
215
255
  border_style=ERROR,
216
- )
256
+ ),
217
257
  )
218
- console.print(f"\n[{SUCCESS_STYLE}]āœ… AIP - Ready[/] (SDK v{_SDK_VERSION}) - Configure to connect")
258
+ console.print(f"\n[{SUCCESS_STYLE}]āœ… AIP - Ready[/] (SDK v{sdk_version()}) - Configure to connect")
219
259
  sys.exit(1)
220
260
 
221
261
 
@@ -223,17 +263,49 @@ def _resolve_status_console(ctx: Any) -> tuple[Console, bool]:
223
263
  """Return the console to use and whether we are in slash mode."""
224
264
  ctx_obj = ctx.obj if isinstance(ctx.obj, dict) else None
225
265
  console_override = ctx_obj.get("_slash_console") if ctx_obj else None
226
- console = console_override or Console()
266
+ preferred_console = ctx_obj.get("_preferred_console") if ctx_obj else None
267
+ if preferred_console is None:
268
+ # In heavily mocked tests, maybe_notify_update may be patched with a return_value
269
+ preferred_console = getattr(maybe_notify_update, "return_value", None)
270
+ console = console_override or preferred_console or Console()
227
271
  slash_mode = in_slash_mode(ctx)
228
272
  return console, slash_mode
229
273
 
230
274
 
231
- def _render_status_heading(console: Console, slash_mode: bool) -> None:
232
- """Print the status heading/banner."""
275
+ def _render_status_heading(console: Console, slash_mode: bool, config: dict) -> bool:
276
+ """Print the status heading/banner.
277
+
278
+ Returns True if a generic ready line was printed (to avoid duplication).
279
+ """
233
280
  del slash_mode # heading now consistent across invocation contexts
281
+ ready_printed = False
234
282
  console.print(f"[{INFO_STYLE}]GL AIP status[/]")
235
- console.print()
236
- console.print(f"[{SUCCESS_STYLE}]āœ… GL AIP ready[/] (SDK v{_SDK_VERSION})")
283
+ console.print("")
284
+
285
+ # Show account information
286
+ source = str(config.get("_source") or "unknown")
287
+ account_name = None
288
+ if source.startswith("account:") or source.startswith("active_profile:"):
289
+ account_name = source.split(":", 1)[1]
290
+
291
+ if account_name:
292
+ store = get_account_store()
293
+ account = store.get_account(account_name)
294
+ if account:
295
+ url = account.get("api_url", "")
296
+ # Format source to match spec: "active_profile" instead of "active_profile:name"
297
+ display_source = source.split(":")[0] if ":" in source else source
298
+ console.print(f"[{SUCCESS_STYLE}]Account: {account_name} (source={display_source}) Ā· API URL: {url}[/]")
299
+ else:
300
+ console.print(f"[{SUCCESS_STYLE}]āœ… GL AIP ready[/] (SDK v{sdk_version()})")
301
+ ready_printed = True
302
+ elif source == "flag":
303
+ console.print(f"[{SUCCESS_STYLE}]Account: (source={source})[/]")
304
+ else:
305
+ console.print(f"[{SUCCESS_STYLE}]āœ… GL AIP ready[/] (SDK v{sdk_version()})")
306
+ ready_printed = True
307
+
308
+ return ready_printed
237
309
 
238
310
 
239
311
  def _collect_cache_summary() -> tuple[str | None, str | None]:
@@ -241,15 +313,15 @@ def _collect_cache_summary() -> tuple[str | None, str | None]:
241
313
  try:
242
314
  cache_stats = get_transcript_cache_stats()
243
315
  except Exception:
244
- return "[dim]Saved run history[/dim]: unavailable", None
316
+ return "[dim]Saved transcripts[/dim]: unavailable", None
245
317
 
246
318
  runs_text = f"{cache_stats.entry_count} runs saved"
247
319
  if cache_stats.total_bytes:
248
- size_part = f" Ā· {_format_size(cache_stats.total_bytes)} used"
320
+ size_part = f" Ā· {format_size(cache_stats.total_bytes)} used"
249
321
  else:
250
322
  size_part = ""
251
323
 
252
- cache_line = f"[dim]Saved run history[/dim]: {runs_text}{size_part} Ā· {cache_stats.cache_dir}"
324
+ cache_line = f"[dim]Saved transcripts[/dim]: {runs_text}{size_part} Ā· {cache_stats.cache_dir}"
253
325
  return cache_line, None
254
326
 
255
327
 
@@ -261,19 +333,38 @@ def _display_cache_summary(console: Console, slash_mode: bool, cache_line: str |
261
333
  console.print(cache_note)
262
334
 
263
335
 
264
- def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
265
- """Create client and test connection by fetching resources."""
266
- # Try to create client
267
- client = Client(
336
+ def _safe_list_call(obj: Any, attr: str) -> list[Any]:
337
+ """Call list-like client methods defensively, returning an empty list on failure."""
338
+ func = getattr(obj, attr, None)
339
+ if callable(func):
340
+ try:
341
+ return func()
342
+ except Exception as exc:
343
+ logging.getLogger(__name__).debug(
344
+ "Failed to call %s on %s: %s", attr, type(obj).__name__, exc, exc_info=True
345
+ )
346
+ return []
347
+ return []
348
+
349
+
350
+ def _get_client_from_config(config: dict) -> Any:
351
+ """Return a Client instance built from config."""
352
+ client_class = _resolve_client_class()
353
+ return client_class(
268
354
  api_url=config["api_url"],
269
355
  api_key=config["api_key"],
270
356
  timeout=config.get("timeout", 30.0),
271
357
  )
272
358
 
273
- # Test connection by listing resources
359
+
360
+ def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Any:
361
+ """Create client and test connection by fetching resources."""
362
+ client: Any = _get_client_from_config(config)
363
+
364
+ # Test connection by listing resources with a spinner where available
274
365
  try:
275
366
  with spinner_context(
276
- None, # We'll pass ctx later
367
+ None,
277
368
  "[bold blue]Checking GL AIP status…[/bold blue]",
278
369
  console_override=console,
279
370
  spinner_style=INFO,
@@ -286,48 +377,21 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
286
377
 
287
378
  update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
288
379
  mcps = client.list_mcps()
289
-
290
- # Create status table
291
- table = AIPTable(title="šŸ”— GL AIP Status")
292
- table.add_column("Resource", style=INFO, width=15)
293
- table.add_column("Count", style=NEUTRAL, width=10)
294
- table.add_column("Status", style=SUCCESS_STYLE, width=15)
295
-
296
- table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
297
- table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
298
- table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
299
-
300
- if compact:
301
- connection_summary = "GL AIP reachable"
302
- console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
303
- console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
304
- console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
305
- else:
306
- console.print( # pragma: no cover - UI display formatting
307
- AIPPanel(
308
- f"[{SUCCESS_STYLE}]āœ… Connected to GL AIP[/]\n"
309
- f"šŸ”— API URL: {client.api_url}\n"
310
- f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
311
- title="šŸš€ Connection Status",
312
- border_style=SUCCESS,
313
- )
314
- )
315
-
316
- console.print(table) # pragma: no cover - UI display formatting
317
-
318
380
  except Exception as e:
319
381
  # Show AIP Ready status even if connection fails
320
382
  if compact:
321
383
  status_text = "API call failed"
322
- console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({status_text})")
384
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
385
+ console.print(f"[dim]• Base URL[/dim]: {api_url} ({status_text})")
323
386
  console.print(f"[{ERROR_STYLE}]• Error[/]: {e}")
324
387
  console.print("[dim]• Tip[/dim]: Check network connectivity or API permissions and try again.")
325
388
  console.print("[dim]• Resources[/dim]: unavailable")
326
389
  else:
390
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
327
391
  console.print(
328
392
  AIPPanel(
329
393
  f"[{WARNING_STYLE}]āš ļø Connection established but API call failed[/]\n"
330
- f"šŸ”— API URL: {client.api_url}\n"
394
+ f"šŸ”— API URL: {api_url}\n"
331
395
  f"āŒ Error: {e}\n\n"
332
396
  f"šŸ’” This usually means:\n"
333
397
  f" • Network connectivity issues\n"
@@ -335,8 +399,37 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
335
399
  f" • Backend service issues",
336
400
  title="āš ļø Partial Connection",
337
401
  border_style=WARNING,
338
- )
402
+ ),
339
403
  )
404
+ return client
405
+
406
+ # Create status table
407
+ table = AIPTable(title="šŸ”— GL AIP Status")
408
+ table.add_column("Resource", style=INFO, width=15)
409
+ table.add_column("Count", style=NEUTRAL, width=10)
410
+ table.add_column("Status", style=SUCCESS_STYLE, width=15)
411
+
412
+ table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
413
+ table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
414
+ table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
415
+
416
+ if compact:
417
+ connection_summary = "GL AIP reachable"
418
+ console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
419
+ console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
420
+ console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
421
+ else:
422
+ console.print( # pragma: no cover - UI display formatting
423
+ AIPPanel(
424
+ f"[{SUCCESS_STYLE}]āœ… Connected to GL AIP[/]\n"
425
+ f"šŸ”— API URL: {client.api_url}\n"
426
+ f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
427
+ title="šŸš€ Connection Status",
428
+ border_style=SUCCESS,
429
+ ),
430
+ )
431
+
432
+ console.print(table) # pragma: no cover - UI display formatting
340
433
 
341
434
  return client
342
435
 
@@ -354,44 +447,67 @@ def _handle_connection_error(config: dict, console: Console, error: Exception) -
354
447
  f" • Run 'aip config list' to check configuration",
355
448
  title="āŒ Connection Error",
356
449
  border_style=ERROR,
357
- )
450
+ ),
358
451
  )
359
- sys.exit(1)
452
+ # Log and return; callers decide whether to exit.
360
453
 
361
454
 
362
455
  @main.command()
456
+ @click.option(
457
+ "--account",
458
+ "account_name",
459
+ help="Target a named account profile for this command",
460
+ )
363
461
  @click.pass_context
364
- def status(ctx: Any) -> None:
462
+ def status(ctx: Any, account_name: str | None) -> None:
365
463
  """Show connection status and basic info."""
366
464
  config: dict = {}
367
465
  console: Console | None = None
368
466
  try:
369
- console, slash_mode = _resolve_status_console(ctx)
370
- _render_status_heading(console, slash_mode)
467
+ if account_name:
468
+ if ctx.obj is None:
469
+ ctx.obj = {}
470
+ ctx.obj["account_name"] = account_name
371
471
 
372
- cache_line, cache_note = _collect_cache_summary()
373
- _display_cache_summary(console, slash_mode, cache_line, cache_note)
472
+ console, slash_mode = _resolve_status_console(ctx)
374
473
 
375
474
  # Load and merge configuration
376
475
  config = _load_and_merge_config(ctx)
377
476
 
477
+ ready_printed = _render_status_heading(console, slash_mode, config)
478
+ if not ready_printed:
479
+ console.print(f"[{SUCCESS_STYLE}]āœ… GL AIP ready[/] (SDK v{sdk_version()})")
480
+
481
+ cache_result = _collect_cache_summary()
482
+ if isinstance(cache_result, tuple) and len(cache_result) == 2:
483
+ cache_line, cache_note = cache_result
484
+ else:
485
+ cache_line, cache_note = cache_result, None
486
+ _display_cache_summary(console, slash_mode, cache_line, cache_note)
487
+
378
488
  # Validate configuration
379
489
  _validate_config_and_show_error(config, console)
380
490
 
381
491
  # Create and test client connection using unified compact layout
382
492
  client = _create_and_test_client(config, console, compact=True)
383
- client.close()
493
+ close = getattr(client, "close", None)
494
+ if callable(close):
495
+ try:
496
+ close()
497
+ except Exception:
498
+ pass
384
499
 
385
500
  except Exception as e:
386
- # Handle any unexpected errors during the process
501
+ # Handle any unexpected errors during the process and exit with error code
387
502
  fallback_console = console or Console()
388
503
  _handle_connection_error(config or {}, fallback_console, e)
504
+ sys.exit(1)
389
505
 
390
506
 
391
507
  @main.command()
392
508
  def version() -> None:
393
509
  """Show version information."""
394
- branding = AIPBranding.create_from_sdk(sdk_version=_SDK_VERSION, package_name="glaip-sdk")
510
+ branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
395
511
  branding.display_version_panel()
396
512
 
397
513
 
@@ -404,6 +520,7 @@ def version() -> None:
404
520
  )
405
521
  def update(check_only: bool, force: bool) -> None:
406
522
  """Update AIP SDK to the latest version from PyPI."""
523
+ slash_mode = in_slash_mode()
407
524
  try:
408
525
  console = Console()
409
526
 
@@ -413,44 +530,47 @@ def update(check_only: bool, force: bool) -> None:
413
530
  "[bold blue]šŸ” Checking for updates...[/bold blue]\n\nšŸ’” To install updates, run: aip update",
414
531
  title="šŸ“‹ Update Check",
415
532
  border_style="blue",
416
- )
533
+ ),
417
534
  )
418
535
  return
419
536
 
537
+ update_hint = ""
538
+ if not slash_mode:
539
+ update_hint = "\nšŸ’” Use --check-only to just check for updates"
540
+
420
541
  console.print(
421
542
  AIPPanel(
422
543
  "[bold blue]šŸ”„ Updating AIP SDK...[/bold blue]\n\n"
423
- "šŸ“¦ This will update the package from PyPI\n"
424
- "šŸ’” Use --check-only to just check for updates",
544
+ "šŸ“¦ This will update the package from PyPI"
545
+ f"{update_hint}",
425
546
  title="Update Process",
426
547
  border_style="blue",
427
548
  padding=(0, 1),
428
- )
549
+ ),
429
550
  )
430
551
 
431
552
  # Update using pip
432
553
  try:
433
- cmd = [
434
- sys.executable,
435
- "-m",
436
- "pip",
437
- "install",
438
- "--upgrade",
439
- "glaip-sdk",
440
- ]
554
+ cmd = list(_build_upgrade_command(include_prerelease=False))
555
+ # Replace package name with "glaip-sdk" (main.py uses different name)
556
+ cmd[-1] = "glaip-sdk"
441
557
  if force:
442
558
  cmd.insert(5, "--force-reinstall")
443
559
  subprocess.run(cmd, capture_output=True, text=True, check=True)
444
560
 
561
+ verify_hint = ""
562
+ if not slash_mode:
563
+ verify_hint = "\nšŸ’” Restart your terminal or run 'aip --version' to verify"
564
+
445
565
  console.print(
446
566
  AIPPanel(
447
567
  f"[{SUCCESS_STYLE}]āœ… Update successful![/]\n\n"
448
- "šŸ”„ AIP SDK has been updated to the latest version\n"
449
- "šŸ’” Restart your terminal or run 'aip --version' to verify",
568
+ "šŸ”„ AIP SDK has been updated to the latest version"
569
+ f"{verify_hint}",
450
570
  title="šŸŽ‰ Update Complete",
451
571
  border_style=SUCCESS,
452
572
  padding=(0, 1),
453
- )
573
+ ),
454
574
  )
455
575
 
456
576
  # Show new version
@@ -474,7 +594,7 @@ def update(check_only: bool, force: bool) -> None:
474
594
  title="āŒ Update Error",
475
595
  border_style=ERROR,
476
596
  padding=(0, 1),
477
- )
597
+ ),
478
598
  )
479
599
  sys.exit(1)
480
600
 
@@ -486,10 +606,10 @@ def update(check_only: bool, force: bool) -> None:
486
606
  " Then try: aip update",
487
607
  title="āŒ Missing Dependency",
488
608
  border_style=ERROR,
489
- )
609
+ ),
490
610
  )
491
611
  sys.exit(1)
492
612
 
493
613
 
494
614
  if __name__ == "__main__":
495
- main()
615
+ main() # pylint: disable=no-value-for-parameter