glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1250 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +271 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  12. glaip_sdk/cli/commands/agents/_common.py +561 -0
  13. glaip_sdk/cli/commands/agents/create.py +151 -0
  14. glaip_sdk/cli/commands/agents/delete.py +64 -0
  15. glaip_sdk/cli/commands/agents/get.py +89 -0
  16. glaip_sdk/cli/commands/agents/list.py +129 -0
  17. glaip_sdk/cli/commands/agents/run.py +264 -0
  18. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  19. glaip_sdk/cli/commands/agents/update.py +112 -0
  20. glaip_sdk/cli/commands/common_config.py +104 -0
  21. glaip_sdk/cli/commands/configure.py +734 -143
  22. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  23. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  24. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  25. glaip_sdk/cli/commands/mcps/create.py +152 -0
  26. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  27. glaip_sdk/cli/commands/mcps/get.py +212 -0
  28. glaip_sdk/cli/commands/mcps/list.py +69 -0
  29. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  30. glaip_sdk/cli/commands/mcps/update.py +190 -0
  31. glaip_sdk/cli/commands/models.py +14 -12
  32. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  33. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  34. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  35. glaip_sdk/cli/commands/tools/_common.py +80 -0
  36. glaip_sdk/cli/commands/tools/create.py +228 -0
  37. glaip_sdk/cli/commands/tools/delete.py +61 -0
  38. glaip_sdk/cli/commands/tools/get.py +103 -0
  39. glaip_sdk/cli/commands/tools/list.py +69 -0
  40. glaip_sdk/cli/commands/tools/script.py +49 -0
  41. glaip_sdk/cli/commands/tools/update.py +102 -0
  42. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  43. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  44. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  45. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  46. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  47. glaip_sdk/cli/commands/update.py +164 -23
  48. glaip_sdk/cli/config.py +49 -7
  49. glaip_sdk/cli/constants.py +38 -0
  50. glaip_sdk/cli/context.py +8 -0
  51. glaip_sdk/cli/core/__init__.py +79 -0
  52. glaip_sdk/cli/core/context.py +124 -0
  53. glaip_sdk/cli/core/output.py +851 -0
  54. glaip_sdk/cli/core/prompting.py +649 -0
  55. glaip_sdk/cli/core/rendering.py +187 -0
  56. glaip_sdk/cli/display.py +45 -32
  57. glaip_sdk/cli/entrypoint.py +20 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +14 -17
  60. glaip_sdk/cli/main.py +344 -167
  61. glaip_sdk/cli/masking.py +21 -33
  62. glaip_sdk/cli/mcp_validators.py +5 -15
  63. glaip_sdk/cli/pager.py +15 -22
  64. glaip_sdk/cli/parsers/__init__.py +1 -3
  65. glaip_sdk/cli/parsers/json_input.py +11 -22
  66. glaip_sdk/cli/resolution.py +5 -10
  67. glaip_sdk/cli/rich_helpers.py +1 -3
  68. glaip_sdk/cli/slash/__init__.py +0 -9
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +65 -29
  72. glaip_sdk/cli/slash/prompt.py +24 -10
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +827 -232
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +12 -52
  91. glaip_sdk/cli/transcript/cache.py +258 -60
  92. glaip_sdk/cli/transcript/capture.py +72 -21
  93. glaip_sdk/cli/transcript/history.py +815 -0
  94. glaip_sdk/cli/transcript/launcher.py +1 -3
  95. glaip_sdk/cli/transcript/viewer.py +79 -329
  96. glaip_sdk/cli/update_notifier.py +385 -24
  97. glaip_sdk/cli/validators.py +16 -18
  98. glaip_sdk/client/__init__.py +3 -1
  99. glaip_sdk/client/_schedule_payloads.py +89 -0
  100. glaip_sdk/client/agent_runs.py +147 -0
  101. glaip_sdk/client/agents.py +370 -100
  102. glaip_sdk/client/base.py +78 -35
  103. glaip_sdk/client/hitl.py +136 -0
  104. glaip_sdk/client/main.py +25 -10
  105. glaip_sdk/client/mcps.py +166 -27
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +583 -79
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +214 -56
  113. glaip_sdk/client/validators.py +20 -48
  114. glaip_sdk/config/constants.py +11 -0
  115. glaip_sdk/exceptions.py +1 -3
  116. glaip_sdk/hitl/__init__.py +48 -0
  117. glaip_sdk/hitl/base.py +64 -0
  118. glaip_sdk/hitl/callback.py +43 -0
  119. glaip_sdk/hitl/local.py +121 -0
  120. glaip_sdk/hitl/remote.py +523 -0
  121. glaip_sdk/icons.py +9 -3
  122. glaip_sdk/mcps/__init__.py +21 -0
  123. glaip_sdk/mcps/base.py +345 -0
  124. glaip_sdk/models/__init__.py +107 -0
  125. glaip_sdk/models/agent.py +47 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/mcp.py +33 -0
  129. glaip_sdk/models/schedule.py +224 -0
  130. glaip_sdk/models/tool.py +33 -0
  131. glaip_sdk/payload_schemas/__init__.py +1 -13
  132. glaip_sdk/payload_schemas/agent.py +1 -3
  133. glaip_sdk/registry/__init__.py +55 -0
  134. glaip_sdk/registry/agent.py +164 -0
  135. glaip_sdk/registry/base.py +139 -0
  136. glaip_sdk/registry/mcp.py +253 -0
  137. glaip_sdk/registry/tool.py +445 -0
  138. glaip_sdk/rich_components.py +58 -2
  139. glaip_sdk/runner/__init__.py +76 -0
  140. glaip_sdk/runner/base.py +84 -0
  141. glaip_sdk/runner/deps.py +112 -0
  142. glaip_sdk/runner/langgraph.py +872 -0
  143. glaip_sdk/runner/logging_config.py +77 -0
  144. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  145. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  146. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  147. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  148. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  149. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  150. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  151. glaip_sdk/schedules/__init__.py +22 -0
  152. glaip_sdk/schedules/base.py +291 -0
  153. glaip_sdk/tools/__init__.py +22 -0
  154. glaip_sdk/tools/base.py +468 -0
  155. glaip_sdk/utils/__init__.py +59 -12
  156. glaip_sdk/utils/a2a/__init__.py +34 -0
  157. glaip_sdk/utils/a2a/event_processor.py +188 -0
  158. glaip_sdk/utils/agent_config.py +4 -14
  159. glaip_sdk/utils/bundler.py +403 -0
  160. glaip_sdk/utils/client.py +111 -0
  161. glaip_sdk/utils/client_utils.py +46 -28
  162. glaip_sdk/utils/datetime_helpers.py +58 -0
  163. glaip_sdk/utils/discovery.py +78 -0
  164. glaip_sdk/utils/display.py +25 -21
  165. glaip_sdk/utils/export.py +143 -0
  166. glaip_sdk/utils/general.py +1 -36
  167. glaip_sdk/utils/import_export.py +15 -16
  168. glaip_sdk/utils/import_resolver.py +524 -0
  169. glaip_sdk/utils/instructions.py +101 -0
  170. glaip_sdk/utils/rendering/__init__.py +115 -1
  171. glaip_sdk/utils/rendering/formatting.py +38 -23
  172. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  173. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  174. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  175. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  176. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  177. glaip_sdk/utils/rendering/models.py +18 -8
  178. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  179. glaip_sdk/utils/rendering/renderer/base.py +534 -882
  180. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  181. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  182. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  183. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  184. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  185. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  186. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  187. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  188. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  189. glaip_sdk/utils/rendering/state.py +204 -0
  190. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  191. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  192. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  193. glaip_sdk/utils/rendering/steps/format.py +176 -0
  194. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  195. glaip_sdk/utils/rendering/timing.py +36 -0
  196. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  197. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  198. glaip_sdk/utils/resource_refs.py +29 -26
  199. glaip_sdk/utils/runtime_config.py +425 -0
  200. glaip_sdk/utils/serialization.py +32 -46
  201. glaip_sdk/utils/sync.py +162 -0
  202. glaip_sdk/utils/tool_detection.py +301 -0
  203. glaip_sdk/utils/tool_storage_provider.py +140 -0
  204. glaip_sdk/utils/validation.py +20 -28
  205. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
  206. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  207. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  208. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  209. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  210. glaip_sdk/cli/commands/agents.py +0 -1412
  211. glaip_sdk/cli/commands/mcps.py +0 -1225
  212. glaip_sdk/cli/commands/tools.py +0 -597
  213. glaip_sdk/cli/utils.py +0 -1330
  214. glaip_sdk/models.py +0 -259
  215. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  216. glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
