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