glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.12__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 (127) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +217 -42
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  8. glaip_sdk/cli/commands/agents/_common.py +561 -0
  9. glaip_sdk/cli/commands/agents/create.py +151 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +369 -23
  55. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +79 -5
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1027 -88
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +87 -0
  60. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  61. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  62. glaip_sdk/cli/slash/tui/layouts/harlequin.py +160 -0
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +119 -12
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +374 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +5 -3
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +50 -8
  78. glaip_sdk/client/hitl.py +136 -0
  79. glaip_sdk/client/main.py +7 -1
  80. glaip_sdk/client/mcps.py +44 -13
  81. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  82. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  83. glaip_sdk/client/payloads/agent/responses.py +43 -0
  84. glaip_sdk/client/run_rendering.py +414 -3
  85. glaip_sdk/client/schedules.py +439 -0
  86. glaip_sdk/client/tools.py +57 -26
  87. glaip_sdk/guardrails/__init__.py +80 -0
  88. glaip_sdk/guardrails/serializer.py +89 -0
  89. glaip_sdk/hitl/__init__.py +48 -0
  90. glaip_sdk/hitl/base.py +64 -0
  91. glaip_sdk/hitl/callback.py +43 -0
  92. glaip_sdk/hitl/local.py +121 -0
  93. glaip_sdk/hitl/remote.py +523 -0
  94. glaip_sdk/models/__init__.py +17 -0
  95. glaip_sdk/models/agent_runs.py +2 -1
  96. glaip_sdk/models/schedule.py +224 -0
  97. glaip_sdk/payload_schemas/agent.py +1 -0
  98. glaip_sdk/payload_schemas/guardrails.py +34 -0
  99. glaip_sdk/registry/tool.py +273 -59
  100. glaip_sdk/runner/__init__.py +20 -3
  101. glaip_sdk/runner/deps.py +5 -8
  102. glaip_sdk/runner/langgraph.py +318 -42
  103. glaip_sdk/runner/logging_config.py +77 -0
  104. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  105. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  106. glaip_sdk/schedules/__init__.py +22 -0
  107. glaip_sdk/schedules/base.py +291 -0
  108. glaip_sdk/tools/base.py +67 -14
  109. glaip_sdk/utils/__init__.py +1 -0
  110. glaip_sdk/utils/bundler.py +138 -2
  111. glaip_sdk/utils/import_resolver.py +43 -11
  112. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  113. glaip_sdk/utils/runtime_config.py +15 -12
  114. glaip_sdk/utils/sync.py +31 -11
  115. glaip_sdk/utils/tool_detection.py +274 -6
  116. glaip_sdk/utils/tool_storage_provider.py +140 -0
  117. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/METADATA +49 -37
  118. glaip_sdk-0.7.12.dist-info/RECORD +219 -0
  119. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/WHEEL +2 -1
  120. glaip_sdk-0.7.12.dist-info/entry_points.txt +2 -0
  121. glaip_sdk-0.7.12.dist-info/top_level.txt +1 -0
  122. glaip_sdk/cli/commands/agents.py +0 -1509
  123. glaip_sdk/cli/commands/mcps.py +0 -1356
  124. glaip_sdk/cli/commands/tools.py +0 -576
  125. glaip_sdk/cli/utils.py +0 -263
  126. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  127. glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
@@ -6,9 +6,12 @@ Author:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import importlib.util
10
+ import os
9
11
  import subprocess
10
12
  import sys
11
13
  from collections.abc import Sequence
14
+ from pathlib import Path
12
15
 
13
16
  import click
14
17
  from rich.console import Console
@@ -18,19 +21,132 @@ from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO_STYLE, SUCCESS_ST
18
21
  PACKAGE_NAME = "glaip-sdk"
19
22
 
20
23
 