@@ -6,10 +6,14 @@ Author:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import os
10
- from collections.abc import Callable
9
+ import importlib
10
+ import logging
11
+ import sys
12
+ from collections.abc import Callable, Iterable, Iterator
13
+ from contextlib import contextmanager
11
14
  from typing import Any, Literal
12
15
 
16
+ import click
13
17
  import httpx
14
18
  from packaging.version import InvalidVersion, Version
15
19
  from rich import box
@@ -17,10 +21,20 @@ from rich.console import Console
17
21
 
18
22
  from glaip_sdk.branding import (
19
23
  ACCENT_STYLE,
24
+ ERROR_STYLE,
25
+ INFO_STYLE,
20
26
  SUCCESS_STYLE,
21
27
  WARNING_STYLE,
22
28
  )
23
- from glaip_sdk.cli.utils import command_hint, format_command_hint
29
+ from glaip_sdk.cli.commands.update import (
30
+ PACKAGE_NAME,
31
+ _build_command_parts,
32
+ _build_manual_upgrade_command,
33
+ _is_uv_managed_environment,
34
+ update_command,
35
+ )
36
+ from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
37
+ from glaip_sdk.cli.hints import command_hint, format_command_hint
24
38
  from glaip_sdk.rich_components import AIPPanel
25
39
 
