glaip-sdk 0.6.3__py3-none-any.whl → 0.6.5__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/agents/base.py +54 -8
- glaip_sdk/cli/auth.py +1 -1
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/utils.py +241 -1732
- glaip_sdk/client/tools.py +5 -3
- glaip_sdk/registry/mcp.py +8 -6
- {glaip_sdk-0.6.3.dist-info → glaip_sdk-0.6.5.dist-info}/METADATA +1 -1
- {glaip_sdk-0.6.3.dist-info → glaip_sdk-0.6.5.dist-info}/RECORD +14 -9
- {glaip_sdk-0.6.3.dist-info → glaip_sdk-0.6.5.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.3.dist-info → glaip_sdk-0.6.5.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/utils.py
CHANGED
|
@@ -1,1754 +1,263 @@
|
|
|
1
|
-
"""CLI utilities for glaip-sdk.
|
|
1
|
+
"""CLI utilities for glaip-sdk (facade for backward compatibility).
|
|
2
|
+
|
|
3
|
+
This module is a backward-compatible facade that re-exports functions from
|
|
4
|
+
glaip_sdk.cli.core.* modules. New code should import directly from the core modules.
|
|
5
|
+
The facade is deprecated and will be removed after consumers migrate to core modules;
|
|
6
|
+
see docs/specs/refactor/cli-core-modularization.md for the migration plan.
|
|
2
7
|
|
|
3
8
|
Authors:
|
|
4
9
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
10
|
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
6
|
-
"""
|
|
11
|
+
""" # pylint: disable=duplicate-code
|
|
7
12
|
|
|
8
13
|
from __future__ import annotations
|
|
9
14
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import json
|
|
13
|
-
import logging
|
|
14
|
-
import os
|
|
15
|
-
import re
|
|
16
|
-
import sys
|
|
17
|
-
from collections.abc import Callable, Iterable
|
|
18
|
-
from contextlib import AbstractContextManager, contextmanager, nullcontext
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
21
|
-
|
|
22
|
-
import click
|
|
23
|
-
import yaml
|
|
24
|
-
from rich.console import Console, Group
|
|
25
|
-
from rich.markdown import Markdown
|
|
26
|
-
from rich.syntax import Syntax
|
|
15
|
+
import threading
|
|
16
|
+
import warnings
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
from glaip_sdk.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
# Re-export from core modules
|
|
19
|
+
from glaip_sdk.cli.core.context import (
|
|
20
|
+
bind_slash_session_context,
|
|
21
|
+
get_client,
|
|
22
|
+
handle_best_effort_check,
|
|
23
|
+
restore_slash_session_context,
|
|
33
24
|
)
|
|
34
|
-
from glaip_sdk.cli import
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
from glaip_sdk.cli.core.output import (
|
|
26
|
+
coerce_to_row,
|
|
27
|
+
detect_export_format,
|
|
28
|
+
fetch_resource_for_export,
|
|
29
|
+
format_datetime_fields,
|
|
30
|
+
format_size,
|
|
31
|
+
handle_ambiguous_resource,
|
|
32
|
+
handle_resource_export,
|
|
33
|
+
output_list,
|
|
34
|
+
output_result,
|
|
35
|
+
parse_json_line,
|
|
36
|
+
resolve_resource,
|
|
37
|
+
sdk_version,
|
|
38
|
+
# Private functions for backward compatibility (used in tests)
|
|
39
|
+
_build_table_group,
|
|
40
|
+
_build_yaml_renderable,
|
|
41
|
+
_coerce_result_payload,
|
|
42
|
+
_create_table,
|
|
43
|
+
_ensure_displayable,
|
|
44
|
+
_format_yaml_text,
|
|
45
|
+
_get_interface_order,
|
|
46
|
+
_handle_empty_items,
|
|
47
|
+
_handle_fallback_numeric_ambiguity,
|
|
48
|
+
_handle_fuzzy_pick_selection,
|
|
49
|
+
_handle_json_output,
|
|
50
|
+
_handle_json_view_ambiguity,
|
|
51
|
+
_handle_markdown_output,
|
|
52
|
+
_handle_plain_output,
|
|
53
|
+
_handle_questionary_ambiguity,
|
|
54
|
+
_handle_table_output,
|
|
55
|
+
_literal_str_representer,
|
|
56
|
+
_normalise_rows,
|
|
57
|
+
_normalize_interface_preference,
|
|
58
|
+
_print_selection_tip,
|
|
59
|
+
_render_markdown_list,
|
|
60
|
+
_render_markdown_output,
|
|
61
|
+
_render_plain_list,
|
|
62
|
+
_resolve_by_id,
|
|
63
|
+
_resolve_by_name_multiple_fuzzy,
|
|
64
|
+
_resolve_by_name_multiple_questionary,
|
|
65
|
+
_resolve_by_name_multiple_with_select,
|
|
66
|
+
_resource_tip_command,
|
|
67
|
+
_should_fallback_to_numeric_prompt,
|
|
68
|
+
_should_sort_rows,
|
|
69
|
+
_should_use_fuzzy_picker,
|
|
70
|
+
_try_fuzzy_pick,
|
|
71
|
+
_try_fuzzy_selection,
|
|
72
|
+
_try_interface_selection,
|
|
73
|
+
_try_questionary_selection,
|
|
74
|
+
_LiteralYamlDumper,
|
|
41
75
|
)
|
|
42
|
-
from glaip_sdk.cli.
|
|
43
|
-
|
|
76
|
+
from glaip_sdk.cli.core.prompting import (
|
|
77
|
+
_FuzzyCompleter, # Private class for backward compatibility (used in tests)
|
|
78
|
+
_fuzzy_pick_for_resources,
|
|
79
|
+
prompt_export_choice_questionary,
|
|
80
|
+
questionary_safe_ask,
|
|
81
|
+
# Private functions for backward compatibility (used in tests)
|
|
82
|
+
_asyncio_loop_running,
|
|
83
|
+
_basic_prompt,
|
|
84
|
+
_build_resource_labels,
|
|
85
|
+
_build_display_parts,
|
|
86
|
+
_build_primary_parts,
|
|
87
|
+
_build_unique_labels,
|
|
88
|
+
_calculate_consecutive_bonus,
|
|
89
|
+
_calculate_exact_match_bonus,
|
|
90
|
+
_calculate_length_bonus,
|
|
91
|
+
_check_fuzzy_pick_requirements,
|
|
92
|
+
_extract_display_fields,
|
|
93
|
+
_extract_fallback_values,
|
|
94
|
+
_extract_id_suffix,
|
|
95
|
+
_get_fallback_columns,
|
|
96
|
+
_fuzzy_pick,
|
|
97
|
+
_fuzzy_score,
|
|
98
|
+
_is_fuzzy_match,
|
|
99
|
+
_is_standard_field,
|
|
100
|
+
_load_questionary_module,
|
|
101
|
+
_make_questionary_choice,
|
|
102
|
+
_perform_fuzzy_search,
|
|
103
|
+
_prompt_with_auto_select,
|
|
104
|
+
_rank_labels,
|
|
105
|
+
_row_display,
|
|
106
|
+
_run_questionary_in_thread,
|
|
107
|
+
_strip_spaces_for_matching,
|
|
44
108
|
)
|
|
45
|
-
from glaip_sdk.cli.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
109
|
+
from glaip_sdk.cli.core.rendering import (
|
|
110
|
+
build_renderer,
|
|
111
|
+
spinner_context,
|
|
112
|
+
stop_spinner,
|
|
113
|
+
update_spinner,
|
|
114
|
+
with_client_and_spinner,
|
|
115
|
+
# Private functions for backward compatibility (used in tests)
|
|
116
|
+
_can_use_spinner,
|
|
117
|
+
_register_renderer_with_session,
|
|
118
|
+
_spinner_stop,
|
|
119
|
+
_spinner_update,
|
|
120
|
+
_stream_supports_tty,
|
|
57
121
|
)
|
|
58
122
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"""Return the questionary module and Choice class if available."""
|
|
64
|
-
module = questionary
|
|
65
|
-
if module is not None:
|
|
66
|
-
return module, getattr(module, "Choice", None)
|
|
67
|
-
|
|
68
|
-
try: # pragma: no cover - optional dependency
|
|
69
|
-
module = __import__("questionary")
|
|
70
|
-
except ImportError:
|
|
71
|
-
return None, None
|
|
72
|
-
|
|
73
|
-
return module, getattr(module, "Choice", None)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _make_questionary_choice(choice_cls: Any | None, **kwargs: Any) -> Any:
|
|
77
|
-
"""Create a questionary Choice instance or lightweight fallback."""
|
|
78
|
-
if choice_cls is None:
|
|
79
|
-
return kwargs
|
|
80
|
-
return choice_cls(**kwargs)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@contextmanager
|
|
84
|
-
def bind_slash_session_context(ctx: Any, session: Any) -> Any:
|
|
85
|
-
"""Temporarily attach a slash session to the Click context.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
ctx: Click context object.
|
|
89
|
-
session: SlashSession instance to bind.
|
|
90
|
-
|
|
91
|
-
Yields:
|
|
92
|
-
None - context manager for use in with statement.
|
|
93
|
-
"""
|
|
94
|
-
ctx_obj = getattr(ctx, "obj", None)
|
|
95
|
-
has_context = isinstance(ctx_obj, dict)
|
|
96
|
-
previous_session = ctx_obj.get("_slash_session") if has_context else None
|
|
97
|
-
if has_context:
|
|
98
|
-
ctx_obj["_slash_session"] = session
|
|
99
|
-
try:
|
|
100
|
-
yield
|
|
101
|
-
finally:
|
|
102
|
-
if has_context:
|
|
103
|
-
if previous_session is None:
|
|
104
|
-
ctx_obj.pop("_slash_session", None)
|
|
105
|
-
else:
|
|
106
|
-
ctx_obj["_slash_session"] = previous_session
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def restore_slash_session_context(ctx_obj: dict[str, Any], previous_session: Any | None) -> None:
|
|
110
|
-
"""Restore slash session context after operation.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
ctx_obj: Click context obj dictionary.
|
|
114
|
-
previous_session: Previous session to restore, or None to remove.
|
|
115
|
-
"""
|
|
116
|
-
if previous_session is None:
|
|
117
|
-
ctx_obj.pop("_slash_session", None)
|
|
118
|
-
else:
|
|
119
|
-
ctx_obj["_slash_session"] = previous_session
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def handle_best_effort_check(
|
|
123
|
-
check_func: Callable[[], None],
|
|
124
|
-
) -> None:
|
|
125
|
-
"""Handle best-effort duplicate/existence checks with proper exception handling.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
check_func: Function that performs the check and raises ClickException if duplicate found.
|
|
129
|
-
"""
|
|
130
|
-
try:
|
|
131
|
-
check_func()
|
|
132
|
-
except click.ClickException:
|
|
133
|
-
raise
|
|
134
|
-
except Exception:
|
|
135
|
-
# Non-fatal: best-effort duplicate check
|
|
136
|
-
pass
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def prompt_export_choice_questionary(
|
|
140
|
-
default_path: Path,
|
|
141
|
-
default_display: str,
|
|
142
|
-
) -> tuple[str, Path | None] | None:
|
|
143
|
-
"""Prompt user for export destination using questionary with numeric shortcuts.
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
default_path: Default export path.
|
|
147
|
-
default_display: Formatted display string for default path.
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
Tuple of (choice, path) or None if cancelled/unavailable.
|
|
151
|
-
Choice can be "default", "custom", or "cancel".
|
|
152
|
-
"""
|
|
153
|
-
questionary_module, choice_cls = _load_questionary_module()
|
|
154
|
-
if questionary_module is None or choice_cls is None:
|
|
155
|
-
return None
|
|
156
|
-
|
|
157
|
-
try:
|
|
158
|
-
question = questionary_module.select(
|
|
159
|
-
"Export transcript",
|
|
160
|
-
choices=[
|
|
161
|
-
_make_questionary_choice(
|
|
162
|
-
choice_cls,
|
|
163
|
-
title=f"Save to default ({default_display})",
|
|
164
|
-
value=("default", default_path),
|
|
165
|
-
shortcut_key="1",
|
|
166
|
-
),
|
|
167
|
-
_make_questionary_choice(
|
|
168
|
-
choice_cls,
|
|
169
|
-
title="Choose a different path",
|
|
170
|
-
value=("custom", None),
|
|
171
|
-
shortcut_key="2",
|
|
172
|
-
),
|
|
173
|
-
_make_questionary_choice(
|
|
174
|
-
choice_cls,
|
|
175
|
-
title="Cancel",
|
|
176
|
-
value=("cancel", None),
|
|
177
|
-
shortcut_key="3",
|
|
178
|
-
),
|
|
179
|
-
],
|
|
180
|
-
use_shortcuts=True,
|
|
181
|
-
instruction="Press 1-3 (or arrows) then Enter.",
|
|
182
|
-
)
|
|
183
|
-
answer = questionary_safe_ask(question)
|
|
184
|
-
except Exception:
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
if answer is None:
|
|
188
|
-
return ("cancel", None)
|
|
189
|
-
return answer
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def questionary_safe_ask(question: Any, *, patch_stdout: bool = False) -> Any:
|
|
193
|
-
"""Run `questionary.Question` safely even when an asyncio loop is active."""
|
|
194
|
-
ask_fn = getattr(question, "unsafe_ask", None)
|
|
195
|
-
if not callable(ask_fn):
|
|
196
|
-
raise RuntimeError("Questionary prompt is missing unsafe_ask()")
|
|
197
|
-
|
|
198
|
-
if not _asyncio_loop_running():
|
|
199
|
-
return ask_fn(patch_stdout=patch_stdout)
|
|
200
|
-
|
|
201
|
-
return _run_questionary_in_thread(question, patch_stdout=patch_stdout)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _asyncio_loop_running() -> bool:
|
|
205
|
-
"""Return True when an asyncio event loop is already running."""
|
|
206
|
-
try:
|
|
207
|
-
asyncio.get_running_loop()
|
|
208
|
-
except RuntimeError:
|
|
209
|
-
return False
|
|
210
|
-
return True
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def _run_questionary_in_thread(question: Any, *, patch_stdout: bool = False) -> Any:
|
|
214
|
-
"""Execute a questionary prompt in a background thread."""
|
|
215
|
-
if getattr(question, "should_skip_question", False):
|
|
216
|
-
return getattr(question, "default", None)
|
|
217
|
-
|
|
218
|
-
application = getattr(question, "application", None)
|
|
219
|
-
run_callable = getattr(application, "run", None) if application is not None else None
|
|
220
|
-
if callable(run_callable):
|
|
221
|
-
try:
|
|
222
|
-
if patch_stdout and pt_patch_stdout is not None:
|
|
223
|
-
with pt_patch_stdout():
|
|
224
|
-
return run_callable(in_thread=True)
|
|
225
|
-
return run_callable(in_thread=True)
|
|
226
|
-
except TypeError:
|
|
227
|
-
pass
|
|
228
|
-
|
|
229
|
-
return question.unsafe_ask(patch_stdout=patch_stdout)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class _LiteralYamlDumper(yaml.SafeDumper):
|
|
233
|
-
"""YAML dumper that emits literal scalars for multiline strings."""
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def _literal_str_representer(dumper: yaml.Dumper, data: str) -> yaml.nodes.ScalarNode:
|
|
237
|
-
"""Represent strings in YAML, using literal blocks for verbose values."""
|
|
238
|
-
needs_literal = "\n" in data or "\r" in data
|
|
239
|
-
if not needs_literal and LITERAL_STRING_THRESHOLD and len(data) >= LITERAL_STRING_THRESHOLD: # pragma: no cover
|
|
240
|
-
needs_literal = True
|
|
241
|
-
|
|
242
|
-
style = "|" if needs_literal else None
|
|
243
|
-
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
_LiteralYamlDumper.add_representer(str, _literal_str_representer)
|
|
247
|
-
|
|
248
|
-
# Optional interactive deps (fuzzy palette)
|
|
249
|
-
try:
|
|
250
|
-
from prompt_toolkit.buffer import Buffer
|
|
251
|
-
from prompt_toolkit.completion import Completion
|
|
252
|
-
from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
|
|
253
|
-
from prompt_toolkit.selection import SelectionType
|
|
254
|
-
from prompt_toolkit.shortcuts import PromptSession, prompt
|
|
255
|
-
|
|
256
|
-
_HAS_PTK = True
|
|
257
|
-
except Exception: # pragma: no cover - optional dependency
|
|
258
|
-
Buffer = None # type: ignore[assignment]
|
|
259
|
-
SelectionType = None # type: ignore[assignment]
|
|
260
|
-
PromptSession = None # type: ignore[assignment]
|
|
261
|
-
prompt = None # type: ignore[assignment]
|
|
262
|
-
pt_patch_stdout = None # type: ignore[assignment]
|
|
263
|
-
_HAS_PTK = False
|
|
123
|
+
# Re-export from other modules for backward compatibility
|
|
124
|
+
from glaip_sdk.cli.context import get_ctx_value
|
|
125
|
+
from glaip_sdk.cli.hints import command_hint
|
|
126
|
+
from glaip_sdk.utils import is_uuid
|
|
264
127
|
|
|
265
|
-
|
|
266
|
-
|
|
128
|
+
# Re-export module-level variables for backward compatibility
|
|
129
|
+
# Note: console is re-exported from output.py since that's where _handle_table_output uses it
|
|
130
|
+
from glaip_sdk.cli.core.output import console
|
|
131
|
+
import logging
|
|
267
132
|
|
|
268
|
-
console = Console()
|
|
269
|
-
pager.console = console
|
|
270
133
|
logger = logging.getLogger("glaip_sdk.cli.utils")
|
|
271
|
-
|
|
272
|
-
_WARNED_SDK_VERSION_FALLBACK = False
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# ----------------------------- Context helpers ---------------------------- #
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def detect_export_format(file_path: str | os.PathLike[str]) -> str:
|
|
279
|
-
"""Backward-compatible proxy to `glaip_sdk.cli.context.detect_export_format`."""
|
|
280
|
-
return _detect_export_format(file_path)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def format_size(num: int | None) -> str:
|
|
284
|
-
"""Format byte counts using short human-friendly units.
|
|
285
|
-
|
|
286
|
-
Args:
|
|
287
|
-
num: Number of bytes to format (can be None or 0)
|
|
288
|
-
|
|
289
|
-
Returns:
|
|
290
|
-
Human-readable size string (e.g., "1.5KB", "2MB")
|
|
291
|
-
"""
|
|
292
|
-
if not num or num <= 0:
|
|
293
|
-
return "0B"
|
|
294
|
-
|
|
295
|
-
units = ["B", "KB", "MB", "GB", "TB"]
|
|
296
|
-
value = float(num)
|
|
297
|
-
for unit in units:
|
|
298
|
-
if value < 1024 or unit == units[-1]:
|
|
299
|
-
if unit == "B" or value >= 100:
|
|
300
|
-
return f"{value:.0f}{unit}"
|
|
301
|
-
if value >= 10:
|
|
302
|
-
return f"{value:.1f}{unit}"
|
|
303
|
-
return f"{value:.2f}{unit}"
|
|
304
|
-
value /= 1024
|
|
305
|
-
return f"{value:.1f}TB" # pragma: no cover - defensive fallback
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def parse_json_line(line: str) -> dict[str, Any] | None:
|
|
309
|
-
"""Parse a JSON line into a dictionary payload.
|
|
310
|
-
|
|
311
|
-
Args:
|
|
312
|
-
line: JSON line string to parse
|
|
313
|
-
|
|
314
|
-
Returns:
|
|
315
|
-
Parsed dictionary or None if parsing fails or result is not a dict
|
|
316
|
-
"""
|
|
317
|
-
line = line.strip()
|
|
318
|
-
if not line:
|
|
319
|
-
return None
|
|
320
|
-
try:
|
|
321
|
-
payload = json.loads(line)
|
|
322
|
-
except json.JSONDecodeError:
|
|
323
|
-
return None
|
|
324
|
-
return payload if isinstance(payload, dict) else None
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def format_datetime_fields(
|
|
328
|
-
data: dict[str, Any], fields: tuple[str, ...] = ("created_at", "updated_at")
|
|
329
|
-
) -> dict[str, Any]:
|
|
330
|
-
"""Format datetime fields in a data dictionary for display.
|
|
331
|
-
|
|
332
|
-
Args:
|
|
333
|
-
data: Dictionary containing the data to format
|
|
334
|
-
fields: Tuple of field names to format (default: created_at, updated_at)
|
|
335
|
-
|
|
336
|
-
Returns:
|
|
337
|
-
New dictionary with formatted datetime fields
|
|
338
|
-
"""
|
|
339
|
-
formatted = data.copy()
|
|
340
|
-
for field in fields:
|
|
341
|
-
if field in formatted:
|
|
342
|
-
formatted[field] = format_datetime(formatted[field])
|
|
343
|
-
return formatted
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
def fetch_resource_for_export(
|
|
347
|
-
ctx: Any,
|
|
348
|
-
resource: Any,
|
|
349
|
-
resource_type: str,
|
|
350
|
-
get_by_id_func: Callable[[str], Any],
|
|
351
|
-
console_override: Console | None = None,
|
|
352
|
-
) -> Any:
|
|
353
|
-
"""Fetch full resource details for export, handling errors gracefully.
|
|
354
|
-
|
|
355
|
-
Args:
|
|
356
|
-
ctx: Click context for spinner management
|
|
357
|
-
resource: Resource object to fetch details for
|
|
358
|
-
resource_type: Type of resource (e.g., "MCP", "Agent", "Tool")
|
|
359
|
-
get_by_id_func: Function to fetch resource by ID
|
|
360
|
-
console_override: Optional console override
|
|
361
|
-
|
|
362
|
-
Returns:
|
|
363
|
-
Resource object with full details, or original resource if fetch fails
|
|
364
|
-
"""
|
|
365
|
-
active_console = console_override or console
|
|
366
|
-
resource_id = str(getattr(resource, "id", "")).strip()
|
|
367
|
-
|
|
368
|
-
if not resource_id:
|
|
369
|
-
return resource
|
|
370
|
-
|
|
371
|
-
try:
|
|
372
|
-
with spinner_context(
|
|
373
|
-
ctx,
|
|
374
|
-
f"[bold blue]Fetching {resource_type} details…[/bold blue]",
|
|
375
|
-
console_override=active_console,
|
|
376
|
-
):
|
|
377
|
-
return get_by_id_func(resource_id)
|
|
378
|
-
except Exception:
|
|
379
|
-
# Return original resource if fetch fails
|
|
380
|
-
return resource
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def handle_resource_export(
|
|
384
|
-
ctx: Any,
|
|
385
|
-
resource: Any,
|
|
386
|
-
export_path: Path,
|
|
387
|
-
resource_type: str,
|
|
388
|
-
get_by_id_func: Callable[[str], Any],
|
|
389
|
-
console_override: Console | None = None,
|
|
390
|
-
) -> None:
|
|
391
|
-
"""Handle resource export to file with format detection and error handling.
|
|
392
|
-
|
|
393
|
-
Args:
|
|
394
|
-
ctx: Click context for spinner management
|
|
395
|
-
resource: Resource object to export
|
|
396
|
-
export_path: Target file path (format detected from extension)
|
|
397
|
-
resource_type: Type of resource (e.g., "agent", "tool")
|
|
398
|
-
get_by_id_func: Function to fetch resource by ID
|
|
399
|
-
console_override: Optional console override
|
|
400
|
-
"""
|
|
401
|
-
active_console = console_override or console
|
|
402
|
-
|
|
403
|
-
# Auto-detect format from file extension
|
|
404
|
-
detected_format = detect_export_format(export_path)
|
|
405
|
-
|
|
406
|
-
# Try to fetch full details for export
|
|
407
|
-
full_resource = fetch_resource_for_export(
|
|
408
|
-
ctx,
|
|
409
|
-
resource,
|
|
410
|
-
resource_type.capitalize(),
|
|
411
|
-
get_by_id_func,
|
|
412
|
-
console_override=active_console,
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
# Export the resource
|
|
416
|
-
try:
|
|
417
|
-
with spinner_context(
|
|
418
|
-
ctx,
|
|
419
|
-
f"[bold blue]Exporting {resource_type}…[/bold blue]",
|
|
420
|
-
console_override=active_console,
|
|
421
|
-
):
|
|
422
|
-
export_resource_to_file_with_validation(full_resource, export_path, detected_format)
|
|
423
|
-
except Exception:
|
|
424
|
-
cli_display.handle_rich_output(
|
|
425
|
-
ctx,
|
|
426
|
-
markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
|
|
427
|
-
)
|
|
428
|
-
# Fallback: export with available data
|
|
429
|
-
export_resource_to_file_with_validation(resource, export_path, detected_format)
|
|
430
|
-
|
|
431
|
-
print_markup(
|
|
432
|
-
f"[{SUCCESS_STYLE}]✅ {resource_type.capitalize()} exported to: {export_path} (format: {detected_format})[/]",
|
|
433
|
-
console=active_console,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def sdk_version() -> str:
|
|
438
|
-
"""Return the current SDK version, warning if metadata is unavailable."""
|
|
439
|
-
version = getattr(_version_module, "__version__", None)
|
|
440
|
-
if isinstance(version, str) and version:
|
|
441
|
-
return version
|
|
442
|
-
|
|
443
|
-
global _WARNED_SDK_VERSION_FALLBACK
|
|
444
|
-
if not _WARNED_SDK_VERSION_FALLBACK:
|
|
445
|
-
_version_logger.warning("Unable to resolve glaip-sdk version metadata; using fallback '0.0.0'.")
|
|
446
|
-
_WARNED_SDK_VERSION_FALLBACK = True
|
|
447
|
-
|
|
448
|
-
return "0.0.0"
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
@contextmanager
|
|
452
|
-
def with_client_and_spinner(
|
|
453
|
-
ctx: Any,
|
|
454
|
-
spinner_message: str,
|
|
455
|
-
*,
|
|
456
|
-
console_override: Console | None = None,
|
|
457
|
-
) -> Any:
|
|
458
|
-
"""Context manager for commands that need client and spinner.
|
|
459
|
-
|
|
460
|
-
Args:
|
|
461
|
-
ctx: Click context.
|
|
462
|
-
spinner_message: Message to display in spinner.
|
|
463
|
-
console_override: Optional console override.
|
|
464
|
-
|
|
465
|
-
Yields:
|
|
466
|
-
Client instance.
|
|
467
|
-
"""
|
|
468
|
-
client = get_client(ctx)
|
|
469
|
-
with spinner_context(ctx, spinner_message, console_override=console_override):
|
|
470
|
-
yield client
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def spinner_context(
|
|
474
|
-
ctx: Any | None,
|
|
475
|
-
message: str,
|
|
476
|
-
*,
|
|
477
|
-
console_override: Console | None = None,
|
|
478
|
-
spinner: str = "dots",
|
|
479
|
-
spinner_style: str = ACCENT_STYLE,
|
|
480
|
-
) -> AbstractContextManager[Any]:
|
|
481
|
-
"""Return a context manager that renders a spinner when appropriate."""
|
|
482
|
-
active_console = console_override or console
|
|
483
|
-
if not _can_use_spinner(ctx, active_console):
|
|
484
|
-
return nullcontext()
|
|
485
|
-
|
|
486
|
-
status = active_console.status(
|
|
487
|
-
message,
|
|
488
|
-
spinner=spinner,
|
|
489
|
-
spinner_style=spinner_style,
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
if not hasattr(status, "__enter__") or not hasattr(status, "__exit__"):
|
|
493
|
-
return nullcontext()
|
|
494
|
-
|
|
495
|
-
return status
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
|
|
499
|
-
"""Check if spinner output is allowed in the current environment."""
|
|
500
|
-
if ctx is not None:
|
|
501
|
-
tty_enabled = bool(get_ctx_value(ctx, "tty", True))
|
|
502
|
-
view = (_get_view(ctx) or "rich").lower()
|
|
503
|
-
if not tty_enabled or view not in {"", "rich"}:
|
|
504
|
-
return False
|
|
505
|
-
|
|
506
|
-
if not active_console.is_terminal:
|
|
507
|
-
return False
|
|
508
|
-
|
|
509
|
-
return _stream_supports_tty(getattr(active_console, "file", None))
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
def _stream_supports_tty(stream: Any) -> bool:
|
|
513
|
-
"""Return True if the provided stream can safely render a spinner."""
|
|
514
|
-
target = stream if hasattr(stream, "isatty") else sys.stdout
|
|
515
|
-
try:
|
|
516
|
-
return bool(target.isatty())
|
|
517
|
-
except Exception:
|
|
518
|
-
return False
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
def update_spinner(status_indicator: Any | None, message: str) -> None:
|
|
522
|
-
"""Update spinner text when a status indicator is active."""
|
|
523
|
-
if status_indicator is None:
|
|
524
|
-
return
|
|
134
|
+
questionary = None # type: ignore[assignment]
|
|
525
135
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
except Exception: # pragma: no cover - defensive update
|
|
529
|
-
pass
|
|
136
|
+
_warn_lock = threading.Lock()
|
|
137
|
+
_warned = False
|
|
530
138
|
|
|
531
139
|
|
|
532
|
-
def
|
|
533
|
-
"""
|
|
534
|
-
|
|
140
|
+
def _warn_once() -> None:
|
|
141
|
+
"""Emit the deprecation warning once in a thread-safe way."""
|
|
142
|
+
global _warned
|
|
143
|
+
if _warned:
|
|
535
144
|
return
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
status_indicator.stop()
|
|
539
|
-
except Exception: # pragma: no cover - defensive stop
|
|
540
|
-
pass
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
# Backwards compatibility aliases for legacy callers
|
|
544
|
-
_spinner_update = update_spinner
|
|
545
|
-
_spinner_stop = stop_spinner
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
# ----------------------------- Client config ----------------------------- #
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
552
|
-
"""Get configured client from context and account store (ctx > account)."""
|
|
553
|
-
# Import here to avoid circular import
|
|
554
|
-
from glaip_sdk.cli.auth import resolve_credentials # noqa: PLC0415
|
|
555
|
-
|
|
556
|
-
module = importlib.import_module("glaip_sdk")
|
|
557
|
-
client_class = cast("type[Client]", module.Client)
|
|
558
|
-
context_config_obj = getattr(ctx, "obj", None)
|
|
559
|
-
context_config = context_config_obj or {}
|
|
560
|
-
|
|
561
|
-
account_name = context_config.get("account_name")
|
|
562
|
-
api_url, api_key, _ = resolve_credentials(
|
|
563
|
-
account_name=account_name,
|
|
564
|
-
api_url=context_config.get("api_url"),
|
|
565
|
-
api_key=context_config.get("api_key"),
|
|
566
|
-
)
|
|
567
|
-
|
|
568
|
-
if not api_url or not api_key:
|
|
569
|
-
configure_hint = command_hint("accounts add", slash_command="login", ctx=ctx)
|
|
570
|
-
actions: list[str] = []
|
|
571
|
-
if configure_hint:
|
|
572
|
-
actions.append(f"Run `{configure_hint}` to add an account profile")
|
|
573
|
-
else:
|
|
574
|
-
actions.append("add an account with 'aip accounts add'")
|
|
575
|
-
raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
|
|
576
|
-
|
|
577
|
-
# Get timeout from context or config
|
|
578
|
-
timeout = context_config.get("timeout")
|
|
579
|
-
if timeout is None:
|
|
580
|
-
raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
|
|
581
|
-
try:
|
|
582
|
-
timeout = float(raw_timeout) if raw_timeout != "0" else None
|
|
583
|
-
except ValueError:
|
|
584
|
-
timeout = None
|
|
585
|
-
if timeout is None:
|
|
586
|
-
# Fallback to legacy config
|
|
587
|
-
file_config = load_config() or {}
|
|
588
|
-
timeout = file_config.get("timeout")
|
|
589
|
-
|
|
590
|
-
return client_class(
|
|
591
|
-
api_url=api_url,
|
|
592
|
-
api_key=api_key,
|
|
593
|
-
timeout=float(timeout or 30.0),
|
|
594
|
-
)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
# ----------------------------- Secret masking ---------------------------- #
|
|
598
|
-
|
|
599
|
-
# ----------------------------- Fuzzy palette ----------------------------- #
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
def _extract_display_fields(row: dict[str, Any]) -> tuple[str, str, str, str]:
|
|
603
|
-
"""Extract display fields from row data."""
|
|
604
|
-
name = str(row.get("name", "")).strip()
|
|
605
|
-
_id = str(row.get("id", "")).strip()
|
|
606
|
-
type_ = str(row.get("type", "")).strip()
|
|
607
|
-
fw = str(row.get("framework", "")).strip()
|
|
608
|
-
return name, _id, type_, fw
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
def _build_primary_parts(name: str, type_: str, fw: str) -> list[str]:
|
|
612
|
-
"""Build primary display parts from name, type, and framework."""
|
|
613
|
-
parts = []
|
|
614
|
-
if name:
|
|
615
|
-
parts.append(name)
|
|
616
|
-
if type_:
|
|
617
|
-
parts.append(type_)
|
|
618
|
-
if fw:
|
|
619
|
-
parts.append(fw)
|
|
620
|
-
return parts
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
def _get_fallback_columns(columns: list[tuple]) -> list[tuple]:
|
|
624
|
-
"""Get first two visible columns for fallback display."""
|
|
625
|
-
return columns[:2]
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
def _is_standard_field(k: str) -> bool:
|
|
629
|
-
"""Check if field is a standard field to skip."""
|
|
630
|
-
return k in ("id", "name", "type", "framework")
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
def _extract_fallback_values(row: dict[str, Any], columns: list[tuple]) -> list[str]:
|
|
634
|
-
"""Extract fallback values from columns."""
|
|
635
|
-
fallback_parts = []
|
|
636
|
-
for k, _hdr, _style, _w in columns:
|
|
637
|
-
if _is_standard_field(k):
|
|
638
|
-
continue
|
|
639
|
-
val = str(row.get(k, "")).strip()
|
|
640
|
-
if val:
|
|
641
|
-
fallback_parts.append(val)
|
|
642
|
-
if len(fallback_parts) >= 2:
|
|
643
|
-
break
|
|
644
|
-
return fallback_parts
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
def _build_display_parts(
|
|
648
|
-
name: str, _id: str, type_: str, fw: str, row: dict[str, Any], columns: list[tuple]
|
|
649
|
-
) -> list[str]:
|
|
650
|
-
"""Build complete display parts list."""
|
|
651
|
-
parts = _build_primary_parts(name, type_, fw)
|
|
652
|
-
|
|
653
|
-
if not parts:
|
|
654
|
-
# Use fallback columns
|
|
655
|
-
fallback_columns = _get_fallback_columns(columns)
|
|
656
|
-
parts.extend(_extract_fallback_values(row, fallback_columns))
|
|
657
|
-
|
|
658
|
-
if _id:
|
|
659
|
-
parts.append(f"[{_id}]")
|
|
660
|
-
|
|
661
|
-
return parts
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
def _row_display(row: dict[str, Any], columns: list[tuple]) -> str:
|
|
665
|
-
"""Build a compact text label for the palette.
|
|
666
|
-
|
|
667
|
-
Prefers: name • type • framework • [id] (when available)
|
|
668
|
-
Falls back to first 2 columns + [id].
|
|
669
|
-
"""
|
|
670
|
-
name, _id, type_, fw = _extract_display_fields(row)
|
|
671
|
-
parts = _build_display_parts(name, _id, type_, fw, row, columns)
|
|
672
|
-
return " • ".join(parts) if parts else (_id or "(row)")
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
def _check_fuzzy_pick_requirements() -> bool:
|
|
676
|
-
"""Check if fuzzy picking requirements are met."""
|
|
677
|
-
return _HAS_PTK and console.is_terminal and os.isatty(1)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
def _build_unique_labels(
|
|
681
|
-
rows: list[dict[str, Any]], columns: list[tuple]
|
|
682
|
-
) -> tuple[list[str], dict[str, dict[str, Any]]]:
|
|
683
|
-
"""Build unique display labels and reverse mapping."""
|
|
684
|
-
labels = []
|
|
685
|
-
by_label: dict[str, dict[str, Any]] = {}
|
|
686
|
-
|
|
687
|
-
for r in rows:
|
|
688
|
-
label = _row_display(r, columns)
|
|
689
|
-
# Ensure uniqueness: if duplicate, suffix with …#n
|
|
690
|
-
if label in by_label:
|
|
691
|
-
i = 2
|
|
692
|
-
base = label
|
|
693
|
-
while f"{base} #{i}" in by_label:
|
|
694
|
-
i += 1
|
|
695
|
-
label = f"{base} #{i}"
|
|
696
|
-
labels.append(label)
|
|
697
|
-
by_label[label] = r
|
|
698
|
-
|
|
699
|
-
return labels, by_label
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
def _basic_prompt(
|
|
703
|
-
message: str,
|
|
704
|
-
completer: Any,
|
|
705
|
-
) -> str | None:
|
|
706
|
-
"""Fallback prompt handler when PromptSession is unavailable or fails."""
|
|
707
|
-
if prompt is None: # pragma: no cover - optional dependency path
|
|
708
|
-
return None
|
|
709
|
-
|
|
710
|
-
try:
|
|
711
|
-
return prompt(
|
|
712
|
-
message=message,
|
|
713
|
-
completer=completer,
|
|
714
|
-
complete_in_thread=True,
|
|
715
|
-
complete_while_typing=True,
|
|
716
|
-
)
|
|
717
|
-
except (KeyboardInterrupt, EOFError):
|
|
718
|
-
return None
|
|
719
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
720
|
-
logger.debug("Fallback prompt failed: %s", exc)
|
|
721
|
-
return None
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
def _prompt_with_auto_select(
|
|
725
|
-
message: str,
|
|
726
|
-
completer: Any,
|
|
727
|
-
choices: Iterable[str],
|
|
728
|
-
) -> str | None:
|
|
729
|
-
"""Prompt with fuzzy completer that auto-selects suggested matches."""
|
|
730
|
-
if not _HAS_PTK or PromptSession is None or Buffer is None or SelectionType is None:
|
|
731
|
-
return _basic_prompt(message, completer)
|
|
732
|
-
|
|
733
|
-
try:
|
|
734
|
-
session = PromptSession(
|
|
735
|
-
message,
|
|
736
|
-
completer=completer,
|
|
737
|
-
complete_in_thread=True,
|
|
738
|
-
complete_while_typing=True,
|
|
739
|
-
reserve_space_for_menu=8,
|
|
740
|
-
)
|
|
741
|
-
except Exception as exc: # pragma: no cover - depends on prompt_toolkit
|
|
742
|
-
logger.debug("PromptSession init failed (%s); falling back to basic prompt.", exc)
|
|
743
|
-
return _basic_prompt(message, completer)
|
|
744
|
-
|
|
745
|
-
buffer = session.default_buffer
|
|
746
|
-
valid_choices = set(choices)
|
|
747
|
-
|
|
748
|
-
def _auto_select(_: Buffer) -> None:
|
|
749
|
-
"""Auto-select text when a valid choice is entered."""
|
|
750
|
-
text = buffer.text
|
|
751
|
-
if not text or text not in valid_choices:
|
|
752
|
-
return
|
|
753
|
-
buffer.cursor_position = 0
|
|
754
|
-
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
|
755
|
-
buffer.cursor_position = len(text)
|
|
756
|
-
|
|
757
|
-
handler_attached = False
|
|
758
|
-
try:
|
|
759
|
-
buffer.on_text_changed += _auto_select
|
|
760
|
-
handler_attached = True
|
|
761
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
762
|
-
logger.debug("Failed to attach auto-select handler: %s", exc)
|
|
763
|
-
|
|
764
|
-
try:
|
|
765
|
-
return session.prompt()
|
|
766
|
-
except (KeyboardInterrupt, EOFError):
|
|
767
|
-
return None
|
|
768
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
769
|
-
logger.debug("PromptSession prompt failed (%s); falling back to basic prompt.", exc)
|
|
770
|
-
return _basic_prompt(message, completer)
|
|
771
|
-
finally:
|
|
772
|
-
if handler_attached:
|
|
773
|
-
try:
|
|
774
|
-
buffer.on_text_changed -= _auto_select
|
|
775
|
-
except Exception: # pragma: no cover - defensive
|
|
776
|
-
pass
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
class _FuzzyCompleter:
|
|
780
|
-
"""Fuzzy completer for prompt_toolkit."""
|
|
781
|
-
|
|
782
|
-
def __init__(self, words: list[str]) -> None:
|
|
783
|
-
"""Initialize fuzzy completer with word list.
|
|
784
|
-
|
|
785
|
-
Args:
|
|
786
|
-
words: List of words to complete from.
|
|
787
|
-
"""
|
|
788
|
-
self.words = words
|
|
789
|
-
|
|
790
|
-
def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
|
|
791
|
-
"""Get fuzzy completions for the current word, ranked by score.
|
|
792
|
-
|
|
793
|
-
Args:
|
|
794
|
-
document: Document object from prompt_toolkit.
|
|
795
|
-
_complete_event: Completion event (unused).
|
|
796
|
-
|
|
797
|
-
Yields:
|
|
798
|
-
Completion objects matching the current word, in ranked order.
|
|
799
|
-
"""
|
|
800
|
-
# Get the entire buffer text (not just word before cursor)
|
|
801
|
-
buffer_text = document.text_before_cursor
|
|
802
|
-
if not buffer_text or not isinstance(buffer_text, str):
|
|
145
|
+
with _warn_lock:
|
|
146
|
+
if _warned:
|
|
803
147
|
return
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
# Yield ranked completions
|
|
809
|
-
for label in ranked_labels:
|
|
810
|
-
# Replace entire buffer text, not just the word before cursor
|
|
811
|
-
# This prevents concatenation issues with hyphenated names
|
|
812
|
-
yield Completion(label, start_position=-len(buffer_text))
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
def _perform_fuzzy_search(answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]) -> dict[str, Any] | None:
|
|
816
|
-
"""Perform fuzzy search fallback and return best match.
|
|
817
|
-
|
|
818
|
-
Returns:
|
|
819
|
-
Selected resource dict or None if cancelled/no match.
|
|
820
|
-
"""
|
|
821
|
-
# Exact label match
|
|
822
|
-
if answer in by_label:
|
|
823
|
-
return by_label[answer]
|
|
824
|
-
|
|
825
|
-
# Fuzzy search fallback using ranked labels
|
|
826
|
-
# Check if query actually matches anything before ranking
|
|
827
|
-
query_lower = answer.lower()
|
|
828
|
-
has_match = False
|
|
829
|
-
for label in labels:
|
|
830
|
-
if _fuzzy_score(query_lower, label.lower()) >= 0:
|
|
831
|
-
has_match = True
|
|
832
|
-
break
|
|
833
|
-
|
|
834
|
-
if not has_match:
|
|
835
|
-
return None
|
|
836
|
-
|
|
837
|
-
ranked_labels = _rank_labels(labels, answer)
|
|
838
|
-
if ranked_labels:
|
|
839
|
-
# Return the top-ranked match
|
|
840
|
-
best_match = ranked_labels[0]
|
|
841
|
-
if best_match in by_label:
|
|
842
|
-
return by_label[best_match]
|
|
843
|
-
|
|
844
|
-
return None
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def _fuzzy_pick(
|
|
848
|
-
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
849
|
-
) -> dict[str, Any] | None: # pragma: no cover - requires interactive prompt toolkit
|
|
850
|
-
"""Open a minimal fuzzy palette using prompt_toolkit.
|
|
851
|
-
|
|
852
|
-
Returns the selected row (dict) or None if cancelled/missing deps.
|
|
853
|
-
"""
|
|
854
|
-
if not _check_fuzzy_pick_requirements():
|
|
855
|
-
return None
|
|
856
|
-
|
|
857
|
-
# Build display labels and mapping
|
|
858
|
-
labels, by_label = _build_unique_labels(rows, columns)
|
|
859
|
-
|
|
860
|
-
# Create fuzzy completer
|
|
861
|
-
completer = _FuzzyCompleter(labels)
|
|
862
|
-
answer = _prompt_with_auto_select(
|
|
863
|
-
f"Find {title.rstrip('s')}: ",
|
|
864
|
-
completer,
|
|
865
|
-
labels,
|
|
866
|
-
)
|
|
867
|
-
if answer is None:
|
|
868
|
-
return None
|
|
869
|
-
|
|
870
|
-
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
def _strip_spaces_for_matching(value: str) -> str:
|
|
874
|
-
"""Remove whitespace from a query for consistent fuzzy matching."""
|
|
875
|
-
return re.sub(r"\s+", "", value)
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
def _is_fuzzy_match(search: Any, target: Any) -> bool:
|
|
879
|
-
"""Case-insensitive fuzzy match with optional spaces; returns False for non-string inputs."""
|
|
880
|
-
# Ensure search is a string
|
|
881
|
-
if not isinstance(search, str) or not isinstance(target, str):
|
|
882
|
-
return False
|
|
883
|
-
|
|
884
|
-
if not search:
|
|
885
|
-
return True
|
|
886
|
-
|
|
887
|
-
# Strip spaces from search query - treat them as optional separators
|
|
888
|
-
# This allows "test agent" to match "test-agent", "test_agent", etc.
|
|
889
|
-
search_no_spaces = _strip_spaces_for_matching(search).lower()
|
|
890
|
-
if not search_no_spaces:
|
|
891
|
-
# If search is only spaces, match everything
|
|
892
|
-
return True
|
|
893
|
-
|
|
894
|
-
search_idx = 0
|
|
895
|
-
for char in target.lower():
|
|
896
|
-
if search_idx < len(search_no_spaces) and search_no_spaces[search_idx] == char:
|
|
897
|
-
search_idx += 1
|
|
898
|
-
if search_idx == len(search_no_spaces):
|
|
899
|
-
return True
|
|
900
|
-
return False
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
def _calculate_exact_match_bonus(search: str, target: str) -> int:
|
|
904
|
-
"""Calculate bonus for exact substring matches.
|
|
905
|
-
|
|
906
|
-
Spaces in search are treated as optional separators (stripped before matching).
|
|
907
|
-
"""
|
|
908
|
-
# Strip spaces from search - treat them as optional separators
|
|
909
|
-
search_no_spaces = _strip_spaces_for_matching(search).lower()
|
|
910
|
-
if not search_no_spaces:
|
|
911
|
-
return 0
|
|
912
|
-
return 100 if search_no_spaces in target.lower() else 0
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
def _calculate_consecutive_bonus(search: str, target: str) -> int:
|
|
916
|
-
"""Case-insensitive consecutive-character bonus."""
|
|
917
|
-
# Strip spaces from search - treat them as optional separators
|
|
918
|
-
search_no_spaces = _strip_spaces_for_matching(search).lower()
|
|
919
|
-
if not search_no_spaces:
|
|
920
|
-
return 0
|
|
921
|
-
|
|
922
|
-
consecutive = 0
|
|
923
|
-
max_consecutive = 0
|
|
924
|
-
search_idx = 0
|
|
925
|
-
|
|
926
|
-
for char in target.lower():
|
|
927
|
-
if search_idx < len(search_no_spaces) and search_no_spaces[search_idx] == char:
|
|
928
|
-
consecutive += 1
|
|
929
|
-
max_consecutive = max(max_consecutive, consecutive)
|
|
930
|
-
search_idx += 1
|
|
931
|
-
else:
|
|
932
|
-
consecutive = 0
|
|
933
|
-
|
|
934
|
-
return max_consecutive * 10
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
def _calculate_length_bonus(search: str, target: str) -> int:
|
|
938
|
-
"""Calculate bonus for shorter search terms.
|
|
939
|
-
|
|
940
|
-
Spaces in search are treated as optional separators (stripped before calculation).
|
|
941
|
-
"""
|
|
942
|
-
# Strip spaces from search - treat them as optional separators
|
|
943
|
-
search_no_spaces = _strip_spaces_for_matching(search)
|
|
944
|
-
if not search_no_spaces:
|
|
945
|
-
return 0
|
|
946
|
-
return max(0, (len(target) - len(search_no_spaces)) * 2)
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
def _fuzzy_score(search: Any, target: str) -> int:
|
|
950
|
-
"""Calculate fuzzy match score.
|
|
951
|
-
|
|
952
|
-
Higher score = better match.
|
|
953
|
-
Returns -1 if no match possible.
|
|
954
|
-
|
|
955
|
-
Args:
|
|
956
|
-
search: Search string (or any type - non-strings return -1)
|
|
957
|
-
target: Target string to match against
|
|
958
|
-
"""
|
|
959
|
-
# Ensure search is a string first
|
|
960
|
-
if not isinstance(search, str):
|
|
961
|
-
return -1
|
|
962
|
-
|
|
963
|
-
if not search:
|
|
964
|
-
return 0
|
|
965
|
-
|
|
966
|
-
if not _is_fuzzy_match(search, target):
|
|
967
|
-
return -1 # Not a fuzzy match
|
|
968
|
-
|
|
969
|
-
# Calculate score based on different factors
|
|
970
|
-
score = 0
|
|
971
|
-
score += _calculate_exact_match_bonus(search, target)
|
|
972
|
-
score += _calculate_consecutive_bonus(search, target)
|
|
973
|
-
score += _calculate_length_bonus(search, target)
|
|
974
|
-
|
|
975
|
-
return score
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
def _extract_id_suffix(label: str) -> str:
|
|
979
|
-
"""Extract ID suffix from label for tie-breaking.
|
|
980
|
-
|
|
981
|
-
Args:
|
|
982
|
-
label: Display label (e.g., "name • [abc123...]")
|
|
983
|
-
|
|
984
|
-
Returns:
|
|
985
|
-
ID suffix string (e.g., "abc123") or empty string if not found
|
|
986
|
-
"""
|
|
987
|
-
# Look for pattern like "[abc123...]" or "[abc123]"
|
|
988
|
-
match = re.search(r"\[([^\]]+)\]", label)
|
|
989
|
-
return match.group(1) if match else ""
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
def _rank_labels(labels: list[str], query: Any) -> list[str]:
|
|
993
|
-
"""Rank labels by fuzzy score with deterministic tie-breaks.
|
|
994
|
-
|
|
995
|
-
Args:
|
|
996
|
-
labels: List of display labels to rank
|
|
997
|
-
query: Search query string (or any type - non-strings return sorted labels)
|
|
998
|
-
|
|
999
|
-
Returns:
|
|
1000
|
-
Labels sorted by fuzzy score (descending), then case-insensitive label,
|
|
1001
|
-
then id suffix for deterministic ordering.
|
|
1002
|
-
"""
|
|
1003
|
-
suffix_cache = {label: _extract_id_suffix(label) for label in labels}
|
|
1004
|
-
|
|
1005
|
-
if not query:
|
|
1006
|
-
# No query: sort by case-insensitive label, then id suffix
|
|
1007
|
-
return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
|
|
1008
|
-
|
|
1009
|
-
# Ensure query is a string
|
|
1010
|
-
if not isinstance(query, str):
|
|
1011
|
-
return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
|
|
1012
|
-
|
|
1013
|
-
query_lower = query.lower()
|
|
1014
|
-
|
|
1015
|
-
# Calculate scores and create tuples for sorting
|
|
1016
|
-
scored_labels = []
|
|
1017
|
-
for label in labels:
|
|
1018
|
-
label_lower = label.lower()
|
|
1019
|
-
score = _fuzzy_score(query_lower, label_lower)
|
|
1020
|
-
if score >= 0: # Only include matches
|
|
1021
|
-
scored_labels.append((score, label_lower, suffix_cache[label], label))
|
|
1022
|
-
|
|
1023
|
-
if not scored_labels:
|
|
1024
|
-
# No fuzzy matches: fall back to deterministic label sorting
|
|
1025
|
-
return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
|
|
1026
|
-
|
|
1027
|
-
# Sort by: score (desc), label (case-insensitive), id suffix, original label
|
|
1028
|
-
scored_labels.sort(key=lambda x: (-x[0], x[1], x[2], x[3]))
|
|
1029
|
-
|
|
1030
|
-
return [label for _score, _label_lower, _id_suffix, label in scored_labels]
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
# ----------------------- Structured renderer helpers -------------------- #
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
def _coerce_result_payload(result: Any) -> Any:
|
|
1037
|
-
"""Convert renderer outputs into plain dict/list structures when possible."""
|
|
1038
|
-
try:
|
|
1039
|
-
to_dict = getattr(result, "to_dict", None)
|
|
1040
|
-
if callable(to_dict):
|
|
1041
|
-
return to_dict()
|
|
1042
|
-
except Exception:
|
|
1043
|
-
return result
|
|
1044
|
-
return result
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
def _ensure_displayable(payload: Any) -> Any:
|
|
1048
|
-
"""Best-effort coercion into JSON/str-safe payloads for console rendering."""
|
|
1049
|
-
if isinstance(payload, (dict, list, str, int, float, bool)) or payload is None:
|
|
1050
|
-
return payload
|
|
1051
|
-
|
|
1052
|
-
if hasattr(payload, "__dict__"):
|
|
1053
|
-
try:
|
|
1054
|
-
return dict(payload)
|
|
1055
|
-
except Exception:
|
|
1056
|
-
try:
|
|
1057
|
-
return dict(payload.__dict__)
|
|
1058
|
-
except Exception:
|
|
1059
|
-
pass
|
|
1060
|
-
|
|
1061
|
-
try:
|
|
1062
|
-
return str(payload)
|
|
1063
|
-
except Exception:
|
|
1064
|
-
return repr(payload)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
def _render_markdown_output(data: Any) -> None:
|
|
1068
|
-
"""Render markdown output using Rich when available."""
|
|
1069
|
-
try:
|
|
1070
|
-
console.print(Markdown(str(data)))
|
|
1071
|
-
except ImportError:
|
|
1072
|
-
click.echo(str(data))
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
def _format_yaml_text(data: Any) -> str:
|
|
1076
|
-
"""Convert structured payloads to YAML for readability."""
|
|
1077
|
-
try:
|
|
1078
|
-
yaml_text = yaml.dump(
|
|
1079
|
-
data,
|
|
1080
|
-
sort_keys=False,
|
|
1081
|
-
default_flow_style=False,
|
|
1082
|
-
allow_unicode=True,
|
|
1083
|
-
Dumper=_LiteralYamlDumper,
|
|
148
|
+
warnings.warn(
|
|
149
|
+
"Importing from glaip_sdk.cli.utils is deprecated. Use glaip_sdk.cli.core.* modules instead.",
|
|
150
|
+
DeprecationWarning,
|
|
151
|
+
stacklevel=3,
|
|
1084
152
|
)
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
""
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
""
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
#
|
|
1147
|
-
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
""
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
""
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
for
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
""
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
"""Return True when rows should be name-sorted prior to rendering."""
|
|
1197
|
-
return TABLE_SORT_ENABLED and rows and isinstance(rows[0], dict) and "name" in rows[0]
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -> Any:
|
|
1201
|
-
"""Build a configured Rich table for the provided columns."""
|
|
1202
|
-
table = AIPTable(title=title, expand=True)
|
|
1203
|
-
for _key, header, style, width in columns:
|
|
1204
|
-
table.add_column(header, style=style, width=width)
|
|
1205
|
-
return table
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
def _build_table_group(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> Group:
|
|
1209
|
-
"""Return a Rich group containing the table and a small footer summary."""
|
|
1210
|
-
table = _create_table(columns, title)
|
|
1211
|
-
for row in rows:
|
|
1212
|
-
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
1213
|
-
footer = markup_text(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
1214
|
-
return Group(table, footer)
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
def _handle_json_output(items: list[Any], rows: list[dict[str, Any]]) -> None:
|
|
1218
|
-
"""Handle JSON output format."""
|
|
1219
|
-
data = rows if rows else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
|
|
1220
|
-
click.echo(json.dumps(data, indent=2, default=str))
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
def _handle_plain_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
1224
|
-
"""Handle plain text output format."""
|
|
1225
|
-
_render_plain_list(rows, title, columns)
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
def _handle_markdown_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
1229
|
-
"""Handle markdown output format."""
|
|
1230
|
-
_render_markdown_list(rows, title, columns)
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
def _handle_empty_items(title: str) -> None:
|
|
1234
|
-
"""Handle case when no items are found."""
|
|
1235
|
-
console.print(markup_text(f"[{WARNING_STYLE}]No {title.lower()} found.[/]"))
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
def _should_use_fuzzy_picker() -> bool:
|
|
1239
|
-
"""Return True when the interactive fuzzy picker can be shown."""
|
|
1240
|
-
return console.is_terminal and os.isatty(1)
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
def _try_fuzzy_pick(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> dict[str, Any] | None:
|
|
1244
|
-
"""Best-effort fuzzy selection; returns None if the picker fails."""
|
|
1245
|
-
if not _should_use_fuzzy_picker():
|
|
1246
|
-
return None
|
|
1247
|
-
|
|
1248
|
-
try:
|
|
1249
|
-
return _fuzzy_pick(rows, columns, title)
|
|
1250
|
-
except Exception:
|
|
1251
|
-
logger.debug("Fuzzy picker failed; falling back to table output", exc_info=True)
|
|
1252
|
-
return None
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
def _resource_tip_command(title: str) -> str | None:
|
|
1256
|
-
"""Resolve the follow-up command hint for the given table title."""
|
|
1257
|
-
title_lower = title.lower()
|
|
1258
|
-
mapping = {
|
|
1259
|
-
"agent": ("agents get", "agents"),
|
|
1260
|
-
"tool": ("tools get", None),
|
|
1261
|
-
"mcp": ("mcps get", None),
|
|
1262
|
-
"model": ("models list", None), # models only ship a list command
|
|
1263
|
-
}
|
|
1264
|
-
for keyword, (cli_command, slash_command) in mapping.items():
|
|
1265
|
-
if keyword in title_lower:
|
|
1266
|
-
return command_hint(cli_command, slash_command=slash_command)
|
|
1267
|
-
return command_hint("agents get", slash_command="agents")
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
def _print_selection_tip(title: str) -> None:
|
|
1271
|
-
"""Print the contextual follow-up tip after a fuzzy selection."""
|
|
1272
|
-
tip_cmd = _resource_tip_command(title)
|
|
1273
|
-
if tip_cmd:
|
|
1274
|
-
console.print(markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]"))
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
def _handle_fuzzy_pick_selection(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> bool:
|
|
1278
|
-
"""Handle fuzzy picker selection.
|
|
1279
|
-
|
|
1280
|
-
Returns:
|
|
1281
|
-
True if a resource was selected and displayed,
|
|
1282
|
-
False if cancelled/no selection.
|
|
1283
|
-
"""
|
|
1284
|
-
picked = _try_fuzzy_pick(rows, columns, title)
|
|
1285
|
-
if picked is None:
|
|
1286
|
-
return False
|
|
1287
|
-
|
|
1288
|
-
table = _create_table(columns, title)
|
|
1289
|
-
table.add_row(*[str(picked.get(key, "N/A")) for key, _, _, _ in columns])
|
|
1290
|
-
console.print(table)
|
|
1291
|
-
_print_selection_tip(title)
|
|
1292
|
-
return True
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
def _handle_table_output(
|
|
1296
|
-
rows: list[dict[str, Any]],
|
|
1297
|
-
columns: list[tuple],
|
|
1298
|
-
title: str,
|
|
1299
|
-
*,
|
|
1300
|
-
use_pager: bool | None = None,
|
|
1301
|
-
) -> None:
|
|
1302
|
-
"""Handle table output with paging."""
|
|
1303
|
-
content = _build_table_group(rows, columns, title)
|
|
1304
|
-
should_page = (
|
|
1305
|
-
pager._should_page_output(len(rows), console.is_terminal and os.isatty(1)) if use_pager is None else use_pager
|
|
1306
|
-
)
|
|
1307
|
-
|
|
1308
|
-
if should_page:
|
|
1309
|
-
ansi = pager._render_ansi(content)
|
|
1310
|
-
if not pager._page_with_system_pager(ansi):
|
|
1311
|
-
with console.pager(styles=True):
|
|
1312
|
-
console.print(content)
|
|
1313
|
-
else:
|
|
1314
|
-
console.print(content)
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
def output_list(
|
|
1318
|
-
ctx: Any,
|
|
1319
|
-
items: list[Any],
|
|
1320
|
-
title: str,
|
|
1321
|
-
columns: list[tuple[str, str, str, int | None]],
|
|
1322
|
-
transform_func: Callable | None = None,
|
|
1323
|
-
*,
|
|
1324
|
-
skip_picker: bool = False,
|
|
1325
|
-
use_pager: bool | None = None,
|
|
1326
|
-
) -> None:
|
|
1327
|
-
"""Display a list with optional fuzzy palette for quick selection."""
|
|
1328
|
-
fmt = _get_view(ctx)
|
|
1329
|
-
rows = _normalise_rows(items, transform_func)
|
|
1330
|
-
rows = masking.mask_rows(rows)
|
|
1331
|
-
|
|
1332
|
-
if fmt == "json":
|
|
1333
|
-
_handle_json_output(items, rows)
|
|
1334
|
-
return
|
|
1335
|
-
|
|
1336
|
-
if fmt == "plain":
|
|
1337
|
-
_handle_plain_output(rows, title, columns)
|
|
1338
|
-
return
|
|
1339
|
-
|
|
1340
|
-
if fmt == "md":
|
|
1341
|
-
_handle_markdown_output(rows, title, columns)
|
|
1342
|
-
return
|
|
1343
|
-
|
|
1344
|
-
if not items:
|
|
1345
|
-
_handle_empty_items(title)
|
|
1346
|
-
return
|
|
1347
|
-
|
|
1348
|
-
if _should_sort_rows(rows):
|
|
1349
|
-
try:
|
|
1350
|
-
rows = sorted(rows, key=lambda r: str(r.get("name", "")).lower())
|
|
1351
|
-
except Exception:
|
|
1352
|
-
pass
|
|
1353
|
-
|
|
1354
|
-
if not skip_picker and _handle_fuzzy_pick_selection(rows, columns, title):
|
|
1355
|
-
return
|
|
1356
|
-
|
|
1357
|
-
_handle_table_output(rows, columns, title, use_pager=use_pager)
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
# ------------------------- Ambiguity handling --------------------------- #
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
def coerce_to_row(item: Any, keys: list[str]) -> dict[str, Any]:
|
|
1364
|
-
"""Coerce an item (dict or object) to a row dict with specified keys.
|
|
1365
|
-
|
|
1366
|
-
Args:
|
|
1367
|
-
item: The item to coerce (dict or object with attributes)
|
|
1368
|
-
keys: List of keys/attribute names to extract
|
|
1369
|
-
|
|
1370
|
-
Returns:
|
|
1371
|
-
Dict with the extracted values, "N/A" for missing values
|
|
1372
|
-
"""
|
|
1373
|
-
result = {}
|
|
1374
|
-
for key in keys:
|
|
1375
|
-
if isinstance(item, dict):
|
|
1376
|
-
value = item.get(key, "N/A")
|
|
1377
|
-
else:
|
|
1378
|
-
value = getattr(item, key, "N/A")
|
|
1379
|
-
result[key] = str(value) if value is not None else "N/A"
|
|
1380
|
-
return result
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
def _register_renderer_with_session(ctx: Any, renderer: RichStreamRenderer) -> None:
|
|
1384
|
-
"""Attach renderer to an active slash session when present."""
|
|
1385
|
-
try:
|
|
1386
|
-
ctx_obj = getattr(ctx, "obj", None)
|
|
1387
|
-
session = ctx_obj.get("_slash_session") if isinstance(ctx_obj, dict) else None
|
|
1388
|
-
if session and hasattr(session, "register_active_renderer"):
|
|
1389
|
-
session.register_active_renderer(renderer)
|
|
1390
|
-
except Exception:
|
|
1391
|
-
# Never let session bookkeeping break renderer creation
|
|
1392
|
-
pass
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
def build_renderer(
|
|
1396
|
-
_ctx: Any,
|
|
1397
|
-
*,
|
|
1398
|
-
save_path: str | os.PathLike[str] | None,
|
|
1399
|
-
verbose: bool = False,
|
|
1400
|
-
_tty_enabled: bool = True,
|
|
1401
|
-
live: bool | None = None,
|
|
1402
|
-
snapshots: bool | None = None,
|
|
1403
|
-
) -> tuple[RichStreamRenderer, Console | CapturingConsole]:
|
|
1404
|
-
"""Build renderer and capturing console for CLI commands.
|
|
1405
|
-
|
|
1406
|
-
Args:
|
|
1407
|
-
_ctx: Click context object for CLI operations.
|
|
1408
|
-
save_path: Path to save output to (enables capturing console).
|
|
1409
|
-
verbose: Whether to enable verbose mode.
|
|
1410
|
-
_tty_enabled: Whether TTY is available for interactive features.
|
|
1411
|
-
live: Whether to enable live rendering mode (overrides verbose default).
|
|
1412
|
-
snapshots: Whether to capture and store snapshots.
|
|
1413
|
-
|
|
1414
|
-
Returns:
|
|
1415
|
-
Tuple of (renderer, capturing_console) for streaming output.
|
|
1416
|
-
"""
|
|
1417
|
-
# Use capturing console if saving output
|
|
1418
|
-
working_console = CapturingConsole(console, capture=True) if save_path else console
|
|
1419
|
-
|
|
1420
|
-
# Configure renderer based on verbose mode and explicit overrides
|
|
1421
|
-
live_enabled = bool(live) if live is not None else not verbose
|
|
1422
|
-
cfg_overrides = {
|
|
1423
|
-
"live": live_enabled,
|
|
1424
|
-
"append_finished_snapshots": bool(snapshots) if snapshots is not None else False,
|
|
1425
|
-
}
|
|
1426
|
-
renderer_console = (
|
|
1427
|
-
working_console.original_console if isinstance(working_console, CapturingConsole) else working_console
|
|
1428
|
-
)
|
|
1429
|
-
factory = make_verbose_renderer if verbose else make_default_renderer
|
|
1430
|
-
factory_options = RendererFactoryOptions(
|
|
1431
|
-
console=renderer_console,
|
|
1432
|
-
cfg_overrides=cfg_overrides,
|
|
1433
|
-
verbose=verbose if factory is make_default_renderer else None,
|
|
1434
|
-
)
|
|
1435
|
-
renderer = factory_options.build(factory)
|
|
1436
|
-
|
|
1437
|
-
# Link the renderer back to the slash session when running from the palette.
|
|
1438
|
-
_register_renderer_with_session(_ctx, renderer)
|
|
1439
|
-
|
|
1440
|
-
return renderer, working_console
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
def _build_resource_labels(resources: list[Any]) -> tuple[list[str], dict[str, Any]]:
|
|
1444
|
-
"""Build unique display labels for resources."""
|
|
1445
|
-
labels = []
|
|
1446
|
-
by_label: dict[str, Any] = {}
|
|
1447
|
-
|
|
1448
|
-
for resource in resources:
|
|
1449
|
-
name = getattr(resource, "name", "Unknown")
|
|
1450
|
-
_id = getattr(resource, "id", "Unknown")
|
|
1451
|
-
|
|
1452
|
-
# Create display label
|
|
1453
|
-
label_parts = []
|
|
1454
|
-
if name and name != "Unknown":
|
|
1455
|
-
label_parts.append(name)
|
|
1456
|
-
label_parts.append(f"[{_id[:8]}...]") # Show first 8 chars of ID
|
|
1457
|
-
label = " • ".join(label_parts)
|
|
1458
|
-
|
|
1459
|
-
# Ensure uniqueness
|
|
1460
|
-
if label in by_label:
|
|
1461
|
-
i = 2
|
|
1462
|
-
base = label
|
|
1463
|
-
while f"{base} #{i}" in by_label:
|
|
1464
|
-
i += 1
|
|
1465
|
-
label = f"{base} #{i}"
|
|
1466
|
-
|
|
1467
|
-
labels.append(label)
|
|
1468
|
-
by_label[label] = resource
|
|
1469
|
-
|
|
1470
|
-
return labels, by_label
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
def _fuzzy_pick_for_resources(
|
|
1474
|
-
resources: list[Any], resource_type: str, _search_term: str
|
|
1475
|
-
) -> Any | None: # pragma: no cover - interactive selection helper
|
|
1476
|
-
"""Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
|
|
1477
|
-
|
|
1478
|
-
Args:
|
|
1479
|
-
resources: List of resource objects to choose from
|
|
1480
|
-
resource_type: Type of resource (e.g., "agent", "tool")
|
|
1481
|
-
search_term: The search term that led to multiple matches
|
|
1482
|
-
|
|
1483
|
-
Returns:
|
|
1484
|
-
Selected resource object or None if cancelled/no selection
|
|
1485
|
-
"""
|
|
1486
|
-
if not _check_fuzzy_pick_requirements():
|
|
1487
|
-
return None
|
|
1488
|
-
|
|
1489
|
-
# Build labels and mapping
|
|
1490
|
-
labels, by_label = _build_resource_labels(resources)
|
|
1491
|
-
|
|
1492
|
-
# Create fuzzy completer
|
|
1493
|
-
completer = _FuzzyCompleter(labels)
|
|
1494
|
-
answer = _prompt_with_auto_select(
|
|
1495
|
-
f"Find {ICON_AGENT} {resource_type.title()}: ",
|
|
1496
|
-
completer,
|
|
1497
|
-
labels,
|
|
1498
|
-
)
|
|
1499
|
-
if answer is None:
|
|
1500
|
-
return None
|
|
1501
|
-
|
|
1502
|
-
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
def _resolve_by_id(ref: str, get_by_id: Callable) -> Any | None:
|
|
1506
|
-
"""Resolve resource by UUID if ref is a valid UUID."""
|
|
1507
|
-
if is_uuid(ref):
|
|
1508
|
-
return get_by_id(ref)
|
|
1509
|
-
return None
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
def _resolve_by_name_multiple_with_select(matches: list[Any], select: int) -> Any:
|
|
1513
|
-
"""Resolve multiple matches using select parameter."""
|
|
1514
|
-
idx = int(select) - 1
|
|
1515
|
-
if not (0 <= idx < len(matches)):
|
|
1516
|
-
raise click.ClickException(f"--select must be 1..{len(matches)}")
|
|
1517
|
-
return matches[idx]
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
def _resolve_by_name_multiple_fuzzy(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
|
|
1521
|
-
"""Resolve multiple matches preferring the fuzzy picker interface."""
|
|
1522
|
-
return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="fuzzy")
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
def _resolve_by_name_multiple_questionary(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
|
|
1526
|
-
"""Resolve multiple matches preferring the questionary interface."""
|
|
1527
|
-
return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="questionary")
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
def resolve_resource(
|
|
1531
|
-
ctx: Any,
|
|
1532
|
-
ref: str,
|
|
1533
|
-
*,
|
|
1534
|
-
get_by_id: Callable,
|
|
1535
|
-
find_by_name: Callable,
|
|
1536
|
-
label: str,
|
|
1537
|
-
select: int | None = None,
|
|
1538
|
-
interface_preference: str = "fuzzy",
|
|
1539
|
-
status_indicator: Any | None = None,
|
|
1540
|
-
) -> Any | None:
|
|
1541
|
-
"""Resolve resource reference (ID or name) with ambiguity handling.
|
|
1542
|
-
|
|
1543
|
-
Args:
|
|
1544
|
-
ctx: Click context
|
|
1545
|
-
ref: Resource reference (ID or name)
|
|
1546
|
-
get_by_id: Function to get resource by ID
|
|
1547
|
-
find_by_name: Function to find resources by name
|
|
1548
|
-
label: Resource type label for error messages
|
|
1549
|
-
select: Optional selection index for ambiguity resolution
|
|
1550
|
-
interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
|
|
1551
|
-
status_indicator: Optional Rich status indicator for wait animations
|
|
1552
|
-
|
|
1553
|
-
Returns:
|
|
1554
|
-
Resolved resource object
|
|
1555
|
-
"""
|
|
1556
|
-
spinner = status_indicator
|
|
1557
|
-
_spinner_update(spinner, f"[bold blue]Resolving {label}…[/bold blue]")
|
|
1558
|
-
|
|
1559
|
-
# Try to resolve by ID first
|
|
1560
|
-
_spinner_update(spinner, f"[bold blue]Fetching {label} by ID…[/bold blue]")
|
|
1561
|
-
result = _resolve_by_id(ref, get_by_id)
|
|
1562
|
-
if result is not None:
|
|
1563
|
-
_spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
|
|
1564
|
-
return result
|
|
1565
|
-
|
|
1566
|
-
# If get_by_id returned None, the resource doesn't exist
|
|
1567
|
-
if is_uuid(ref):
|
|
1568
|
-
_spinner_stop(spinner)
|
|
1569
|
-
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1570
|
-
|
|
1571
|
-
# Find resources by name
|
|
1572
|
-
_spinner_update(spinner, f"[bold blue]Searching {label}s matching '{ref}'…[/bold blue]")
|
|
1573
|
-
matches = find_by_name(name=ref)
|
|
1574
|
-
if not matches:
|
|
1575
|
-
_spinner_stop(spinner)
|
|
1576
|
-
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1577
|
-
|
|
1578
|
-
if len(matches) == 1:
|
|
1579
|
-
_spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
|
|
1580
|
-
return matches[0]
|
|
1581
|
-
|
|
1582
|
-
# Multiple matches found, handle ambiguity
|
|
1583
|
-
if select:
|
|
1584
|
-
_spinner_stop(spinner)
|
|
1585
|
-
return _resolve_by_name_multiple_with_select(matches, select)
|
|
1586
|
-
|
|
1587
|
-
# Choose interface based on preference
|
|
1588
|
-
_spinner_stop(spinner)
|
|
1589
|
-
preference = (interface_preference or "fuzzy").lower()
|
|
1590
|
-
if preference not in {"fuzzy", "questionary"}:
|
|
1591
|
-
preference = "fuzzy"
|
|
1592
|
-
if preference == "fuzzy":
|
|
1593
|
-
return _resolve_by_name_multiple_fuzzy(ctx, ref, matches, label)
|
|
1594
|
-
else:
|
|
1595
|
-
return _resolve_by_name_multiple_questionary(ctx, ref, matches, label)
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
|
|
1599
|
-
"""Handle ambiguity in JSON view by returning first match."""
|
|
1600
|
-
return matches[0]
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1604
|
-
"""Handle ambiguity using questionary interactive interface."""
|
|
1605
|
-
questionary_module, choice_cls = _load_questionary_module()
|
|
1606
|
-
if not (questionary_module and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
|
|
1607
|
-
raise click.ClickException("Interactive selection not available")
|
|
1608
|
-
|
|
1609
|
-
# Escape special characters for questionary
|
|
1610
|
-
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1611
|
-
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1612
|
-
|
|
1613
|
-
picked_idx = questionary_module.select(
|
|
1614
|
-
f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
|
|
1615
|
-
choices=[
|
|
1616
|
-
_make_questionary_choice(
|
|
1617
|
-
choice_cls,
|
|
1618
|
-
title=(
|
|
1619
|
-
f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
|
|
1620
|
-
f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
|
|
1621
|
-
),
|
|
1622
|
-
value=i,
|
|
1623
|
-
)
|
|
1624
|
-
for i, m in enumerate(matches)
|
|
1625
|
-
],
|
|
1626
|
-
use_indicator=True,
|
|
1627
|
-
qmark="🧭",
|
|
1628
|
-
instruction="↑/↓ to select • Enter to confirm",
|
|
1629
|
-
).ask()
|
|
1630
|
-
if picked_idx is None:
|
|
1631
|
-
raise click.ClickException("Selection cancelled")
|
|
1632
|
-
return matches[picked_idx]
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
def _handle_fallback_numeric_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1636
|
-
"""Handle ambiguity using numeric prompt fallback."""
|
|
1637
|
-
# Escape special characters for display
|
|
1638
|
-
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1639
|
-
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1640
|
-
|
|
1641
|
-
console.print(markup_text(f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"))
|
|
1642
|
-
table = AIPTable(
|
|
1643
|
-
title=f"Select {safe_resource_type.title()}",
|
|
1644
|
-
)
|
|
1645
|
-
table.add_column("#", style="dim", width=3)
|
|
1646
|
-
table.add_column("ID", style="dim", width=36)
|
|
1647
|
-
table.add_column("Name", style=ACCENT_STYLE)
|
|
1648
|
-
for i, m in enumerate(matches, 1):
|
|
1649
|
-
table.add_row(str(i), str(getattr(m, "id", "")), str(getattr(m, "name", "")))
|
|
1650
|
-
console.print(table)
|
|
1651
|
-
choice_str = click.prompt(
|
|
1652
|
-
f"Select {safe_resource_type} (1-{len(matches)})",
|
|
1653
|
-
)
|
|
1654
|
-
try:
|
|
1655
|
-
choice = int(choice_str)
|
|
1656
|
-
except ValueError as err:
|
|
1657
|
-
raise click.ClickException("Invalid selection") from err
|
|
1658
|
-
if 1 <= choice <= len(matches):
|
|
1659
|
-
return matches[choice - 1]
|
|
1660
|
-
raise click.ClickException("Invalid selection")
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
def _should_fallback_to_numeric_prompt(exception: Exception) -> bool:
|
|
1664
|
-
"""Determine if we should fallback to numeric prompt for this exception."""
|
|
1665
|
-
# Re-raise cancellation - user explicitly cancelled
|
|
1666
|
-
if "Selection cancelled" in str(exception):
|
|
1667
|
-
return False
|
|
1668
|
-
|
|
1669
|
-
# Fall back to numeric prompt for other exceptions
|
|
1670
|
-
return True
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
def _normalize_interface_preference(preference: str) -> str:
|
|
1674
|
-
"""Normalize and validate interface preference."""
|
|
1675
|
-
normalized = (preference or "questionary").lower()
|
|
1676
|
-
return normalized if normalized in {"fuzzy", "questionary"} else "questionary"
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
def _get_interface_order(preference: str) -> tuple[str, str]:
|
|
1680
|
-
"""Get the ordered interface preferences."""
|
|
1681
|
-
interface_orders = {
|
|
1682
|
-
"fuzzy": ("fuzzy", "questionary"),
|
|
1683
|
-
"questionary": ("questionary", "fuzzy"),
|
|
1684
|
-
}
|
|
1685
|
-
return interface_orders.get(preference, ("questionary", "fuzzy"))
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
def _try_fuzzy_selection(
|
|
1689
|
-
resource_type: str,
|
|
1690
|
-
ref: str,
|
|
1691
|
-
matches: list[Any],
|
|
1692
|
-
) -> Any | None:
|
|
1693
|
-
"""Try fuzzy interface selection."""
|
|
1694
|
-
picked = _fuzzy_pick_for_resources(matches, resource_type, ref)
|
|
1695
|
-
return picked if picked else None
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
def _try_questionary_selection(
|
|
1699
|
-
resource_type: str,
|
|
1700
|
-
ref: str,
|
|
1701
|
-
matches: list[Any],
|
|
1702
|
-
) -> Any | None:
|
|
1703
|
-
"""Try questionary interface selection."""
|
|
1704
|
-
try:
|
|
1705
|
-
return _handle_questionary_ambiguity(resource_type, ref, matches)
|
|
1706
|
-
except Exception as exc:
|
|
1707
|
-
if not _should_fallback_to_numeric_prompt(exc):
|
|
1708
|
-
raise
|
|
1709
|
-
return None
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
def _try_interface_selection(
|
|
1713
|
-
interface_order: tuple[str, str],
|
|
1714
|
-
resource_type: str,
|
|
1715
|
-
ref: str,
|
|
1716
|
-
matches: list[Any],
|
|
1717
|
-
) -> Any | None:
|
|
1718
|
-
"""Try interface selection in order, return result or None if all failed."""
|
|
1719
|
-
interface_handlers = {
|
|
1720
|
-
"fuzzy": _try_fuzzy_selection,
|
|
1721
|
-
"questionary": _try_questionary_selection,
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
for interface in interface_order:
|
|
1725
|
-
handler = interface_handlers.get(interface)
|
|
1726
|
-
if handler:
|
|
1727
|
-
result = handler(resource_type, ref, matches)
|
|
1728
|
-
if result:
|
|
1729
|
-
return result
|
|
1730
|
-
|
|
1731
|
-
return None
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
def handle_ambiguous_resource(
|
|
1735
|
-
ctx: Any,
|
|
1736
|
-
resource_type: str,
|
|
1737
|
-
ref: str,
|
|
1738
|
-
matches: list[Any],
|
|
1739
|
-
*,
|
|
1740
|
-
interface_preference: str = "questionary",
|
|
1741
|
-
) -> Any:
|
|
1742
|
-
"""Handle multiple resource matches gracefully."""
|
|
1743
|
-
if _get_view(ctx) == "json":
|
|
1744
|
-
return _handle_json_view_ambiguity(matches)
|
|
1745
|
-
|
|
1746
|
-
preference = _normalize_interface_preference(interface_preference)
|
|
1747
|
-
interface_order = _get_interface_order(preference)
|
|
1748
|
-
|
|
1749
|
-
result = _try_interface_selection(interface_order, resource_type, ref, matches)
|
|
1750
|
-
|
|
1751
|
-
if result is not None:
|
|
1752
|
-
return result
|
|
1753
|
-
|
|
1754
|
-
return _handle_fallback_numeric_ambiguity(resource_type, ref, matches)
|
|
153
|
+
_warned = True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_warn_once()
|
|
157
|
+
|
|
158
|
+
# Re-export everything for backward compatibility
|
|
159
|
+
__all__ = [
|
|
160
|
+
# Context
|
|
161
|
+
"bind_slash_session_context",
|
|
162
|
+
"get_client",
|
|
163
|
+
"get_ctx_value", # Re-exported from context module
|
|
164
|
+
"handle_best_effort_check",
|
|
165
|
+
"restore_slash_session_context",
|
|
166
|
+
# Prompting
|
|
167
|
+
"_FuzzyCompleter", # Private class for backward compatibility (used in tests)
|
|
168
|
+
"_asyncio_loop_running", # Private function for backward compatibility (used in tests)
|
|
169
|
+
"_basic_prompt", # Private function for backward compatibility (used in tests)
|
|
170
|
+
"_build_display_parts", # Private function for backward compatibility (used in tests)
|
|
171
|
+
"_build_primary_parts", # Private function for backward compatibility (used in tests)
|
|
172
|
+
"_build_resource_labels", # Private function for backward compatibility (used in tests)
|
|
173
|
+
"_build_unique_labels", # Private function for backward compatibility (used in tests)
|
|
174
|
+
"_calculate_consecutive_bonus", # Private function for backward compatibility (used in tests)
|
|
175
|
+
"_calculate_exact_match_bonus", # Private function for backward compatibility (used in tests)
|
|
176
|
+
"_calculate_length_bonus", # Private function for backward compatibility (used in tests)
|
|
177
|
+
"_check_fuzzy_pick_requirements", # Private function for backward compatibility (used in tests)
|
|
178
|
+
"_extract_display_fields", # Private function for backward compatibility (used in tests)
|
|
179
|
+
"_extract_fallback_values", # Private function for backward compatibility (used in tests)
|
|
180
|
+
"_extract_id_suffix", # Private function for backward compatibility (used in tests)
|
|
181
|
+
"_fuzzy_pick", # Private function for backward compatibility (used in tests)
|
|
182
|
+
"_fuzzy_pick_for_resources",
|
|
183
|
+
"_fuzzy_score", # Private function for backward compatibility (used in tests)
|
|
184
|
+
"_get_fallback_columns", # Private function for backward compatibility (used in tests)
|
|
185
|
+
"_is_fuzzy_match", # Private function for backward compatibility (used in tests)
|
|
186
|
+
"_is_standard_field", # Private function for backward compatibility (used in tests)
|
|
187
|
+
"_load_questionary_module", # Private function for backward compatibility (used in tests)
|
|
188
|
+
"_make_questionary_choice", # Private function for backward compatibility (used in tests)
|
|
189
|
+
"_perform_fuzzy_search", # Private function for backward compatibility (used in tests)
|
|
190
|
+
"_prompt_with_auto_select", # Private function for backward compatibility (used in tests)
|
|
191
|
+
"_rank_labels", # Private function for backward compatibility (used in tests)
|
|
192
|
+
"_row_display", # Private function for backward compatibility (used in tests)
|
|
193
|
+
"_run_questionary_in_thread", # Private function for backward compatibility (used in tests)
|
|
194
|
+
"_strip_spaces_for_matching", # Private function for backward compatibility (used in tests)
|
|
195
|
+
"prompt_export_choice_questionary",
|
|
196
|
+
"questionary_safe_ask",
|
|
197
|
+
# Rendering
|
|
198
|
+
"_can_use_spinner", # Private function for backward compatibility (used in tests)
|
|
199
|
+
"_register_renderer_with_session", # Private function for backward compatibility (used in tests)
|
|
200
|
+
"_spinner_stop", # Private function for backward compatibility (used in tests)
|
|
201
|
+
"_spinner_update", # Private function for backward compatibility (used in tests)
|
|
202
|
+
"_stream_supports_tty", # Private function for backward compatibility (used in tests)
|
|
203
|
+
"build_renderer",
|
|
204
|
+
"console", # Module-level variable for backward compatibility
|
|
205
|
+
"logger", # Module-level variable for backward compatibility
|
|
206
|
+
"questionary", # Module-level variable for backward compatibility
|
|
207
|
+
"spinner_context",
|
|
208
|
+
"stop_spinner",
|
|
209
|
+
"update_spinner",
|
|
210
|
+
"with_client_and_spinner",
|
|
211
|
+
# Output
|
|
212
|
+
"_LiteralYamlDumper", # Private class for backward compatibility (used in tests)
|
|
213
|
+
"_build_table_group", # Private function for backward compatibility (used in tests)
|
|
214
|
+
"_build_yaml_renderable", # Private function for backward compatibility (used in tests)
|
|
215
|
+
"_coerce_result_payload", # Private function for backward compatibility (used in tests)
|
|
216
|
+
"_create_table", # Private function for backward compatibility (used in tests)
|
|
217
|
+
"_ensure_displayable", # Private function for backward compatibility (used in tests)
|
|
218
|
+
"_format_yaml_text", # Private function for backward compatibility (used in tests)
|
|
219
|
+
"_get_interface_order", # Private function for backward compatibility (used in tests)
|
|
220
|
+
"_handle_empty_items", # Private function for backward compatibility (used in tests)
|
|
221
|
+
"_handle_fallback_numeric_ambiguity", # Private function for backward compatibility (used in tests)
|
|
222
|
+
"_handle_fuzzy_pick_selection", # Private function for backward compatibility (used in tests)
|
|
223
|
+
"_handle_json_output", # Private function for backward compatibility (used in tests)
|
|
224
|
+
"_handle_json_view_ambiguity", # Private function for backward compatibility (used in tests)
|
|
225
|
+
"_handle_markdown_output", # Private function for backward compatibility (used in tests)
|
|
226
|
+
"_handle_plain_output", # Private function for backward compatibility (used in tests)
|
|
227
|
+
"_handle_questionary_ambiguity", # Private function for backward compatibility (used in tests)
|
|
228
|
+
"_handle_table_output", # Private function for backward compatibility (used in tests)
|
|
229
|
+
"_literal_str_representer", # Private function for backward compatibility (used in tests)
|
|
230
|
+
"_normalise_rows", # Private function for backward compatibility (used in tests)
|
|
231
|
+
"_normalize_interface_preference", # Private function for backward compatibility (used in tests)
|
|
232
|
+
"_print_selection_tip", # Private function for backward compatibility (used in tests)
|
|
233
|
+
"_render_markdown_list", # Private function for backward compatibility (used in tests)
|
|
234
|
+
"_render_markdown_output", # Private function for backward compatibility (used in tests)
|
|
235
|
+
"_render_plain_list", # Private function for backward compatibility (used in tests)
|
|
236
|
+
"_resolve_by_id", # Private function for backward compatibility (used in tests)
|
|
237
|
+
"_resolve_by_name_multiple_fuzzy", # Private function for backward compatibility (used in tests)
|
|
238
|
+
"_resolve_by_name_multiple_questionary", # Private function for backward compatibility (used in tests)
|
|
239
|
+
"_resolve_by_name_multiple_with_select", # Private function for backward compatibility (used in tests)
|
|
240
|
+
"_resource_tip_command", # Private function for backward compatibility (used in tests)
|
|
241
|
+
"_should_fallback_to_numeric_prompt", # Private function for backward compatibility (used in tests)
|
|
242
|
+
"_should_sort_rows", # Private function for backward compatibility (used in tests)
|
|
243
|
+
"_should_use_fuzzy_picker", # Private function for backward compatibility (used in tests)
|
|
244
|
+
"_try_fuzzy_pick", # Private function for backward compatibility (used in tests)
|
|
245
|
+
"_try_fuzzy_selection", # Private function for backward compatibility (used in tests)
|
|
246
|
+
"_try_interface_selection", # Private function for backward compatibility (used in tests)
|
|
247
|
+
"_try_questionary_selection", # Private function for backward compatibility (used in tests)
|
|
248
|
+
"coerce_to_row",
|
|
249
|
+
"command_hint", # Re-exported from hints module
|
|
250
|
+
"detect_export_format",
|
|
251
|
+
"fetch_resource_for_export",
|
|
252
|
+
"format_datetime_fields",
|
|
253
|
+
"format_size",
|
|
254
|
+
"handle_ambiguous_resource",
|
|
255
|
+
"handle_resource_export",
|
|
256
|
+
"output_list",
|
|
257
|
+
"output_result",
|
|
258
|
+
"parse_json_line",
|
|
259
|
+
"resolve_resource",
|
|
260
|
+
"sdk_version",
|
|
261
|
+
# Utils
|
|
262
|
+
"is_uuid", # Re-exported from glaip_sdk.utils for backward compatibility
|
|
263
|
+
]
|