21
- def _build_upgrade_command(include_prerelease: bool) -> Sequence[str]:
22
- """Return the pip command used to upgrade the SDK."""
23
- command = [
24
- sys.executable,
25
- "-m",
26
- "pip",
27
- "install",
28
- "--upgrade",
29
- PACKAGE_NAME,
30
- ]
24
+ def _is_uv_managed_environment() -> bool:
25
+ """Check if running in a uv-managed tool environment.
26
+
27
+ Uses a path-based heuristic against sys.executable, sys.prefix, and UV_TOOL_DIR
28
+ or UV_TOOL_BIN to detect a case-insensitive "uv/tools" segment. Update if uv
29
+ changes its layout.
30
+ """
31
+ if _has_uv_tool_path(sys.executable):
32
+ return True
33
+ if _has_uv_tool_path(sys.prefix):
34
+ return True
35
+ uv_tool_dir = os.environ.get("UV_TOOL_DIR") or os.environ.get("UV_TOOL_BIN")
36
+ if uv_tool_dir and _has_uv_tool_path(uv_tool_dir):
37
+ return True
38
+ return False
39
+
40
+
41
+ def _has_uv_tool_path(path: str) -> bool:
42
+ """Return True when a path contains a case-insensitive uv/tools segment."""
43
+ parts = [part.lower() for part in Path(path).parts]
44
+ for idx, part in enumerate(parts[:-1]):
45
+ if part == "uv" and parts[idx + 1] == "tools":
46
+ return True
47
+ return False
48
+
49
+
50
+ def _is_pip_available() -> bool:
51
+ """Return True when pip can be imported in the current interpreter."""
52
+ return importlib.util.find_spec("pip") is not None
53
+
54
+
55
+ def _build_missing_pip_guidance(
56
+ *,
57
+ include_prerelease: bool,
58
+ package_name: str = PACKAGE_NAME,
59
+ force_reinstall: bool = False,
60
+ ) -> tuple[str, str]:
61
+ """Return error and troubleshooting guidance when pip is unavailable."""
62
+ manual_cmd = _build_manual_upgrade_command(
63
+ include_prerelease,
64
+ package_name=package_name,
65
+ is_uv=True,
66
+ force_reinstall=force_reinstall,
67
+ )
68
+ error_detail = "pip is not available in this environment."
69
+ troubleshooting = (
70
+ "💡 Troubleshooting:\n"
71
+ f" • If you installed via uv tool, run: {manual_cmd}\n"
72
+ " • Otherwise install pip: python -m ensurepip"
73
+ )
74
+ return error_detail, troubleshooting
75
+
76
+
77
+ def _build_command_parts(
78
+ *,
79
+ package_name: str = PACKAGE_NAME,
80
+ is_uv: bool | None = None,
81
+ force_reinstall: bool = False,
82
+ include_prerelease: bool = False,
83
+ ) -> tuple[list[str], str]:
84
+ """Build the common parts of upgrade commands.
85
+
86
+ Returns:
87
+ Tuple of (command parts list, force_reinstall flag name).
88
+ For uv: (["uv", "tool", "install", "--upgrade", package_name], "--reinstall")
89
+ For pip: (["pip", "install", "--upgrade", package_name], "--force-reinstall")
90
+ """
91
+ if is_uv is None:
92
+ is_uv = _is_uv_managed_environment()
93
+
94
+ if is_uv:
95
+ command = ["uv", "tool", "install", "--upgrade", package_name]
96
+ reinstall_flag = "--reinstall"
97
+ else:
98
+ command = ["pip", "install", "--upgrade", package_name]
99
+ reinstall_flag = "--force-reinstall"
100
+
101
+ if force_reinstall:
102
+ command.insert(-1, reinstall_flag)
103
+
31
104
  if include_prerelease:
32
105
  command.append("--pre")
