glaip-sdk 0.0.14__py3-none-any.whl → 0.0.16__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/branding.py +27 -1
- glaip_sdk/cli/commands/agents.py +27 -20
- glaip_sdk/cli/commands/configure.py +39 -50
- glaip_sdk/cli/commands/mcps.py +2 -6
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +1 -3
- glaip_sdk/cli/config.py +42 -0
- glaip_sdk/cli/context.py +142 -0
- glaip_sdk/cli/display.py +92 -26
- glaip_sdk/cli/main.py +141 -124
- glaip_sdk/cli/masking.py +148 -0
- glaip_sdk/cli/mcp_validators.py +2 -2
- glaip_sdk/cli/pager.py +272 -0
- glaip_sdk/cli/parsers/json_input.py +2 -2
- glaip_sdk/cli/resolution.py +12 -10
- 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 +99 -369
- glaip_sdk/client/_agent_payloads.py +504 -0
- glaip_sdk/client/agents.py +194 -551
- glaip_sdk/client/base.py +92 -20
- glaip_sdk/client/main.py +6 -0
- glaip_sdk/client/run_rendering.py +275 -0
- glaip_sdk/config/constants.py +3 -0
- 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.14.dist-info → glaip_sdk-0.0.16.dist-info}/METADATA +1 -1
- glaip_sdk-0.0.16.dist-info/RECORD +72 -0
- glaip_sdk-0.0.14.dist-info/RECORD +0 -64
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.16.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.16.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/pager.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Pager-related helpers for CLI output.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shlex
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"console",
|
|
23
|
+
"_prepare_pager_env",
|
|
24
|
+
"_render_ansi",
|
|
25
|
+
"_pager_header",
|
|
26
|
+
"_should_use_pager",
|
|
27
|
+
"_resolve_pager_command",
|
|
28
|
+
"_run_less_pager",
|
|
29
|
+
"_run_more_pager",
|
|
30
|
+
"_run_pager_with_temp_file",
|
|
31
|
+
"_page_with_system_pager",
|
|
32
|
+
"_should_page_output",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
console: Console | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_console() -> Console:
|
|
39
|
+
"""Return the active console instance.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Console: The active Rich console instance
|
|
43
|
+
"""
|
|
44
|
+
global console
|
|
45
|
+
try:
|
|
46
|
+
from glaip_sdk.cli import utils as cli_utils
|
|
47
|
+
except Exception: # pragma: no cover - fallback during import cycles
|
|
48
|
+
cli_utils = None
|
|
49
|
+
|
|
50
|
+
current_console = getattr(cli_utils, "console", None) if cli_utils else None
|
|
51
|
+
if current_console is not None and current_console is not console:
|
|
52
|
+
console = current_console
|
|
53
|
+
|
|
54
|
+
if console is None:
|
|
55
|
+
console = Console()
|
|
56
|
+
return console
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _prepare_pager_env(clear_on_exit: bool = True) -> None:
|
|
60
|
+
"""Configure LESS flags for a predictable, high-quality UX.
|
|
61
|
+
|
|
62
|
+
Sets sensible defaults for the system pager:
|
|
63
|
+
-R : pass ANSI color escapes
|
|
64
|
+
-S : chop long lines (horizontal scroll with ←/→)
|
|
65
|
+
(No -F, no -X) so we open a full-screen pager and clear on exit.
|
|
66
|
+
Toggle wrapping with AIP_PAGER_WRAP=1 to drop -S.
|
|
67
|
+
Power users can override via AIP_LESS_FLAGS.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
clear_on_exit: Whether to clear the pager on exit (default: True)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
None
|
|
74
|
+
"""
|
|
75
|
+
os.environ.pop("LESSSECURE", None)
|
|
76
|
+
if os.getenv("LESS") is None:
|
|
77
|
+
want_wrap = os.getenv("AIP_PAGER_WRAP", "0") == "1"
|
|
78
|
+
base = "-R" if want_wrap else "-RS"
|
|
79
|
+
default_flags = base if clear_on_exit else (base + "FX")
|
|
80
|
+
os.environ["LESS"] = os.getenv("AIP_LESS_FLAGS", default_flags)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _render_ansi(renderable: Any) -> str:
|
|
84
|
+
"""Render a Rich renderable to an ANSI string suitable for piping to 'less'.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
renderable: Any Rich-compatible renderable object
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
str: ANSI string representation of the renderable
|
|
91
|
+
"""
|
|
92
|
+
active_console = _get_console()
|
|
93
|
+
buf = io.StringIO()
|
|
94
|
+
tmp_console = Console(
|
|
95
|
+
file=buf,
|
|
96
|
+
force_terminal=True,
|
|
97
|
+
color_system=active_console.color_system or "auto",
|
|
98
|
+
width=active_console.size.width or 100,
|
|
99
|
+
legacy_windows=False,
|
|
100
|
+
soft_wrap=False,
|
|
101
|
+
record=False,
|
|
102
|
+
)
|
|
103
|
+
tmp_console.print(renderable)
|
|
104
|
+
return buf.getvalue()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _pager_header() -> str:
|
|
108
|
+
"""Generate pager header with navigation instructions.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
str: Header text containing navigation help, or empty string if disabled
|
|
112
|
+
"""
|
|
113
|
+
v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
|
|
114
|
+
if v in {"0", "false", "off"}:
|
|
115
|
+
return ""
|
|
116
|
+
return "\n".join(
|
|
117
|
+
[
|
|
118
|
+
"TABLE VIEW — ↑/↓ PgUp/PgDn, ←/→ horiz scroll (with -S), /search, n/N next/prev, h help, q quit",
|
|
119
|
+
"───────────────────────────────────────────────────────────────────────────────────────────────",
|
|
120
|
+
"",
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _should_use_pager() -> bool:
|
|
126
|
+
"""Check if we should attempt to use a system pager.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
bool: True if we should use a pager, False otherwise
|
|
130
|
+
"""
|
|
131
|
+
active_console = _get_console()
|
|
132
|
+
if not (active_console.is_terminal and os.isatty(1)):
|
|
133
|
+
return False
|
|
134
|
+
if (os.getenv("TERM") or "").lower() == "dumb":
|
|
135
|
+
return False
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _resolve_pager_command() -> tuple[list[str] | None, str | None]:
|
|
140
|
+
"""Resolve the pager command and path to use.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
tuple[list[str] | None, str | None]: A tuple containing:
|
|
144
|
+
- list[str] | None: The pager command parts if PAGER is set to less, None otherwise
|
|
145
|
+
- str | None: The path to the less executable if found, None otherwise
|
|
146
|
+
"""
|
|
147
|
+
pager_cmd = None
|
|
148
|
+
pager_env = os.getenv("PAGER")
|
|
149
|
+
if pager_env:
|
|
150
|
+
parts = shlex.split(pager_env)
|
|
151
|
+
if parts and os.path.basename(parts[0]).lower() == "less":
|
|
152
|
+
pager_cmd = parts
|
|
153
|
+
|
|
154
|
+
less_path = shutil.which("less")
|
|
155
|
+
return pager_cmd, less_path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _run_less_pager(
|
|
159
|
+
pager_cmd: list[str] | None, less_path: str | None, tmp_path: str
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Run less pager with appropriate command and flags.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
pager_cmd: Custom pager command parts if PAGER is set to less, None otherwise
|
|
165
|
+
less_path: Path to the less executable, None if not found
|
|
166
|
+
tmp_path: Path to temporary file containing content to display
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
None
|
|
170
|
+
"""
|
|
171
|
+
if pager_cmd:
|
|
172
|
+
subprocess.run([*pager_cmd, tmp_path], check=False)
|
|
173
|
+
else:
|
|
174
|
+
flags = os.getenv("LESS", "-RS").split()
|
|
175
|
+
subprocess.run([less_path, *flags, tmp_path], check=False)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _run_more_pager(tmp_path: str) -> None:
|
|
179
|
+
"""Run more pager as fallback.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tmp_path: Path to temporary file containing content to display
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
None
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
FileNotFoundError: If more command is not found
|
|
189
|
+
"""
|
|
190
|
+
more_path = shutil.which("more")
|
|
191
|
+
if more_path:
|
|
192
|
+
subprocess.run([more_path, tmp_path], check=False)
|
|
193
|
+
else:
|
|
194
|
+
raise FileNotFoundError("more command not found")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _run_pager_with_temp_file(
|
|
198
|
+
pager_runner: Callable[[str], None], ansi_text: str
|
|
199
|
+
) -> bool:
|
|
200
|
+
"""Run a pager using a temporary file containing the content.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
pager_runner: Function that takes a temp file path and runs the pager
|
|
204
|
+
ansi_text: ANSI-formatted text content to display
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
bool: True if pager executed successfully, False if there was an exception
|
|
208
|
+
"""
|
|
209
|
+
_prepare_pager_env(clear_on_exit=True)
|
|
210
|
+
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tmp:
|
|
211
|
+
tmp.write(_pager_header())
|
|
212
|
+
tmp.write(ansi_text)
|
|
213
|
+
tmp_path = tmp.name
|
|
214
|
+
try:
|
|
215
|
+
pager_runner(tmp_path)
|
|
216
|
+
return True
|
|
217
|
+
except Exception:
|
|
218
|
+
return False
|
|
219
|
+
finally:
|
|
220
|
+
try:
|
|
221
|
+
os.unlink(tmp_path)
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _page_with_system_pager(ansi_text: str) -> bool:
|
|
227
|
+
"""Prefer 'less' with a temp file so stdin remains the TTY.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
ansi_text: ANSI-formatted text content to display in the pager
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
bool: True if pager was executed successfully, False otherwise
|
|
234
|
+
"""
|
|
235
|
+
if not _should_use_pager():
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
pager_cmd, less_path = _resolve_pager_command()
|
|
239
|
+
|
|
240
|
+
if pager_cmd or less_path:
|
|
241
|
+
return _run_pager_with_temp_file(
|
|
242
|
+
lambda tmp_path: _run_less_pager(pager_cmd, less_path, tmp_path), ansi_text
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if platform.system().lower().startswith("win"):
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
return _run_pager_with_temp_file(_run_more_pager, ansi_text)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _should_page_output(row_count: int, is_tty: bool) -> bool:
|
|
252
|
+
"""Determine if output should be paginated based on content size and terminal.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
row_count: Number of rows in the content to display
|
|
256
|
+
is_tty: Whether the output is going to a terminal
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
bool: True if output should be paginated, False otherwise
|
|
260
|
+
"""
|
|
261
|
+
active_console = _get_console()
|
|
262
|
+
pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
|
|
263
|
+
if pager_env in ("0", "off", "false"):
|
|
264
|
+
return False
|
|
265
|
+
if pager_env in ("1", "on", "true"):
|
|
266
|
+
return is_tty
|
|
267
|
+
try:
|
|
268
|
+
term_h = active_console.size.height or 24
|
|
269
|
+
approx_lines = 5 + row_count
|
|
270
|
+
return is_tty and (approx_lines >= term_h * 0.5)
|
|
271
|
+
except Exception:
|
|
272
|
+
return is_tty
|
|
@@ -17,7 +17,7 @@ import click
|
|
|
17
17
|
def _format_file_error(
|
|
18
18
|
prefix: str, file_path_str: str, resolved_path: Path, *, detail: str | None = None
|
|
19
19
|
) -> str:
|
|
20
|
-
"""Format a file-related error message with path context.
|
|
20
|
+
r"""Format a file-related error message with path context.
|
|
21
21
|
|
|
22
22
|
Args:
|
|
23
23
|
prefix: Main error message
|
|
@@ -31,7 +31,7 @@ def _format_file_error(
|
|
|
31
31
|
Examples:
|
|
32
32
|
>>> from pathlib import Path
|
|
33
33
|
>>> _format_file_error("File not found", "config.json", Path("/abs/config.json"))
|
|
34
|
-
'File not found: config.json
|
|
34
|
+
'File not found: config.json\nResolved path: /abs/config.json'
|
|
35
35
|
"""
|
|
36
36
|
parts = [f"{prefix}: {file_path_str}", f"Resolved path: {resolved_path}"]
|
|
37
37
|
if detail:
|
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 = (
|
|
@@ -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
|
|