26
40
  FetchLatestVersion = Callable[[], str | None]
@@ -28,6 +42,9 @@ FetchLatestVersion = Callable[[], str | None]
28
42
  PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
29
43
  DEFAULT_TIMEOUT = 1.5 # seconds
30
44
 
45
+ _LOGGER = logging.getLogger(__name__)
46
+ _UPDATE_VERSIONS_KEY = "_update_notifier_versions"
47
+
31
48
 
32
49
  def _parse_version(value: str) -> Version | None:
33
50
  """Parse a version string into a `Version`, returning None on failure."""
@@ -43,13 +60,16 @@ def _fetch_latest_version(package_name: str) -> str | None:
43
60
  timeout = httpx.Timeout(DEFAULT_TIMEOUT)
44
61
 
45
62
  try:
46
- with httpx.Client(timeout=timeout) as client:
47
- response = client.get(url, headers={"Accept": "application/json"})
48
- response.raise_for_status()
49
- payload = response.json()
50
- except httpx.HTTPError:
63
+ with _suppress_library_logging():
64
+ with httpx.Client(timeout=timeout) as client:
65
+ response = client.get(url, headers={"Accept": "application/json"})
66
+ response.raise_for_status()
67
+ payload = response.json()
68
+ except httpx.HTTPError as exc:
69
+ _LOGGER.debug("Update check failed: %s", exc, exc_info=True)
51
70
  return None
52
- except ValueError:
71
+ except ValueError as exc:
72
+ _LOGGER.debug("Invalid JSON while checking for updates: %s", exc, exc_info=True)
53
73
  return None
54
74
 
55
75
  info = payload.get("info") if isinstance(payload, dict) else None
@@ -61,13 +81,19 @@ def _fetch_latest_version(package_name: str) -> str | None:
61
81
 
62
82
  def _should_check_for_updates() -> bool:
63
83
  """Return False when update checks are explicitly disabled."""
64
- return os.getenv("AIP_NO_UPDATE_CHECK") is None
84
+ # Check module attribute first (for test overrides), then fall back to imported constant
85
+ module = sys.modules.get(__name__)
86
+ if module and hasattr(module, "UPDATE_CHECK_ENABLED"):
87
+ return getattr(module, "UPDATE_CHECK_ENABLED")
88
+ return UPDATE_CHECK_ENABLED
65
89
 
66
90
 
67
91
  def _build_update_panel(
68
92
  current_version: str,
69
93
  latest_version: str,
70
94
  command_text: str,
95
+ *,
96
+ show_command_hint: bool,
71
97
  ) -> AIPPanel:
72
98
  """Create a Rich panel that prompts the user to update."""
73
99
  command_markup = format_command_hint(command_text) or command_text
@@ -75,9 +101,10 @@ def _build_update_panel(
75
101
  f"[{WARNING_STYLE}]✨ Update available![/] "
76
102
  f"{current_version} → {latest_version}\n\n"
77
103
  "See the latest release notes:\n"
78
- f"https://pypi.org/project/glaip-sdk/{latest_version}/\n\n"
79
- f"[{ACCENT_STYLE}]Run[/] {command_markup} to install."
104
+ f"https://pypi.org/project/glaip-sdk/{latest_version}/"
80
105
  )
