glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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 (161) hide show
  1. glaip_sdk/__init__.py +6 -3
  2. glaip_sdk/_version.py +12 -5
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1126 -0
  5. glaip_sdk/branding.py +79 -15
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +699 -0
  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 +503 -183
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +774 -137
  14. glaip_sdk/cli/commands/mcps.py +1124 -181
  15. glaip_sdk/cli/commands/models.py +25 -10
  16. glaip_sdk/cli/commands/tools.py +144 -92
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +61 -0
  19. glaip_sdk/cli/config.py +95 -0
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +150 -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 +143 -53
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +24 -18
  30. glaip_sdk/cli/main.py +420 -145
  31. glaip_sdk/cli/masking.py +136 -0
  32. glaip_sdk/cli/mcp_validators.py +287 -0
  33. glaip_sdk/cli/pager.py +266 -0
  34. glaip_sdk/cli/parsers/__init__.py +7 -0
  35. glaip_sdk/cli/parsers/json_input.py +177 -0
  36. glaip_sdk/cli/resolution.py +28 -21
  37. glaip_sdk/cli/rich_helpers.py +27 -0
  38. glaip_sdk/cli/slash/__init__.py +15 -0
  39. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +282 -0
  42. glaip_sdk/cli/slash/prompt.py +245 -0
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +1679 -0
  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 +872 -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 +31 -0
  52. glaip_sdk/cli/transcript/cache.py +536 -0
  53. glaip_sdk/cli/transcript/capture.py +329 -0
  54. glaip_sdk/cli/transcript/export.py +38 -0
  55. glaip_sdk/cli/transcript/history.py +815 -0
  56. glaip_sdk/cli/transcript/launcher.py +77 -0
  57. glaip_sdk/cli/transcript/viewer.py +372 -0
  58. glaip_sdk/cli/update_notifier.py +290 -0
  59. glaip_sdk/cli/utils.py +247 -1238
  60. glaip_sdk/cli/validators.py +16 -18
  61. glaip_sdk/client/__init__.py +2 -1
  62. glaip_sdk/client/_agent_payloads.py +520 -0
  63. glaip_sdk/client/agent_runs.py +147 -0
  64. glaip_sdk/client/agents.py +940 -574
  65. glaip_sdk/client/base.py +163 -48
  66. glaip_sdk/client/main.py +35 -12
  67. glaip_sdk/client/mcps.py +126 -18
  68. glaip_sdk/client/run_rendering.py +415 -0
  69. glaip_sdk/client/shared.py +21 -0
  70. glaip_sdk/client/tools.py +195 -37
  71. glaip_sdk/client/validators.py +20 -48
  72. glaip_sdk/config/constants.py +15 -5
  73. glaip_sdk/exceptions.py +16 -9
  74. glaip_sdk/icons.py +25 -0
  75. glaip_sdk/mcps/__init__.py +21 -0
  76. glaip_sdk/mcps/base.py +345 -0
  77. glaip_sdk/models/__init__.py +90 -0
  78. glaip_sdk/models/agent.py +47 -0
  79. glaip_sdk/models/agent_runs.py +116 -0
  80. glaip_sdk/models/common.py +42 -0
  81. glaip_sdk/models/mcp.py +33 -0
  82. glaip_sdk/models/tool.py +33 -0
  83. glaip_sdk/payload_schemas/__init__.py +7 -0
  84. glaip_sdk/payload_schemas/agent.py +85 -0
  85. glaip_sdk/registry/__init__.py +55 -0
  86. glaip_sdk/registry/agent.py +164 -0
  87. glaip_sdk/registry/base.py +139 -0
  88. glaip_sdk/registry/mcp.py +253 -0
  89. glaip_sdk/registry/tool.py +231 -0
  90. glaip_sdk/rich_components.py +98 -2
  91. glaip_sdk/runner/__init__.py +59 -0
  92. glaip_sdk/runner/base.py +84 -0
  93. glaip_sdk/runner/deps.py +115 -0
  94. glaip_sdk/runner/langgraph.py +597 -0
  95. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  99. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
  102. glaip_sdk/tools/__init__.py +22 -0
  103. glaip_sdk/tools/base.py +435 -0
  104. glaip_sdk/utils/__init__.py +59 -13
  105. glaip_sdk/utils/a2a/__init__.py +34 -0
  106. glaip_sdk/utils/a2a/event_processor.py +188 -0
  107. glaip_sdk/utils/agent_config.py +53 -40
  108. glaip_sdk/utils/bundler.py +267 -0
  109. glaip_sdk/utils/client.py +111 -0
  110. glaip_sdk/utils/client_utils.py +58 -26
  111. glaip_sdk/utils/datetime_helpers.py +58 -0
  112. glaip_sdk/utils/discovery.py +78 -0
  113. glaip_sdk/utils/display.py +65 -32
  114. glaip_sdk/utils/export.py +143 -0
  115. glaip_sdk/utils/general.py +1 -36
  116. glaip_sdk/utils/import_export.py +20 -25
  117. glaip_sdk/utils/import_resolver.py +492 -0
  118. glaip_sdk/utils/instructions.py +101 -0
  119. glaip_sdk/utils/rendering/__init__.py +115 -1
  120. glaip_sdk/utils/rendering/formatting.py +85 -43
  121. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  122. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
  123. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  124. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  125. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  126. glaip_sdk/utils/rendering/models.py +39 -7
  127. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  128. glaip_sdk/utils/rendering/renderer/base.py +672 -759
  129. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  130. glaip_sdk/utils/rendering/renderer/debug.py +75 -22
  131. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  132. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  133. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  134. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  135. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  136. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  137. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  138. glaip_sdk/utils/rendering/state.py +204 -0
  139. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  140. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  141. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  142. glaip_sdk/utils/rendering/steps/format.py +176 -0
  143. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  144. glaip_sdk/utils/rendering/timing.py +36 -0
  145. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  146. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  147. glaip_sdk/utils/resource_refs.py +29 -26
  148. glaip_sdk/utils/runtime_config.py +422 -0
  149. glaip_sdk/utils/serialization.py +184 -51
  150. glaip_sdk/utils/sync.py +142 -0
  151. glaip_sdk/utils/tool_detection.py +33 -0
  152. glaip_sdk/utils/validation.py +21 -30
  153. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
  154. glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
  155. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
  156. glaip_sdk/models.py +0 -250
  157. glaip_sdk/utils/rendering/renderer/progress.py +0 -118
  158. glaip_sdk/utils/rendering/steps.py +0 -232
  159. glaip_sdk/utils/rich_utils.py +0 -29
  160. glaip_sdk-0.0.7.dist-info/RECORD +0 -55
  161. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.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,25 +13,83 @@ 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
