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