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