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/utils.py
CHANGED
|
@@ -2,23 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Authors:
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
|
-
import io
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
-
import platform
|
|
14
|
-
import shlex
|
|
15
|
-
import shutil
|
|
16
|
-
import subprocess
|
|
17
13
|
import sys
|
|
18
|
-
import tempfile
|
|
19
14
|
from collections.abc import Callable
|
|
20
15
|
from contextlib import AbstractContextManager, nullcontext
|
|
21
|
-
from pathlib import Path
|
|
22
16
|
from typing import TYPE_CHECKING, Any
|
|
23
17
|
|
|
24
18
|
import click
|
|
@@ -27,6 +21,8 @@ from rich.markdown import Markdown
|
|
|
27
21
|
from rich.pretty import Pretty
|
|
28
22
|
from rich.text import Text
|
|
29
23
|
|
|
24
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
25
|
+
|
|
30
26
|
# Optional interactive deps (fuzzy palette)
|
|
31
27
|
try:
|
|
32
28
|
from prompt_toolkit.completion import Completion
|
|
@@ -43,8 +39,16 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
43
39
|
|
|
44
40
|
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
45
41
|
from glaip_sdk import Client
|
|
46
|
-
from glaip_sdk.cli
|
|
47
|
-
from glaip_sdk.
|
|
42
|
+
from glaip_sdk.cli import masking, pager
|
|
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
|
|
48
52
|
from glaip_sdk.utils import is_uuid
|
|
49
53
|
from glaip_sdk.utils.rendering.renderer import (
|
|
50
54
|
CapturingConsole,
|
|
@@ -53,180 +57,61 @@ from glaip_sdk.utils.rendering.renderer import (
|
|
|
53
57
|
)
|
|
54
58
|
|
|
55
59
|
console = Console()
|
|
60
|
+
pager.console = console
|
|
56
61
|
logger = logging.getLogger("glaip_sdk.cli.utils")
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
# ----------------------------- Context helpers ---------------------------- #
|
|
60
65
|
|
|
61
66
|
|
|
62
|
-
def
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
return default
|
|
66
|
-
|
|
67
|
-
obj = getattr(ctx, "obj", None)
|
|
68
|
-
if obj is None:
|
|
69
|
-
return default
|
|
70
|
-
|
|
71
|
-
if isinstance(obj, dict):
|
|
72
|
-
return obj.get(key, default)
|
|
73
|
-
|
|
74
|
-
getter = getattr(obj, "get", None)
|
|
75
|
-
if callable(getter):
|
|
76
|
-
try:
|
|
77
|
-
return getter(key, default)
|
|
78
|
-
except TypeError:
|
|
79
|
-
return default
|
|
80
|
-
|
|
81
|
-
return getattr(obj, key, default) if hasattr(obj, key) else default
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# ----------------------------- Pager helpers ----------------------------- #
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _prepare_pager_env(
|
|
88
|
-
clear_on_exit: bool = True,
|
|
89
|
-
) -> None: # pragma: no cover - terminal UI setup
|
|
90
|
-
"""
|
|
91
|
-
Configure LESS flags for a predictable, high-quality UX:
|
|
92
|
-
-R : pass ANSI color escapes
|
|
93
|
-
-S : chop long lines (horizontal scroll with ←/→)
|
|
94
|
-
(No -F, no -X) so we open a full-screen pager and clear on exit.
|
|
95
|
-
Toggle wrapping with AIP_PAGER_WRAP=1 to drop -S.
|
|
96
|
-
Power users can override via AIP_LESS_FLAGS.
|
|
97
|
-
"""
|
|
98
|
-
os.environ.pop("LESSSECURE", None)
|
|
99
|
-
if os.getenv("LESS") is None:
|
|
100
|
-
want_wrap = os.getenv("AIP_PAGER_WRAP", "0") == "1"
|
|
101
|
-
base = "-R" if want_wrap else "-RS"
|
|
102
|
-
default_flags = base if clear_on_exit else (base + "FX")
|
|
103
|
-
os.environ["LESS"] = os.getenv("AIP_LESS_FLAGS", default_flags)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _render_ansi(
|
|
107
|
-
renderable: Any,
|
|
108
|
-
) -> str:
|
|
109
|
-
"""Render a Rich renderable to an ANSI string suitable for piping to 'less'."""
|
|
110
|
-
buf = io.StringIO()
|
|
111
|
-
tmp_console = Console(
|
|
112
|
-
file=buf,
|
|
113
|
-
force_terminal=True,
|
|
114
|
-
color_system=console.color_system or "auto",
|
|
115
|
-
width=console.size.width or 100,
|
|
116
|
-
legacy_windows=False,
|
|
117
|
-
soft_wrap=False,
|
|
118
|
-
record=False,
|
|
119
|
-
)
|
|
120
|
-
tmp_console.print(renderable)
|
|
121
|
-
return buf.getvalue()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _pager_header() -> str:
|
|
125
|
-
v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
|
|
126
|
-
if v in {"0", "false", "off"}:
|
|
127
|
-
return ""
|
|
128
|
-
return "\n".join(
|
|
129
|
-
[
|
|
130
|
-
"TABLE VIEW — ↑/↓ PgUp/PgDn, ←/→ horiz scroll (with -S), /search, n/N next/prev, h help, q quit",
|
|
131
|
-
"───────────────────────────────────────────────────────────────────────────────────────────────",
|
|
132
|
-
"",
|
|
133
|
-
]
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _should_use_pager() -> bool:
|
|
138
|
-
"""Check if we should attempt to use a system pager."""
|
|
139
|
-
if not (console.is_terminal and os.isatty(1)):
|
|
140
|
-
return False
|
|
141
|
-
if (os.getenv("TERM") or "").lower() == "dumb":
|
|
142
|
-
return False
|
|
143
|
-
return True
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _resolve_pager_command() -> tuple[list[str] | None, str | None]:
|
|
147
|
-
"""Resolve the pager command and path to use."""
|
|
148
|
-
pager_cmd = None
|
|
149
|
-
pager_env = os.getenv("PAGER")
|
|
150
|
-
if pager_env:
|
|
151
|
-
parts = shlex.split(pager_env)
|
|
152
|
-
if parts and os.path.basename(parts[0]).lower() == "less":
|
|
153
|
-
pager_cmd = parts
|
|
154
|
-
|
|
155
|
-
less_path = shutil.which("less")
|
|
156
|
-
return pager_cmd, less_path
|
|
157
|
-
|
|
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)
|
|
158
70
|
|
|
159
|
-
def _run_less_pager(
|
|
160
|
-
pager_cmd: list[str] | None, less_path: str | None, tmp_path: str
|
|
161
|
-
) -> None:
|
|
162
|
-
"""Run less pager with appropriate command and flags."""
|
|
163
|
-
if pager_cmd:
|
|
164
|
-
subprocess.run([*pager_cmd, tmp_path], check=False)
|
|
165
|
-
else:
|
|
166
|
-
flags = os.getenv("LESS", "-RS").split()
|
|
167
|
-
subprocess.run([less_path, *flags, tmp_path], check=False)
|
|
168
71
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
more_path = shutil.which("more")
|
|
173
|
-
if more_path:
|
|
174
|
-
subprocess.run([more_path, tmp_path], check=False)
|
|
175
|
-
else:
|
|
176
|
-
raise FileNotFoundError("more command not found")
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _run_pager_with_temp_file(
|
|
180
|
-
pager_runner: Callable[[str], None], ansi_text: str
|
|
181
|
-
) -> bool:
|
|
182
|
-
"""Run a pager using a temporary file containing the content."""
|
|
183
|
-
_prepare_pager_env(clear_on_exit=True)
|
|
184
|
-
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tmp:
|
|
185
|
-
tmp.write(_pager_header())
|
|
186
|
-
tmp.write(ansi_text)
|
|
187
|
-
tmp_path = tmp.name
|
|
188
|
-
try:
|
|
189
|
-
pager_runner(tmp_path)
|
|
190
|
-
return True
|
|
191
|
-
except Exception:
|
|
192
|
-
# If pager fails, return False to indicate paging was not successful
|
|
193
|
-
return False
|
|
194
|
-
finally:
|
|
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:
|
|
195
75
|
try:
|
|
196
|
-
|
|
197
|
-
except
|
|
198
|
-
|
|
76
|
+
ctx = click.get_current_context(silent=True)
|
|
77
|
+
except RuntimeError:
|
|
78
|
+
ctx = None
|
|
199
79
|
|
|
200
|
-
|
|
201
|
-
def _page_with_system_pager(
|
|
202
|
-
ansi_text: str,
|
|
203
|
-
) -> bool: # pragma: no cover - spawns real pager
|
|
204
|
-
"""Prefer 'less' with a temp file so stdin remains the TTY."""
|
|
205
|
-
if not _should_use_pager():
|
|
80
|
+
if ctx is None:
|
|
206
81
|
return False
|
|
207
82
|
|
|
208
|
-
|
|
83
|
+
obj = getattr(ctx, "obj", None)
|
|
84
|
+
if isinstance(obj, dict):
|
|
85
|
+
return bool(obj.get("_slash_session"))
|
|
209
86
|
|
|
210
|
-
|
|
211
|
-
return _run_pager_with_temp_file(
|
|
212
|
-
lambda tmp_path: _run_less_pager(pager_cmd, less_path, tmp_path), ansi_text
|
|
213
|
-
)
|
|
87
|
+
return bool(getattr(obj, "_slash_session", False))
|
|
214
88
|
|
|
215
|
-
# Windows 'more' is poor with ANSI; let Rich fallback handle it
|
|
216
|
-
if platform.system().lower().startswith("win"):
|
|
217
|
-
return False
|
|
218
89
|
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
221
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.
|
|
222
102
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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}"
|
|
227
111
|
|
|
228
|
-
|
|
229
|
-
|
|
112
|
+
if not cli_command:
|
|
113
|
+
return None
|
|
114
|
+
return f"aip {cli_command}"
|
|
230
115
|
|
|
231
116
|
|
|
232
117
|
def spinner_context(
|
|
@@ -238,7 +123,6 @@ def spinner_context(
|
|
|
238
123
|
spinner_style: str = "cyan",
|
|
239
124
|
) -> AbstractContextManager[Any]:
|
|
240
125
|
"""Return a context manager that renders a spinner when appropriate."""
|
|
241
|
-
|
|
242
126
|
active_console = console_override or console
|
|
243
127
|
if not _can_use_spinner(ctx, active_console):
|
|
244
128
|
return nullcontext()
|
|
@@ -252,7 +136,6 @@ def spinner_context(
|
|
|
252
136
|
|
|
253
137
|
def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
|
|
254
138
|
"""Check if spinner output is allowed in the current environment."""
|
|
255
|
-
|
|
256
139
|
if ctx is not None:
|
|
257
140
|
tty_enabled = bool(get_ctx_value(ctx, "tty", True))
|
|
258
141
|
view = (_get_view(ctx) or "rich").lower()
|
|
@@ -267,7 +150,6 @@ def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
|
|
|
267
150
|
|
|
268
151
|
def _stream_supports_tty(stream: Any) -> bool:
|
|
269
152
|
"""Return True if the provided stream can safely render a spinner."""
|
|
270
|
-
|
|
271
153
|
target = stream if hasattr(stream, "isatty") else sys.stdout
|
|
272
154
|
try:
|
|
273
155
|
return bool(target.isatty())
|
|
@@ -277,7 +159,6 @@ def _stream_supports_tty(stream: Any) -> bool:
|
|
|
277
159
|
|
|
278
160
|
def update_spinner(status_indicator: Any | None, message: str) -> None:
|
|
279
161
|
"""Update spinner text when a status indicator is active."""
|
|
280
|
-
|
|
281
162
|
if status_indicator is None:
|
|
282
163
|
return
|
|
283
164
|
|
|
@@ -289,7 +170,6 @@ def update_spinner(status_indicator: Any | None, message: str) -> None:
|
|
|
289
170
|
|
|
290
171
|
def stop_spinner(status_indicator: Any | None) -> None:
|
|
291
172
|
"""Stop an active spinner safely."""
|
|
292
|
-
|
|
293
173
|
if status_indicator is None:
|
|
294
174
|
return
|
|
295
175
|
|
|
@@ -336,9 +216,12 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
|
336
216
|
}
|
|
337
217
|
|
|
338
218
|
if not config.get("api_url") or not config.get("api_key"):
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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)}.")
|
|
342
225
|
|
|
343
226
|
return Client(
|
|
344
227
|
api_url=config.get("api_url"),
|
|
@@ -349,61 +232,6 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
|
349
232
|
|
|
350
233
|
# ----------------------------- Secret masking ---------------------------- #
|
|
351
234
|
|
|
352
|
-
_DEFAULT_MASK_FIELDS = {
|
|
353
|
-
"api_key",
|
|
354
|
-
"apikey",
|
|
355
|
-
"token",
|
|
356
|
-
"access_token",
|
|
357
|
-
"secret",
|
|
358
|
-
"client_secret",
|
|
359
|
-
"password",
|
|
360
|
-
"private_key",
|
|
361
|
-
"bearer",
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def _mask_value(v: Any) -> str:
|
|
366
|
-
s = str(v)
|
|
367
|
-
if len(s) <= 8:
|
|
368
|
-
return "••••"
|
|
369
|
-
return f"{s[:4]}••••••••{s[-4:]}"
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
def _mask_any(value: Any, mask_fields: set[str]) -> Any:
|
|
373
|
-
"""Recursively mask sensitive fields in mappings / lists."""
|
|
374
|
-
|
|
375
|
-
if isinstance(value, dict):
|
|
376
|
-
masked: dict[Any, Any] = {}
|
|
377
|
-
for key, raw in value.items():
|
|
378
|
-
if isinstance(key, str) and key.lower() in mask_fields and raw is not None:
|
|
379
|
-
masked[key] = _mask_value(raw)
|
|
380
|
-
else:
|
|
381
|
-
masked[key] = _mask_any(raw, mask_fields)
|
|
382
|
-
return masked
|
|
383
|
-
|
|
384
|
-
if isinstance(value, list):
|
|
385
|
-
return [_mask_any(item, mask_fields) for item in value]
|
|
386
|
-
|
|
387
|
-
return value
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any]:
|
|
391
|
-
"""Mask a single row (legacy function, now uses _mask_any)."""
|
|
392
|
-
if not mask_fields:
|
|
393
|
-
return row
|
|
394
|
-
return _mask_any(row, mask_fields)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def _resolve_mask_fields() -> set[str]:
|
|
398
|
-
if os.getenv("AIP_MASK_OFF", "0") in ("1", "true", "on", "yes"):
|
|
399
|
-
return set()
|
|
400
|
-
env_fields = (os.getenv("AIP_MASK_FIELDS") or "").strip()
|
|
401
|
-
if env_fields:
|
|
402
|
-
parts = [p.strip().lower() for p in env_fields.split(",") if p.strip()]
|
|
403
|
-
return set(parts)
|
|
404
|
-
return set(_DEFAULT_MASK_FIELDS)
|
|
405
|
-
|
|
406
|
-
|
|
407
235
|
# ----------------------------- Fuzzy palette ----------------------------- #
|
|
408
236
|
|
|
409
237
|
|
|
@@ -470,8 +298,8 @@ def _build_display_parts(
|
|
|
470
298
|
|
|
471
299
|
|
|
472
300
|
def _row_display(row: dict[str, Any], columns: list[tuple]) -> str:
|
|
473
|
-
"""
|
|
474
|
-
|
|
301
|
+
"""Build a compact text label for the palette.
|
|
302
|
+
|
|
475
303
|
Prefers: name • type • framework • [id] (when available)
|
|
476
304
|
Falls back to first 2 columns + [id].
|
|
477
305
|
"""
|
|
@@ -564,8 +392,8 @@ def _perform_fuzzy_search(
|
|
|
564
392
|
def _fuzzy_pick(
|
|
565
393
|
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
566
394
|
) -> dict[str, Any] | None: # pragma: no cover - requires interactive prompt toolkit
|
|
567
|
-
"""
|
|
568
|
-
|
|
395
|
+
"""Open a minimal fuzzy palette using prompt_toolkit.
|
|
396
|
+
|
|
569
397
|
Returns the selected row (dict) or None if cancelled/missing deps.
|
|
570
398
|
"""
|
|
571
399
|
if not _check_fuzzy_pick_requirements():
|
|
@@ -634,8 +462,8 @@ def _calculate_length_bonus(search: str, target: str) -> int:
|
|
|
634
462
|
|
|
635
463
|
|
|
636
464
|
def _fuzzy_score(search: str, target: str) -> int:
|
|
637
|
-
"""
|
|
638
|
-
|
|
465
|
+
"""Calculate fuzzy match score.
|
|
466
|
+
|
|
639
467
|
Higher score = better match.
|
|
640
468
|
Returns -1 if no match possible.
|
|
641
469
|
"""
|
|
@@ -667,16 +495,6 @@ def _coerce_result_payload(result: Any) -> Any:
|
|
|
667
495
|
return result
|
|
668
496
|
|
|
669
497
|
|
|
670
|
-
def _apply_mask_if_configured(payload: Any) -> Any:
|
|
671
|
-
mask_fields = _resolve_mask_fields()
|
|
672
|
-
if not mask_fields:
|
|
673
|
-
return payload
|
|
674
|
-
try:
|
|
675
|
-
return _mask_any(payload, mask_fields)
|
|
676
|
-
except Exception:
|
|
677
|
-
return payload
|
|
678
|
-
|
|
679
|
-
|
|
680
498
|
def _ensure_displayable(payload: Any) -> Any:
|
|
681
499
|
if isinstance(payload, dict | list | str | int | float | bool) or payload is None:
|
|
682
500
|
return payload
|
|
@@ -708,12 +526,19 @@ def output_result(
|
|
|
708
526
|
result: Any,
|
|
709
527
|
title: str = "Result",
|
|
710
528
|
panel_title: str | None = None,
|
|
711
|
-
success_message: str | None = None,
|
|
712
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
|
+
"""
|
|
713
538
|
fmt = _get_view(ctx)
|
|
714
539
|
|
|
715
540
|
data = _coerce_result_payload(result)
|
|
716
|
-
data =
|
|
541
|
+
data = masking.mask_payload(data)
|
|
717
542
|
data = _ensure_displayable(data)
|
|
718
543
|
|
|
719
544
|
if fmt == "json":
|
|
@@ -728,20 +553,12 @@ def output_result(
|
|
|
728
553
|
_render_markdown_output(data)
|
|
729
554
|
return
|
|
730
555
|
|
|
731
|
-
|
|
732
|
-
console.print(Text(f"[green]✅ {success_message}[/green]"))
|
|
733
|
-
|
|
556
|
+
renderable = Pretty(data)
|
|
734
557
|
if panel_title:
|
|
735
|
-
console.print(
|
|
736
|
-
AIPPanel(
|
|
737
|
-
Pretty(data),
|
|
738
|
-
title=panel_title,
|
|
739
|
-
border_style="blue",
|
|
740
|
-
)
|
|
741
|
-
)
|
|
558
|
+
console.print(AIPPanel(renderable, title=panel_title))
|
|
742
559
|
else:
|
|
743
560
|
console.print(Text(f"[cyan]{title}:[/cyan]"))
|
|
744
|
-
console.print(
|
|
561
|
+
console.print(renderable)
|
|
745
562
|
|
|
746
563
|
|
|
747
564
|
# ----------------------------- List rendering ---------------------------- #
|
|
@@ -771,16 +588,6 @@ def _normalise_rows(
|
|
|
771
588
|
return []
|
|
772
589
|
|
|
773
590
|
|
|
774
|
-
def _mask_rows_if_configured(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
775
|
-
mask_fields = _resolve_mask_fields()
|
|
776
|
-
if not mask_fields:
|
|
777
|
-
return rows
|
|
778
|
-
try:
|
|
779
|
-
return [_maybe_mask_row(row, mask_fields) for row in rows]
|
|
780
|
-
except Exception:
|
|
781
|
-
return rows
|
|
782
|
-
|
|
783
|
-
|
|
784
591
|
def _render_plain_list(
|
|
785
592
|
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
786
593
|
) -> None:
|
|
@@ -832,20 +639,6 @@ def _build_table_group(
|
|
|
832
639
|
return Group(table, footer)
|
|
833
640
|
|
|
834
641
|
|
|
835
|
-
def _should_page_output(row_count: int, is_tty: bool) -> bool:
|
|
836
|
-
pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
|
|
837
|
-
if pager_env in ("0", "off", "false"):
|
|
838
|
-
return False
|
|
839
|
-
if pager_env in ("1", "on", "true"):
|
|
840
|
-
return is_tty
|
|
841
|
-
try:
|
|
842
|
-
term_h = console.size.height or 24
|
|
843
|
-
approx_lines = 5 + row_count
|
|
844
|
-
return is_tty and (approx_lines >= term_h * 0.5)
|
|
845
|
-
except Exception:
|
|
846
|
-
return is_tty
|
|
847
|
-
|
|
848
|
-
|
|
849
642
|
def _handle_json_output(items: list[Any], rows: list[dict[str, Any]]) -> None:
|
|
850
643
|
"""Handle JSON output format."""
|
|
851
644
|
data = (
|
|
@@ -877,7 +670,6 @@ def _handle_empty_items(title: str) -> None:
|
|
|
877
670
|
|
|
878
671
|
def _should_use_fuzzy_picker() -> bool:
|
|
879
672
|
"""Return True when the interactive fuzzy picker can be shown."""
|
|
880
|
-
|
|
881
673
|
return console.is_terminal and os.isatty(1)
|
|
882
674
|
|
|
883
675
|
|
|
@@ -885,7 +677,6 @@ def _try_fuzzy_pick(
|
|
|
885
677
|
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
886
678
|
) -> dict[str, Any] | None:
|
|
887
679
|
"""Best-effort fuzzy selection; returns None if the picker fails."""
|
|
888
|
-
|
|
889
680
|
if not _should_use_fuzzy_picker():
|
|
890
681
|
return None
|
|
891
682
|
|
|
@@ -896,36 +687,34 @@ def _try_fuzzy_pick(
|
|
|
896
687
|
return None
|
|
897
688
|
|
|
898
689
|
|
|
899
|
-
def _resource_tip_command(title: str) -> str:
|
|
690
|
+
def _resource_tip_command(title: str) -> str | None:
|
|
900
691
|
"""Resolve the follow-up command hint for the given table title."""
|
|
901
|
-
|
|
902
692
|
title_lower = title.lower()
|
|
903
693
|
mapping = {
|
|
904
|
-
"agent": "
|
|
905
|
-
"tool": "
|
|
906
|
-
"mcp": "
|
|
907
|
-
"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
|
|
908
698
|
}
|
|
909
|
-
for keyword,
|
|
699
|
+
for keyword, (cli_command, slash_command) in mapping.items():
|
|
910
700
|
if keyword in title_lower:
|
|
911
|
-
return
|
|
912
|
-
return "
|
|
701
|
+
return command_hint(cli_command, slash_command=slash_command)
|
|
702
|
+
return command_hint("agents get", slash_command="agents")
|
|
913
703
|
|
|
914
704
|
|
|
915
705
|
def _print_selection_tip(title: str) -> None:
|
|
916
706
|
"""Print the contextual follow-up tip after a fuzzy selection."""
|
|
917
|
-
|
|
918
707
|
tip_cmd = _resource_tip_command(title)
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
708
|
+
if tip_cmd:
|
|
709
|
+
console.print(
|
|
710
|
+
Text.from_markup(f"\n[dim]Tip: use `{tip_cmd} <ID>` for details[/dim]")
|
|
711
|
+
)
|
|
922
712
|
|
|
923
713
|
|
|
924
714
|
def _handle_fuzzy_pick_selection(
|
|
925
715
|
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
926
716
|
) -> bool:
|
|
927
717
|
"""Handle fuzzy picker selection, returns True if selection was made."""
|
|
928
|
-
|
|
929
718
|
picked = _try_fuzzy_pick(rows, columns, title)
|
|
930
719
|
if picked is None:
|
|
931
720
|
return False
|
|
@@ -947,14 +736,14 @@ def _handle_table_output(
|
|
|
947
736
|
"""Handle table output with paging."""
|
|
948
737
|
content = _build_table_group(rows, columns, title)
|
|
949
738
|
should_page = (
|
|
950
|
-
_should_page_output(len(rows), console.is_terminal and os.isatty(1))
|
|
739
|
+
pager._should_page_output(len(rows), console.is_terminal and os.isatty(1))
|
|
951
740
|
if use_pager is None
|
|
952
741
|
else use_pager
|
|
953
742
|
)
|
|
954
743
|
|
|
955
744
|
if should_page:
|
|
956
|
-
ansi = _render_ansi(content)
|
|
957
|
-
if not _page_with_system_pager(ansi):
|
|
745
|
+
ansi = pager._render_ansi(content)
|
|
746
|
+
if not pager._page_with_system_pager(ansi):
|
|
958
747
|
with console.pager(styles=True):
|
|
959
748
|
console.print(content)
|
|
960
749
|
else:
|
|
@@ -974,7 +763,7 @@ def output_list(
|
|
|
974
763
|
"""Display a list with optional fuzzy palette for quick selection."""
|
|
975
764
|
fmt = _get_view(ctx)
|
|
976
765
|
rows = _normalise_rows(items, transform_func)
|
|
977
|
-
rows =
|
|
766
|
+
rows = masking.mask_rows(rows)
|
|
978
767
|
|
|
979
768
|
if fmt == "json":
|
|
980
769
|
_handle_json_output(items, rows)
|
|
@@ -1004,50 +793,6 @@ def output_list(
|
|
|
1004
793
|
_handle_table_output(rows, columns, title, use_pager=use_pager)
|
|
1005
794
|
|
|
1006
795
|
|
|
1007
|
-
# ------------------------- Output flags decorator ------------------------ #
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
def _set_view(ctx: Any, _param: Any, value: str) -> None:
|
|
1011
|
-
if not value:
|
|
1012
|
-
return
|
|
1013
|
-
ctx.ensure_object(dict)
|
|
1014
|
-
ctx.obj["view"] = value
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
def _set_json(ctx: Any, _param: Any, value: bool) -> None:
|
|
1018
|
-
if not value:
|
|
1019
|
-
return
|
|
1020
|
-
ctx.ensure_object(dict)
|
|
1021
|
-
ctx.obj["view"] = "json"
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
def output_flags() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
1025
|
-
"""Decorator to allow output format flags on any subcommand."""
|
|
1026
|
-
|
|
1027
|
-
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
1028
|
-
f = click.option(
|
|
1029
|
-
"--json",
|
|
1030
|
-
"json_mode",
|
|
1031
|
-
is_flag=True,
|
|
1032
|
-
expose_value=False,
|
|
1033
|
-
help="Shortcut for --view json",
|
|
1034
|
-
callback=_set_json,
|
|
1035
|
-
)(f)
|
|
1036
|
-
f = click.option(
|
|
1037
|
-
"-o",
|
|
1038
|
-
"--output",
|
|
1039
|
-
"--view",
|
|
1040
|
-
"view_opt",
|
|
1041
|
-
type=click.Choice(["rich", "plain", "json", "md"]),
|
|
1042
|
-
expose_value=False,
|
|
1043
|
-
help="Output format",
|
|
1044
|
-
callback=_set_view,
|
|
1045
|
-
)(f)
|
|
1046
|
-
return f
|
|
1047
|
-
|
|
1048
|
-
return decorator
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
796
|
# ------------------------- Ambiguity handling --------------------------- #
|
|
1052
797
|
|
|
1053
798
|
|
|
@@ -1084,14 +829,16 @@ def build_renderer(
|
|
|
1084
829
|
"""Build renderer and capturing console for CLI commands.
|
|
1085
830
|
|
|
1086
831
|
Args:
|
|
1087
|
-
|
|
1088
|
-
save_path: Path to save output to (enables capturing)
|
|
1089
|
-
theme: Color theme ("dark" or "light")
|
|
1090
|
-
verbose: Whether to enable verbose mode
|
|
1091
|
-
|
|
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.
|
|
1092
839
|
|
|
1093
840
|
Returns:
|
|
1094
|
-
Tuple of (renderer, capturing_console)
|
|
841
|
+
Tuple of (renderer, capturing_console) for streaming output.
|
|
1095
842
|
"""
|
|
1096
843
|
# Use capturing console if saving output
|
|
1097
844
|
working_console = console
|
|
@@ -1170,8 +917,7 @@ def _build_resource_labels(resources: list[Any]) -> tuple[list[str], dict[str, A
|
|
|
1170
917
|
def _fuzzy_pick_for_resources(
|
|
1171
918
|
resources: list[Any], resource_type: str, _search_term: str
|
|
1172
919
|
) -> Any | None: # pragma: no cover - interactive selection helper
|
|
1173
|
-
"""
|
|
1174
|
-
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.
|
|
1175
921
|
|
|
1176
922
|
Args:
|
|
1177
923
|
resources: List of resource objects to choose from
|
|
@@ -1400,19 +1146,3 @@ def handle_ambiguous_resource(
|
|
|
1400
1146
|
else:
|
|
1401
1147
|
# Re-raise cancellation exceptions
|
|
1402
1148
|
raise
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
def detect_export_format(file_path: str | Path) -> str:
|
|
1406
|
-
"""Detect export format from file extension.
|
|
1407
|
-
|
|
1408
|
-
Args:
|
|
1409
|
-
file_path: Path to the export file
|
|
1410
|
-
|
|
1411
|
-
Returns:
|
|
1412
|
-
"yaml" if file extension is .yaml or .yml, "json" otherwise
|
|
1413
|
-
"""
|
|
1414
|
-
path = Path(file_path)
|
|
1415
|
-
if path.suffix.lower() in [".yaml", ".yml"]:
|
|
1416
|
-
return "yaml"
|
|
1417
|
-
else:
|
|
1418
|
-
return "json"
|