- from glaip_sdk.branding import AIPBranding
16
+ from glaip_sdk.branding import (
17
+ ERROR,
18
+ ERROR_STYLE,
19
+ INFO,
20
+ INFO_STYLE,
21
+ NEUTRAL,
22
+ SUCCESS,
23
+ SUCCESS_STYLE,
24
+ WARNING,
25
+ WARNING_STYLE,
26
+ AIPBranding,
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
18
31
  from glaip_sdk.cli.commands.agents import agents_group
19
32
  from glaip_sdk.cli.commands.configure import (
20
33
  config_group,
21
34
  configure_command,
22
- load_config,
23
35
  )
24
36
  from glaip_sdk.cli.commands.mcps import mcps_group
25
37
  from glaip_sdk.cli.commands.models import models_group
26
38
  from glaip_sdk.cli.commands.tools import tools_group
27
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
39
+ from glaip_sdk.cli.commands.transcripts import transcripts_group
40
+ from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
41
+ from glaip_sdk.cli.config import load_config
42
+ from glaip_sdk.cli.hints import in_slash_mode
43
+ from glaip_sdk.cli.transcript import get_transcript_cache_stats
44
+ from glaip_sdk.cli.update_notifier import maybe_notify_update
45
+ from glaip_sdk.cli.utils import format_size, sdk_version, spinner_context, update_spinner
46
+ from glaip_sdk.config.constants import (
47
+ DEFAULT_AGENT_RUN_TIMEOUT,
48
+ )
49
+ from glaip_sdk.icons import ICON_AGENT
28
50
  from glaip_sdk.rich_components import AIPPanel, AIPTable
29
51
 
30
52
 
31
- @click.group()
32
- @click.version_option(version=_SDK_VERSION, prog_name="aip")
33
- @click.option("--api-url", envvar="AIP_API_URL", help="AIP API URL")
34
- @click.option("--api-key", envvar="AIP_API_KEY", help="AIP API Key")
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
+
70
+ # Import SlashSession for potential mocking in tests
71
+ try:
72
+ from glaip_sdk.cli.slash import SlashSession
73
+ except ImportError: # pragma: no cover - optional slash dependencies
74
+ # Slash dependencies might not be available in all environments
75
+ SlashSession = None
76
+
77
+ # Constants
78
+ AVAILABLE_STATUS = "āœ… Available"
79
+
80
+
81
+ @click.group(invoke_without_command=True)
82
+ @click.version_option(package_name="glaip-sdk", prog_name="aip")
83
+ @click.option(
84
+ "--api-url",
85
+ help="(Deprecated) AIP API URL; use profiles via --account instead",
86
+ hidden=True,
87
+ )
88
+ @click.option(
89
+ "--api-key",
90
+ help="(Deprecated) AIP API Key; use profiles via --account instead",
91
+ hidden=True,
92
+ )
35
93
  @click.option("--timeout", default=30.0, help="Request timeout in seconds")
36
94
  @click.option(
37
95
  "--view",
@@ -41,6 +99,12 @@ from glaip_sdk.rich_components import AIPPanel, AIPTable
41
99
  help="Output view format",
42
100
  )
43
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
+ )
44
108
  @click.pass_context
