glaip-sdk 0.0.20__py3-none-any.whl → 0.1.1__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.
- glaip_sdk/_version.py +1 -3
- glaip_sdk/branding.py +2 -6
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +11 -30
- glaip_sdk/cli/commands/agents.py +64 -107
- glaip_sdk/cli/commands/configure.py +12 -36
- glaip_sdk/cli/commands/mcps.py +25 -63
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/tools.py +22 -35
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +1 -3
- glaip_sdk/cli/display.py +4 -12
- glaip_sdk/cli/io.py +8 -14
- glaip_sdk/cli/main.py +10 -30
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +3 -9
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/agent_session.py +5 -10
- glaip_sdk/cli/slash/prompt.py +3 -10
- glaip_sdk/cli/slash/session.py +46 -98
- glaip_sdk/cli/transcript/cache.py +6 -19
- glaip_sdk/cli/transcript/capture.py +6 -20
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +187 -46
- glaip_sdk/cli/update_notifier.py +165 -21
- glaip_sdk/cli/utils.py +33 -85
- glaip_sdk/cli/validators.py +11 -12
- glaip_sdk/client/_agent_payloads.py +10 -30
- glaip_sdk/client/agents.py +33 -63
- glaip_sdk/client/base.py +6 -22
- glaip_sdk/client/mcps.py +1 -3
- glaip_sdk/client/run_rendering.py +121 -24
- glaip_sdk/client/tools.py +8 -24
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/icons.py +9 -3
- glaip_sdk/models.py +14 -33
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/client_utils.py +7 -21
- glaip_sdk/utils/display.py +2 -6
- glaip_sdk/utils/general.py +1 -3
- glaip_sdk/utils/import_export.py +3 -9
- glaip_sdk/utils/rendering/formatting.py +52 -12
- glaip_sdk/utils/rendering/models.py +17 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +1 -5
- glaip_sdk/utils/rendering/renderer/base.py +1107 -320
- glaip_sdk/utils/rendering/renderer/config.py +3 -5
- glaip_sdk/utils/rendering/renderer/debug.py +4 -14
- glaip_sdk/utils/rendering/renderer/panels.py +1 -3
- glaip_sdk/utils/rendering/renderer/progress.py +3 -11
- glaip_sdk/utils/rendering/renderer/stream.py +10 -22
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps.py +899 -25
- glaip_sdk/utils/resource_refs.py +4 -13
- glaip_sdk/utils/serialization.py +14 -46
- glaip_sdk/utils/validation.py +4 -4
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/METADATA +12 -1
- glaip_sdk-0.1.1.dist-info/RECORD +82 -0
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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}
|
|
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
|
-
|
|
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(
|
|
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"]
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -147,9 +147,7 @@ def format_command_hint(
|
|
|
147
147
|
|
|
148
148
|
highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
|
|
149
149
|
if description:
|
|
150
|
-
highlighted +=
|
|
151
|
-
f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
|
|
152
|
-
)
|
|
150
|
+
highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
|
|
153
151
|
return highlighted
|
|
154
152
|
|
|
155
153
|
|
|
@@ -234,7 +232,7 @@ _spinner_stop = stop_spinner
|
|
|
234
232
|
def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
235
233
|
"""Get configured client from context, env, and config file (ctx > env > file)."""
|
|
236
234
|
module = importlib.import_module("glaip_sdk")
|
|
237
|
-
client_class = cast("type[Client]",
|
|
235
|
+
client_class = cast("type[Client]", module.Client)
|
|
238
236
|
file_config = load_config() or {}
|
|
239
237
|
context_config_obj = getattr(ctx, "obj", None)
|
|
240
238
|
context_config = context_config_obj or {}
|
|
@@ -419,9 +417,7 @@ def _prompt_with_auto_select(
|
|
|
419
417
|
reserve_space_for_menu=8,
|
|
420
418
|
)
|
|
421
419
|
except Exception as exc: # pragma: no cover - depends on prompt_toolkit
|
|
422
|
-
logger.debug(
|
|
423
|
-
"PromptSession init failed (%s); falling back to basic prompt.", exc
|
|
424
|
-
)
|
|
420
|
+
logger.debug("PromptSession init failed (%s); falling back to basic prompt.", exc)
|
|
425
421
|
return _basic_prompt(message, completer)
|
|
426
422
|
|
|
427
423
|
buffer = session.default_buffer
|
|
@@ -447,9 +443,7 @@ def _prompt_with_auto_select(
|
|
|
447
443
|
except (KeyboardInterrupt, EOFError):
|
|
448
444
|
return None
|
|
449
445
|
except Exception as exc: # pragma: no cover - defensive
|
|
450
|
-
logger.debug(
|
|
451
|
-
"PromptSession prompt failed (%s); falling back to basic prompt.", exc
|
|
452
|
-
)
|
|
446
|
+
logger.debug("PromptSession prompt failed (%s); falling back to basic prompt.", exc)
|
|
453
447
|
return _basic_prompt(message, completer)
|
|
454
448
|
finally:
|
|
455
449
|
if handler_attached:
|
|
@@ -465,9 +459,7 @@ class _FuzzyCompleter:
|
|
|
465
459
|
def __init__(self, words: list[str]) -> None:
|
|
466
460
|
self.words = words
|
|
467
461
|
|
|
468
|
-
def get_completions(
|
|
469
|
-
self, document: Any, _complete_event: Any
|
|
470
|
-
) -> Any: # pragma: no cover
|
|
462
|
+
def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
|
|
471
463
|
word = document.get_word_before_cursor()
|
|
472
464
|
if not word:
|
|
473
465
|
return
|
|
@@ -492,9 +484,7 @@ class _FuzzyCompleter:
|
|
|
492
484
|
return False
|
|
493
485
|
|
|
494
486
|
|
|
495
|
-
def _perform_fuzzy_search(
|
|
496
|
-
answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]
|
|
497
|
-
) -> dict[str, Any] | None:
|
|
487
|
+
def _perform_fuzzy_search(answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]) -> dict[str, Any] | None:
|
|
498
488
|
"""Perform fuzzy search fallback and return best match."""
|
|
499
489
|
# Exact label match
|
|
500
490
|
if answer in by_label:
|
|
@@ -615,7 +605,7 @@ def _coerce_result_payload(result: Any) -> Any:
|
|
|
615
605
|
|
|
616
606
|
|
|
617
607
|
def _ensure_displayable(payload: Any) -> Any:
|
|
618
|
-
if isinstance(payload, dict
|
|
608
|
+
if isinstance(payload, (dict, list, str, int, float, bool)) or payload is None:
|
|
619
609
|
return payload
|
|
620
610
|
|
|
621
611
|
if hasattr(payload, "__dict__"):
|
|
@@ -686,9 +676,7 @@ def output_result(
|
|
|
686
676
|
# _PICK_THRESHOLD = int(os.getenv("AIP_PICK_THRESHOLD", "5") or "5")
|
|
687
677
|
|
|
688
678
|
|
|
689
|
-
def _normalise_rows(
|
|
690
|
-
items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None
|
|
691
|
-
) -> list[dict[str, Any]]:
|
|
679
|
+
def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
692
680
|
try:
|
|
693
681
|
rows: list[dict[str, Any]] = []
|
|
694
682
|
for item in items:
|
|
@@ -707,9 +695,7 @@ def _normalise_rows(
|
|
|
707
695
|
return []
|
|
708
696
|
|
|
709
697
|
|
|
710
|
-
def _render_plain_list(
|
|
711
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
712
|
-
) -> None:
|
|
698
|
+
def _render_plain_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
713
699
|
if not rows:
|
|
714
700
|
click.echo(f"No {title.lower()} found.")
|
|
715
701
|
return
|
|
@@ -718,9 +704,7 @@ def _render_plain_list(
|
|
|
718
704
|
click.echo(row_str)
|
|
719
705
|
|
|
720
706
|
|
|
721
|
-
def _render_markdown_list(
|
|
722
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
723
|
-
) -> None:
|
|
707
|
+
def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
724
708
|
if not rows:
|
|
725
709
|
click.echo(f"No {title.lower()} found.")
|
|
726
710
|
return
|
|
@@ -748,9 +732,7 @@ def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -
|
|
|
748
732
|
return table
|
|
749
733
|
|
|
750
734
|
|
|
751
|
-
def _build_table_group(
|
|
752
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
753
|
-
) -> Group:
|
|
735
|
+
def _build_table_group(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> Group:
|
|
754
736
|
table = _create_table(columns, title)
|
|
755
737
|
for row in rows:
|
|
756
738
|
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
@@ -760,24 +742,16 @@ def _build_table_group(
|
|
|
760
742
|
|
|
761
743
|
def _handle_json_output(items: list[Any], rows: list[dict[str, Any]]) -> None:
|
|
762
744
|
"""Handle JSON output format."""
|
|
763
|
-
data = (
|
|
764
|
-
rows
|
|
765
|
-
if rows
|
|
766
|
-
else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
|
|
767
|
-
)
|
|
745
|
+
data = rows if rows else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
|
|
768
746
|
click.echo(json.dumps(data, indent=2, default=str))
|
|
769
747
|
|
|
770
748
|
|
|
771
|
-
def _handle_plain_output(
|
|
772
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
773
|
-
) -> None:
|
|
749
|
+
def _handle_plain_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
774
750
|
"""Handle plain text output format."""
|
|
775
751
|
_render_plain_list(rows, title, columns)
|
|
776
752
|
|
|
777
753
|
|
|
778
|
-
def _handle_markdown_output(
|
|
779
|
-
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
780
|
-
) -> None:
|
|
754
|
+
def _handle_markdown_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
781
755
|
"""Handle markdown output format."""
|
|
782
756
|
_render_markdown_list(rows, title, columns)
|
|
783
757
|
|
|
@@ -792,9 +766,7 @@ def _should_use_fuzzy_picker() -> bool:
|
|
|
792
766
|
return console.is_terminal and os.isatty(1)
|
|
793
767
|
|
|
794
768
|
|
|
795
|
-
def _try_fuzzy_pick(
|
|
796
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
797
|
-
) -> dict[str, Any] | None:
|
|
769
|
+
def _try_fuzzy_pick(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> dict[str, Any] | None:
|
|
798
770
|
"""Best-effort fuzzy selection; returns None if the picker fails."""
|
|
799
771
|
if not _should_use_fuzzy_picker():
|
|
800
772
|
return None
|
|
@@ -825,14 +797,10 @@ def _print_selection_tip(title: str) -> None:
|
|
|
825
797
|
"""Print the contextual follow-up tip after a fuzzy selection."""
|
|
826
798
|
tip_cmd = _resource_tip_command(title)
|
|
827
799
|
if tip_cmd:
|
|
828
|
-
console.print(
|
|
829
|
-
markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]")
|
|
830
|
-
)
|
|
800
|
+
console.print(markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]"))
|
|
831
801
|
|
|
832
802
|
|
|
833
|
-
def _handle_fuzzy_pick_selection(
|
|
834
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
835
|
-
) -> bool:
|
|
803
|
+
def _handle_fuzzy_pick_selection(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> bool:
|
|
836
804
|
"""Handle fuzzy picker selection, returns True if selection was made."""
|
|
837
805
|
picked = _try_fuzzy_pick(rows, columns, title)
|
|
838
806
|
if picked is None:
|
|
@@ -855,9 +823,7 @@ def _handle_table_output(
|
|
|
855
823
|
"""Handle table output with paging."""
|
|
856
824
|
content = _build_table_group(rows, columns, title)
|
|
857
825
|
should_page = (
|
|
858
|
-
pager._should_page_output(len(rows), console.is_terminal and os.isatty(1))
|
|
859
|
-
if use_pager is None
|
|
860
|
-
else use_pager
|
|
826
|
+
pager._should_page_output(len(rows), console.is_terminal and os.isatty(1)) if use_pager is None else use_pager
|
|
861
827
|
)
|
|
862
828
|
|
|
863
829
|
if should_page:
|
|
@@ -982,7 +948,6 @@ def build_renderer(
|
|
|
982
948
|
theme=theme,
|
|
983
949
|
style=style,
|
|
984
950
|
live=live_enabled,
|
|
985
|
-
show_delegate_tool_panels=False,
|
|
986
951
|
append_finished_snapshots=bool(snapshots)
|
|
987
952
|
if snapshots is not None
|
|
988
953
|
else RendererConfig.append_finished_snapshots,
|
|
@@ -990,9 +955,7 @@ def build_renderer(
|
|
|
990
955
|
|
|
991
956
|
# Create the renderer instance
|
|
992
957
|
renderer = RichStreamRenderer(
|
|
993
|
-
working_console.original_console
|
|
994
|
-
if isinstance(working_console, CapturingConsole)
|
|
995
|
-
else working_console,
|
|
958
|
+
working_console.original_console if isinstance(working_console, CapturingConsole) else working_console,
|
|
996
959
|
cfg=renderer_cfg,
|
|
997
960
|
verbose=verbose,
|
|
998
961
|
)
|
|
@@ -1080,22 +1043,14 @@ def _resolve_by_name_multiple_with_select(matches: list[Any], select: int) -> An
|
|
|
1080
1043
|
return matches[idx]
|
|
1081
1044
|
|
|
1082
1045
|
|
|
1083
|
-
def _resolve_by_name_multiple_fuzzy(
|
|
1084
|
-
ctx: Any, ref: str, matches: list[Any], label: str
|
|
1085
|
-
) -> Any:
|
|
1046
|
+
def _resolve_by_name_multiple_fuzzy(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
|
|
1086
1047
|
"""Resolve multiple matches preferring the fuzzy picker interface."""
|
|
1087
|
-
return handle_ambiguous_resource(
|
|
1088
|
-
ctx, label.lower(), ref, matches, interface_preference="fuzzy"
|
|
1089
|
-
)
|
|
1048
|
+
return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="fuzzy")
|
|
1090
1049
|
|
|
1091
1050
|
|
|
1092
|
-
def _resolve_by_name_multiple_questionary(
|
|
1093
|
-
ctx: Any, ref: str, matches: list[Any], label: str
|
|
1094
|
-
) -> Any:
|
|
1051
|
+
def _resolve_by_name_multiple_questionary(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
|
|
1095
1052
|
"""Resolve multiple matches preferring the questionary interface."""
|
|
1096
|
-
return handle_ambiguous_resource(
|
|
1097
|
-
ctx, label.lower(), ref, matches, interface_preference="questionary"
|
|
1098
|
-
)
|
|
1053
|
+
return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="questionary")
|
|
1099
1054
|
|
|
1100
1055
|
|
|
1101
1056
|
def resolve_resource(
|
|
@@ -1140,9 +1095,7 @@ def resolve_resource(
|
|
|
1140
1095
|
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1141
1096
|
|
|
1142
1097
|
# Find resources by name
|
|
1143
|
-
_spinner_update(
|
|
1144
|
-
spinner, f"[bold blue]Searching {label}s matching '{ref}'…[/bold blue]"
|
|
1145
|
-
)
|
|
1098
|
+
_spinner_update(spinner, f"[bold blue]Searching {label}s matching '{ref}'…[/bold blue]")
|
|
1146
1099
|
matches = find_by_name(name=ref)
|
|
1147
1100
|
if not matches:
|
|
1148
1101
|
_spinner_stop(spinner)
|
|
@@ -1173,9 +1126,7 @@ def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
|
|
|
1173
1126
|
return matches[0]
|
|
1174
1127
|
|
|
1175
1128
|
|
|
1176
|
-
def _handle_questionary_ambiguity(
|
|
1177
|
-
resource_type: str, ref: str, matches: list[Any]
|
|
1178
|
-
) -> Any:
|
|
1129
|
+
def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1179
1130
|
"""Handle ambiguity using questionary interactive interface."""
|
|
1180
1131
|
if not (questionary and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
|
|
1181
1132
|
raise click.ClickException("Interactive selection not available")
|
|
@@ -1188,7 +1139,10 @@ def _handle_questionary_ambiguity(
|
|
|
1188
1139
|
f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
|
|
1189
1140
|
choices=[
|
|
1190
1141
|
questionary.Choice(
|
|
1191
|
-
title=
|
|
1142
|
+
title=(
|
|
1143
|
+
f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
|
|
1144
|
+
f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
|
|
1145
|
+
),
|
|
1192
1146
|
value=i,
|
|
1193
1147
|
)
|
|
1194
1148
|
for i, m in enumerate(matches)
|
|
@@ -1202,19 +1156,13 @@ def _handle_questionary_ambiguity(
|
|
|
1202
1156
|
return matches[picked_idx]
|
|
1203
1157
|
|
|
1204
1158
|
|
|
1205
|
-
def _handle_fallback_numeric_ambiguity(
|
|
1206
|
-
resource_type: str, ref: str, matches: list[Any]
|
|
1207
|
-
) -> Any:
|
|
1159
|
+
def _handle_fallback_numeric_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1208
1160
|
"""Handle ambiguity using numeric prompt fallback."""
|
|
1209
1161
|
# Escape special characters for display
|
|
1210
1162
|
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1211
1163
|
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1212
1164
|
|
|
1213
|
-
console.print(
|
|
1214
|
-
markup_text(
|
|
1215
|
-
f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"
|
|
1216
|
-
)
|
|
1217
|
-
)
|
|
1165
|
+
console.print(markup_text(f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"))
|
|
1218
1166
|
table = AIPTable(
|
|
1219
1167
|
title=f"Select {safe_resource_type.title()}",
|
|
1220
1168
|
)
|
|
@@ -1229,8 +1177,8 @@ def _handle_fallback_numeric_ambiguity(
|
|
|
1229
1177
|
)
|
|
1230
1178
|
try:
|
|
1231
1179
|
choice = int(choice_str)
|
|
1232
|
-
except ValueError:
|
|
1233
|
-
raise click.ClickException("Invalid selection")
|
|
1180
|
+
except ValueError as err:
|
|
1181
|
+
raise click.ClickException("Invalid selection") from err
|
|
1234
1182
|
if 1 <= choice <= len(matches):
|
|
1235
1183
|
return matches[choice - 1]
|
|
1236
1184
|
raise click.ClickException("Invalid selection")
|
glaip_sdk/cli/validators.py
CHANGED
|
@@ -42,7 +42,7 @@ def validate_agent_name_cli(name: str) -> str:
|
|
|
42
42
|
try:
|
|
43
43
|
return validate_agent_name(name)
|
|
44
44
|
except ValueError as e:
|
|
45
|
-
raise click.ClickException(str(e))
|
|
45
|
+
raise click.ClickException(str(e)) from e
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def validate_agent_instruction_cli(instruction: str) -> str:
|
|
@@ -60,7 +60,7 @@ def validate_agent_instruction_cli(instruction: str) -> str:
|
|
|
60
60
|
try:
|
|
61
61
|
return validate_agent_instruction(instruction)
|
|
62
62
|
except ValueError as e:
|
|
63
|
-
raise click.ClickException(str(e))
|
|
63
|
+
raise click.ClickException(str(e)) from e
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def validate_timeout_cli(timeout: int) -> int:
|
|
@@ -78,7 +78,7 @@ def validate_timeout_cli(timeout: int) -> int:
|
|
|
78
78
|
try:
|
|
79
79
|
return validate_timeout(timeout)
|
|
80
80
|
except ValueError as e:
|
|
81
|
-
raise click.ClickException(str(e))
|
|
81
|
+
raise click.ClickException(str(e)) from e
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def validate_tool_name_cli(name: str) -> str:
|
|
@@ -96,7 +96,7 @@ def validate_tool_name_cli(name: str) -> str:
|
|
|
96
96
|
try:
|
|
97
97
|
return validate_tool_name(name)
|
|
98
98
|
except ValueError as e:
|
|
99
|
-
raise click.ClickException(str(e))
|
|
99
|
+
raise click.ClickException(str(e)) from e
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def validate_mcp_name_cli(name: str) -> str:
|
|
@@ -114,7 +114,7 @@ def validate_mcp_name_cli(name: str) -> str:
|
|
|
114
114
|
try:
|
|
115
115
|
return validate_mcp_name(name)
|
|
116
116
|
except ValueError as e:
|
|
117
|
-
raise click.ClickException(str(e))
|
|
117
|
+
raise click.ClickException(str(e)) from e
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Path:
|
|
@@ -133,7 +133,7 @@ def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Pa
|
|
|
133
133
|
try:
|
|
134
134
|
return validate_file_path(file_path, must_exist)
|
|
135
135
|
except ValueError as e:
|
|
136
|
-
raise click.ClickException(str(e))
|
|
136
|
+
raise click.ClickException(str(e)) from e
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -> Path:
|
|
@@ -152,7 +152,7 @@ def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -
|
|
|
152
152
|
try:
|
|
153
153
|
return validate_directory_path(dir_path, must_exist)
|
|
154
154
|
except ValueError as e:
|
|
155
|
-
raise click.ClickException(str(e))
|
|
155
|
+
raise click.ClickException(str(e)) from e
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def validate_url_cli(url: str) -> str:
|
|
@@ -170,7 +170,7 @@ def validate_url_cli(url: str) -> str:
|
|
|
170
170
|
try:
|
|
171
171
|
return validate_url(url)
|
|
172
172
|
except ValueError as e:
|
|
173
|
-
raise click.ClickException(str(e))
|
|
173
|
+
raise click.ClickException(str(e)) from e
|
|
174
174
|
|
|
175
175
|
|
|
176
176
|
def validate_api_key_cli(api_key: str) -> str:
|
|
@@ -188,7 +188,7 @@ def validate_api_key_cli(api_key: str) -> str:
|
|
|
188
188
|
try:
|
|
189
189
|
return validate_api_key(api_key)
|
|
190
190
|
except ValueError as e:
|
|
191
|
-
raise click.ClickException(str(e))
|
|
191
|
+
raise click.ClickException(str(e)) from e
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
def coerce_timeout_cli(value: int | float | str) -> int:
|
|
@@ -206,7 +206,7 @@ def coerce_timeout_cli(value: int | float | str) -> int:
|
|
|
206
206
|
try:
|
|
207
207
|
return coerce_timeout(value)
|
|
208
208
|
except ValueError as e:
|
|
209
|
-
raise click.ClickException(str(e))
|
|
209
|
+
raise click.ClickException(str(e)) from e
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
def validate_name_uniqueness_cli(
|
|
@@ -230,8 +230,7 @@ def validate_name_uniqueness_cli(
|
|
|
230
230
|
existing = finder_func(name=name)
|
|
231
231
|
if existing:
|
|
232
232
|
raise click.ClickException(
|
|
233
|
-
f"A {resource_type.lower()} named '{name}' already exists. "
|
|
234
|
-
"Please choose a unique name."
|
|
233
|
+
f"A {resource_type.lower()} named '{name}' already exists. Please choose a unique name."
|
|
235
234
|
)
|
|
236
235
|
except click.ClickException:
|
|
237
236
|
raise
|