33
- return command
106
+
107
+ return command, reinstall_flag
108
+
109
+
110
+ def _build_upgrade_command(
111
+ include_prerelease: bool,
112
+ *,
113
+ package_name: str = PACKAGE_NAME,
114
+ is_uv: bool | None = None,
115
+ force_reinstall: bool = False,
116
+ ) -> Sequence[str]:
117
+ """Return the command used to upgrade the SDK (pip or uv tool install)."""
118
+ if is_uv is None:
119
+ is_uv = _is_uv_managed_environment()
120
+
121
+ command_parts, _ = _build_command_parts(
122
+ package_name=package_name,
123
+ is_uv=is_uv,
124
+ force_reinstall=force_reinstall,
125
+ include_prerelease=include_prerelease,
126
+ )
127
+
128
+ # For pip, prepend sys.executable and -m
129
+ if not is_uv:
130
+ command_parts = [sys.executable, "-m"] + command_parts
131
+
132
+ return command_parts
133
+
134
+
135
+ def _build_manual_upgrade_command(
136
+ include_prerelease: bool,
137
+ *,
138
+ package_name: str = PACKAGE_NAME,
139
+ is_uv: bool | None = None,
140
+ force_reinstall: bool = False,
141
+ ) -> str:
142
+ """Return a manual upgrade command string matching the active environment."""
143
+ command_parts, _ = _build_command_parts(
144
+ package_name=package_name,
145
+ is_uv=is_uv,
146
+ force_reinstall=force_reinstall,
147
+ include_prerelease=include_prerelease,
148
+ )
149
+ return " ".join(command_parts)
34
150
 
35
151
 
36
152
  @click.command(name="update")
@@ -40,22 +156,52 @@ def _build_upgrade_command(include_prerelease: bool) -> Sequence[str]:
40
156
  is_flag=True,
41
157
  help="Include pre-release versions when upgrading.",
42
158
  )
43
- def update_command(include_prerelease: bool) -> None:
44
- """Upgrade the glaip-sdk package using pip."""
159
+ @click.option(
160
+ "--reinstall",
161
+ "force_reinstall",
162
+ is_flag=True,
163
+ help="Force reinstall even if already up-to-date.",
164
+ )
165
+ def update_command(include_prerelease: bool, force_reinstall: bool) -> None:
166
+ """Upgrade the glaip-sdk package using pip or uv tool install."""
45
167
  console = Console()
46
- upgrade_cmd = _build_upgrade_command(include_prerelease)
168
+ # Call _is_uv_managed_environment() once and pass explicitly to avoid redundant calls
169
+ is_uv = _is_uv_managed_environment()
170
+ if not is_uv and not _is_pip_available():
171
+ error_detail, troubleshooting = _build_missing_pip_guidance(
172
+ include_prerelease=include_prerelease,
173
+ force_reinstall=force_reinstall,
174
+ )
175
+ raise click.ClickException(f"{error_detail}\n{troubleshooting}")
176
+ upgrade_cmd = _build_upgrade_command(
177
+ include_prerelease,
178
+ is_uv=is_uv,
179
+ force_reinstall=force_reinstall,
180
+ )
181
+
182
+ # Determine the appropriate manual command for error messages
183
+ manual_cmd = _build_manual_upgrade_command(
184
+ include_prerelease,
185
+ is_uv=is_uv,
186
+ force_reinstall=force_reinstall,
187
+ )
188
+
47
189
  console.print(f"[{ACCENT_STYLE}]Upgrading {PACKAGE_NAME} using[/] [{INFO_STYLE}]{' '.join(upgrade_cmd)}[/]")
48
190
 
49
191
  try:
50
192
  subprocess.run(upgrade_cmd, check=True)
51
193
  except FileNotFoundError as exc:
194
+ if is_uv:
195
+ raise click.ClickException(
196
+ f"Unable to locate uv executable. Please ensure uv is installed and on your PATH.\n"
197
+ f"Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh\n"
198
+ f"Or run manually: {manual_cmd}"
199
+ ) from exc
52
200
  raise click.ClickException(
53
201
  "Unable to locate Python executable to run pip. Please ensure Python is installed and try again."
54
202
  ) from exc
55
203
  except subprocess.CalledProcessError as exc:
56
- console.print(
57
- f"[{ERROR_STYLE}]Automatic upgrade failed.[/] Please run `pip install -U {PACKAGE_NAME}` manually."
58
- )
204
+ console.print(f"[{ERROR_STYLE}]Automatic upgrade failed.[/] Please run `{manual_cmd}` manually.")
59
205
  raise click.ClickException("Automatic upgrade failed.") from exc