106
+ if show_command_hint:
107
+ message += f"\n\n[{ACCENT_STYLE}]Run[/] {command_markup} to install."
81
108
  return AIPPanel(
82
109
  message,
83
110
  title=f"[{SUCCESS_STYLE}]AIP SDK Update[/]",
@@ -97,11 +124,7 @@ def maybe_notify_update(
97
124
  slash_command: str | None = None,
98
125
  style: Literal["panel", "inline"] = "panel",
99
126
  ) -> None:
100
- """Check PyPI for a newer version and display a prompt if one exists.
101
-
102
- This function deliberately swallows network errors to avoid impacting CLI
103
- startup time when offline or when PyPI is unavailable.
104
- """
127
+ """Check PyPI for a newer version and display a prompt if one exists."""
105
128
  if not _should_check_for_updates():
106
129
  return
107
130
 
@@ -120,18 +143,356 @@ def maybe_notify_update(
120
143
  return
121
144
 
122
145
  active_console = console or Console()
146
+ should_prompt = _should_prompt_for_action(active_console, ctx)
147
+
123
148
  if style == "inline":
149
+ if should_prompt:
150
+ message = (
151
+ f"[{WARNING_STYLE}]✨ Update[/] "
152
+ f"{current_version} → {latest_version} "
153
+ "- choose Update now or Skip to continue."
154
+ )
155
+ active_console.print(message)
156
+ _stash_update_versions(ctx, current_version, latest_version)
157
+ _handle_update_decision(active_console, ctx)
158
+ return
159
+
124
160
  command_markup = format_command_hint(command_text) or command_text
125
- message = (
126
- f"[{WARNING_STYLE}]✨ Update[/] "
127
- f"{current_version} → {latest_version} "
128
- f"- {command_markup}"
129
- )
130
- active_console.print(message)
161
+ active_console.print(f"[{WARNING_STYLE}]✨ Update[/] {current_version} → {latest_version} - {command_markup}")
131
162
  return
132
163
 
133
- panel = _build_update_panel(current_version, latest_version, command_text)
164
+ panel = _build_update_panel(
165
+ current_version,
166
+ latest_version,
167
+ command_text,
168
+ show_command_hint=not should_prompt,
169
+ )
134
170
  active_console.print(panel)
171
+ if should_prompt:
172
+ _stash_update_versions(ctx, current_version, latest_version)
173
+ _handle_update_decision(active_console, ctx)
174
+
175
+
176
+ def _handle_update_decision(console: Console, ctx: Any) -> None:
177
+ """Prompt the user to take action on the available update."""
178
+ choice = _prompt_update_decision(console)
179
+ if choice == "skip":
180
+ return
181
+
182
+ _run_update_command(console, ctx)
183
+
184
+
185
+ def _should_prompt_for_action(console: Console, ctx: Any | None) -> bool:
186
+ """Return True when we can safely block for interactive input."""
187
+ if ctx is None or not hasattr(ctx, "invoke"):
188
+ return False
189
+
190
+ is_interactive = getattr(console, "is_interactive", False)
191
+ if not isinstance(is_interactive, bool) or not is_interactive:
192
+ return False
193
+
194
+ is_terminal = getattr(console, "is_terminal", False)
195
+ if not isinstance(is_terminal, bool) or not is_terminal:
196
+ return False
197
+
198
+ input_method = getattr(console, "input", None)
199
+ return callable(input_method)
200
+
201
+
202
+ def _prompt_update_decision(console: Console) -> Literal["update", "skip"]:
203
+ """Ask the user to choose between updating now or skipping."""
204
+ console.print(
205
+ f"[{ACCENT_STYLE}]Select an option to continue:[/]\n"
206
+ f" [{SUCCESS_STYLE}]1.[/] Update now\n"
207
+ f" [{WARNING_STYLE}]2.[/] Skip\n"
208
+ )
209
+ console.print("[dim]Press Enter after typing your choice.[/]")
210
+
211
+ while True:
212
+ try:
213
+ raw_response = console.input("Choice [1/2]: ")
214
+ # Strip whitespace and convert to lowercase
215
+ response = raw_response.strip().lower()
216
+ # Remove any non-printable control characters (but keep printable chars)
217
+ # This handles cases where ANSI escape sequences or other control chars leak into input
218
+ response = "".join(char for char in response if char.isprintable() or char.isspace())
219
+ response = response.strip() # Strip again after filtering
220
+ except (KeyboardInterrupt, EOFError):
221
+ console.print(f"\n[{WARNING_STYLE}]Update skipped.[/]")
222
+ return "skip"
223
+
224
+ if response in {"1", "update", "u"}:
225
+ return "update"
226
+ if response in {"2", "skip", "s"}:
227
+ return "skip"
228
+
229
+ console.print(f"[{ERROR_STYLE}]Please enter 1 to update now or 2 to skip.[/]")
230
+
231
+
232
+ def _get_manual_upgrade_command(is_uv: bool) -> str:
233
+ """Get the manual upgrade command for the given environment type.
234
+
235
+ Args:
236
+ is_uv: True if running in uv tool environment, False for pip environment.
237
+
238
+ Returns:
239
+ Manual upgrade command string.
240
+ """
241
+ try:
242
+ return _build_manual_upgrade_command(include_prerelease=False, is_uv=is_uv)
243
+ except Exception:
244
+ # Fallback: rebuild from shared command parts to avoid hardcoded strings.
245
+ try:
246
+ command_parts, _ = _build_command_parts(
247
+ package_name=PACKAGE_NAME,
248
+ is_uv=is_uv,
249
+ force_reinstall=False,
250
+ include_prerelease=False,
251
+ )
252
+ except Exception:
253
+ command_parts = (
254
+ ["uv", "tool", "install", "--upgrade", PACKAGE_NAME]
255
+ if is_uv
256
+ else ["pip", "install", "--upgrade", PACKAGE_NAME]
257
+ )
258
+ return " ".join(command_parts)
259
+
260
+
261
+ def _show_proactive_uv_guidance(console: Console, is_uv: bool) -> None:
262
+ """Show proactive guidance for uv environments before update attempt.
263
+
264
+ Args:
265
+ console: Rich console for output.
266
+ is_uv: True if running in uv tool environment.
267
+ """
268
+ if not is_uv:
269
+ return
270
+
271
+ manual_cmd = _get_manual_upgrade_command(is_uv=True)
272
+ console.print(
273
+ f"[{INFO_STYLE}]💡 Detected uv tool environment.[/] "
274
+ f"If automatic update fails, run: [{ACCENT_STYLE}]{manual_cmd}[/]"
275
+ )
276
+
277
+
278
+ def _show_error_guidance(console: Console, is_uv: bool) -> None:
279
+ """Show error guidance with correct manual command based on environment.
280
+
281
+ Args:
282
+ console: Rich console for output.
283
+ is_uv: True if running in uv tool environment.
284
+ """
285
+ try:
286
+ manual_cmd = _get_manual_upgrade_command(is_uv=is_uv)
287
+ console.print(f"[{INFO_STYLE}]💡 Tip:[/] Run this command manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
288
+ except Exception as exc: # pragma: no cover - defensive guard
289
+ _LOGGER.debug("Failed to render update tip: %s", exc, exc_info=True)
290
+
291
+
292
+ def _run_update_command(console: Console, ctx: Any) -> None:
293
+ """Invoke the built-in update command and surface any errors."""
294
+ # Detect uv environment proactively before attempting update
295
+ is_uv = _is_uv_managed_environment()
296
+
297
+ # Provide proactive guidance for uv environments
298
+ # This helps users on older versions (e.g., 0.6.19) that don't have uv detection
299
+ # in their update command
300
+ _show_proactive_uv_guidance(console, is_uv)
301
+
302
+ try:
303
+ ctx.invoke(update_command)
304
+ except click.ClickException as exc:
305
+ exc.show()
306
+ console.print(f"[{ERROR_STYLE}]Update command exited with an error.[/]")
307
+ _show_error_guidance(console, is_uv)
308
+ except click.Abort:
309
+ console.print(f"[{WARNING_STYLE}]Update aborted by user.[/]")
310
+ except Exception as exc: # pragma: no cover - defensive guard
311
+ console.print(f"[{ERROR_STYLE}]Unexpected error while running update: {exc}[/]")
312
+ # Also provide guidance for unexpected errors in uv environments
313
+ if is_uv:
314
+ manual_cmd = _get_manual_upgrade_command(is_uv=True)
315
+ console.print(f"[{INFO_STYLE}]💡 Tip:[/] Try running manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
316
+ else:
317
+ new_version = _refresh_installed_version(console, ctx)
318
+ _maybe_retry_update(console, ctx, new_version, is_uv)
319
+
320
+
321
+ @contextmanager
322
+ def _suppress_library_logging(
323
+ logger_names: Iterable[str] | None = None, *, level: int = logging.WARNING
324
+ ) -> Iterator[None]:
325
+ """Temporarily raise log level for selected libraries during update checks."""
326
+ names = tuple(logger_names) if logger_names is not None else ("httpx",)
327
+ captured: list[tuple[logging.Logger, int]] = []
328
+ try:
329
+ for name in names:
330
+ logger = logging.getLogger(name)
331
+ captured.append((logger, logger.level))
332
+ logger.setLevel(level)
333
+ yield
334
+ finally:
335
+ for logger, previous_level in captured:
336
+ logger.setLevel(previous_level)
337
+
338
+
339
+ def _refresh_installed_version(console: Console, ctx: Any) -> str | None:
340
+ """Reload runtime metadata after an in-process upgrade."""
341
+ new_version: str | None = None
342
+ branding_module: Any | None = None
343
+
344
+ try:
345
+ version_module = importlib.reload(importlib.import_module("glaip_sdk._version"))
346
+ new_version = getattr(version_module, "__version__", None)
347
+ except Exception as exc: # pragma: no cover - defensive guard
348
+ _LOGGER.debug("Failed to reload glaip_sdk._version: %s", exc, exc_info=True)
349
+
350
+ try:
351
+ branding_module = importlib.reload(importlib.import_module("glaip_sdk.branding"))
352
+ if new_version:
353
+ branding_module.SDK_VERSION = new_version
354
+ except Exception as exc: # pragma: no cover - defensive guard
355
+ _LOGGER.debug("Failed to update branding metadata: %s", exc, exc_info=True)
356
+ branding_module = None
357
+
358
+ session = _get_slash_session(ctx)
359
+ if session and hasattr(session, "refresh_branding"):
360
+ try:
361
+ branding_cls = getattr(branding_module, "AIPBranding", None) if branding_module else None
362
+ session.refresh_branding(new_version, branding_cls=branding_cls)
363
+ return new_version
364
+ except Exception as exc: # pragma: no cover - defensive guard
365
+ _LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
366
+
367
+ if new_version:
368
+ console.print(f"[{SUCCESS_STYLE}]CLI now running glaip-sdk {new_version}.[/]")
369
+ return new_version
370
+
371
+
372
+ def _get_slash_session(ctx: Any) -> Any | None:
373
+ """Return active slash session from the Click context if present."""
374
+ ctx_obj = getattr(ctx, "obj", None)
375
+ if isinstance(ctx_obj, dict):
376
+ return ctx_obj.get("_slash_session")
377
+ return None
378
+
379
+
380
+ def _stash_update_versions(ctx: Any | None, current_version: str, latest_version: str) -> None:
381
+ """Persist update versions in the Click context for post-update checks."""
382
+ if ctx is None:
383
+ return
384
+ ctx_obj = getattr(ctx, "obj", None)
385
+ if isinstance(ctx_obj, dict):
386
+ ctx_obj[_UPDATE_VERSIONS_KEY] = {"current": current_version, "latest": latest_version}
387
+
388
+
389
+ def _get_update_versions(ctx: Any | None) -> tuple[str | None, str | None]:
390
+ """Return current/latest versions captured during the update prompt."""
391
+ if ctx is None:
392
+ return None, None
393
+ ctx_obj = getattr(ctx, "obj", None)
394
+ if not isinstance(ctx_obj, dict):
395
+ return None, None
396
+ payload = ctx_obj.get(_UPDATE_VERSIONS_KEY)
397
+ if not isinstance(payload, dict):
398
+ return None, None
399
+ current = payload.get("current")
400
+ latest = payload.get("latest")
401
+ return (
402
+ current if isinstance(current, str) else None,
403
+ latest if isinstance(latest, str) else None,
404
+ )
405
+
406
+
407
+ def _should_retry_update(
408
+ ctx: Any,
409
+ console: Console,
410
+ new_version: str | None,
411
+ ) -> tuple[str, str, Version, Version, Version] | None:
412
+ """Check if update retry is needed and return parsed versions if so."""
413
+ if ctx is None or not hasattr(ctx, "invoke"):
414
+ return None
415
+ if not _should_prompt_for_action(console, ctx):
416
+ return None
417
+
418
+ current_version, latest_version = _get_update_versions(ctx)
419
+ if not current_version or not latest_version or not new_version:
420
+ return None
421
+
422
+ current = _parse_version(current_version)
423
+ latest = _parse_version(latest_version)
424
+ installed = _parse_version(new_version)
425
+ if current is None or latest is None or installed is None:
426
+ return None
427
+
428
+ if installed >= latest:
429
+ return None
430
+
431
+ # Note: installed > current case is handled in _maybe_retry_update()
432
+ # to allow warning message to be printed before returning
433
+
434
+ return current_version, latest_version, current, latest, installed
435
+
436
+
437
+ def _handle_reinstall_error(console: Console, exc: Exception, is_uv: bool) -> None:
438
+ """Handle errors during reinstall attempt."""
439
+ if isinstance(exc, click.ClickException):
440
+ exc.show()
441
+ console.print(f"[{ERROR_STYLE}]Reinstall attempt failed.[/]")
442
+ _show_error_guidance(console, is_uv)
443
+ elif isinstance(exc, click.Abort):
444
+ console.print(f"[{WARNING_STYLE}]Reinstall skipped by user.[/]")
445
+ else:
446
+ console.print(f"[{ERROR_STYLE}]Unexpected error while reinstalling: {exc}[/]")
447
+ if is_uv:
448
+ manual_cmd = _get_manual_upgrade_command(is_uv=True)
449
+ console.print(f"[{INFO_STYLE}]💡 Tip:[/] Try running manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
450
+
451
+
452
+ def _check_final_version(
453
+ console: Console, new_version: str | None, latest_version: str, latest: Version, is_uv: bool
454
+ ) -> None:
455
+ """Check and report final version status after reinstall."""
456
+ installed = _parse_version(new_version) if isinstance(new_version, str) else None
457
+ if installed is None or installed < latest:
458
+ console.print(
459
+ f"[{WARNING_STYLE}]Still on {new_version}. Your package index may not have {latest_version} yet.[/]"
460
+ )
461
+ if is_uv:
462
+ console.print(
463
+ f"[{INFO_STYLE}]💡 Tip:[/] If you need PyPI immediately, set "
464
+ f"[{ACCENT_STYLE}]UV_INDEX_URL=https://pypi.org/simple[/]."
465
+ )
466
+
467
+
468
+ def _maybe_retry_update(
469
+ console: Console,
470
+ ctx: Any,
471
+ new_version: str | None,
472
+ is_uv: bool,
473
+ ) -> None:
474
+ """Retry once with reinstall when the update did not advance versions."""
475
+ versions = _should_retry_update(ctx, console, new_version)
476
+ if versions is None:
477
+ return
478
+
479
+ current_version, latest_version, current, latest, installed = versions
480
+ if installed > current:
481
+ console.print(f"[{WARNING_STYLE}]Update installed {new_version}, but {latest_version} is still available.[/]")
482
+ return
483
+
484
+ console.print(
485
+ f"[{WARNING_STYLE}]Update completed but version stayed at {new_version}. Retrying with reinstall...[/]"
486
+ )
487
+
488
+ try:
489
+ ctx.invoke(update_command, force_reinstall=True)
490
+ except Exception as exc:
491
+ _handle_reinstall_error(console, exc, is_uv)
492
+ return
493
+
494
+ new_version = _refresh_installed_version(console, ctx)
495
+ _check_final_version(console, new_version, latest_version, latest, is_uv)
135
496
 
136
497
 
137
498
  __all__ = ["maybe_notify_update"]
@@ -13,6 +13,7 @@ from typing import Any
13
13
 
14
14
  import click
15
15
 
16
+ from glaip_sdk.cli.core.context import handle_best_effort_check
16
17
  from glaip_sdk.utils.validation import (
17
18
  coerce_timeout,
18
19
  validate_agent_instruction,
@@ -42,7 +43,7 @@ def validate_agent_name_cli(name: str) -> str:
42
43
  try:
43
44
  return validate_agent_name(name)
44
45
  except ValueError as e:
45
- raise click.ClickException(str(e))
46
+ raise click.ClickException(str(e)) from e
46
47
 
47
48
 
48
49
  def validate_agent_instruction_cli(instruction: str) -> str:
@@ -60,7 +61,7 @@ def validate_agent_instruction_cli(instruction: str) -> str:
60
61
  try:
61
62
  return validate_agent_instruction(instruction)
62
63
  except ValueError as e:
63
- raise click.ClickException(str(e))
64
+ raise click.ClickException(str(e)) from e
64
65
 
65
66
 
66
67
  def validate_timeout_cli(timeout: int) -> int:
@@ -78,7 +79,7 @@ def validate_timeout_cli(timeout: int) -> int:
78
79
  try:
79
80
  return validate_timeout(timeout)
80
81
  except ValueError as e:
81
- raise click.ClickException(str(e))
82
+ raise click.ClickException(str(e)) from e
82
83
 
83
84
 
84
85
  def validate_tool_name_cli(name: str) -> str:
@@ -96,7 +97,7 @@ def validate_tool_name_cli(name: str) -> str:
96
97
  try:
97
98
  return validate_tool_name(name)
98
99
  except ValueError as e:
99
- raise click.ClickException(str(e))
100
+ raise click.ClickException(str(e)) from e
100
101
 
101
102
 
102
103
  def validate_mcp_name_cli(name: str) -> str:
@@ -114,7 +115,7 @@ def validate_mcp_name_cli(name: str) -> str:
114
115
  try:
115
116
  return validate_mcp_name(name)
116
117
  except ValueError as e:
117
- raise click.ClickException(str(e))
118
+ raise click.ClickException(str(e)) from e
118
119
 
119
120
 
120
121
  def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Path:
@@ -133,7 +134,7 @@ def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Pa
133
134
  try:
134
135
  return validate_file_path(file_path, must_exist)
135
136
  except ValueError as e:
136
- raise click.ClickException(str(e))
137
+ raise click.ClickException(str(e)) from e
137
138
 
138
139
 
139
140
  def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -> Path:
@@ -152,7 +153,7 @@ def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -
152
153
  try:
153
154
  return validate_directory_path(dir_path, must_exist)
154
155
  except ValueError as e:
155
- raise click.ClickException(str(e))
156
+ raise click.ClickException(str(e)) from e
156
157
 
157
158
 
158
159
  def validate_url_cli(url: str) -> str:
@@ -170,7 +171,7 @@ def validate_url_cli(url: str) -> str:
170
171
  try:
171
172
  return validate_url(url)
172
173
  except ValueError as e:
173
- raise click.ClickException(str(e))
174
+ raise click.ClickException(str(e)) from e
174
175
 
175
176
 
176
177
  def validate_api_key_cli(api_key: str) -> str:
@@ -188,7 +189,7 @@ def validate_api_key_cli(api_key: str) -> str:
188
189
  try:
189
190
  return validate_api_key(api_key)
190
191
  except ValueError as e:
191
- raise click.ClickException(str(e))
192
+ raise click.ClickException(str(e)) from e
192
193
 
193
194
 
194
195
  def coerce_timeout_cli(value: int | float | str) -> int:
@@ -206,7 +207,7 @@ def coerce_timeout_cli(value: int | float | str) -> int:
206
207
  try:
207
208
  return coerce_timeout(value)
208
209
  except ValueError as e:
209
- raise click.ClickException(str(e))
210
+ raise click.ClickException(str(e)) from e
210
211
 
211
212
 
212
213
  def validate_name_uniqueness_cli(
@@ -226,15 +227,12 @@ def validate_name_uniqueness_cli(
226
227
  Raises:
227
228
  click.ClickException: If name is not unique
228
229
  """
229
- try:
230
+
231
+ def _check_duplicate() -> None:
230
232
  existing = finder_func(name=name)
231
233
  if existing:
232
234
  raise click.ClickException(
233
- f"A {resource_type.lower()} named '{name}' already exists. "
234
- "Please choose a unique name."
235
+ f"A {resource_type.lower()} named '{name}' already exists. Please choose a unique name."
235
236
  )
236
- except click.ClickException:
237
- raise
238
- except Exception:
239
- # Non-fatal: best-effort duplicate check
240
- pass
237
+
238
+ handle_best_effort_check(_check_duplicate)
@@ -5,6 +5,8 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ from glaip_sdk.client.agent_runs import AgentRunsClient
8
9
  from glaip_sdk.client.main import Client
10
+ from glaip_sdk.client.schedules import AgentScheduleManager, ScheduleClient
9
11
 
10
- __all__ = ["Client"]
12
+ __all__ = ["AgentRunsClient", "AgentScheduleManager", "Client", "ScheduleClient"]
@@ -0,0 +1,89 @@
1
+ """Schedule request payload builders for AIP SDK.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from glaip_sdk.models.schedule import ScheduleConfig
11
+
12
+
13
+ @dataclass
14
+ class ScheduleListParams:
15
+ """Parameters for listing schedules.
16
+
17
+ Args:
18
+ limit: Maximum number of schedules to return (1-100, default 50)
19
+ page: Page number for pagination (default 1)
20
+ agent_id: Filter schedules by agent ID
21
+ """
22
+
23
+ limit: int | None = None
24
+ page: int | None = None
25
+ agent_id: str | None = None
26
+
27
+ def to_query_params(self) -> dict[str, Any]:
28
+ """Convert to query parameters dictionary.
29
+
30
+ Returns:
31
+ Dictionary of non-None parameters for the API request
32
+ """
33
+ params: dict[str, Any] = {}
34
+ if self.limit is not None:
35
+ params["limit"] = self.limit
36
+ if self.page is not None:
37
+ params["page"] = self.page
38
+ if self.agent_id is not None:
39
+ params["agent_id"] = self.agent_id
40
+ return params
41
+
42
+
43
+ def normalize_schedule(
44
+ schedule: ScheduleConfig | dict[str, str] | str | None,
45
+ ) -> dict[str, str] | None:
46
+ """Normalize schedule input to a dictionary for API requests.
47
+
48
+ Accepts multiple input formats for user convenience:
49
+ - ScheduleConfig: Pydantic model with cron fields
50
+ - dict: Dictionary with cron fields (minute, hour, etc.)
51
+ - str: Cron string like "0 9 * * 1-5"
52
+ - None: Returns None
53
+
54
+ Args:
55
+ schedule: Schedule in various formats
56
+
57
+ Returns:
58
+ Dictionary suitable for API request or None
59
+
60
+ Raises:
61
+ ValueError: If cron string format is invalid
62
+ TypeError: If schedule is an unsupported type
63
+
64
+ Examples:
65
+ >>> normalize_schedule(ScheduleConfig(minute="0", hour="9"))
66
+ {'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '*'}
67
+
68
+ >>> normalize_schedule({"minute": "0", "hour": "9"})
69
+ {'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '*'}
70
+
71
+ >>> normalize_schedule("0 9 * * 1-5")
72
+ {'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '1-5'}
73
+ """
74
+ if schedule is None:
75
+ return None
76
+
77
+ if isinstance(schedule, ScheduleConfig):
78
+ return schedule.model_dump()
79
+
80
+ if isinstance(schedule, dict):
81
+ # Validate and merge with defaults
82
+ return ScheduleConfig(**schedule).model_dump()
83
+
84
+ if isinstance(schedule, str):
85
+ # Parse cron string
86
+ config = ScheduleConfig.from_cron_string(schedule)
87
+ return config.model_dump()
88
+
89
+ raise TypeError(f"schedule must be ScheduleConfig, dict, or str, got {type(schedule).__name__}")