glaip-sdk 0.0.15__py3-none-any.whl → 0.0.17__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 +1 -1
- glaip_sdk/branding.py +28 -2
- glaip_sdk/cli/commands/agents.py +36 -27
- glaip_sdk/cli/commands/configure.py +46 -52
- glaip_sdk/cli/commands/mcps.py +19 -22
- glaip_sdk/cli/commands/tools.py +19 -13
- glaip_sdk/cli/config.py +42 -0
- glaip_sdk/cli/display.py +97 -30
- glaip_sdk/cli/main.py +141 -124
- glaip_sdk/cli/mcp_validators.py +2 -2
- glaip_sdk/cli/pager.py +3 -2
- glaip_sdk/cli/parsers/json_input.py +2 -2
- glaip_sdk/cli/resolution.py +12 -10
- glaip_sdk/cli/rich_helpers.py +29 -0
- glaip_sdk/cli/slash/agent_session.py +7 -0
- glaip_sdk/cli/slash/prompt.py +21 -2
- glaip_sdk/cli/slash/session.py +15 -21
- glaip_sdk/cli/update_notifier.py +8 -2
- glaip_sdk/cli/utils.py +115 -58
- glaip_sdk/client/_agent_payloads.py +504 -0
- glaip_sdk/client/agents.py +633 -559
- glaip_sdk/client/base.py +92 -20
- glaip_sdk/client/main.py +14 -0
- glaip_sdk/client/run_rendering.py +275 -0
- glaip_sdk/config/constants.py +4 -1
- glaip_sdk/exceptions.py +15 -0
- glaip_sdk/models.py +5 -0
- glaip_sdk/payload_schemas/__init__.py +19 -0
- glaip_sdk/payload_schemas/agent.py +87 -0
- glaip_sdk/rich_components.py +12 -0
- glaip_sdk/utils/client_utils.py +12 -0
- glaip_sdk/utils/import_export.py +2 -2
- glaip_sdk/utils/rendering/formatting.py +5 -0
- glaip_sdk/utils/rendering/models.py +22 -0
- glaip_sdk/utils/rendering/renderer/base.py +9 -1
- glaip_sdk/utils/rendering/renderer/panels.py +0 -1
- glaip_sdk/utils/rendering/steps.py +59 -0
- glaip_sdk/utils/serialization.py +24 -3
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/METADATA +2 -2
- glaip_sdk-0.0.17.dist-info/RECORD +73 -0
- glaip_sdk-0.0.15.dist-info/RECORD +0 -67
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/resolution.py
CHANGED
|
@@ -32,20 +32,22 @@ def resolve_resource_reference(
|
|
|
32
32
|
This is a common pattern used across all resource types.
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
|
-
ctx: Click context
|
|
36
|
-
|
|
37
|
-
reference: Resource ID or name
|
|
38
|
-
resource_type: Type of resource
|
|
39
|
-
get_by_id_func: Function to get resource by ID
|
|
40
|
-
find_by_name_func: Function to find resources by name
|
|
41
|
-
label: Label for error messages
|
|
42
|
-
select: Selection index for ambiguous matches
|
|
35
|
+
ctx: Click context for CLI operations.
|
|
36
|
+
_client: API client instance for backend operations.
|
|
37
|
+
reference: Resource ID or name to resolve.
|
|
38
|
+
resource_type: Type of resource being resolved.
|
|
39
|
+
get_by_id_func: Function to get resource by ID.
|
|
40
|
+
find_by_name_func: Function to find resources by name.
|
|
41
|
+
label: Label for error messages and user feedback.
|
|
42
|
+
select: Selection index for ambiguous matches in non-interactive mode.
|
|
43
|
+
interface_preference: Interface preference for user interaction ("fuzzy" or "questionary").
|
|
44
|
+
spinner_message: Custom message to show during resolution process.
|
|
43
45
|
|
|
44
46
|
Returns:
|
|
45
|
-
Resolved resource object
|
|
47
|
+
Resolved resource object or None if not found.
|
|
46
48
|
|
|
47
49
|
Raises:
|
|
48
|
-
click.ClickException: If resolution fails
|
|
50
|
+
click.ClickException: If resolution fails.
|
|
49
51
|
"""
|
|
50
52
|
try:
|
|
51
53
|
message = (
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared helpers for creating and printing Rich markup content.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.markup import MarkupError
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def markup_text(message: str, **kwargs: Any) -> Text:
|
|
17
|
+
"""Create a Rich Text instance from markup with graceful fallback."""
|
|
18
|
+
try:
|
|
19
|
+
return Text.from_markup(message, **kwargs)
|
|
20
|
+
except MarkupError:
|
|
21
|
+
return Text(message, **kwargs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_markup(
|
|
25
|
+
message: str, *, console: Console | None = None, **kwargs: Any
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Print markup-aware text to the provided console (default: new Console)."""
|
|
28
|
+
target_console = console or Console()
|
|
29
|
+
target_console.print(markup_text(message, **kwargs))
|
|
@@ -22,6 +22,12 @@ class AgentRunSession:
|
|
|
22
22
|
"""Per-agent execution context for the command palette."""
|
|
23
23
|
|
|
24
24
|
def __init__(self, session: SlashSession, agent: Any) -> None:
|
|
25
|
+
"""Initialize the agent run session.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
session: The slash session context
|
|
29
|
+
agent: The agent to interact with
|
|
30
|
+
"""
|
|
25
31
|
self.session = session
|
|
26
32
|
self.agent = agent
|
|
27
33
|
self.console = session.console
|
|
@@ -39,6 +45,7 @@ class AgentRunSession:
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
def run(self) -> None:
|
|
48
|
+
"""Run the interactive agent session loop."""
|
|
42
49
|
self.session.set_contextual_commands(
|
|
43
50
|
self._contextual_completion_help, include_global=False
|
|
44
51
|
)
|
glaip_sdk/cli/slash/prompt.py
CHANGED
|
@@ -46,6 +46,11 @@ if _HAS_PROMPT_TOOLKIT:
|
|
|
46
46
|
"""Provide slash command completions inside the prompt."""
|
|
47
47
|
|
|
48
48
|
def __init__(self, session: SlashSession) -> None:
|
|
49
|
+
"""Initialize the slash completer.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
session: The slash session context
|
|
53
|
+
"""
|
|
49
54
|
self._session = session
|
|
50
55
|
|
|
51
56
|
def get_completions(
|
|
@@ -53,6 +58,15 @@ if _HAS_PROMPT_TOOLKIT:
|
|
|
53
58
|
document: Any,
|
|
54
59
|
_complete_event: Any, # type: ignore[no-any-return]
|
|
55
60
|
) -> Iterable[Completion]: # pragma: no cover - UI
|
|
61
|
+
"""Get completions for slash commands.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
document: The document being edited
|
|
65
|
+
_complete_event: The completion event
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
Completion objects for matching commands
|
|
69
|
+
"""
|
|
56
70
|
if Completion is None:
|
|
57
71
|
return
|
|
58
72
|
|
|
@@ -66,7 +80,14 @@ if _HAS_PROMPT_TOOLKIT:
|
|
|
66
80
|
else: # pragma: no cover - fallback when prompt_toolkit is missing
|
|
67
81
|
|
|
68
82
|
class SlashCompleter: # type: ignore[too-many-ancestors]
|
|
83
|
+
"""Fallback slash completer when prompt_toolkit is not available."""
|
|
84
|
+
|
|
69
85
|
def __init__(self, session: SlashSession) -> None: # noqa: D401 - stub
|
|
86
|
+
"""Initialize the fallback slash completer.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session: The slash session context
|
|
90
|
+
"""
|
|
70
91
|
self._session = session
|
|
71
92
|
|
|
72
93
|
|
|
@@ -76,7 +97,6 @@ def setup_prompt_toolkit(
|
|
|
76
97
|
interactive: bool,
|
|
77
98
|
) -> tuple[Any | None, Any | None]:
|
|
78
99
|
"""Configure prompt_toolkit session and style for interactive mode."""
|
|
79
|
-
|
|
80
100
|
if not (interactive and _HAS_PROMPT_TOOLKIT):
|
|
81
101
|
return None, None
|
|
82
102
|
|
|
@@ -105,7 +125,6 @@ def setup_prompt_toolkit(
|
|
|
105
125
|
|
|
106
126
|
def _create_key_bindings(session: SlashSession) -> Any:
|
|
107
127
|
"""Create prompt_toolkit key bindings for the command palette."""
|
|
108
|
-
|
|
109
128
|
if KeyBindings is None:
|
|
110
129
|
return None
|
|
111
130
|
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -19,7 +19,7 @@ from rich.table import Table
|
|
|
19
19
|
|
|
20
20
|
from glaip_sdk.branding import AIPBranding
|
|
21
21
|
from glaip_sdk.cli.commands.configure import configure_command, load_config
|
|
22
|
-
from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, get_client
|
|
22
|
+
from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, command_hint, get_client
|
|
23
23
|
from glaip_sdk.rich_components import AIPPanel
|
|
24
24
|
|
|
25
25
|
from .agent_session import AgentRunSession
|
|
@@ -49,6 +49,12 @@ class SlashSession:
|
|
|
49
49
|
"""Interactive command palette controller."""
|
|
50
50
|
|
|
51
51
|
def __init__(self, ctx: click.Context, *, console: Console | None = None) -> None:
|
|
52
|
+
"""Initialize the slash session.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
ctx: The Click context
|
|
56
|
+
console: Optional console instance, creates default if None
|
|
57
|
+
"""
|
|
52
58
|
self.ctx = ctx
|
|
53
59
|
self.console = console or Console()
|
|
54
60
|
self._commands: dict[str, SlashCommand] = {}
|
|
@@ -90,7 +96,6 @@ class SlashSession:
|
|
|
90
96
|
|
|
91
97
|
def run(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
92
98
|
"""Start the command palette session loop."""
|
|
93
|
-
|
|
94
99
|
if not self._interactive:
|
|
95
100
|
self._run_non_interactive(initial_commands)
|
|
96
101
|
return
|
|
@@ -153,7 +158,6 @@ class SlashSession:
|
|
|
153
158
|
|
|
154
159
|
def _ensure_configuration(self) -> bool:
|
|
155
160
|
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
156
|
-
|
|
157
161
|
while not self._configuration_ready():
|
|
158
162
|
self.console.print(
|
|
159
163
|
"[yellow]Configuration required.[/] Launching `/login` wizard..."
|
|
@@ -173,7 +177,6 @@ class SlashSession:
|
|
|
173
177
|
|
|
174
178
|
def _configuration_ready(self) -> bool:
|
|
175
179
|
"""Check whether API URL and credentials are available."""
|
|
176
|
-
|
|
177
180
|
config = self._load_config()
|
|
178
181
|
api_url = self._get_api_url(config)
|
|
179
182
|
if not api_url:
|
|
@@ -188,7 +191,6 @@ class SlashSession:
|
|
|
188
191
|
|
|
189
192
|
def handle_command(self, raw: str, *, invoked_from_agent: bool = False) -> bool:
|
|
190
193
|
"""Parse and execute a single slash command string."""
|
|
191
|
-
|
|
192
194
|
verb, args = self._parse(raw)
|
|
193
195
|
if not verb:
|
|
194
196
|
self.console.print("[red]Unrecognised command[/red]")
|
|
@@ -308,9 +310,13 @@ class SlashSession:
|
|
|
308
310
|
return True
|
|
309
311
|
|
|
310
312
|
if not agents:
|
|
311
|
-
self.
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
|
|
314
|
+
if hint:
|
|
315
|
+
self.console.print(
|
|
316
|
+
f"[yellow]No agents available. Use `{hint}` to add one.[/yellow]"
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
self.console.print("[yellow]No agents available.[/yellow]")
|
|
314
320
|
return True
|
|
315
321
|
|
|
316
322
|
if args:
|
|
@@ -373,7 +379,7 @@ class SlashSession:
|
|
|
373
379
|
self._register(
|
|
374
380
|
SlashCommand(
|
|
375
381
|
name="login",
|
|
376
|
-
help="Run `
|
|
382
|
+
help="Run `/login` (alias `/configure`) to set credentials.",
|
|
377
383
|
handler=SlashSession._cmd_login,
|
|
378
384
|
aliases=("configure",),
|
|
379
385
|
)
|
|
@@ -419,12 +425,10 @@ class SlashSession:
|
|
|
419
425
|
@property
|
|
420
426
|
def verbose_enabled(self) -> bool:
|
|
421
427
|
"""Return whether verbose agent runs are enabled."""
|
|
422
|
-
|
|
423
428
|
return self._verbose_enabled
|
|
424
429
|
|
|
425
430
|
def set_verbose(self, enabled: bool, *, announce: bool = True) -> None:
|
|
426
431
|
"""Enable or disable verbose mode with optional announcement."""
|
|
427
|
-
|
|
428
432
|
if self._verbose_enabled == enabled:
|
|
429
433
|
if announce:
|
|
430
434
|
self._print_verbose_status(context="already")
|
|
@@ -437,12 +441,10 @@ class SlashSession:
|
|
|
437
441
|
|
|
438
442
|
def toggle_verbose(self, *, announce: bool = True) -> None:
|
|
439
443
|
"""Flip verbose mode state."""
|
|
440
|
-
|
|
441
444
|
self.set_verbose(not self._verbose_enabled, announce=announce)
|
|
442
445
|
|
|
443
446
|
def _cmd_verbose(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
444
447
|
"""Slash handler for `/verbose` command."""
|
|
445
|
-
|
|
446
448
|
if args:
|
|
447
449
|
self.console.print(
|
|
448
450
|
"Usage: `/verbose` toggles verbose streaming output. Press Ctrl+T as a shortcut."
|
|
@@ -470,30 +472,25 @@ class SlashSession:
|
|
|
470
472
|
# ------------------------------------------------------------------
|
|
471
473
|
def register_active_renderer(self, renderer: Any) -> None:
|
|
472
474
|
"""Register the renderer currently streaming an agent run."""
|
|
473
|
-
|
|
474
475
|
self._active_renderer = renderer
|
|
475
476
|
self._sync_active_renderer()
|
|
476
477
|
|
|
477
478
|
def clear_active_renderer(self, renderer: Any | None = None) -> None:
|
|
478
479
|
"""Clear the active renderer if it matches the provided instance."""
|
|
479
|
-
|
|
480
480
|
if renderer is not None and renderer is not self._active_renderer:
|
|
481
481
|
return
|
|
482
482
|
self._active_renderer = None
|
|
483
483
|
|
|
484
484
|
def notify_agent_run_started(self) -> None:
|
|
485
485
|
"""Mark that an agent run is in progress."""
|
|
486
|
-
|
|
487
486
|
self.clear_active_renderer()
|
|
488
487
|
|
|
489
488
|
def notify_agent_run_finished(self) -> None:
|
|
490
489
|
"""Mark that the active agent run has completed."""
|
|
491
|
-
|
|
492
490
|
self.clear_active_renderer()
|
|
493
491
|
|
|
494
492
|
def _sync_active_renderer(self) -> None:
|
|
495
493
|
"""Ensure the active renderer reflects the current verbose state."""
|
|
496
|
-
|
|
497
494
|
renderer = self._active_renderer
|
|
498
495
|
if renderer is None:
|
|
499
496
|
return
|
|
@@ -622,18 +619,15 @@ class SlashSession:
|
|
|
622
619
|
self, commands: dict[str, str] | None, *, include_global: bool = True
|
|
623
620
|
) -> None:
|
|
624
621
|
"""Set context-specific commands that should appear in completions."""
|
|
625
|
-
|
|
626
622
|
self._contextual_commands = dict(commands or {})
|
|
627
623
|
self._contextual_include_global = include_global if commands else True
|
|
628
624
|
|
|
629
625
|
def get_contextual_commands(self) -> dict[str, str]: # type: ignore[no-any-return]
|
|
630
626
|
"""Return a copy of the currently active contextual commands."""
|
|
631
|
-
|
|
632
627
|
return dict(self._contextual_commands)
|
|
633
628
|
|
|
634
629
|
def should_include_global_commands(self) -> bool:
|
|
635
630
|
"""Return whether global slash commands should appear in completions."""
|
|
636
|
-
|
|
637
631
|
return self._contextual_include_global
|
|
638
632
|
|
|
639
633
|
def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
|
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -13,6 +13,7 @@ import httpx
|
|
|
13
13
|
from packaging.version import InvalidVersion, Version
|
|
14
14
|
from rich.console import Console
|
|
15
15
|
|
|
16
|
+
from glaip_sdk.cli.utils import command_hint
|
|
16
17
|
from glaip_sdk.rich_components import AIPPanel
|
|
17
18
|
|
|
18
19
|
FetchLatestVersion = Callable[[], str | None]
|
|
@@ -59,6 +60,7 @@ def _should_check_for_updates() -> bool:
|
|
|
59
60
|
def _build_update_panel(
|
|
60
61
|
current_version: str,
|
|
61
62
|
latest_version: str,
|
|
63
|
+
command_text: str,
|
|
62
64
|
) -> AIPPanel:
|
|
63
65
|
"""Create a Rich panel that prompts the user to update."""
|
|
64
66
|
message = (
|
|
@@ -66,7 +68,7 @@ def _build_update_panel(
|
|
|
66
68
|
f"{current_version} → {latest_version}\n\n"
|
|
67
69
|
"See the latest release notes:\n"
|
|
68
70
|
f"https://pypi.org/project/glaip-sdk/{latest_version}/\n\n"
|
|
69
|
-
"[cyan]Run[/cyan] [bold]
|
|
71
|
+
f"[cyan]Run[/cyan] [bold]{command_text}[/bold] to install."
|
|
70
72
|
)
|
|
71
73
|
return AIPPanel(
|
|
72
74
|
message,
|
|
@@ -99,8 +101,12 @@ def maybe_notify_update(
|
|
|
99
101
|
if current is None or latest is None or latest <= current:
|
|
100
102
|
return
|
|
101
103
|
|
|
104
|
+
command_text = command_hint("update")
|
|
105
|
+
if command_text is None:
|
|
106
|
+
return
|
|
107
|
+
|
|
102
108
|
active_console = console or Console()
|
|
103
|
-
panel = _build_update_panel(current_version, latest_version)
|
|
109
|
+
panel = _build_update_panel(current_version, latest_version, command_text)
|
|
104
110
|
active_console.print(panel)
|
|
105
111
|
|
|
106
112
|
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -19,7 +19,9 @@ import click
|
|
|
19
19
|
from rich.console import Console, Group
|
|
20
20
|
from rich.markdown import Markdown
|
|
21
21
|
from rich.pretty import Pretty
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
from glaip_sdk.cli.rich_helpers import markup_text
|
|
24
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
23
25
|
|
|
24
26
|
# Optional interactive deps (fuzzy palette)
|
|
25
27
|
try:
|
|
@@ -38,9 +40,15 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
38
40
|
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
39
41
|
from glaip_sdk import Client
|
|
40
42
|
from glaip_sdk.cli import masking, pager
|
|
41
|
-
from glaip_sdk.cli.
|
|
42
|
-
from glaip_sdk.cli.context import
|
|
43
|
-
|
|
43
|
+
from glaip_sdk.cli.config import load_config
|
|
44
|
+
from glaip_sdk.cli.context import (
|
|
45
|
+
_get_view,
|
|
46
|
+
get_ctx_value,
|
|
47
|
+
)
|
|
48
|
+
from glaip_sdk.cli.context import (
|
|
49
|
+
detect_export_format as _detect_export_format,
|
|
50
|
+
)
|
|
51
|
+
from glaip_sdk.rich_components import AIPTable
|
|
44
52
|
from glaip_sdk.utils import is_uuid
|
|
45
53
|
from glaip_sdk.utils.rendering.renderer import (
|
|
46
54
|
CapturingConsole,
|
|
@@ -53,6 +61,59 @@ pager.console = console
|
|
|
53
61
|
logger = logging.getLogger("glaip_sdk.cli.utils")
|
|
54
62
|
|
|
55
63
|
|
|
64
|
+
# ----------------------------- Context helpers ---------------------------- #
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def detect_export_format(file_path: str | os.PathLike[str]) -> str:
|
|
68
|
+
"""Backward-compatible proxy to `glaip_sdk.cli.context.detect_export_format`."""
|
|
69
|
+
return _detect_export_format(file_path)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def in_slash_mode(ctx: click.Context | None = None) -> bool:
|
|
73
|
+
"""Return True when running inside the slash command palette."""
|
|
74
|
+
if ctx is None:
|
|
75
|
+
try:
|
|
76
|
+
ctx = click.get_current_context(silent=True)
|
|
77
|
+
except RuntimeError:
|
|
78
|
+
ctx = None
|
|
79
|
+
|
|
80
|
+
if ctx is None:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
obj = getattr(ctx, "obj", None)
|
|
84
|
+
if isinstance(obj, dict):
|
|
85
|
+
return bool(obj.get("_slash_session"))
|
|
86
|
+
|
|
87
|
+
return bool(getattr(obj, "_slash_session", False))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def command_hint(
|
|
91
|
+
cli_command: str | None,
|
|
92
|
+
slash_command: str | None = None,
|
|
93
|
+
*,
|
|
94
|
+
ctx: click.Context | None = None,
|
|
95
|
+
) -> str | None:
|
|
96
|
+
"""Return the appropriate command string for the current mode.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
cli_command: Command string without the ``aip`` prefix (e.g., ``"status"``).
|
|
100
|
+
slash_command: Slash command counterpart (e.g., ``"status"`` or ``"/status"``).
|
|
101
|
+
ctx: Optional Click context override.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The formatted command string for the active mode, or ``None`` when no
|
|
105
|
+
equivalent command exists in that mode.
|
|
106
|
+
"""
|
|
107
|
+
if in_slash_mode(ctx):
|
|
108
|
+
if not slash_command:
|
|
109
|
+
return None
|
|
110
|
+
return slash_command if slash_command.startswith("/") else f"/{slash_command}"
|
|
111
|
+
|
|
112
|
+
if not cli_command:
|
|
113
|
+
return None
|
|
114
|
+
return f"aip {cli_command}"
|
|
115
|
+
|
|
116
|
+
|
|
56
117
|
def spinner_context(
|
|
57
118
|
ctx: Any | None,
|
|
58
119
|
message: str,
|
|
@@ -62,7 +123,6 @@ def spinner_context(
|
|
|
62
123
|
spinner_style: str = "cyan",
|
|
63
124
|
) -> AbstractContextManager[Any]:
|
|
64
125
|
"""Return a context manager that renders a spinner when appropriate."""
|
|
65
|
-
|
|
66
126
|
active_console = console_override or console
|
|
67
127
|
if not _can_use_spinner(ctx, active_console):
|
|
68
128
|
return nullcontext()
|
|
@@ -76,7 +136,6 @@ def spinner_context(
|
|
|
76
136
|
|
|
77
137
|
def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
|
|
78
138
|
"""Check if spinner output is allowed in the current environment."""
|
|
79
|
-
|
|
80
139
|
if ctx is not None:
|
|
81
140
|
tty_enabled = bool(get_ctx_value(ctx, "tty", True))
|
|
82
141
|
view = (_get_view(ctx) or "rich").lower()
|
|
@@ -91,7 +150,6 @@ def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
|
|
|
91
150
|
|
|
92
151
|
def _stream_supports_tty(stream: Any) -> bool:
|
|
93
152
|
"""Return True if the provided stream can safely render a spinner."""
|
|
94
|
-
|
|
95
153
|
target = stream if hasattr(stream, "isatty") else sys.stdout
|
|
96
154
|
try:
|
|
97
155
|
return bool(target.isatty())
|
|
@@ -101,7 +159,6 @@ def _stream_supports_tty(stream: Any) -> bool:
|
|
|
101
159
|
|
|
102
160
|
def update_spinner(status_indicator: Any | None, message: str) -> None:
|
|
103
161
|
"""Update spinner text when a status indicator is active."""
|
|
104
|
-
|
|
105
162
|
if status_indicator is None:
|
|
106
163
|
return
|
|
107
164
|
|
|
@@ -113,7 +170,6 @@ def update_spinner(status_indicator: Any | None, message: str) -> None:
|
|
|
113
170
|
|
|
114
171
|
def stop_spinner(status_indicator: Any | None) -> None:
|
|
115
172
|
"""Stop an active spinner safely."""
|
|
116
|
-
|
|
117
173
|
if status_indicator is None:
|
|
118
174
|
return
|
|
119
175
|
|
|
@@ -160,9 +216,12 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
|
160
216
|
}
|
|
161
217
|
|
|
162
218
|
if not config.get("api_url") or not config.get("api_key"):
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
configure_hint = command_hint("configure", slash_command="login", ctx=ctx)
|
|
220
|
+
actions = []
|
|
221
|
+
if configure_hint:
|
|
222
|
+
actions.append(f"Run `{configure_hint}`")
|
|
223
|
+
actions.append("set AIP_* env vars")
|
|
224
|
+
raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
|
|
166
225
|
|
|
167
226
|
return Client(
|
|
168
227
|
api_url=config.get("api_url"),
|
|
@@ -171,6 +230,8 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
|
171
230
|
)
|
|
172
231
|
|
|
173
232
|
|
|
233
|
+
# ----------------------------- Secret masking ---------------------------- #
|
|
234
|
+
|
|
174
235
|
# ----------------------------- Fuzzy palette ----------------------------- #
|
|
175
236
|
|
|
176
237
|
|
|
@@ -237,8 +298,8 @@ def _build_display_parts(
|
|
|
237
298
|
|
|
238
299
|
|
|
239
300
|
def _row_display(row: dict[str, Any], columns: list[tuple]) -> str:
|
|
240
|
-
"""
|
|
241
|
-
|
|
301
|
+
"""Build a compact text label for the palette.
|
|
302
|
+
|
|
242
303
|
Prefers: name • type • framework • [id] (when available)
|
|
243
304
|
Falls back to first 2 columns + [id].
|
|
244
305
|
"""
|
|
@@ -331,8 +392,8 @@ def _perform_fuzzy_search(
|
|
|
331
392
|
def _fuzzy_pick(
|
|
332
393
|
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
333
394
|
) -> dict[str, Any] | None: # pragma: no cover - requires interactive prompt toolkit
|
|
334
|
-
"""
|
|
335
|
-
|
|
395
|
+
"""Open a minimal fuzzy palette using prompt_toolkit.
|
|
396
|
+
|
|
336
397
|
Returns the selected row (dict) or None if cancelled/missing deps.
|
|
337
398
|
"""
|
|
338
399
|
if not _check_fuzzy_pick_requirements():
|
|
@@ -401,8 +462,8 @@ def _calculate_length_bonus(search: str, target: str) -> int:
|
|
|
401
462
|
|
|
402
463
|
|
|
403
464
|
def _fuzzy_score(search: str, target: str) -> int:
|
|
404
|
-
"""
|
|
405
|
-
|
|
465
|
+
"""Calculate fuzzy match score.
|
|
466
|
+
|
|
406
467
|
Higher score = better match.
|
|
407
468
|
Returns -1 if no match possible.
|
|
408
469
|
"""
|
|
@@ -465,8 +526,15 @@ def output_result(
|
|
|
465
526
|
result: Any,
|
|
466
527
|
title: str = "Result",
|
|
467
528
|
panel_title: str | None = None,
|
|
468
|
-
success_message: str | None = None,
|
|
469
529
|
) -> None:
|
|
530
|
+
"""Output a result to the console with optional title.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
ctx: Click context
|
|
534
|
+
result: Result data to output
|
|
535
|
+
title: Optional title for the output
|
|
536
|
+
panel_title: Optional Rich panel title for structured output
|
|
537
|
+
"""
|
|
470
538
|
fmt = _get_view(ctx)
|
|
471
539
|
|
|
472
540
|
data = _coerce_result_payload(result)
|
|
@@ -485,20 +553,12 @@ def output_result(
|
|
|
485
553
|
_render_markdown_output(data)
|
|
486
554
|
return
|
|
487
555
|
|
|
488
|
-
|
|
489
|
-
console.print(Text(f"[green]✅ {success_message}[/green]"))
|
|
490
|
-
|
|
556
|
+
renderable = Pretty(data)
|
|
491
557
|
if panel_title:
|
|
492
|
-
console.print(
|
|
493
|
-
AIPPanel(
|
|
494
|
-
Pretty(data),
|
|
495
|
-
title=panel_title,
|
|
496
|
-
border_style="blue",
|
|
497
|
-
)
|
|
498
|
-
)
|
|
558
|
+
console.print(AIPPanel(renderable, title=panel_title))
|
|
499
559
|
else:
|
|
500
|
-
console.print(
|
|
501
|
-
console.print(
|
|
560
|
+
console.print(markup_text(f"[cyan]{title}:[/cyan]"))
|
|
561
|
+
console.print(renderable)
|
|
502
562
|
|
|
503
563
|
|
|
504
564
|
# ----------------------------- List rendering ---------------------------- #
|
|
@@ -575,7 +635,7 @@ def _build_table_group(
|
|
|
575
635
|
table = _create_table(columns, title)
|
|
576
636
|
for row in rows:
|
|
577
637
|
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
578
|
-
footer =
|
|
638
|
+
footer = markup_text(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
579
639
|
return Group(table, footer)
|
|
580
640
|
|
|
581
641
|
|
|
@@ -605,12 +665,11 @@ def _handle_markdown_output(
|
|
|
605
665
|
|
|
606
666
|
def _handle_empty_items(title: str) -> None:
|
|
607
667
|
"""Handle case when no items are found."""
|
|
608
|
-
console.print(
|
|
668
|
+
console.print(markup_text(f"[yellow]No {title.lower()} found.[/yellow]"))
|
|
609
669
|
|
|
610
670
|
|
|
611
671
|
def _should_use_fuzzy_picker() -> bool:
|
|
612
672
|
"""Return True when the interactive fuzzy picker can be shown."""
|
|
613
|
-
|
|
614
673
|
return console.is_terminal and os.isatty(1)
|
|
615
674
|
|
|
616
675
|
|
|
@@ -618,7 +677,6 @@ def _try_fuzzy_pick(
|
|
|
618
677
|
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
619
678
|
) -> dict[str, Any] | None:
|
|
620
679
|
"""Best-effort fuzzy selection; returns None if the picker fails."""
|
|
621
|
-
|
|
622
680
|
if not _should_use_fuzzy_picker():
|
|
623
681
|
return None
|
|
624
682
|
|
|
@@ -629,36 +687,34 @@ def _try_fuzzy_pick(
|
|
|
629
687
|
return None
|
|
630
688
|
|
|
631
689
|
|
|
632
|
-
def _resource_tip_command(title: str) -> str:
|
|
690
|
+
def _resource_tip_command(title: str) -> str | None:
|
|
633
691
|
"""Resolve the follow-up command hint for the given table title."""
|
|
634
|
-
|
|
635
692
|
title_lower = title.lower()
|
|
636
693
|
mapping = {
|
|
637
|
-
"agent": "
|
|
638
|
-
"tool": "
|
|
639
|
-
"mcp": "
|
|
640
|
-
"model": "
|
|
694
|
+
"agent": ("agents get", "agents"),
|
|
695
|
+
"tool": ("tools get", None),
|
|
696
|
+
"mcp": ("mcps get", None),
|
|
697
|
+
"model": ("models list", None), # models only ship a list command
|
|
641
698
|
}
|
|
642
|
-
for keyword,
|
|
699
|
+
for keyword, (cli_command, slash_command) in mapping.items():
|
|
643
700
|
if keyword in title_lower:
|
|
644
|
-
return
|
|
645
|
-
return "
|
|
701
|
+
return command_hint(cli_command, slash_command=slash_command)
|
|
702
|
+
return command_hint("agents get", slash_command="agents")
|
|
646
703
|
|
|
647
704
|
|
|
648
705
|
def _print_selection_tip(title: str) -> None:
|
|
649
706
|
"""Print the contextual follow-up tip after a fuzzy selection."""
|
|
650
|
-
|
|
651
707
|
tip_cmd = _resource_tip_command(title)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
708
|
+
if tip_cmd:
|
|
709
|
+
console.print(
|
|
710
|
+
markup_text(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]")
|
|
711
|
+
)
|
|
655
712
|
|
|
656
713
|
|
|
657
714
|
def _handle_fuzzy_pick_selection(
|
|
658
715
|
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
659
716
|
) -> bool:
|
|
660
717
|
"""Handle fuzzy picker selection, returns True if selection was made."""
|
|
661
|
-
|
|
662
718
|
picked = _try_fuzzy_pick(rows, columns, title)
|
|
663
719
|
if picked is None:
|
|
664
720
|
return False
|
|
@@ -773,14 +829,16 @@ def build_renderer(
|
|
|
773
829
|
"""Build renderer and capturing console for CLI commands.
|
|
774
830
|
|
|
775
831
|
Args:
|
|
776
|
-
|
|
777
|
-
save_path: Path to save output to (enables capturing)
|
|
778
|
-
theme: Color theme ("dark" or "light")
|
|
779
|
-
verbose: Whether to enable verbose mode
|
|
780
|
-
|
|
832
|
+
_ctx: Click context object for CLI operations.
|
|
833
|
+
save_path: Path to save output to (enables capturing console).
|
|
834
|
+
theme: Color theme ("dark" or "light").
|
|
835
|
+
verbose: Whether to enable verbose mode.
|
|
836
|
+
_tty_enabled: Whether TTY is available for interactive features.
|
|
837
|
+
live: Whether to enable live rendering mode (overrides verbose default).
|
|
838
|
+
snapshots: Whether to capture and store snapshots.
|
|
781
839
|
|
|
782
840
|
Returns:
|
|
783
|
-
Tuple of (renderer, capturing_console)
|
|
841
|
+
Tuple of (renderer, capturing_console) for streaming output.
|
|
784
842
|
"""
|
|
785
843
|
# Use capturing console if saving output
|
|
786
844
|
working_console = console
|
|
@@ -859,8 +917,7 @@ def _build_resource_labels(resources: list[Any]) -> tuple[list[str], dict[str, A
|
|
|
859
917
|
def _fuzzy_pick_for_resources(
|
|
860
918
|
resources: list[Any], resource_type: str, _search_term: str
|
|
861
919
|
) -> Any | None: # pragma: no cover - interactive selection helper
|
|
862
|
-
"""
|
|
863
|
-
Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
|
|
920
|
+
"""Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
|
|
864
921
|
|
|
865
922
|
Args:
|
|
866
923
|
resources: List of resource objects to choose from
|
|
@@ -1035,7 +1092,7 @@ def _handle_fallback_numeric_ambiguity(
|
|
|
1035
1092
|
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1036
1093
|
|
|
1037
1094
|
console.print(
|
|
1038
|
-
|
|
1095
|
+
markup_text(
|
|
1039
1096
|
f"[yellow]Multiple {safe_resource_type}s found matching '{safe_ref}':[/yellow]"
|
|
1040
1097
|
)
|
|
1041
1098
|
)
|