glaip-sdk 0.1.3__py3-none-any.whl → 0.6.19__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/__init__.py +44 -4
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/auth.py +254 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +213 -73
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +729 -113
- glaip_sdk/cli/commands/mcps.py +241 -72
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +49 -57
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/config.py +48 -4
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +35 -19
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +241 -121
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +771 -140
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +27 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -499
- glaip_sdk/cli/update_notifier.py +14 -5
- glaip_sdk/cli/utils.py +243 -1252
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +45 -9
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +291 -35
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +122 -12
- glaip_sdk/client/run_rendering.py +466 -89
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +155 -10
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/hitl/__init__.py +15 -0
- glaip_sdk/hitl/local.py +151 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +870 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +275 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +25 -13
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/METADATA +56 -21
- glaip_sdk-0.6.19.dist-info/RECORD +163 -0
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.19.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.19.dist-info/top_level.txt +1 -0
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.3.dist-info/RECORD +0 -83
- glaip_sdk-0.1.3.dist-info/entry_points.txt +0 -3
glaip_sdk/cli/utils.py
CHANGED
|
@@ -1,1272 +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 logging
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
from collections.abc import Callable, Iterable
|
|
16
|
-
from contextlib import AbstractContextManager, nullcontext
|
|
17
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
18
|
-
|
|
19
|
-
import click
|
|
20
|
-
from rich.console import Console, Group
|
|
21
|
-
from rich.markdown import Markdown
|
|
22
|
-
from rich.pretty import Pretty
|
|
15
|
+
import threading
|
|
16
|
+
import warnings
|
|
23
17
|
|
|
24
|
-
from
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,
|
|
30
24
|
)
|
|
31
|
-
from glaip_sdk.cli.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
62
75
|
)
|
|
63
|
-
from glaip_sdk.cli.
|
|
64
|
-
|
|
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,
|
|
65
108
|
)
|
|
66
|
-
from glaip_sdk.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
|
72
121
|
)
|
|
73
122
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# ----------------------------- Context helpers ---------------------------- #
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def detect_export_format(file_path: str | os.PathLike[str]) -> str:
|
|
83
|
-
"""Backward-compatible proxy to `glaip_sdk.cli.context.detect_export_format`."""
|
|
84
|
-
return _detect_export_format(file_path)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def in_slash_mode(ctx: click.Context | None = None) -> bool:
|
|
88
|
-
"""Return True when running inside the slash command palette."""
|
|
89
|
-
if ctx is None:
|
|
90
|
-
try:
|
|
91
|
-
ctx = click.get_current_context(silent=True)
|
|
92
|
-
except RuntimeError:
|
|
93
|
-
ctx = None
|
|
94
|
-
|
|
95
|
-
if ctx is None:
|
|
96
|
-
return False
|
|
97
|
-
|
|
98
|
-
obj = getattr(ctx, "obj", None)
|
|
99
|
-
if isinstance(obj, dict):
|
|
100
|
-
return bool(obj.get("_slash_session"))
|
|
101
|
-
|
|
102
|
-
return bool(getattr(obj, "_slash_session", False))
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def command_hint(
|
|
106
|
-
cli_command: str | None,
|
|
107
|
-
slash_command: str | None = None,
|
|
108
|
-
*,
|
|
109
|
-
ctx: click.Context | None = None,
|
|
110
|
-
) -> str | None:
|
|
111
|
-
"""Return the appropriate command string for the current mode.
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
cli_command: Command string without the ``aip`` prefix (e.g., ``"status"``).
|
|
115
|
-
slash_command: Slash command counterpart (e.g., ``"status"`` or ``"/status"``).
|
|
116
|
-
ctx: Optional Click context override.
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
The formatted command string for the active mode, or ``None`` when no
|
|
120
|
-
equivalent command exists in that mode.
|
|
121
|
-
"""
|
|
122
|
-
if in_slash_mode(ctx):
|
|
123
|
-
if not slash_command:
|
|
124
|
-
return None
|
|
125
|
-
return slash_command if slash_command.startswith("/") else f"/{slash_command}"
|
|
126
|
-
|
|
127
|
-
if not cli_command:
|
|
128
|
-
return None
|
|
129
|
-
return f"aip {cli_command}"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def format_command_hint(
|
|
133
|
-
command: str | None,
|
|
134
|
-
description: str | None = None,
|
|
135
|
-
) -> str | None:
|
|
136
|
-
"""Return a Rich markup string that highlights a command hint.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
command: Command text to highlight (already formatted for the active mode).
|
|
140
|
-
description: Optional short description to display alongside the command.
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
Markup string suitable for Rich rendering, or ``None`` when ``command`` is falsy.
|
|
144
|
-
"""
|
|
145
|
-
if not command:
|
|
146
|
-
return None
|
|
147
|
-
|
|
148
|
-
highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
|
|
149
|
-
if description:
|
|
150
|
-
highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
|
|
151
|
-
return highlighted
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def spinner_context(
|
|
155
|
-
ctx: Any | None,
|
|
156
|
-
message: str,
|
|
157
|
-
*,
|
|
158
|
-
console_override: Console | None = None,
|
|
159
|
-
spinner: str = "dots",
|
|
160
|
-
spinner_style: str = ACCENT_STYLE,
|
|
161
|
-
) -> AbstractContextManager[Any]:
|
|
162
|
-
"""Return a context manager that renders a spinner when appropriate."""
|
|
163
|
-
active_console = console_override or console
|
|
164
|
-
if not _can_use_spinner(ctx, active_console):
|
|
165
|
-
return nullcontext()
|
|
166
|
-
|
|
167
|
-
status = active_console.status(
|
|
168
|
-
message,
|
|
169
|
-
spinner=spinner,
|
|
170
|
-
spinner_style=spinner_style,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
if not hasattr(status, "__enter__") or not hasattr(status, "__exit__"):
|
|
174
|
-
return nullcontext()
|
|
175
|
-
|
|
176
|
-
return status
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
|
|
180
|
-
"""Check if spinner output is allowed in the current environment."""
|
|
181
|
-
if ctx is not None:
|
|
182
|
-
tty_enabled = bool(get_ctx_value(ctx, "tty", True))
|
|
183
|
-
view = (_get_view(ctx) or "rich").lower()
|
|
184
|
-
if not tty_enabled or view not in {"", "rich"}:
|
|
185
|
-
return False
|
|
186
|
-
|
|
187
|
-
if not active_console.is_terminal:
|
|
188
|
-
return False
|
|
189
|
-
|
|
190
|
-
return _stream_supports_tty(getattr(active_console, "file", None))
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _stream_supports_tty(stream: Any) -> bool:
|
|
194
|
-
"""Return True if the provided stream can safely render a spinner."""
|
|
195
|
-
target = stream if hasattr(stream, "isatty") else sys.stdout
|
|
196
|
-
try:
|
|
197
|
-
return bool(target.isatty())
|
|
198
|
-
except Exception:
|
|
199
|
-
return 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
|
|
200
127
|
|
|
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
|
|
201
132
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if status_indicator is None:
|
|
205
|
-
return
|
|
133
|
+
logger = logging.getLogger("glaip_sdk.cli.utils")
|
|
134
|
+
questionary = None # type: ignore[assignment]
|
|
206
135
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
except Exception: # pragma: no cover - defensive update
|
|
210
|
-
pass
|
|
136
|
+
_warn_lock = threading.Lock()
|
|
137
|
+
_warned = False
|
|
211
138
|
|
|
212
139
|
|
|
213
|
-
def
|
|
214
|
-
"""
|
|
215
|
-
|
|
140
|
+
def _warn_once() -> None:
|
|
141
|
+
"""Emit the deprecation warning once in a thread-safe way."""
|
|
142
|
+
global _warned
|
|
143
|
+
if _warned:
|
|
216
144
|
return
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
status_indicator.stop()
|
|
220
|
-
except Exception: # pragma: no cover - defensive stop
|
|
221
|
-
pass
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
# Backwards compatibility aliases for legacy callers
|
|
225
|
-
_spinner_update = update_spinner
|
|
226
|
-
_spinner_stop = stop_spinner
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
# ----------------------------- Client config ----------------------------- #
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
233
|
-
"""Get configured client from context, env, and config file (ctx > env > file)."""
|
|
234
|
-
module = importlib.import_module("glaip_sdk")
|
|
235
|
-
client_class = cast("type[Client]", module.Client)
|
|
236
|
-
file_config = load_config() or {}
|
|
237
|
-
context_config_obj = getattr(ctx, "obj", None)
|
|
238
|
-
context_config = context_config_obj or {}
|
|
239
|
-
|
|
240
|
-
raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
|
|
241
|
-
try:
|
|
242
|
-
timeout_value = float(raw_timeout)
|
|
243
|
-
except ValueError:
|
|
244
|
-
timeout_value = None
|
|
245
|
-
|
|
246
|
-
env_config = {
|
|
247
|
-
"api_url": os.getenv("AIP_API_URL"),
|
|
248
|
-
"api_key": os.getenv("AIP_API_KEY"),
|
|
249
|
-
"timeout": timeout_value if timeout_value else None,
|
|
250
|
-
}
|
|
251
|
-
env_config = {k: v for k, v in env_config.items() if v not in (None, "", 0)}
|
|
252
|
-
|
|
253
|
-
# Merge config sources: context > env > file
|
|
254
|
-
config = {
|
|
255
|
-
**file_config,
|
|
256
|
-
**env_config,
|
|
257
|
-
**{k: v for k, v in context_config.items() if v is not None},
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if not config.get("api_url") or not config.get("api_key"):
|
|
261
|
-
configure_hint = command_hint("configure", slash_command="login", ctx=ctx)
|
|
262
|
-
actions = []
|
|
263
|
-
if configure_hint:
|
|
264
|
-
actions.append(f"Run `{configure_hint}`")
|
|
265
|
-
actions.append("set AIP_* env vars")
|
|
266
|
-
raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
|
|
267
|
-
|
|
268
|
-
return client_class(
|
|
269
|
-
api_url=config.get("api_url"),
|
|
270
|
-
api_key=config.get("api_key"),
|
|
271
|
-
timeout=float(config.get("timeout") or 30.0),
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# ----------------------------- Secret masking ---------------------------- #
|
|
276
|
-
|
|
277
|
-
# ----------------------------- Fuzzy palette ----------------------------- #
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def _extract_display_fields(row: dict[str, Any]) -> tuple[str, str, str, str]:
|
|
281
|
-
"""Extract display fields from row data."""
|
|
282
|
-
name = str(row.get("name", "")).strip()
|
|
283
|
-
_id = str(row.get("id", "")).strip()
|
|
284
|
-
type_ = str(row.get("type", "")).strip()
|
|
285
|
-
fw = str(row.get("framework", "")).strip()
|
|
286
|
-
return name, _id, type_, fw
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def _build_primary_parts(name: str, type_: str, fw: str) -> list[str]:
|
|
290
|
-
"""Build primary display parts from name, type, and framework."""
|
|
291
|
-
parts = []
|
|
292
|
-
if name:
|
|
293
|
-
parts.append(name)
|
|
294
|
-
if type_:
|
|
295
|
-
parts.append(type_)
|
|
296
|
-
if fw:
|
|
297
|
-
parts.append(fw)
|
|
298
|
-
return parts
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def _get_fallback_columns(columns: list[tuple]) -> list[tuple]:
|
|
302
|
-
"""Get first two visible columns for fallback display."""
|
|
303
|
-
return columns[:2]
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def _is_standard_field(k: str) -> bool:
|
|
307
|
-
"""Check if field is a standard field to skip."""
|
|
308
|
-
return k in ("id", "name", "type", "framework")
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _extract_fallback_values(row: dict[str, Any], columns: list[tuple]) -> list[str]:
|
|
312
|
-
"""Extract fallback values from columns."""
|
|
313
|
-
fallback_parts = []
|
|
314
|
-
for k, _hdr, _style, _w in columns:
|
|
315
|
-
if _is_standard_field(k):
|
|
316
|
-
continue
|
|
317
|
-
val = str(row.get(k, "")).strip()
|
|
318
|
-
if val:
|
|
319
|
-
fallback_parts.append(val)
|
|
320
|
-
if len(fallback_parts) >= 2:
|
|
321
|
-
break
|
|
322
|
-
return fallback_parts
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def _build_display_parts(
|
|
326
|
-
name: str, _id: str, type_: str, fw: str, row: dict[str, Any], columns: list[tuple]
|
|
327
|
-
) -> list[str]:
|
|
328
|
-
"""Build complete display parts list."""
|
|
329
|
-
parts = _build_primary_parts(name, type_, fw)
|
|
330
|
-
|
|
331
|
-
if not parts:
|
|
332
|
-
# Use fallback columns
|
|
333
|
-
fallback_columns = _get_fallback_columns(columns)
|
|
334
|
-
parts.extend(_extract_fallback_values(row, fallback_columns))
|
|
335
|
-
|
|
336
|
-
if _id:
|
|
337
|
-
parts.append(f"[{_id}]")
|
|
338
|
-
|
|
339
|
-
return parts
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def _row_display(row: dict[str, Any], columns: list[tuple]) -> str:
|
|
343
|
-
"""Build a compact text label for the palette.
|
|
344
|
-
|
|
345
|
-
Prefers: name • type • framework • [id] (when available)
|
|
346
|
-
Falls back to first 2 columns + [id].
|
|
347
|
-
"""
|
|
348
|
-
name, _id, type_, fw = _extract_display_fields(row)
|
|
349
|
-
parts = _build_display_parts(name, _id, type_, fw, row, columns)
|
|
350
|
-
return " • ".join(parts) if parts else (_id or "(row)")
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def _check_fuzzy_pick_requirements() -> bool:
|
|
354
|
-
"""Check if fuzzy picking requirements are met."""
|
|
355
|
-
return _HAS_PTK and console.is_terminal and os.isatty(1)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def _build_unique_labels(
|
|
359
|
-
rows: list[dict[str, Any]], columns: list[tuple]
|
|
360
|
-
) -> tuple[list[str], dict[str, dict[str, Any]]]:
|
|
361
|
-
"""Build unique display labels and reverse mapping."""
|
|
362
|
-
labels = []
|
|
363
|
-
by_label: dict[str, dict[str, Any]] = {}
|
|
364
|
-
|
|
365
|
-
for r in rows:
|
|
366
|
-
label = _row_display(r, columns)
|
|
367
|
-
# Ensure uniqueness: if duplicate, suffix with …#n
|
|
368
|
-
if label in by_label:
|
|
369
|
-
i = 2
|
|
370
|
-
base = label
|
|
371
|
-
while f"{base} #{i}" in by_label:
|
|
372
|
-
i += 1
|
|
373
|
-
label = f"{base} #{i}"
|
|
374
|
-
labels.append(label)
|
|
375
|
-
by_label[label] = r
|
|
376
|
-
|
|
377
|
-
return labels, by_label
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def _basic_prompt(
|
|
381
|
-
message: str,
|
|
382
|
-
completer: Any,
|
|
383
|
-
) -> str | None:
|
|
384
|
-
"""Fallback prompt handler when PromptSession is unavailable or fails."""
|
|
385
|
-
if prompt is None: # pragma: no cover - optional dependency path
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
|
-
try:
|
|
389
|
-
return prompt(
|
|
390
|
-
message=message,
|
|
391
|
-
completer=completer,
|
|
392
|
-
complete_in_thread=True,
|
|
393
|
-
complete_while_typing=True,
|
|
394
|
-
)
|
|
395
|
-
except (KeyboardInterrupt, EOFError):
|
|
396
|
-
return None
|
|
397
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
398
|
-
logger.debug("Fallback prompt failed: %s", exc)
|
|
399
|
-
return None
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def _prompt_with_auto_select(
|
|
403
|
-
message: str,
|
|
404
|
-
completer: Any,
|
|
405
|
-
choices: Iterable[str],
|
|
406
|
-
) -> str | None:
|
|
407
|
-
"""Prompt with fuzzy completer that auto-selects suggested matches."""
|
|
408
|
-
if not _HAS_PTK or PromptSession is None or Buffer is None or SelectionType is None:
|
|
409
|
-
return _basic_prompt(message, completer)
|
|
410
|
-
|
|
411
|
-
try:
|
|
412
|
-
session = PromptSession(
|
|
413
|
-
message,
|
|
414
|
-
completer=completer,
|
|
415
|
-
complete_in_thread=True,
|
|
416
|
-
complete_while_typing=True,
|
|
417
|
-
reserve_space_for_menu=8,
|
|
418
|
-
)
|
|
419
|
-
except Exception as exc: # pragma: no cover - depends on prompt_toolkit
|
|
420
|
-
logger.debug("PromptSession init failed (%s); falling back to basic prompt.", exc)
|
|
421
|
-
return _basic_prompt(message, completer)
|
|
422
|
-
|
|
423
|
-
buffer = session.default_buffer
|
|
424
|
-
valid_choices = set(choices)
|
|
425
|
-
|
|
426
|
-
def _auto_select(_: Buffer) -> None:
|
|
427
|
-
text = buffer.text
|
|
428
|
-
if not text or text not in valid_choices:
|
|
429
|
-
return
|
|
430
|
-
buffer.cursor_position = 0
|
|
431
|
-
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
|
432
|
-
buffer.cursor_position = len(text)
|
|
433
|
-
|
|
434
|
-
handler_attached = False
|
|
435
|
-
try:
|
|
436
|
-
buffer.on_text_changed += _auto_select
|
|
437
|
-
handler_attached = True
|
|
438
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
439
|
-
logger.debug("Failed to attach auto-select handler: %s", exc)
|
|
440
|
-
|
|
441
|
-
try:
|
|
442
|
-
return session.prompt()
|
|
443
|
-
except (KeyboardInterrupt, EOFError):
|
|
444
|
-
return None
|
|
445
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
446
|
-
logger.debug("PromptSession prompt failed (%s); falling back to basic prompt.", exc)
|
|
447
|
-
return _basic_prompt(message, completer)
|
|
448
|
-
finally:
|
|
449
|
-
if handler_attached:
|
|
450
|
-
try:
|
|
451
|
-
buffer.on_text_changed -= _auto_select
|
|
452
|
-
except Exception: # pragma: no cover - defensive
|
|
453
|
-
pass
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
class _FuzzyCompleter:
|
|
457
|
-
"""Fuzzy completer for prompt_toolkit."""
|
|
458
|
-
|
|
459
|
-
def __init__(self, words: list[str]) -> None:
|
|
460
|
-
self.words = words
|
|
461
|
-
|
|
462
|
-
def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
|
|
463
|
-
word = document.get_word_before_cursor()
|
|
464
|
-
if not word:
|
|
145
|
+
with _warn_lock:
|
|
146
|
+
if _warned:
|
|
465
147
|
return
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
""
|
|
489
|
-
#
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
#
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
for
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
""
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
""
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
#
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
#
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
""
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
for
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
""
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
for
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
""
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
""
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
""
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if not _is_fuzzy_match(search, target):
|
|
583
|
-
return -1 # Not a fuzzy match
|
|
584
|
-
|
|
585
|
-
# Calculate score based on different factors
|
|
586
|
-
score = 0
|
|
587
|
-
score += _calculate_exact_match_bonus(search, target)
|
|
588
|
-
score += _calculate_consecutive_bonus(search, target)
|
|
589
|
-
score += _calculate_length_bonus(search, target)
|
|
590
|
-
|
|
591
|
-
return score
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
# ----------------------------- Pretty outputs ---------------------------- #
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def _coerce_result_payload(result: Any) -> Any:
|
|
598
|
-
try:
|
|
599
|
-
to_dict = getattr(result, "to_dict", None)
|
|
600
|
-
if callable(to_dict):
|
|
601
|
-
return to_dict()
|
|
602
|
-
except Exception:
|
|
603
|
-
return result
|
|
604
|
-
return result
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
def _ensure_displayable(payload: Any) -> Any:
|
|
608
|
-
if isinstance(payload, (dict, list, str, int, float, bool)) or payload is None:
|
|
609
|
-
return payload
|
|
610
|
-
|
|
611
|
-
if hasattr(payload, "__dict__"):
|
|
612
|
-
try:
|
|
613
|
-
return dict(payload)
|
|
614
|
-
except Exception:
|
|
615
|
-
try:
|
|
616
|
-
return dict(payload.__dict__)
|
|
617
|
-
except Exception:
|
|
618
|
-
pass
|
|
619
|
-
|
|
620
|
-
try:
|
|
621
|
-
return str(payload)
|
|
622
|
-
except Exception:
|
|
623
|
-
return repr(payload)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
def _render_markdown_output(data: Any) -> None:
|
|
627
|
-
try:
|
|
628
|
-
console.print(Markdown(str(data)))
|
|
629
|
-
except ImportError:
|
|
630
|
-
click.echo(str(data))
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
def output_result(
|
|
634
|
-
ctx: Any,
|
|
635
|
-
result: Any,
|
|
636
|
-
title: str = "Result",
|
|
637
|
-
panel_title: str | None = None,
|
|
638
|
-
) -> None:
|
|
639
|
-
"""Output a result to the console with optional title.
|
|
640
|
-
|
|
641
|
-
Args:
|
|
642
|
-
ctx: Click context
|
|
643
|
-
result: Result data to output
|
|
644
|
-
title: Optional title for the output
|
|
645
|
-
panel_title: Optional Rich panel title for structured output
|
|
646
|
-
"""
|
|
647
|
-
fmt = _get_view(ctx)
|
|
648
|
-
|
|
649
|
-
data = _coerce_result_payload(result)
|
|
650
|
-
data = masking.mask_payload(data)
|
|
651
|
-
data = _ensure_displayable(data)
|
|
652
|
-
|
|
653
|
-
if fmt == "json":
|
|
654
|
-
click.echo(json.dumps(data, indent=2, default=str))
|
|
655
|
-
return
|
|
656
|
-
|
|
657
|
-
if fmt == "plain":
|
|
658
|
-
click.echo(str(data))
|
|
659
|
-
return
|
|
660
|
-
|
|
661
|
-
if fmt == "md":
|
|
662
|
-
_render_markdown_output(data)
|
|
663
|
-
return
|
|
664
|
-
|
|
665
|
-
renderable = Pretty(data)
|
|
666
|
-
if panel_title:
|
|
667
|
-
console.print(AIPPanel(renderable, title=panel_title))
|
|
668
|
-
else:
|
|
669
|
-
console.print(markup_text(f"[{ACCENT_STYLE}]{title}:[/]"))
|
|
670
|
-
console.print(renderable)
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
# ----------------------------- List rendering ---------------------------- #
|
|
674
|
-
|
|
675
|
-
# Threshold no longer used - fuzzy palette is always default for TTY
|
|
676
|
-
# _PICK_THRESHOLD = int(os.getenv("AIP_PICK_THRESHOLD", "5") or "5")
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
680
|
-
try:
|
|
681
|
-
rows: list[dict[str, Any]] = []
|
|
682
|
-
for item in items:
|
|
683
|
-
if transform_func:
|
|
684
|
-
rows.append(transform_func(item))
|
|
685
|
-
elif hasattr(item, "to_dict"):
|
|
686
|
-
rows.append(item.to_dict())
|
|
687
|
-
elif hasattr(item, "__dict__"):
|
|
688
|
-
rows.append(vars(item))
|
|
689
|
-
elif isinstance(item, dict):
|
|
690
|
-
rows.append(item)
|
|
691
|
-
else:
|
|
692
|
-
rows.append({"value": item})
|
|
693
|
-
return rows
|
|
694
|
-
except Exception:
|
|
695
|
-
return []
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
def _render_plain_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
699
|
-
if not rows:
|
|
700
|
-
click.echo(f"No {title.lower()} found.")
|
|
701
|
-
return
|
|
702
|
-
for row in rows:
|
|
703
|
-
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
704
|
-
click.echo(row_str)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
708
|
-
if not rows:
|
|
709
|
-
click.echo(f"No {title.lower()} found.")
|
|
710
|
-
return
|
|
711
|
-
headers = [header for _, header, _, _ in columns]
|
|
712
|
-
click.echo(f"| {' | '.join(headers)} |")
|
|
713
|
-
click.echo(f"| {' | '.join('---' for _ in headers)} |")
|
|
714
|
-
for row in rows:
|
|
715
|
-
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
716
|
-
click.echo(f"| {row_str} |")
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
|
|
720
|
-
return (
|
|
721
|
-
os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
|
|
722
|
-
and rows
|
|
723
|
-
and isinstance(rows[0], dict)
|
|
724
|
-
and "name" in rows[0]
|
|
725
|
-
)
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -> Any:
|
|
729
|
-
table = AIPTable(title=title, expand=True)
|
|
730
|
-
for _key, header, style, width in columns:
|
|
731
|
-
table.add_column(header, style=style, width=width)
|
|
732
|
-
return table
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
def _build_table_group(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> Group:
|
|
736
|
-
table = _create_table(columns, title)
|
|
737
|
-
for row in rows:
|
|
738
|
-
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
739
|
-
footer = markup_text(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
740
|
-
return Group(table, footer)
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
def _handle_json_output(items: list[Any], rows: list[dict[str, Any]]) -> None:
|
|
744
|
-
"""Handle JSON output format."""
|
|
745
|
-
data = rows if rows else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
|
|
746
|
-
click.echo(json.dumps(data, indent=2, default=str))
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
def _handle_plain_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
750
|
-
"""Handle plain text output format."""
|
|
751
|
-
_render_plain_list(rows, title, columns)
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
def _handle_markdown_output(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
755
|
-
"""Handle markdown output format."""
|
|
756
|
-
_render_markdown_list(rows, title, columns)
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
def _handle_empty_items(title: str) -> None:
|
|
760
|
-
"""Handle case when no items are found."""
|
|
761
|
-
console.print(markup_text(f"[{WARNING_STYLE}]No {title.lower()} found.[/]"))
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
def _should_use_fuzzy_picker() -> bool:
|
|
765
|
-
"""Return True when the interactive fuzzy picker can be shown."""
|
|
766
|
-
return console.is_terminal and os.isatty(1)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
def _try_fuzzy_pick(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> dict[str, Any] | None:
|
|
770
|
-
"""Best-effort fuzzy selection; returns None if the picker fails."""
|
|
771
|
-
if not _should_use_fuzzy_picker():
|
|
772
|
-
return None
|
|
773
|
-
|
|
774
|
-
try:
|
|
775
|
-
return _fuzzy_pick(rows, columns, title)
|
|
776
|
-
except Exception:
|
|
777
|
-
logger.debug("Fuzzy picker failed; falling back to table output", exc_info=True)
|
|
778
|
-
return None
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
def _resource_tip_command(title: str) -> str | None:
|
|
782
|
-
"""Resolve the follow-up command hint for the given table title."""
|
|
783
|
-
title_lower = title.lower()
|
|
784
|
-
mapping = {
|
|
785
|
-
"agent": ("agents get", "agents"),
|
|
786
|
-
"tool": ("tools get", None),
|
|
787
|
-
"mcp": ("mcps get", None),
|
|
788
|
-
"model": ("models list", None), # models only ship a list command
|
|
789
|
-
}
|
|
790
|
-
for keyword, (cli_command, slash_command) in mapping.items():
|
|
791
|
-
if keyword in title_lower:
|
|
792
|
-
return command_hint(cli_command, slash_command=slash_command)
|
|
793
|
-
return command_hint("agents get", slash_command="agents")
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
def _print_selection_tip(title: str) -> None:
|
|
797
|
-
"""Print the contextual follow-up tip after a fuzzy selection."""
|
|
798
|
-
tip_cmd = _resource_tip_command(title)
|
|
799
|
-
if tip_cmd:
|
|
800
|
-
console.print(markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]"))
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
def _handle_fuzzy_pick_selection(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> bool:
|
|
804
|
-
"""Handle fuzzy picker selection, returns True if selection was made."""
|
|
805
|
-
picked = _try_fuzzy_pick(rows, columns, title)
|
|
806
|
-
if picked is None:
|
|
807
|
-
return False
|
|
808
|
-
|
|
809
|
-
table = _create_table(columns, title)
|
|
810
|
-
table.add_row(*[str(picked.get(key, "N/A")) for key, _, _, _ in columns])
|
|
811
|
-
console.print(table)
|
|
812
|
-
_print_selection_tip(title)
|
|
813
|
-
return True
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
def _handle_table_output(
|
|
817
|
-
rows: list[dict[str, Any]],
|
|
818
|
-
columns: list[tuple],
|
|
819
|
-
title: str,
|
|
820
|
-
*,
|
|
821
|
-
use_pager: bool | None = None,
|
|
822
|
-
) -> None:
|
|
823
|
-
"""Handle table output with paging."""
|
|
824
|
-
content = _build_table_group(rows, columns, title)
|
|
825
|
-
should_page = (
|
|
826
|
-
pager._should_page_output(len(rows), console.is_terminal and os.isatty(1)) if use_pager is None else use_pager
|
|
827
|
-
)
|
|
828
|
-
|
|
829
|
-
if should_page:
|
|
830
|
-
ansi = pager._render_ansi(content)
|
|
831
|
-
if not pager._page_with_system_pager(ansi):
|
|
832
|
-
with console.pager(styles=True):
|
|
833
|
-
console.print(content)
|
|
834
|
-
else:
|
|
835
|
-
console.print(content)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
def output_list(
|
|
839
|
-
ctx: Any,
|
|
840
|
-
items: list[Any],
|
|
841
|
-
title: str,
|
|
842
|
-
columns: list[tuple[str, str, str, int | None]],
|
|
843
|
-
transform_func: Callable | None = None,
|
|
844
|
-
*,
|
|
845
|
-
skip_picker: bool = False,
|
|
846
|
-
use_pager: bool | None = None,
|
|
847
|
-
) -> None:
|
|
848
|
-
"""Display a list with optional fuzzy palette for quick selection."""
|
|
849
|
-
fmt = _get_view(ctx)
|
|
850
|
-
rows = _normalise_rows(items, transform_func)
|
|
851
|
-
rows = masking.mask_rows(rows)
|
|
852
|
-
|
|
853
|
-
if fmt == "json":
|
|
854
|
-
_handle_json_output(items, rows)
|
|
855
|
-
return
|
|
856
|
-
|
|
857
|
-
if fmt == "plain":
|
|
858
|
-
_handle_plain_output(rows, title, columns)
|
|
859
|
-
return
|
|
860
|
-
|
|
861
|
-
if fmt == "md":
|
|
862
|
-
_handle_markdown_output(rows, title, columns)
|
|
863
|
-
return
|
|
864
|
-
|
|
865
|
-
if not items:
|
|
866
|
-
_handle_empty_items(title)
|
|
867
|
-
return
|
|
868
|
-
|
|
869
|
-
if _should_sort_rows(rows):
|
|
870
|
-
try:
|
|
871
|
-
rows = sorted(rows, key=lambda r: str(r.get("name", "")).lower())
|
|
872
|
-
except Exception:
|
|
873
|
-
pass
|
|
874
|
-
|
|
875
|
-
if not skip_picker and _handle_fuzzy_pick_selection(rows, columns, title):
|
|
876
|
-
return
|
|
877
|
-
|
|
878
|
-
_handle_table_output(rows, columns, title, use_pager=use_pager)
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
# ------------------------- Ambiguity handling --------------------------- #
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
def coerce_to_row(item: Any, keys: list[str]) -> dict[str, Any]:
|
|
885
|
-
"""Coerce an item (dict or object) to a row dict with specified keys.
|
|
886
|
-
|
|
887
|
-
Args:
|
|
888
|
-
item: The item to coerce (dict or object with attributes)
|
|
889
|
-
keys: List of keys/attribute names to extract
|
|
890
|
-
|
|
891
|
-
Returns:
|
|
892
|
-
Dict with the extracted values, "N/A" for missing values
|
|
893
|
-
"""
|
|
894
|
-
result = {}
|
|
895
|
-
for key in keys:
|
|
896
|
-
if isinstance(item, dict):
|
|
897
|
-
value = item.get(key, "N/A")
|
|
898
|
-
else:
|
|
899
|
-
value = getattr(item, key, "N/A")
|
|
900
|
-
result[key] = str(value) if value is not None else "N/A"
|
|
901
|
-
return result
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
def _register_renderer_with_session(ctx: Any, renderer: RichStreamRenderer) -> None:
|
|
905
|
-
"""Attach renderer to an active slash session when present."""
|
|
906
|
-
try:
|
|
907
|
-
ctx_obj = getattr(ctx, "obj", None)
|
|
908
|
-
session = ctx_obj.get("_slash_session") if isinstance(ctx_obj, dict) else None
|
|
909
|
-
if session and hasattr(session, "register_active_renderer"):
|
|
910
|
-
session.register_active_renderer(renderer)
|
|
911
|
-
except Exception:
|
|
912
|
-
# Never let session bookkeeping break renderer creation
|
|
913
|
-
pass
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
def build_renderer(
|
|
917
|
-
_ctx: Any,
|
|
918
|
-
*,
|
|
919
|
-
save_path: str | os.PathLike[str] | None,
|
|
920
|
-
verbose: bool = False,
|
|
921
|
-
_tty_enabled: bool = True,
|
|
922
|
-
live: bool | None = None,
|
|
923
|
-
snapshots: bool | None = None,
|
|
924
|
-
) -> tuple[RichStreamRenderer, Console | CapturingConsole]:
|
|
925
|
-
"""Build renderer and capturing console for CLI commands.
|
|
926
|
-
|
|
927
|
-
Args:
|
|
928
|
-
_ctx: Click context object for CLI operations.
|
|
929
|
-
save_path: Path to save output to (enables capturing console).
|
|
930
|
-
verbose: Whether to enable verbose mode.
|
|
931
|
-
_tty_enabled: Whether TTY is available for interactive features.
|
|
932
|
-
live: Whether to enable live rendering mode (overrides verbose default).
|
|
933
|
-
snapshots: Whether to capture and store snapshots.
|
|
934
|
-
|
|
935
|
-
Returns:
|
|
936
|
-
Tuple of (renderer, capturing_console) for streaming output.
|
|
937
|
-
"""
|
|
938
|
-
# Use capturing console if saving output
|
|
939
|
-
working_console = CapturingConsole(console, capture=True) if save_path else console
|
|
940
|
-
|
|
941
|
-
# Configure renderer based on verbose mode and explicit overrides
|
|
942
|
-
live_enabled = bool(live) if live is not None else not verbose
|
|
943
|
-
renderer_cfg = RendererConfig(
|
|
944
|
-
live=live_enabled,
|
|
945
|
-
append_finished_snapshots=bool(snapshots)
|
|
946
|
-
if snapshots is not None
|
|
947
|
-
else RendererConfig.append_finished_snapshots,
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
# Create the renderer instance
|
|
951
|
-
renderer = RichStreamRenderer(
|
|
952
|
-
working_console.original_console if isinstance(working_console, CapturingConsole) else working_console,
|
|
953
|
-
cfg=renderer_cfg,
|
|
954
|
-
verbose=verbose,
|
|
955
|
-
)
|
|
956
|
-
|
|
957
|
-
# Link the renderer back to the slash session when running from the palette.
|
|
958
|
-
_register_renderer_with_session(_ctx, renderer)
|
|
959
|
-
|
|
960
|
-
return renderer, working_console
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
def _build_resource_labels(resources: list[Any]) -> tuple[list[str], dict[str, Any]]:
|
|
964
|
-
"""Build unique display labels for resources."""
|
|
965
|
-
labels = []
|
|
966
|
-
by_label: dict[str, Any] = {}
|
|
967
|
-
|
|
968
|
-
for resource in resources:
|
|
969
|
-
name = getattr(resource, "name", "Unknown")
|
|
970
|
-
_id = getattr(resource, "id", "Unknown")
|
|
971
|
-
|
|
972
|
-
# Create display label
|
|
973
|
-
label_parts = []
|
|
974
|
-
if name and name != "Unknown":
|
|
975
|
-
label_parts.append(name)
|
|
976
|
-
label_parts.append(f"[{_id[:8]}...]") # Show first 8 chars of ID
|
|
977
|
-
label = " • ".join(label_parts)
|
|
978
|
-
|
|
979
|
-
# Ensure uniqueness
|
|
980
|
-
if label in by_label:
|
|
981
|
-
i = 2
|
|
982
|
-
base = label
|
|
983
|
-
while f"{base} #{i}" in by_label:
|
|
984
|
-
i += 1
|
|
985
|
-
label = f"{base} #{i}"
|
|
986
|
-
|
|
987
|
-
labels.append(label)
|
|
988
|
-
by_label[label] = resource
|
|
989
|
-
|
|
990
|
-
return labels, by_label
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
def _fuzzy_pick_for_resources(
|
|
994
|
-
resources: list[Any], resource_type: str, _search_term: str
|
|
995
|
-
) -> Any | None: # pragma: no cover - interactive selection helper
|
|
996
|
-
"""Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
|
|
997
|
-
|
|
998
|
-
Args:
|
|
999
|
-
resources: List of resource objects to choose from
|
|
1000
|
-
resource_type: Type of resource (e.g., "agent", "tool")
|
|
1001
|
-
search_term: The search term that led to multiple matches
|
|
1002
|
-
|
|
1003
|
-
Returns:
|
|
1004
|
-
Selected resource object or None if cancelled/no selection
|
|
1005
|
-
"""
|
|
1006
|
-
if not _check_fuzzy_pick_requirements():
|
|
1007
|
-
return None
|
|
1008
|
-
|
|
1009
|
-
# Build labels and mapping
|
|
1010
|
-
labels, by_label = _build_resource_labels(resources)
|
|
1011
|
-
|
|
1012
|
-
# Create fuzzy completer
|
|
1013
|
-
completer = _FuzzyCompleter(labels)
|
|
1014
|
-
answer = _prompt_with_auto_select(
|
|
1015
|
-
f"Find {ICON_AGENT} {resource_type.title()}: ",
|
|
1016
|
-
completer,
|
|
1017
|
-
labels,
|
|
1018
|
-
)
|
|
1019
|
-
if answer is None:
|
|
1020
|
-
return None
|
|
1021
|
-
|
|
1022
|
-
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
def _resolve_by_id(ref: str, get_by_id: Callable) -> Any | None:
|
|
1026
|
-
"""Resolve resource by UUID if ref is a valid UUID."""
|
|
1027
|
-
if is_uuid(ref):
|
|
1028
|
-
return get_by_id(ref)
|
|
1029
|
-
return None
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
def _resolve_by_name_multiple_with_select(matches: list[Any], select: int) -> Any:
|
|
1033
|
-
"""Resolve multiple matches using select parameter."""
|
|
1034
|
-
idx = int(select) - 1
|
|
1035
|
-
if not (0 <= idx < len(matches)):
|
|
1036
|
-
raise click.ClickException(f"--select must be 1..{len(matches)}")
|
|
1037
|
-
return matches[idx]
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
def _resolve_by_name_multiple_fuzzy(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
|
|
1041
|
-
"""Resolve multiple matches preferring the fuzzy picker interface."""
|
|
1042
|
-
return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="fuzzy")
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
def _resolve_by_name_multiple_questionary(ctx: Any, ref: str, matches: list[Any], label: str) -> Any:
|
|
1046
|
-
"""Resolve multiple matches preferring the questionary interface."""
|
|
1047
|
-
return handle_ambiguous_resource(ctx, label.lower(), ref, matches, interface_preference="questionary")
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
def resolve_resource(
|
|
1051
|
-
ctx: Any,
|
|
1052
|
-
ref: str,
|
|
1053
|
-
*,
|
|
1054
|
-
get_by_id: Callable,
|
|
1055
|
-
find_by_name: Callable,
|
|
1056
|
-
label: str,
|
|
1057
|
-
select: int | None = None,
|
|
1058
|
-
interface_preference: str = "fuzzy",
|
|
1059
|
-
status_indicator: Any | None = None,
|
|
1060
|
-
) -> Any | None:
|
|
1061
|
-
"""Resolve resource reference (ID or name) with ambiguity handling.
|
|
1062
|
-
|
|
1063
|
-
Args:
|
|
1064
|
-
ctx: Click context
|
|
1065
|
-
ref: Resource reference (ID or name)
|
|
1066
|
-
get_by_id: Function to get resource by ID
|
|
1067
|
-
find_by_name: Function to find resources by name
|
|
1068
|
-
label: Resource type label for error messages
|
|
1069
|
-
select: Optional selection index for ambiguity resolution
|
|
1070
|
-
interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
|
|
1071
|
-
status_indicator: Optional Rich status indicator for wait animations
|
|
1072
|
-
|
|
1073
|
-
Returns:
|
|
1074
|
-
Resolved resource object
|
|
1075
|
-
"""
|
|
1076
|
-
spinner = status_indicator
|
|
1077
|
-
_spinner_update(spinner, f"[bold blue]Resolving {label}…[/bold blue]")
|
|
1078
|
-
|
|
1079
|
-
# Try to resolve by ID first
|
|
1080
|
-
_spinner_update(spinner, f"[bold blue]Fetching {label} by ID…[/bold blue]")
|
|
1081
|
-
result = _resolve_by_id(ref, get_by_id)
|
|
1082
|
-
if result is not None:
|
|
1083
|
-
_spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
|
|
1084
|
-
return result
|
|
1085
|
-
|
|
1086
|
-
# If get_by_id returned None, the resource doesn't exist
|
|
1087
|
-
if is_uuid(ref):
|
|
1088
|
-
_spinner_stop(spinner)
|
|
1089
|
-
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1090
|
-
|
|
1091
|
-
# Find resources by name
|
|
1092
|
-
_spinner_update(spinner, f"[bold blue]Searching {label}s matching '{ref}'…[/bold blue]")
|
|
1093
|
-
matches = find_by_name(name=ref)
|
|
1094
|
-
if not matches:
|
|
1095
|
-
_spinner_stop(spinner)
|
|
1096
|
-
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1097
|
-
|
|
1098
|
-
if len(matches) == 1:
|
|
1099
|
-
_spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
|
|
1100
|
-
return matches[0]
|
|
1101
|
-
|
|
1102
|
-
# Multiple matches found, handle ambiguity
|
|
1103
|
-
if select:
|
|
1104
|
-
_spinner_stop(spinner)
|
|
1105
|
-
return _resolve_by_name_multiple_with_select(matches, select)
|
|
1106
|
-
|
|
1107
|
-
# Choose interface based on preference
|
|
1108
|
-
_spinner_stop(spinner)
|
|
1109
|
-
preference = (interface_preference or "fuzzy").lower()
|
|
1110
|
-
if preference not in {"fuzzy", "questionary"}:
|
|
1111
|
-
preference = "fuzzy"
|
|
1112
|
-
if preference == "fuzzy":
|
|
1113
|
-
return _resolve_by_name_multiple_fuzzy(ctx, ref, matches, label)
|
|
1114
|
-
else:
|
|
1115
|
-
return _resolve_by_name_multiple_questionary(ctx, ref, matches, label)
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
|
|
1119
|
-
"""Handle ambiguity in JSON view by returning first match."""
|
|
1120
|
-
return matches[0]
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1124
|
-
"""Handle ambiguity using questionary interactive interface."""
|
|
1125
|
-
if not (questionary and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
|
|
1126
|
-
raise click.ClickException("Interactive selection not available")
|
|
1127
|
-
|
|
1128
|
-
# Escape special characters for questionary
|
|
1129
|
-
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1130
|
-
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1131
|
-
|
|
1132
|
-
picked_idx = questionary.select(
|
|
1133
|
-
f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
|
|
1134
|
-
choices=[
|
|
1135
|
-
questionary.Choice(
|
|
1136
|
-
title=(
|
|
1137
|
-
f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
|
|
1138
|
-
f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
|
|
1139
|
-
),
|
|
1140
|
-
value=i,
|
|
1141
|
-
)
|
|
1142
|
-
for i, m in enumerate(matches)
|
|
1143
|
-
],
|
|
1144
|
-
use_indicator=True,
|
|
1145
|
-
qmark="🧭",
|
|
1146
|
-
instruction="↑/↓ to select • Enter to confirm",
|
|
1147
|
-
).ask()
|
|
1148
|
-
if picked_idx is None:
|
|
1149
|
-
raise click.ClickException("Selection cancelled")
|
|
1150
|
-
return matches[picked_idx]
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
def _handle_fallback_numeric_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1154
|
-
"""Handle ambiguity using numeric prompt fallback."""
|
|
1155
|
-
# Escape special characters for display
|
|
1156
|
-
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1157
|
-
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1158
|
-
|
|
1159
|
-
console.print(markup_text(f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"))
|
|
1160
|
-
table = AIPTable(
|
|
1161
|
-
title=f"Select {safe_resource_type.title()}",
|
|
1162
|
-
)
|
|
1163
|
-
table.add_column("#", style="dim", width=3)
|
|
1164
|
-
table.add_column("ID", style="dim", width=36)
|
|
1165
|
-
table.add_column("Name", style=ACCENT_STYLE)
|
|
1166
|
-
for i, m in enumerate(matches, 1):
|
|
1167
|
-
table.add_row(str(i), str(getattr(m, "id", "")), str(getattr(m, "name", "")))
|
|
1168
|
-
console.print(table)
|
|
1169
|
-
choice_str = click.prompt(
|
|
1170
|
-
f"Select {safe_resource_type} (1-{len(matches)})",
|
|
1171
|
-
)
|
|
1172
|
-
try:
|
|
1173
|
-
choice = int(choice_str)
|
|
1174
|
-
except ValueError as err:
|
|
1175
|
-
raise click.ClickException("Invalid selection") from err
|
|
1176
|
-
if 1 <= choice <= len(matches):
|
|
1177
|
-
return matches[choice - 1]
|
|
1178
|
-
raise click.ClickException("Invalid selection")
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
def _should_fallback_to_numeric_prompt(exception: Exception) -> bool:
|
|
1182
|
-
"""Determine if we should fallback to numeric prompt for this exception."""
|
|
1183
|
-
# Re-raise cancellation - user explicitly cancelled
|
|
1184
|
-
if "Selection cancelled" in str(exception):
|
|
1185
|
-
return False
|
|
1186
|
-
|
|
1187
|
-
# Fall back to numeric prompt for other exceptions
|
|
1188
|
-
return True
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
def _normalize_interface_preference(preference: str) -> str:
|
|
1192
|
-
"""Normalize and validate interface preference."""
|
|
1193
|
-
normalized = (preference or "questionary").lower()
|
|
1194
|
-
return normalized if normalized in {"fuzzy", "questionary"} else "questionary"
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
def _get_interface_order(preference: str) -> tuple[str, str]:
|
|
1198
|
-
"""Get the ordered interface preferences."""
|
|
1199
|
-
interface_orders = {
|
|
1200
|
-
"fuzzy": ("fuzzy", "questionary"),
|
|
1201
|
-
"questionary": ("questionary", "fuzzy"),
|
|
1202
|
-
}
|
|
1203
|
-
return interface_orders.get(preference, ("questionary", "fuzzy"))
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
def _try_fuzzy_selection(
|
|
1207
|
-
resource_type: str,
|
|
1208
|
-
ref: str,
|
|
1209
|
-
matches: list[Any],
|
|
1210
|
-
) -> Any | None:
|
|
1211
|
-
"""Try fuzzy interface selection."""
|
|
1212
|
-
picked = _fuzzy_pick_for_resources(matches, resource_type, ref)
|
|
1213
|
-
return picked if picked else None
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
def _try_questionary_selection(
|
|
1217
|
-
resource_type: str,
|
|
1218
|
-
ref: str,
|
|
1219
|
-
matches: list[Any],
|
|
1220
|
-
) -> Any | None:
|
|
1221
|
-
"""Try questionary interface selection."""
|
|
1222
|
-
try:
|
|
1223
|
-
return _handle_questionary_ambiguity(resource_type, ref, matches)
|
|
1224
|
-
except Exception as exc:
|
|
1225
|
-
if not _should_fallback_to_numeric_prompt(exc):
|
|
1226
|
-
raise
|
|
1227
|
-
return None
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
def _try_interface_selection(
|
|
1231
|
-
interface_order: tuple[str, str],
|
|
1232
|
-
resource_type: str,
|
|
1233
|
-
ref: str,
|
|
1234
|
-
matches: list[Any],
|
|
1235
|
-
) -> Any | None:
|
|
1236
|
-
"""Try interface selection in order, return result or None if all failed."""
|
|
1237
|
-
interface_handlers = {
|
|
1238
|
-
"fuzzy": _try_fuzzy_selection,
|
|
1239
|
-
"questionary": _try_questionary_selection,
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
for interface in interface_order:
|
|
1243
|
-
handler = interface_handlers.get(interface)
|
|
1244
|
-
if handler:
|
|
1245
|
-
result = handler(resource_type, ref, matches)
|
|
1246
|
-
if result:
|
|
1247
|
-
return result
|
|
1248
|
-
|
|
1249
|
-
return None
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
def handle_ambiguous_resource(
|
|
1253
|
-
ctx: Any,
|
|
1254
|
-
resource_type: str,
|
|
1255
|
-
ref: str,
|
|
1256
|
-
matches: list[Any],
|
|
1257
|
-
*,
|
|
1258
|
-
interface_preference: str = "questionary",
|
|
1259
|
-
) -> Any:
|
|
1260
|
-
"""Handle multiple resource matches gracefully."""
|
|
1261
|
-
if _get_view(ctx) == "json":
|
|
1262
|
-
return _handle_json_view_ambiguity(matches)
|
|
1263
|
-
|
|
1264
|
-
preference = _normalize_interface_preference(interface_preference)
|
|
1265
|
-
interface_order = _get_interface_order(preference)
|
|
1266
|
-
|
|
1267
|
-
result = _try_interface_selection(interface_order, resource_type, ref, matches)
|
|
1268
|
-
|
|
1269
|
-
if result is not None:
|
|
1270
|
-
return result
|
|
1271
|
-
|
|
1272
|
-
return _handle_fallback_numeric_ambiguity(resource_type, ref, matches)
|
|
148
|
+
warnings.warn(
|
|
149
|
+
"Importing from glaip_sdk.cli.utils is deprecated. Use glaip_sdk.cli.core.* modules instead.",
|
|
150
|
+
DeprecationWarning,
|
|
151
|
+
stacklevel=3,
|
|
152
|
+
)
|
|
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
|
+
]
|