glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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 (217) 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 +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -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/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.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,59 +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(f"\n[{SUCCESS_STYLE}]āœ… AIP - Ready[/] (SDK v{_SDK_VERSION}) - Configure to connect")
219
- 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)
220
291
 
221
292
 
222
293
  def _resolve_status_console(ctx: Any) -> tuple[Console, bool]:
223
294
  """Return the console to use and whether we are in slash mode."""
224
295
  ctx_obj = ctx.obj if isinstance(ctx.obj, dict) else None
225
296
  console_override = ctx_obj.get("_slash_console") if ctx_obj else None
226
- 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()
227
302
  slash_mode = in_slash_mode(ctx)
228
303
  return console, slash_mode
229
304
 
230
305
 
231
- def _render_status_heading(console: Console, slash_mode: bool) -> None:
232
- """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
+ """
233
311
  del slash_mode # heading now consistent across invocation contexts
312
+ ready_printed = False
234
313
  console.print(f"[{INFO_STYLE}]GL AIP status[/]")
235
- console.print()
236
- 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
237
340
 
238
341
 
239
342
  def _collect_cache_summary() -> tuple[str | None, str | None]:
@@ -241,15 +344,15 @@ def _collect_cache_summary() -> tuple[str | None, str | None]:
241
344
  try:
242
345
  cache_stats = get_transcript_cache_stats()
243
346
  except Exception:
244
- return "[dim]Saved run history[/dim]: unavailable", None
347
+ return "[dim]Saved transcripts[/dim]: unavailable", None
245
348
 
246
349
  runs_text = f"{cache_stats.entry_count} runs saved"
247
350
  if cache_stats.total_bytes:
248
- size_part = f" Ā· {_format_size(cache_stats.total_bytes)} used"
351
+ size_part = f" Ā· {format_size(cache_stats.total_bytes)} used"
249
352
  else:
250
353
  size_part = ""
251
354
 
252
- cache_line = f"[dim]Saved run history[/dim]: {runs_text}{size_part} Ā· {cache_stats.cache_dir}"
355
+ cache_line = f"[dim]Saved transcripts[/dim]: {runs_text}{size_part} Ā· {cache_stats.cache_dir}"
253
356
  return cache_line, None
254
357
 
255
358
 
@@ -261,19 +364,38 @@ def _display_cache_summary(console: Console, slash_mode: bool, cache_line: str |
261
364
  console.print(cache_note)
262
365
 
263
366
 
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(
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(
268
385
  api_url=config["api_url"],
269
386
  api_key=config["api_key"],
270
387
  timeout=config.get("timeout", 30.0),
271
388
  )
272
389
 
273
- # 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
274
396
  try:
275
397
  with spinner_context(
276
- None, # We'll pass ctx later
398
+ None,
277
399
  "[bold blue]Checking GL AIP status…[/bold blue]",
278
400
  console_override=console,
279
401
  spinner_style=INFO,
@@ -286,48 +408,21 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
286
408
 
287
409
  update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
288
410
  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
411
  except Exception as e:
319
412
  # Show AIP Ready status even if connection fails
320
413
  if compact:
321
414
  status_text = "API call failed"
322
- 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})")
323
417
  console.print(f"[{ERROR_STYLE}]• Error[/]: {e}")
324
418
  console.print("[dim]• Tip[/dim]: Check network connectivity or API permissions and try again.")
325
419
  console.print("[dim]• Resources[/dim]: unavailable")
326
420
  else:
421
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
327
422
  console.print(
328
423
  AIPPanel(
329
424
  f"[{WARNING_STYLE}]āš ļø Connection established but API call failed[/]\n"
330
- f"šŸ”— API URL: {client.api_url}\n"
425
+ f"šŸ”— API URL: {api_url}\n"
331
426
  f"āŒ Error: {e}\n\n"
332
427
  f"šŸ’” This usually means:\n"
333
428
  f" • Network connectivity issues\n"
@@ -335,8 +430,37 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
335
430
  f" • Backend service issues",
336
431
  title="āš ļø Partial Connection",
337
432
  border_style=WARNING,
338
- )
433
+ ),
339
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
340
464
 
341
465
  return client
342
466
 
@@ -354,44 +478,67 @@ def _handle_connection_error(config: dict, console: Console, error: Exception) -
354
478
  f" • Run 'aip config list' to check configuration",
355
479
  title="āŒ Connection Error",
356
480
  border_style=ERROR,
357
- )
481
+ ),
358
482
  )
359
- sys.exit(1)
483
+ # Log and return; callers decide whether to exit.
360
484
 
361
485
 
362
486
  @main.command()
487
+ @click.option(
488
+ "--account",
489
+ "account_name",
490
+ help="Target a named account profile for this command",
491
+ )
363
492
  @click.pass_context
364
- def status(ctx: Any) -> None:
493
+ def status(ctx: Any, account_name: str | None) -> None:
365
494
  """Show connection status and basic info."""
366
495
  config: dict = {}
367
496
  console: Console | None = None
368
497
  try:
369
- console, slash_mode = _resolve_status_console(ctx)
370
- _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
371
502
 
