glaip-sdk 0.1.0__py3-none-any.whl → 0.1.2__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 (63) hide show
  1. glaip_sdk/_version.py +1 -3
  2. glaip_sdk/branding.py +2 -6
  3. glaip_sdk/cli/agent_config.py +2 -6
  4. glaip_sdk/cli/auth.py +11 -30
  5. glaip_sdk/cli/commands/agents.py +45 -107
  6. glaip_sdk/cli/commands/configure.py +12 -36
  7. glaip_sdk/cli/commands/mcps.py +26 -63
  8. glaip_sdk/cli/commands/models.py +2 -4
  9. glaip_sdk/cli/commands/tools.py +22 -35
  10. glaip_sdk/cli/commands/update.py +3 -8
  11. glaip_sdk/cli/config.py +1 -3
  12. glaip_sdk/cli/display.py +4 -12
  13. glaip_sdk/cli/io.py +8 -14
  14. glaip_sdk/cli/main.py +10 -30
  15. glaip_sdk/cli/mcp_validators.py +5 -15
  16. glaip_sdk/cli/pager.py +3 -9
  17. glaip_sdk/cli/parsers/json_input.py +11 -22
  18. glaip_sdk/cli/resolution.py +3 -9
  19. glaip_sdk/cli/rich_helpers.py +1 -3
  20. glaip_sdk/cli/slash/agent_session.py +5 -10
  21. glaip_sdk/cli/slash/prompt.py +3 -10
  22. glaip_sdk/cli/slash/session.py +46 -95
  23. glaip_sdk/cli/transcript/cache.py +6 -19
  24. glaip_sdk/cli/transcript/capture.py +6 -20
  25. glaip_sdk/cli/transcript/launcher.py +1 -3
  26. glaip_sdk/cli/transcript/viewer.py +11 -40
  27. glaip_sdk/cli/update_notifier.py +165 -21
  28. glaip_sdk/cli/utils.py +33 -84
  29. glaip_sdk/cli/validators.py +11 -12
  30. glaip_sdk/client/_agent_payloads.py +10 -30
  31. glaip_sdk/client/agents.py +33 -63
  32. glaip_sdk/client/base.py +77 -35
  33. glaip_sdk/client/mcps.py +1 -3
  34. glaip_sdk/client/run_rendering.py +6 -14
  35. glaip_sdk/client/tools.py +8 -24
  36. glaip_sdk/client/validators.py +20 -48
  37. glaip_sdk/exceptions.py +1 -3
  38. glaip_sdk/models.py +14 -33
  39. glaip_sdk/payload_schemas/agent.py +1 -3
  40. glaip_sdk/utils/agent_config.py +4 -14
  41. glaip_sdk/utils/client_utils.py +7 -21
  42. glaip_sdk/utils/display.py +2 -6
  43. glaip_sdk/utils/general.py +1 -3
  44. glaip_sdk/utils/import_export.py +3 -9
  45. glaip_sdk/utils/rendering/formatting.py +2 -5
  46. glaip_sdk/utils/rendering/models.py +2 -6
  47. glaip_sdk/utils/rendering/renderer/__init__.py +1 -3
  48. glaip_sdk/utils/rendering/renderer/base.py +63 -189
  49. glaip_sdk/utils/rendering/renderer/debug.py +4 -14
  50. glaip_sdk/utils/rendering/renderer/panels.py +1 -3
  51. glaip_sdk/utils/rendering/renderer/progress.py +3 -11
  52. glaip_sdk/utils/rendering/renderer/stream.py +7 -19
  53. glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
  54. glaip_sdk/utils/rendering/step_tree_state.py +1 -3
  55. glaip_sdk/utils/rendering/steps.py +29 -83
  56. glaip_sdk/utils/resource_refs.py +4 -13
  57. glaip_sdk/utils/serialization.py +14 -46
  58. glaip_sdk/utils/validation.py +4 -4
  59. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.1.2.dist-info}/METADATA +1 -1
  60. glaip_sdk-0.1.2.dist-info/RECORD +82 -0
  61. glaip_sdk-0.1.0.dist-info/RECORD +0 -82
  62. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.1.2.dist-info}/WHEEL +0 -0
  63. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -124,9 +124,7 @@ def _json_default(value: Any) -> Any:
124
124
  return repr(value)
125
125
 
126
126
 
127
- def _write_manifest(
128
- entries: Iterable[dict[str, Any]], cache_dir: Path | None = None
129
- ) -> None:
127
+ def _write_manifest(entries: Iterable[dict[str, Any]], cache_dir: Path | None = None) -> None:
130
128
  path = manifest_path(cache_dir)
131
129
  with path.open("w", encoding="utf-8") as fh:
132
130
  for entry in entries:
@@ -212,8 +210,7 @@ def latest_manifest_entry(cache_dir: Path | None = None) -> dict[str, Any] | Non
212
210
  return None
213
211
  return max(
214
212
  entries,
215
- key=lambda e: _parse_iso(e.get("created_at"))
216
- or datetime.min.replace(tzinfo=timezone.utc),
213
+ key=lambda e: _parse_iso(e.get("created_at")) or datetime.min.replace(tzinfo=timezone.utc),
217
214
  )
218
215
 
219
216
 
@@ -237,11 +234,7 @@ def export_transcript(
237
234
  ) -> Path:
238
235
  """Copy a cached transcript to the requested destination path."""
239
236
  directory = ensure_cache_dir(cache_dir)
240
- entry = (
241
- resolve_manifest_entry(run_id, directory)
242
- if run_id
243
- else latest_manifest_entry(directory)
244
- )
237
+ entry = resolve_manifest_entry(run_id, directory) if run_id else latest_manifest_entry(directory)
245
238
  if entry is None:
246
239
  raise FileNotFoundError("No cached transcripts available for export.")
247
240
 