45
109
  def main(
46
110
  ctx: Any,
@@ -49,187 +113,395 @@ def main(
49
113
  timeout: float | None,
50
114
  view: str | None,
51
115
  no_tty: bool,
116
+ account_name: str | None,
52
117
  ) -> None:
53
- """GL AIP SDK Command Line Interface.
118
+ r"""GL AIP SDK Command Line Interface.
54
119
 
55
120
  A comprehensive CLI for managing GL AIP resources including
56
121
  agents, tools, MCPs, and more.
57
122
 
123
+ \b
58
124
  Examples:
59
- aip version # Show detailed version info
60
- aip configure # Configure credentials
61
- aip agents list # List all agents
62
- aip tools create my_tool.py # Create a new tool
63
- aip agents run my-agent "Hello world" # Run an agent
125
+ aip version # Show detailed version info
126
+ aip configure # Configure credentials
127
+ aip accounts add prod # Add account profile
128
+ aip accounts use staging # Switch account
129
+ aip agents list # List all agents
130
+ aip tools create my_tool.py # Create a new tool
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'.
64
135
  """
65
-
66
136
  # Store configuration in context
67
137
  ctx.ensure_object(dict)
68
138
  ctx.obj["api_url"] = api_url
69
139
  ctx.obj["api_key"] = api_key
70
140
  ctx.obj["timeout"] = timeout
71
141
  ctx.obj["view"] = view
142
+ ctx.obj["account_name"] = account_name
143
+
144
+ _suppress_chatty_loggers()
72
145
 
73
146
  ctx.obj["tty"] = not no_tty
74
147
 
148
+ launching_slash = (
149
+ ctx.invoked_subcommand is None
150
+ and not ctx.resilient_parsing
151
+ and _should_launch_slash(ctx)
152
+ and SlashSession is not None
153
+ )
154
+
155
+ if not ctx.resilient_parsing and ctx.obj["tty"] and not launching_slash:
156
+ console = Console()
157
+ preferred_console = maybe_notify_update(
158
+ sdk_version(),
159
+ console=console,
160
+ ctx=ctx,
161
+ slash_command="update",
162
+ )
163
+ ctx.obj["_preferred_console"] = preferred_console or console
164
+
165
+ if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
166
+ if launching_slash:
167
+ session = SlashSession(ctx)
168
+ session.run()
169
+ ctx.exit()
170
+ else:
171
+ click.echo(ctx.get_help())
172
+ ctx.exit()
173
+
75
174
 
76
175
  # Add command groups
176
+ main.add_command(accounts_group)
77
177
  main.add_command(agents_group)
78
178
  main.add_command(config_group)
79
179
  main.add_command(tools_group)
80
180
  main.add_command(mcps_group)
81
181
  main.add_command(models_group)
182
+ main.add_command(transcripts_group)
82
183
 
83
184
  # Add top-level commands
84
185
  main.add_command(configure_command)
186
+ main.add_command(update_command)
85
187
 
86
188
 
87
189
  # Tip: `--version` is provided by click.version_option above.
88
190
 
89
191
 
90
- @main.command()
91
- @click.pass_context
92
- def status(ctx: Any) -> None:
93
- """Show connection status and basic info."""
94
- config = {}
95
- try:
96
- console = Console()
192
+ def _should_launch_slash(ctx: click.Context) -> bool:
193
+ """Determine whether to open the command palette automatically."""
194
+ ctx_obj = ctx.obj or {}
195
+ if not bool(ctx_obj.get("tty", True)):
196
+ return False
97
197
 
98
- # Display AIP status banner
99
- branding = AIPBranding.create_from_sdk(
100
- sdk_version=_SDK_VERSION, package_name="glaip-sdk"
101
- )
102
- branding.display_welcome_panel(title="šŸš€ AIP Status")
198
+ if not (sys.stdin.isatty() and sys.stdout.isatty()):
199
+ return False
200
+
201
+ return True
202
+
203
+
204
+ def _load_and_merge_config(ctx: click.Context) -> dict:
205
+ """Load configuration from multiple sources and merge them."""
206
+ context_config = ctx.obj or {}
207
+ account_name = context_config.get("account_name")
208
+
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
+ )
215
+
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")
219
+
220
+ return {
221
+ "api_url": api_url,
222
+ "api_key": api_key,
223
+ "timeout": timeout,
224
+ "_source": source, # Track where credentials came from
225
+ }
103
226
 
104
- # Show AIP Ready status
227
+
228
+ def _validate_config_and_show_error(config: dict, console: Console) -> None:
229
+ """Validate configuration and show error if incomplete."""
230
+ store = get_account_store()
231
+ has_accounts = bool(store.list_accounts())
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"
105
234
  console.print(
106
- f"\n[bold green]āœ… AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
235
+ AIPPanel(
236
+ f"[{ERROR_STYLE}]āŒ Configuration incomplete[/]\n\n"
237
+ f"šŸ” Current config:\n"
238
+ f" • API URL: {config.get('api_url', 'Not set')}\n"
239
+ f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
240
+ f"šŸ’” To fix this:\n"
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}",
244
+ title="āŒ Configuration Error",
245
+ border_style=ERROR,
246
+ ),
107
247
  )
248
+ console.print(f"\n[{SUCCESS_STYLE}]āœ… AIP - Ready[/] (SDK v{sdk_version()}) - Configure to connect")
249
+ sys.exit(1)
108
250
 
109
- # Load config from file and merge with context
110
- file_config = load_config()
111
- context_config = ctx.obj or {}
112
251
 
113
- # Load environment variables (middle priority)
252
+ def _resolve_status_console(ctx: Any) -> tuple[Console, bool]:
253
+ """Return the console to use and whether we are in slash mode."""
254
+ ctx_obj = ctx.obj if isinstance(ctx.obj, dict) else None
255
+ console_override = ctx_obj.get("_slash_console") if ctx_obj else None
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()
261
+ slash_mode = in_slash_mode(ctx)
262
+ return console, slash_mode
114
263
 
115
- env_config = {}
116
- if os.getenv("AIP_API_URL"):
117
- env_config["api_url"] = os.getenv("AIP_API_URL")
118
- if os.getenv("AIP_API_KEY"):
119
- env_config["api_key"] = os.getenv("AIP_API_KEY")
120
264
 
121
- # Filter out None values from context config to avoid overriding other configs
122
- filtered_context = {k: v for k, v in context_config.items() if v is not None}
265
+ def _render_status_heading(console: Console, slash_mode: bool, config: dict) -> bool:
266
+ """Print the status heading/banner.
123
267
 
124
- # Merge configs: file (low) -> env (mid) -> CLI args (high)
125
- config = {**file_config, **env_config, **filtered_context}
268
+ Returns True if a generic ready line was printed (to avoid duplication).
269
+ """
270
+ del slash_mode # heading now consistent across invocation contexts
271
+ ready_printed = False
272
+ console.print(f"[{INFO_STYLE}]GL AIP status[/]")
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
299
+
300
+
301
+ def _collect_cache_summary() -> tuple[str | None, str | None]:
302
+ """Collect transcript cache summary and optional note."""
303
+ try:
304
+ cache_stats = get_transcript_cache_stats()
305
+ except Exception:
306
+ return "[dim]Saved transcripts[/dim]: unavailable", None
126
307
 
127
- if not config.get("api_url") or not config.get("api_key"):
128
- console.print(
129
- AIPPanel(
130
- "[bold red]āŒ Configuration incomplete[/bold red]\n\n"
131
- f"šŸ” Current config:\n"
132
- f" • API URL: {config.get('api_url', 'Not set')}\n"
133
- f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
134
- f"šŸ’” To fix this:\n"
135
- f" • Run 'aip configure' to set up credentials\n"
136
- f" • Or run 'aip config list' to see current config",
137
- title="āŒ Configuration Error",
138
- border_style="red",
139
- )
140
- )
141
- console.print(
142
- f"\n[bold green]āœ… AIP - Ready[/bold green] (SDK v{_SDK_VERSION}) - Configure to connect"
143
- )
144
- sys.exit(1)
308
+ runs_text = f"{cache_stats.entry_count} runs saved"
309
+ if cache_stats.total_bytes:
310
+ size_part = f" Ā· {format_size(cache_stats.total_bytes)} used"
311
+ else:
312
+ size_part = ""
145
313
 
146
- # Try to create client
147
- client = Client(
148
- api_url=config["api_url"],
149
- api_key=config["api_key"],
150
- timeout=config.get("timeout", 30.0),
151
- )
314
+ cache_line = f"[dim]Saved transcripts[/dim]: {runs_text}{size_part} Ā· {cache_stats.cache_dir}"
315
+ return cache_line, None
152
316
 
153
- # Test connection by listing resources
154
- try:
155
- agents = client.list_agents()
156
- tools = client.list_tools()
157
- mcps = client.list_mcps()
158
317
 
159
- # Create status table
160
- table = AIPTable(title="šŸ”— GL AIP Status")
161
- table.add_column("Resource", style="cyan", width=15)
162
- table.add_column("Count", style="green", width=10)
163
- table.add_column("Status", style="green", width=15)
318
+ def _display_cache_summary(console: Console, slash_mode: bool, cache_line: str | None, cache_note: str | None) -> None:
319
+ """Render the cache summary details."""
320
+ if cache_line:
321
+ console.print(cache_line)
322
+ if cache_note and not slash_mode:
323
+ console.print(cache_note)
164
324
 
165
- table.add_row("Agents", str(len(agents)), "āœ… Available")
166
- table.add_row("Tools", str(len(tools)), "āœ… Available")
167
- table.add_row("MCPs", str(len(mcps)), "āœ… Available")
168
325
 
169
- console.print(
170
- AIPPanel(
171
- f"[bold green]āœ… Connected to GL AIP[/bold green]\n"
172
- f"šŸ”— API URL: {client.api_url}\n"
173
- f"šŸ¤– Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
174
- title="šŸš€ Connection Status",
175
- border_style="green",
176
- )
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
177
335
  )
336
+ return []
337
+ return []
178
338
 
179
- console.print(table)
180
339
 
181
- except Exception as e:
182
- # Show AIP Ready status even if connection fails
183
- console.print(
184
- f"\n[bold green]āœ… AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
185
- )
340
+ def _get_client_from_config(config: dict) -> Any:
341
+ """Return a Client instance built from config."""
342
+ return Client(
343
+ api_url=config["api_url"],
344
+ api_key=config["api_key"],
345
+ timeout=config.get("timeout", 30.0),
346
+ )
347
+
186
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
354
+ try:
355
+ with spinner_context(
356
+ None,
357
+ "[bold blue]Checking GL AIP status…[/bold blue]",
358
+ console_override=console,
359
+ spinner_style=INFO,
360
+ ) as status_indicator:
361
+ update_spinner(status_indicator, "[bold blue]Fetching agents…[/bold blue]")
362
+ agents = client.list_agents()
363
+
364
+ update_spinner(status_indicator, "[bold blue]Fetching tools…[/bold blue]")
365
+ tools = client.list_tools()
366
+
367
+ update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
368
+ mcps = client.list_mcps()
369
+ except Exception as e:
370
+ # Show AIP Ready status even if connection fails
371
+ if compact:
372
+ status_text = "API call failed"
373
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
374
+ console.print(f"[dim]• Base URL[/dim]: {api_url} ({status_text})")
375
+ console.print(f"[{ERROR_STYLE}]• Error[/]: {e}")
376
+ console.print("[dim]• Tip[/dim]: Check network connectivity or API permissions and try again.")
377
+ console.print("[dim]• Resources[/dim]: unavailable")
378
+ else:
379
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
187
380
  console.print(
188
381
  AIPPanel(
189
- f"[bold yellow]āš ļø Connection established but API call failed[/bold yellow]\n"
190
- f"šŸ”— API URL: {client.api_url}\n"
382
+ f"[{WARNING_STYLE}]āš ļø Connection established but API call failed[/]\n"
383
+ f"šŸ”— API URL: {api_url}\n"
191
384
  f"āŒ Error: {e}\n\n"
192
385
  f"šŸ’” This usually means:\n"
193
386
  f" • Network connectivity issues\n"
194
387
  f" • API permissions problems\n"
195
388
  f" • Backend service issues",
196
389
  title="āš ļø Partial Connection",
197
- border_style="yellow",
198
- )
390
+ border_style=WARNING,
391
+ ),
199
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
+ )
200
420
 
201
- client.close()
421
+ console.print(table) # pragma: no cover - UI display formatting
422
+
423
+ return client
424
+
425
+
426
+ def _handle_connection_error(config: dict, console: Console, error: Exception) -> None:
427
+ """Handle connection errors and show troubleshooting information."""
428
+ console.print(
429
+ AIPPanel(
430
+ f"[{ERROR_STYLE}]āŒ Connection failed[/]\n\n"
431
+ f"šŸ” Error: {error}\n\n"
432
+ f"šŸ’” Troubleshooting steps:\n"
433
+ f" • Verify your API URL and key are correct\n"
434
+ f" • Check network connectivity to {config.get('api_url', 'your API')}\n"
435
+ f" • Run 'aip configure' to update credentials\n"
436
+ f" • Run 'aip config list' to check configuration",
437
+ title="āŒ Connection Error",
438
+ border_style=ERROR,
439
+ ),
440
+ )
441
+ # Log and return; callers decide whether to exit.
442
+
443
+
444
+ @main.command()
445
+ @click.option(
446
+ "--account",
447
+ "account_name",
448
+ help="Target a named account profile for this command",
449
+ )
450
+ @click.pass_context
451
+ def status(ctx: Any, account_name: str | None) -> None:
452
+ """Show connection status and basic info."""
453
+ config: dict = {}
454
+ console: Console | None = None
455
+ try:
456
+ if account_name:
457
+ if ctx.obj is None:
458
+ ctx.obj = {}
459
+ ctx.obj["account_name"] = account_name
460
+
461
+ console, slash_mode = _resolve_status_console(ctx)
462
+
463
+ # Load and merge configuration
464
+ config = _load_and_merge_config(ctx)
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
+
477
+ # Validate configuration
478
+ _validate_config_and_show_error(config, console)
479
+
480
+ # Create and test client connection using unified compact layout
481
+ client = _create_and_test_client(config, console, compact=True)
482
+ close = getattr(client, "close", None)
483
+ if callable(close):
484
+ try:
485
+ close()
486
+ except Exception:
487
+ pass
202
488
 
203
489
  except Exception as e:
204
- console.print(
205
- AIPPanel(
206
- f"[bold red]āŒ Connection failed[/bold red]\n\n"
207
- f"šŸ” Error: {e}\n\n"
208
- f"šŸ’” Troubleshooting steps:\n"
209
- f" • Run 'aip config list' to check configuration\n"
210
- f" • Run 'aip configure' to update credentials\n"
211
- f" • Verify your API URL and key are correct\n"
212
- f" • Check network connectivity to {config.get('api_url', 'your API')}",
213
- title="āŒ Connection Error",
214
- border_style="red",
215
- )
216
- )
490
+ # Handle any unexpected errors during the process and exit with error code
491
+ fallback_console = console or Console()
492
+ _handle_connection_error(config or {}, fallback_console, e)
217
493
  sys.exit(1)
218
494
 
219
495
 
220
496
  @main.command()
221
497
  def version() -> None:
222
498
  """Show version information."""
223
- branding = AIPBranding.create_from_sdk(
224
- sdk_version=_SDK_VERSION, package_name="glaip-sdk"
225
- )
499
+ branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
226
500
  branding.display_version_panel()
227
501
 
228
502
 
229
503
  @main.command()
230
- @click.option(
231
- "--check-only", is_flag=True, help="Only check for updates without installing"
232
- )
504
+ @click.option("--check-only", is_flag=True, help="Only check for updates without installing")
233
505
  @click.option(
234
506
  "--force",
235
507
  is_flag=True,
@@ -237,54 +509,57 @@ def version() -> None:
237
509
  )
238
510
  def update(check_only: bool, force: bool) -> None:
239
511
  """Update AIP SDK to the latest version from PyPI."""
512
+ slash_mode = in_slash_mode()
240
513
  try:
241
514
  console = Console()
242
515
 
243
516
  if check_only:
244
517
  console.print(
245
518
  AIPPanel(
246
- "[bold blue]šŸ” Checking for updates...[/bold blue]\n\n"
247
- "šŸ’” To install updates, run: aip update",
519
+ "[bold blue]šŸ” Checking for updates...[/bold blue]\n\nšŸ’” To install updates, run: aip update",
248
520
  title="šŸ“‹ Update Check",
249
521
  border_style="blue",
250
- )
522
+ ),
251
523
  )
252
524
  return
253
525
 
526
+ update_hint = ""
527
+ if not slash_mode:
528
+ update_hint = "\nšŸ’” Use --check-only to just check for updates"
529
+
254
530
  console.print(
255
531
  AIPPanel(
256
532
  "[bold blue]šŸ”„ Updating AIP SDK...[/bold blue]\n\n"
257
- "šŸ“¦ This will update the package from PyPI\n"
258
- "šŸ’” Use --check-only to just check for updates",
259
- title="šŸš€ Update Process",
533
+ "šŸ“¦ This will update the package from PyPI"
534
+ f"{update_hint}",
535
+ title="Update Process",
260
536
  border_style="blue",
261
537
  padding=(0, 1),
262
- )
538
+ ),
263
539
  )
264
540
 
265
541
  # Update using pip
266
542
  try:
267
- cmd = [
268
- sys.executable,
269
- "-m",
270
- "pip",
271
- "install",
272
- "--upgrade",
273
- "glaip-sdk",
274
- ]
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"
275
546
  if force:
276
547
  cmd.insert(5, "--force-reinstall")
277
548
  subprocess.run(cmd, capture_output=True, text=True, check=True)
278
549
 
550
+ verify_hint = ""
551
+ if not slash_mode:
552
+ verify_hint = "\nšŸ’” Restart your terminal or run 'aip --version' to verify"
553
+
279
554
  console.print(
280
555
  AIPPanel(
281
- "[bold green]āœ… Update successful![/bold green]\n\n"
282
- "šŸ”„ AIP SDK has been updated to the latest version\n"
283
- "šŸ’” Restart your terminal or run 'aip --version' to verify",
556
+ f"[{SUCCESS_STYLE}]āœ… Update successful![/]\n\n"
557
+ "šŸ”„ AIP SDK has been updated to the latest version"
558
+ f"{verify_hint}",
284
559
  title="šŸŽ‰ Update Complete",
285
- border_style="green",
560
+ border_style=SUCCESS,
286
561
  padding=(0, 1),
287
- )
562
+ ),
288
563
  )
289
564
 
290
565
  # Show new version
@@ -299,31 +574,31 @@ def update(check_only: bool, force: bool) -> None:
299
574
  except subprocess.CalledProcessError as e:
300
575
  console.print(
301
576
  AIPPanel(
302
- f"[bold red]āŒ Update failed[/bold red]\n\n"
577
+ f"[{ERROR_STYLE}]āŒ Update failed[/]\n\n"
303
578
  f"šŸ” Error: {e.stderr}\n\n"
304
579
  "šŸ’” Troubleshooting:\n"
305
580
  " • Check your internet connection\n"
306
581
  " • Try running: pip install --upgrade glaip-sdk\n"
307
582
  " • Check if you have write permissions",
308
583
  title="āŒ Update Error",
309
- border_style="red",
584
+ border_style=ERROR,
310
585
  padding=(0, 1),
311
- )
586
+ ),
312
587
  )
313
588
  sys.exit(1)
314
589
 
315
590
  except ImportError:
316
591
  console.print(
317
592
  AIPPanel(
318
- "[bold red]āŒ Rich library not available[/bold red]\n\n"
593
+ f"[{ERROR_STYLE}]āŒ Rich library not available[/]\n\n"
319
594
  "šŸ’” Install rich: pip install rich\n"
320
595
  " Then try: aip update",
321
596
  title="āŒ Missing Dependency",
322
- border_style="red",
323
- )
597
+ border_style=ERROR,
598
+ ),
324
599
  )
325
600
  sys.exit(1)
326
601
 
327
602
 
328
603
  if __name__ == "__main__":
329
- main()
604
+ main() # pylint: disable=no-value-for-parameter