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