372
- cache_line, cache_note = _collect_cache_summary()
373
- _display_cache_summary(console, slash_mode, cache_line, cache_note)
503
+ console, slash_mode = _resolve_status_console(ctx)
374
504
 
375
505
  # Load and merge configuration
376
506
  config = _load_and_merge_config(ctx)
377
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
+
378
519
  # Validate configuration
379
520
  _validate_config_and_show_error(config, console)
380
521
 
381
522
  # Create and test client connection using unified compact layout
382
523
  client = _create_and_test_client(config, console, compact=True)
383
- client.close()
524
+ close = getattr(client, "close", None)
525
+ if callable(close):
526
+ try:
527
+ close()
528
+ except Exception:
529
+ pass
384
530
 
385
531
  except Exception as e:
386
- # Handle any unexpected errors during the process
532
+ # Handle any unexpected errors during the process and exit with error code
387
533
  fallback_console = console or Console()
388
534
  _handle_connection_error(config or {}, fallback_console, e)
535
+ sys.exit(1)
389
536
 
390
537
 
391
538
  @main.command()
392
539
  def version() -> None:
393
540
  """Show version information."""
394
- branding = AIPBranding.create_from_sdk(sdk_version=_SDK_VERSION, package_name="glaip-sdk")
541
+ branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
395
542
  branding.display_version_panel()
396
543
 
397
544
 
@@ -404,6 +551,7 @@ def version() -> None:
404
551
  )
405
552
  def update(check_only: bool, force: bool) -> None:
406
553
  """Update AIP SDK to the latest version from PyPI."""
554
+ slash_mode = in_slash_mode()
407
555
  try:
408
556
  console = Console()
409
557
 
@@ -413,44 +561,73 @@ def update(check_only: bool, force: bool) -> None:
413
561
  "[bold blue]šŸ” Checking for updates...[/bold blue]\n\nšŸ’” To install updates, run: aip update",
414
562
  title="šŸ“‹ Update Check",
415
563
  border_style="blue",
416
- )
564
+ ),
417
565
  )
418
566
  return
419
567
 
568
+ update_hint = ""
569
+ if not slash_mode:
570
+ update_hint = "\nšŸ’” Use --check-only to just check for updates"
571
+
420
572
  console.print(
421
573
  AIPPanel(
422
574
  "[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",
575
+ "šŸ“¦ This will update the package from PyPI"
576
+ f"{update_hint}",
425
577
  title="Update Process",
426
578
  border_style="blue",
427
579
  padding=(0, 1),
428
- )
580
+ ),
429
581
  )
430
582
 
431
- # Update using pip
583
+ # Update using pip or uv tool install
432
584
  try:
433
- cmd = [
434
- sys.executable,
435
- "-m",
436
- "pip",
437
- "install",
438
- "--upgrade",
439
- "glaip-sdk",
440
- ]
441
- if force:
442
- 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
+ )
443
616
  subprocess.run(cmd, capture_output=True, text=True, check=True)
444
617
 
618
+ verify_hint = ""
619
+ if not slash_mode:
620
+ verify_hint = "\nšŸ’” Restart your terminal or run 'aip --version' to verify"
621
+
445
622
  console.print(
446
623
  AIPPanel(
447
624
  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",
625
+ "šŸ”„ AIP SDK has been updated to the latest version"
626
+ f"{verify_hint}",
450
627
  title="šŸŽ‰ Update Complete",
451
628
  border_style=SUCCESS,
452
629
  padding=(0, 1),
453
- )
630
+ ),
454
631
  )
455
632
 
456
633
  # Show new version
@@ -462,19 +639,39 @@ def update(check_only: bool, force: bool) -> None:
462
639
  )
463
640
  console.print(f"šŸ“‹ New version: {version_result.stdout.strip()}")
464
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)
465
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
+
466
668
  console.print(
467
669
  AIPPanel(
468
- f"[{ERROR_STYLE}]āŒ Update failed[/]\n\n"
469
- f"šŸ” Error: {e.stderr}\n\n"
470
- "šŸ’” Troubleshooting:\n"
471
- " • Check your internet connection\n"
472
- " • Try running: pip install --upgrade glaip-sdk\n"
473
- " • Check if you have write permissions",
474
- title="āŒ Update Error",
670
+ f"[{ERROR_STYLE}]āŒ Update failed[/]\n\nšŸ” Error: {e.stderr}\n\n{troubleshooting}",
671
+ title=UPDATE_ERROR_TITLE,
475
672
  border_style=ERROR,
476
673
  padding=(0, 1),
477
- )
674
+ ),
478
675
  )
479
676
  sys.exit(1)
480
677
 
@@ -486,10 +683,10 @@ def update(check_only: bool, force: bool) -> None:
486
683
  " Then try: aip update",
487
684
  title="āŒ Missing Dependency",
488
685
  border_style=ERROR,
489
- )
686
+ ),
490
687
  )
491
688
  sys.exit(1)
492
689
 
493
690
 
494
691
  if __name__ == "__main__":
495
- main()
692
+ main() # pylint: disable=no-value-for-parameter