@@ -259,9 +252,7 @@ def export_transcript(
259
252
  lines = cache_file.read_text(encoding="utf-8").splitlines()
260
253
  records = [json.loads(line) for line in lines if line.strip()]
261
254
  except json.JSONDecodeError as exc:
262
- raise FileNotFoundError(
263
- f"Cached transcript file is corrupted: {cache_file}"
264
- ) from exc
255
+ raise FileNotFoundError(f"Cached transcript file is corrupted: {cache_file}") from exc
265
256
 
266
257
  with destination.open("w", encoding="utf-8") as fh:
267
258
  for idx, record in enumerate(records):
@@ -276,12 +267,8 @@ def export_transcript(
276
267
  def suggest_filename(entry: dict[str, Any] | None = None) -> str:
277
268
  """Return a friendly filename suggestion for exporting a transcript."""
278
269
  run_id = entry.get("run_id") if entry else uuid.uuid4().hex
279
- created_at = (
280
- entry.get("created_at") if entry else datetime.now(timezone.utc).isoformat()
281
- )
282
- timestamp = (
283
- created_at.replace(":", "").replace("-", "").replace("T", "_").split("+")[0]
284
- )
270
+ created_at = entry.get("created_at") if entry else datetime.now(timezone.utc).isoformat()
271
+ timestamp = created_at.replace(":", "").replace("-", "").replace("T", "_").split("+")[0]
285
272
  return f"aip-run-{timestamp}-{run_id}.jsonl"
286
273
 
287
274
 
@@ -65,11 +65,7 @@ def compute_finished_at(renderer: Any) -> float | None:
65
65
 
66
66
  if started_at is None:
67
67
  stream_processor = getattr(renderer, "stream_processor", None)
68
- started_at = (
69
- getattr(stream_processor, "streaming_started_at", None)
70
- if stream_processor is not None
71
- else None
72
- )
68
+ started_at = getattr(stream_processor, "streaming_started_at", None) if stream_processor is not None else None
73
69
  if started_at is None or duration is None:
74
70
  return None
75
71
  try:
@@ -78,9 +74,7 @@ def compute_finished_at(renderer: Any) -> float | None:
78
74
  return None
79
75
 
80
76
 
81
- def extract_server_run_id(
82
- meta: dict[str, Any], events: list[dict[str, Any]]
83
- ) -> str | None:
77
+ def extract_server_run_id(meta: dict[str, Any], events: list[dict[str, Any]]) -> str | None:
84
78
  """Derive a server-side run identifier from renderer metadata."""
85
79
  run_id = meta.get("run_id") or meta.get("id")
86
80
  if run_id:
@@ -107,9 +101,7 @@ def _coerce_meta(meta: Any) -> dict[str, Any]:
107
101
  return {"value": coerce_result_text(meta)}
108
102
 
109
103
 
110
- def register_last_transcript(
111
- ctx: Any, payload: TranscriptPayload, store_result: TranscriptStoreResult
112
- ) -> None:
104
+ def register_last_transcript(ctx: Any, payload: TranscriptPayload, store_result: TranscriptStoreResult) -> None:
113
105
  """Persist last-run transcript references onto the Click context."""
114
106
  ctx_obj = getattr(ctx, "obj", None)
115
107
  if not isinstance(ctx_obj, dict):
@@ -213,9 +205,7 @@ def _derive_transcript_meta(
213
205
 
214
206
  stream_processor = getattr(renderer, "stream_processor", None)
215
207
  stream_started_at = (
216
- getattr(stream_processor, "streaming_started_at", None)
217
- if stream_processor is not None
218
- else None
208
+ getattr(stream_processor, "streaming_started_at", None) if stream_processor is not None else None
219
209
  )
220
210
  finished_at = compute_finished_at(renderer)
221
211
  model_name = meta.get("model") or model
@@ -236,16 +226,12 @@ def store_transcript_for_session(
236
226
  if not hasattr(renderer, "get_transcript_events"):
237
227
  return None
238
228
 
239
- events, aggregated_output, final_output = _collect_renderer_outputs(
240
- renderer, final_result
241
- )
229
+ events, aggregated_output, final_output = _collect_renderer_outputs(renderer, final_result)
242
230
 
243
231
  if not (events or aggregated_output or final_output):
244
232
  return None
245
233
 
246
- meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(
247
- renderer, model
248
- )
234
+ meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(renderer, model)
249
235
 
250
236
  payload: TranscriptPayload = build_transcript_payload(
251
237
  events=events,
@@ -20,9 +20,7 @@ from glaip_sdk.cli.transcript.capture import StoredTranscriptContext
20
20
  from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
21
21
 
22
22
 
23
- def should_launch_post_run_viewer(
24
- ctx: Any, console: Console, *, slash_mode: bool
25
- ) -> bool:
23
+ def should_launch_post_run_viewer(ctx: Any, console: Console, *, slash_mode: bool) -> bool:
26
24
  """Return True if the viewer should open automatically."""
27
25
  if slash_mode:
28
26
  return False
@@ -72,9 +72,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
72
72
 
73
73
  def run(self) -> None:
74
74
  """Enter the interactive loop."""
75
- if not self.ctx.events and not (
76
- self.ctx.default_output or self.ctx.final_output
77
- ):
75
+ if not self.ctx.events and not (self.ctx.default_output or self.ctx.final_output):
78
76
  return
79
77
  if self._view_mode == "transcript":
80
78
  self._render()
@@ -91,9 +89,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
91
89
  except Exception: # pragma: no cover - platform quirks
92
90
  pass
93
91
 
94
- header = (
95
- f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
96
- )
92
+ header = f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
97
93
  agent_label = self.ctx.manifest_entry.get("agent_name") or "unknown agent"
98
94
  model = self.ctx.manifest_entry.get("model") or self.ctx.meta.get("model")
99
95
  agent_id = self.ctx.manifest_entry.get("agent_id")
@@ -134,9 +130,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
134
130
  self._render_final_panel()
135
131
 
136
132
  self.console.print("[bold]Transcript Events[/bold]")
137
- self.console.print(
138
- "[dim]────────────────────────────────────────────────────────[/dim]"
139
- )
133
+ self.console.print("[dim]────────────────────────────────────────────────────────[/dim]")
140
134
 
141
135
  base_received_ts: datetime | None = None
142
136
  for event in self.ctx.events:
@@ -152,11 +146,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
152
146
  self.console.print()
153
147
 
154
148
  def _render_final_panel(self) -> None:
155
- content = (
156
- self.ctx.final_output
157
- or self.ctx.default_output
158
- or "No response content captured."
159
- )
149
+ content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
160
150
  title = "Final Result"
161
151
  duration_text = self._extract_final_duration()
162
152
  if duration_text:
@@ -218,9 +208,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
218
208
  raw = str(path)
219
209
  return raw if len(raw) <= 80 else f"…{raw[-77:]}"
220
210
 
221
- selection = self._prompt_export_choice(
222
- default_path, _display_path(default_path)
223
- )
211
+ selection = self._prompt_export_choice(default_path, _display_path(default_path))
224
212
  if selection is None:
225
213
  self._legacy_export_prompt(default_path, _display_path)
226
214
  return
@@ -246,9 +234,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
246
234
  except Exception as exc: # pragma: no cover - unexpected IO failures
247
235
  self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
248
236
 
249
- def _prompt_export_choice(
250
- self, default_path: Path, default_display: str
251
- ) -> tuple[str, Any] | None:
237
+ def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
252
238
  """Render interactive export menu with numeric shortcuts."""
253
239
  if not self.console.is_terminal or questionary is None or Choice is None:
254
240
  return None
@@ -305,9 +291,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
305
291
  candidate = Path.cwd() / candidate
306
292
  return candidate
307
293
 
308
- def _legacy_export_prompt(
309
- self, default_path: Path, formatter: Callable[[Path], str]
310
- ) -> None:
294
+ def _legacy_export_prompt(self, default_path: Path, formatter: Callable[[Path], str]) -> None:
311
295
  """Fallback export workflow when interactive UI is unavailable."""
312
296
  self.console.print("[dim]Export options (fallback mode)[/dim]")
313
297
  self.console.print(f" 1. Save to default ({formatter(default_path)})")
@@ -353,20 +337,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
353
337
  self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
354
338
 
355
339
  def _print_command_hint(self) -> None:
356
- self.console.print(
357
- "[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]"
358
- )
340
+ self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
359
341
  self.console.print()
360
342
 
361
343
  def _get_user_query(self) -> str | None:
362
344
  meta = self.ctx.meta or {}
363
345
  manifest = self.ctx.manifest_entry or {}
364
- return (
365
- meta.get("input_message")
366
- or meta.get("query")
367
- or meta.get("message")
368
- or manifest.get("input_message")
369
- )
346
+ return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
370
347
 
371
348
  def _render_user_query(self, query: str) -> None:
372
349
  panel = AIPPanel(
@@ -730,11 +707,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
730
707
  status = metadata.get("status")
731
708
  event_time = metadata.get("time")
732
709
 
733
- if (
734
- status == "running"
735
- and step.get("started_at") is None
736
- and isinstance(event_time, (int, float))
737
- ):
710
+ if status == "running" and step.get("started_at") is None and isinstance(event_time, (int, float)):
738
711
  try:
739
712
  step["started_at"] = float(event_time)
740
713
  except Exception:
@@ -760,9 +733,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
760
733
  started_at = step.get("started_at")
761
734
  duration_value: float | None = None
762
735
 
763
- if isinstance(event_time, (int, float)) and isinstance(
764
- started_at, (int, float)
765
- ):
736
+ if isinstance(event_time, (int, float)) and isinstance(started_at, (int, float)):
766
737
  try:
767
738
  delta = float(event_time) - float(started_at)
768
739
  if delta >= 0:
@@ -6,10 +6,14 @@ Author:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import importlib
10
+ import logging
9
11
  import os
10
- from collections.abc import Callable
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,9 +21,11 @@ from rich.console import Console
17
21
 
18
22
  from glaip_sdk.branding import (
19
23
  ACCENT_STYLE,
24
+ ERROR_STYLE,
20
25
  SUCCESS_STYLE,
21
26
  WARNING_STYLE,
22
27
  )
28
+ from glaip_sdk.cli.commands.update import update_command
23
29
  from glaip_sdk.cli.utils import command_hint, format_command_hint
24
30
  from glaip_sdk.rich_components import AIPPanel
25
31
 
@@ -28,6 +34,8 @@ FetchLatestVersion = Callable[[], str | None]
28
34
  PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
29
35
  DEFAULT_TIMEOUT = 1.5 # seconds
30
36
 
37
+ _LOGGER = logging.getLogger(__name__)
38
+
31
39
 
32
40
  def _parse_version(value: str) -> Version | None:
33
41
  """Parse a version string into a `Version`, returning None on failure."""
@@ -43,13 +51,16 @@ def _fetch_latest_version(package_name: str) -> str | None:
43
51
  timeout = httpx.Timeout(DEFAULT_TIMEOUT)
44
52
 
45
53
  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:
54
+ with _suppress_library_logging():
55
+ with httpx.Client(timeout=timeout) as client:
56
+ response = client.get(url, headers={"Accept": "application/json"})
57
+ response.raise_for_status()
58
+ payload = response.json()
59
+ except httpx.HTTPError as exc:
60
+ _LOGGER.debug("Update check failed: %s", exc, exc_info=True)
51
61
  return None
52
- except ValueError:
62
+ except ValueError as exc:
63
+ _LOGGER.debug("Invalid JSON while checking for updates: %s", exc, exc_info=True)
53
64
  return None
54
65
 
55
66
  info = payload.get("info") if isinstance(payload, dict) else None
@@ -68,6 +79,8 @@ def _build_update_panel(
68
79
  current_version: str,
69
80
  latest_version: str,
70
81
  command_text: str,
82
+ *,
83
+ show_command_hint: bool,
71
84
  ) -> AIPPanel:
72
85
  """Create a Rich panel that prompts the user to update."""
73
86
  command_markup = format_command_hint(command_text) or command_text
@@ -75,9 +88,10 @@ def _build_update_panel(
75
88
  f"[{WARNING_STYLE}]✨ Update available![/] "
76
89
  f"{current_version} → {latest_version}\n\n"
77
90
  "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."
91
+ f"https://pypi.org/project/glaip-sdk/{latest_version}/"
80
92
  )
93
+ if show_command_hint:
94
+ message += f"\n\n[{ACCENT_STYLE}]Run[/] {command_markup} to install."
81
95
  return AIPPanel(
82
96
  message,
83
97
  title=f"[{SUCCESS_STYLE}]AIP SDK Update[/]",
@@ -97,11 +111,7 @@ def maybe_notify_update(
97
111
  slash_command: str | None = None,
98
112
  style: Literal["panel", "inline"] = "panel",
99
113
  ) -> 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
- """
114
+ """Check PyPI for a newer version and display a prompt if one exists."""
105
115
  if not _should_check_for_updates():
106
116
  return
107
117
 
@@ -120,18 +130,152 @@ def maybe_notify_update(
120
130
  return
121
131
 
122
132
  active_console = console or Console()
133
+ should_prompt = _should_prompt_for_action(active_console, ctx)
134
+
123
135
  if style == "inline":
136
+ if should_prompt:
137
+ message = (
138
+ f"[{WARNING_STYLE}]✨ Update[/] "
139
+ f"{current_version} → {latest_version} "
140
+ "- choose Update now or Skip to continue."
141
+ )
142
+ active_console.print(message)
143
+ _handle_update_decision(active_console, ctx)
144
+ return
145
+
124
146
  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)
147
+ active_console.print(f"[{WARNING_STYLE}]✨ Update[/] {current_version} → {latest_version} - {command_markup}")
131
148
  return
132
149
 
133
- panel = _build_update_panel(current_version, latest_version, command_text)
150
+ panel = _build_update_panel(
151
+ current_version,
152
+ latest_version,
153
+ command_text,
154
+ show_command_hint=not should_prompt,
155
+ )
134
156
  active_console.print(panel)
157
+ if should_prompt:
158
+ _handle_update_decision(active_console, ctx)
159
+
160
+
161
+ def _handle_update_decision(console: Console, ctx: Any) -> None:
162
+ """Prompt the user to take action on the available update."""
163
+ choice = _prompt_update_decision(console)
164
+ if choice == "skip":
165
+ return
166
+
167
+ _run_update_command(console, ctx)
168
+
169
+
170
+ def _should_prompt_for_action(console: Console, ctx: Any | None) -> bool:
171
+ """Return True when we can safely block for interactive input."""
172
+ if ctx is None or not hasattr(ctx, "invoke"):
173
+ return False
174
+
175
+ is_interactive = getattr(console, "is_interactive", False)
176
+ if not isinstance(is_interactive, bool) or not is_interactive:
177
+ return False
178
+
179
+ is_terminal = getattr(console, "is_terminal", False)
180
+ if not isinstance(is_terminal, bool) or not is_terminal:
181
+ return False
182
+
183
+ input_method = getattr(console, "input", None)
184
+ return callable(input_method)
185
+
186
+
187
+ def _prompt_update_decision(console: Console) -> Literal["update", "skip"]:
188
+ """Ask the user to choose between updating now or skipping."""
189
+ console.print(
190
+ f"[{ACCENT_STYLE}]Select an option to continue:[/]\n"
191
+ f" [{SUCCESS_STYLE}]1.[/] Update now\n"
192
+ f" [{WARNING_STYLE}]2.[/] Skip\n"
193
+ )
194
+ console.print("[dim]Press Enter after typing your choice.[/]")
195
+
196
+ while True:
197
+ try:
198
+ response = console.input("Choice [1/2]: ").strip().lower()
199
+ except (KeyboardInterrupt, EOFError):
200
+ console.print(f"\n[{WARNING_STYLE}]Update skipped.[/]")
201
+ return "skip"
202
+
203
+ if response in {"1", "update", "u"}:
204
+ return "update"
205
+ if response in {"2", "skip", "s"}:
206
+ return "skip"
207
+
208
+ console.print(f"[{ERROR_STYLE}]Please enter 1 to update now or 2 to skip.[/]")
209
+
210
+
211
+ def _run_update_command(console: Console, ctx: Any) -> None:
212
+ """Invoke the built-in update command and surface any errors."""
213
+ try:
214
+ ctx.invoke(update_command)
215
+ except click.ClickException as exc:
216
+ exc.show()
217
+ console.print(f"[{ERROR_STYLE}]Update command exited with an error.[/]")
218
+ except click.Abort:
219
+ console.print(f"[{WARNING_STYLE}]Update aborted by user.[/]")
220
+ except Exception as exc: # pragma: no cover - defensive guard
221
+ console.print(f"[{ERROR_STYLE}]Unexpected error while running update: {exc}[/]")
222
+ else:
223
+ _refresh_installed_version(console, ctx)
224
+
225
+
226
+ @contextmanager
227
+ def _suppress_library_logging(
228
+ logger_names: Iterable[str] | None = None, *, level: int = logging.WARNING
229
+ ) -> Iterator[None]:
230
+ """Temporarily raise log level for selected libraries during update checks."""
231
+ names = tuple(logger_names) if logger_names is not None else ("httpx",)
232
+ captured: list[tuple[logging.Logger, int]] = []
233
+ try:
234
+ for name in names:
235
+ logger = logging.getLogger(name)
236
+ captured.append((logger, logger.level))
237
+ logger.setLevel(level)
238
+ yield
239
+ finally:
240
+ for logger, previous_level in captured:
241
+ logger.setLevel(previous_level)
242
+
243
+
244
+ def _refresh_installed_version(console: Console, ctx: Any) -> None:
245
+ """Reload runtime metadata after an in-process upgrade."""
246
+ new_version: str | None = None
247
+
248
+ try:
249
+ version_module = importlib.reload(importlib.import_module("glaip_sdk._version"))
250
+ new_version = getattr(version_module, "__version__", None)
251
+ except Exception as exc: # pragma: no cover - defensive guard
252
+ _LOGGER.debug("Failed to reload glaip_sdk._version: %s", exc, exc_info=True)
253
+
254
+ try:
255
+ branding_module = importlib.import_module("glaip_sdk.branding")
256
+ if new_version:
257
+ branding_module.SDK_VERSION = new_version
258
+ except Exception as exc: # pragma: no cover - defensive guard
259
+ _LOGGER.debug("Failed to update branding metadata: %s", exc, exc_info=True)
260
+
261
+ session = _get_slash_session(ctx)
262
+ if session and hasattr(session, "refresh_branding"):
263
+ try:
264
+ session.refresh_branding(new_version)
265
+ return
266
+ except Exception as exc: # pragma: no cover - defensive guard
267
+ _LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
268
+
269
+ if new_version:
270
+ console.print(f"[{SUCCESS_STYLE}]CLI now running glaip-sdk {new_version}.[/]")
271
+
272
+
273
+ def _get_slash_session(ctx: Any) -> Any | None:
274
+ """Return active slash session from the Click context if present."""
275
+ ctx_obj = getattr(ctx, "obj", None)
276
+ if isinstance(ctx_obj, dict):
277
+ return ctx_obj.get("_slash_session")
278
+ return None
135
279
 
136
280
 
137
281
  __all__ = ["maybe_notify_update"]