60
206
 
61
207
  console.print(f"[{SUCCESS_STYLE}]✅ {PACKAGE_NAME} upgraded successfully.[/]")
glaip_sdk/cli/config.py CHANGED
@@ -47,6 +47,7 @@ _PRESERVE_KEYS = {
47
47
  "version",
48
48
  "active_account",
49
49
  "accounts",
50
+ "tui",
50
51
  }
51
52
 
52
53
 
@@ -29,12 +29,6 @@ from glaip_sdk.cli.io import export_resource_to_file_with_validation
29
29
  from glaip_sdk.cli.rich_helpers import markup_text, print_markup
30
30
  from glaip_sdk.rich_components import AIPPanel, AIPTable
31
31
  from glaip_sdk.utils import format_datetime, is_uuid
32
-
33
- try:
34
- from glaip_sdk import _version as _version_module
35
- except ImportError: # pragma: no cover - defensive import
36
- _version_module = None
37
-
38
32
  from .prompting import (
39
33
  _fuzzy_pick,
40
34
  _fuzzy_pick_for_resources,
@@ -43,6 +37,9 @@ from .prompting import (
43
37
  )
44
38
  from .rendering import _spinner_stop, _spinner_update, spinner_context
45
39
 
40
+ _VERSION_MODULE_MISSING = object()
41
+ _version_module: Any | None = _VERSION_MODULE_MISSING
42
+
46
43
  console = Console()
47
44
  pager.console = console
48
45
  logger = logging.getLogger("glaip_sdk.cli.core.output")
@@ -236,7 +233,15 @@ def handle_resource_export(
236
233
 
237
234
  def sdk_version() -> str:
238
235
  """Return the current SDK version, warning if metadata is unavailable."""
239
- global _WARNED_SDK_VERSION_FALLBACK
236
+ global _WARNED_SDK_VERSION_FALLBACK, _version_module
237
+
238
+ if _version_module is _VERSION_MODULE_MISSING:
239
+ try:
240
+ from importlib import import_module # noqa: PLC0415
241
+
242
+ _version_module = import_module("glaip_sdk._version")
243
+ except Exception: # pragma: no cover - defensive fallback
244
+ _version_module = None
240
245
 
241
246
  if _version_module is None:
242
247
  if not _WARNED_SDK_VERSION_FALLBACK:
@@ -0,0 +1,20 @@
1
+ """Entry point wrapper for early logging configuration.
2
+
3
+ This must be imported BEFORE glaip_sdk.cli.main to catch import-time warnings.
4
+
5
+ Authors:
6
+ Raymond Christopher (raymond.christopher@gdplabs.id)
7
+ """
8
+
9
+ import sys
10
+
11
+ # Configure logging BEFORE importing anything else
12
+ from glaip_sdk.runner.logging_config import setup_cli_logging
13
+
14
+ setup_cli_logging()
15
+
16
+ # Now import and run CLI
17
+ from glaip_sdk.cli import main # noqa: E402
18
+
19
+ if __name__ == "__main__":
20
+ sys.exit(main()) # pylint: disable=no-value-for-parameter
glaip_sdk/cli/main.py CHANGED
@@ -11,8 +11,6 @@ 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
14
  from glaip_sdk.branding import (
17
15
  ERROR,
18
16
  ERROR_STYLE,
@@ -37,21 +35,50 @@ from glaip_sdk.cli.commands.mcps import mcps_group
37
35
  from glaip_sdk.cli.commands.models import models_group
38
36
  from glaip_sdk.cli.commands.tools import tools_group
39
37
  from glaip_sdk.cli.commands.transcripts import transcripts_group
40
- from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
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
+ )
41
46
  from glaip_sdk.cli.config import load_config
42
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
43
50
  from glaip_sdk.cli.transcript import get_transcript_cache_stats
44
51
  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
52
  from glaip_sdk.config.constants import (
47
53
  DEFAULT_AGENT_RUN_TIMEOUT,
48
54
  )
49
55
  from glaip_sdk.icons import ICON_AGENT
50
56
  from glaip_sdk.rich_components import AIPPanel, AIPTable
51
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
+
52
73
 
53
74
  def _suppress_chatty_loggers() -> None:
54
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
55
82
  noisy_loggers = [
56
83
  "glaip_sdk.client",
57
84
  "httpx",
@@ -227,26 +254,40 @@ def _load_and_merge_config(ctx: click.Context) -> dict:
227
254
 
228
255
  def _validate_config_and_show_error(config: dict, console: Console) -> None:
229
256
  """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"
234
- console.print(
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
- ),
247
- )
248
- console.print(f"\n[{SUCCESS_STYLE}]✅ AIP - Ready[/] (SDK v{sdk_version()}) - Configure to connect")
249
- 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)
250
291
 
251
292
 
252
293
  def _resolve_status_console(ctx: Any) -> tuple[Console, bool]:
@@ -339,14 +380,15 @@ def _safe_list_call(obj: Any, attr: str) -> list[Any]:
339
380
 
340
381
  def _get_client_from_config(config: dict) -> Any:
341
382
  """Return a Client instance built from config."""
342
- return Client(
383
+ client_class = _resolve_client_class()
384
+ return client_class(
343
385
  api_url=config["api_url"],
344
386
  api_key=config["api_key"],
345
387
  timeout=config.get("timeout", 30.0),
346
388
  )
347
389
 
348
390
 
349
- def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
391
+ def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Any:
350
392
  """Create client and test connection by fetching resources."""
351
393
  client: Any = _get_client_from_config(config)
352
394
 
@@ -538,13 +580,39 @@ def update(check_only: bool, force: bool) -> None:
538
580
  ),
539
581
  )
540
582
 
541
- # Update using pip
583
+ # Update using pip or uv tool install
542
584
  try:
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"
546
- if force:
547
- 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
+ )
548
616
  subprocess.run(cmd, capture_output=True, text=True, check=True)
549
617
 
550
618
  verify_hint = ""
@@ -571,16 +639,36 @@ def update(check_only: bool, force: bool) -> None:
571
639
  )
572
640
  console.print(f"📋 New version: {version_result.stdout.strip()}")
573
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)
574
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
+
575
668
  console.print(
576
669
  AIPPanel(
577
- f"[{ERROR_STYLE}]❌ Update failed[/]\n\n"
578
- f"🔍 Error: {e.stderr}\n\n"
579
- "💡 Troubleshooting:\n"
580
- " • Check your internet connection\n"
581
- " • Try running: pip install --upgrade glaip-sdk\n"
582
- " • Check if you have write permissions",
583
- title="❌ Update Error",
670
+ f"[{ERROR_STYLE}]❌ Update failed[/]\n\n🔍 Error: {e.stderr}\n\n{troubleshooting}",
671
+ title=UPDATE_ERROR_TITLE,
584
672
  border_style=ERROR,
585
673
  padding=(0, 1),
586
674
  ),
glaip_sdk/cli/pager.py CHANGED
@@ -46,11 +46,11 @@ def _get_console() -> Console:
46
46
  """
47
47
  global console
48
48
  try:
49
- cli_utils = importlib.import_module("glaip_sdk.cli.utils")
49
+ cli_output = importlib.import_module("glaip_sdk.cli.core.output")
50
50
  except Exception: # pragma: no cover - fallback during import cycles
51
- cli_utils = None
51
+ cli_output = None
52
52
 
53
- current_console = getattr(cli_utils, "console", None) if cli_utils else None
53
+ current_console = getattr(cli_output, "console", None) if cli_output else None
54
54
  if current_console is not None and current_console is not console:
55
55
  console = current_console
56
56
 
@@ -13,7 +13,8 @@ from typing import Any
13
13
  import click
14
14
 
15
15
  from glaip_sdk.branding import ACCENT_STYLE
16
- from glaip_sdk.cli.utils import resolve_resource, spinner_context
16
+ from glaip_sdk.cli.core.output import resolve_resource
17
+ from glaip_sdk.cli.core.rendering import spinner_context
17
18
 
18
19
 
19
20
  def resolve_resource_reference(