glaip-sdk 0.0.4__py3-none-any.whl → 0.0.5b1__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 -5
- glaip_sdk/branding.py +18 -17
- glaip_sdk/cli/__init__.py +1 -1
- glaip_sdk/cli/agent_config.py +82 -0
- glaip_sdk/cli/commands/__init__.py +3 -3
- glaip_sdk/cli/commands/agents.py +570 -673
- glaip_sdk/cli/commands/configure.py +2 -2
- glaip_sdk/cli/commands/mcps.py +148 -143
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +250 -179
- glaip_sdk/cli/display.py +244 -0
- glaip_sdk/cli/io.py +106 -0
- glaip_sdk/cli/main.py +14 -18
- glaip_sdk/cli/resolution.py +59 -0
- glaip_sdk/cli/utils.py +305 -264
- glaip_sdk/cli/validators.py +235 -0
- glaip_sdk/client/__init__.py +3 -224
- glaip_sdk/client/agents.py +631 -191
- glaip_sdk/client/base.py +66 -4
- glaip_sdk/client/main.py +226 -0
- glaip_sdk/client/mcps.py +143 -18
- glaip_sdk/client/tools.py +146 -11
- glaip_sdk/config/constants.py +10 -1
- glaip_sdk/models.py +42 -2
- glaip_sdk/rich_components.py +29 -0
- glaip_sdk/utils/__init__.py +18 -171
- glaip_sdk/utils/agent_config.py +181 -0
- glaip_sdk/utils/client_utils.py +159 -79
- glaip_sdk/utils/display.py +100 -0
- glaip_sdk/utils/general.py +94 -0
- glaip_sdk/utils/import_export.py +140 -0
- glaip_sdk/utils/rendering/formatting.py +6 -1
- glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
- glaip_sdk/utils/rendering/renderer/base.py +340 -247
- glaip_sdk/utils/rendering/renderer/debug.py +3 -2
- glaip_sdk/utils/rendering/renderer/panels.py +11 -10
- glaip_sdk/utils/rendering/steps.py +1 -1
- glaip_sdk/utils/resource_refs.py +192 -0
- glaip_sdk/utils/rich_utils.py +29 -0
- glaip_sdk/utils/serialization.py +285 -0
- glaip_sdk/utils/validation.py +273 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/METADATA +22 -21
- glaip_sdk-0.0.5b1.dist-info/RECORD +55 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/WHEEL +1 -1
- glaip_sdk-0.0.5b1.dist-info/entry_points.txt +3 -0
- glaip_sdk/cli/commands/init.py +0 -93
- glaip_sdk-0.0.4.dist-info/RECORD +0 -41
- glaip_sdk-0.0.4.dist-info/entry_points.txt +0 -2
glaip_sdk/cli/utils.py
CHANGED
|
@@ -17,12 +17,9 @@ import tempfile
|
|
|
17
17
|
from typing import TYPE_CHECKING, Any
|
|
18
18
|
|
|
19
19
|
import click
|
|
20
|
-
from rich import box
|
|
21
20
|
from rich.console import Console, Group
|
|
22
21
|
from rich.markdown import Markdown
|
|
23
|
-
from rich.panel import Panel
|
|
24
22
|
from rich.pretty import Pretty
|
|
25
|
-
from rich.table import Table
|
|
26
23
|
from rich.text import Text
|
|
27
24
|
|
|
28
25
|
# Optional interactive deps (fuzzy palette)
|
|
@@ -44,6 +41,7 @@ if TYPE_CHECKING:
|
|
|
44
41
|
|
|
45
42
|
from glaip_sdk import Client
|
|
46
43
|
from glaip_sdk.cli.commands.configure import load_config
|
|
44
|
+
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
47
45
|
from glaip_sdk.utils import is_uuid
|
|
48
46
|
from glaip_sdk.utils.rendering.renderer import (
|
|
49
47
|
CapturingConsole,
|
|
@@ -57,7 +55,9 @@ console = Console()
|
|
|
57
55
|
# ----------------------------- Pager helpers ----------------------------- #
|
|
58
56
|
|
|
59
57
|
|
|
60
|
-
def _prepare_pager_env(
|
|
58
|
+
def _prepare_pager_env(
|
|
59
|
+
clear_on_exit: bool = True,
|
|
60
|
+
) -> None: # pragma: no cover - terminal UI setup
|
|
61
61
|
"""
|
|
62
62
|
Configure LESS flags for a predictable, high-quality UX:
|
|
63
63
|
-R : pass ANSI color escapes
|
|
@@ -74,7 +74,9 @@ def _prepare_pager_env(clear_on_exit: bool = True) -> None:
|
|
|
74
74
|
os.environ["LESS"] = os.getenv("AIP_LESS_FLAGS", default_flags)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def _render_ansi(
|
|
77
|
+
def _render_ansi(
|
|
78
|
+
renderable,
|
|
79
|
+
) -> str: # pragma: no cover - rendering requires real terminal
|
|
78
80
|
"""Render a Rich renderable to an ANSI string suitable for piping to 'less'."""
|
|
79
81
|
buf = io.StringIO()
|
|
80
82
|
tmp_console = Console(
|
|
@@ -90,7 +92,7 @@ def _render_ansi(renderable) -> str:
|
|
|
90
92
|
return buf.getvalue()
|
|
91
93
|
|
|
92
94
|
|
|
93
|
-
def _pager_header() -> str:
|
|
95
|
+
def _pager_header() -> str: # pragma: no cover - terminal UI helper
|
|
94
96
|
v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
|
|
95
97
|
if v in {"0", "false", "off"}:
|
|
96
98
|
return ""
|
|
@@ -103,7 +105,9 @@ def _pager_header() -> str:
|
|
|
103
105
|
)
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
def _page_with_system_pager(
|
|
108
|
+
def _page_with_system_pager(
|
|
109
|
+
ansi_text: str,
|
|
110
|
+
) -> bool: # pragma: no cover - spawns real pager
|
|
107
111
|
"""Prefer 'less' with a temp file so stdin remains the TTY."""
|
|
108
112
|
if not (console.is_terminal and os.isatty(1)):
|
|
109
113
|
return False
|
|
@@ -161,7 +165,7 @@ def _page_with_system_pager(ansi_text: str) -> bool:
|
|
|
161
165
|
|
|
162
166
|
|
|
163
167
|
def _get_view(ctx) -> str:
|
|
164
|
-
obj = ctx.obj or {}
|
|
168
|
+
obj = (ctx.obj or {}) if ctx is not None else {}
|
|
165
169
|
return obj.get("view") or obj.get("format") or "rich"
|
|
166
170
|
|
|
167
171
|
|
|
@@ -173,13 +177,20 @@ def get_client(ctx) -> Client:
|
|
|
173
177
|
file_config = load_config() or {}
|
|
174
178
|
context_config = (ctx.obj or {}) if ctx else {}
|
|
175
179
|
|
|
180
|
+
raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
|
|
181
|
+
try:
|
|
182
|
+
timeout_value = float(raw_timeout)
|
|
183
|
+
except ValueError:
|
|
184
|
+
timeout_value = None
|
|
185
|
+
|
|
176
186
|
env_config = {
|
|
177
187
|
"api_url": os.getenv("AIP_API_URL"),
|
|
178
188
|
"api_key": os.getenv("AIP_API_KEY"),
|
|
179
|
-
"timeout":
|
|
189
|
+
"timeout": timeout_value if timeout_value else None,
|
|
180
190
|
}
|
|
181
191
|
env_config = {k: v for k, v in env_config.items() if v not in (None, "", 0)}
|
|
182
192
|
|
|
193
|
+
# Merge config sources: context > env > file
|
|
183
194
|
config = {
|
|
184
195
|
**file_config,
|
|
185
196
|
**env_config,
|
|
@@ -220,28 +231,22 @@ def _mask_value(v: Any) -> str:
|
|
|
220
231
|
return f"{s[:4]}••••••••{s[-4:]}"
|
|
221
232
|
|
|
222
233
|
|
|
223
|
-
def _mask_any(
|
|
224
|
-
"""Recursively mask sensitive fields in
|
|
225
|
-
|
|
226
|
-
Args:
|
|
227
|
-
x: The data to mask (dict, list, or primitive)
|
|
228
|
-
mask_fields: Set of field names to mask (case-insensitive)
|
|
234
|
+
def _mask_any(value: Any, mask_fields: set[str]) -> Any:
|
|
235
|
+
"""Recursively mask sensitive fields in mappings / lists."""
|
|
229
236
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for k, v in x.items():
|
|
236
|
-
if k.lower() in mask_fields and v is not None:
|
|
237
|
-
out[k] = _mask_value(v)
|
|
237
|
+
if isinstance(value, dict):
|
|
238
|
+
masked: dict[Any, Any] = {}
|
|
239
|
+
for key, raw in value.items():
|
|
240
|
+
if isinstance(key, str) and key.lower() in mask_fields and raw is not None:
|
|
241
|
+
masked[key] = _mask_value(raw)
|
|
238
242
|
else:
|
|
239
|
-
|
|
240
|
-
return
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
masked[key] = _mask_any(raw, mask_fields)
|
|
244
|
+
return masked
|
|
245
|
+
|
|
246
|
+
if isinstance(value, list):
|
|
247
|
+
return [_mask_any(item, mask_fields) for item in value]
|
|
248
|
+
|
|
249
|
+
return value
|
|
245
250
|
|
|
246
251
|
|
|
247
252
|
def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any]:
|
|
@@ -297,19 +302,18 @@ def _row_display(row: dict[str, Any], columns: list[tuple]) -> str:
|
|
|
297
302
|
return " • ".join(parts) if parts else (_id or "(row)")
|
|
298
303
|
|
|
299
304
|
|
|
300
|
-
def
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"""
|
|
304
|
-
Open a minimal fuzzy palette using prompt_toolkit.
|
|
305
|
-
Returns the selected row (dict) or None if cancelled/missing deps.
|
|
306
|
-
"""
|
|
307
|
-
if not (_HAS_PTK and console.is_terminal and os.isatty(1)):
|
|
308
|
-
return None
|
|
305
|
+
def _check_fuzzy_pick_requirements() -> bool:
|
|
306
|
+
"""Check if fuzzy picking requirements are met."""
|
|
307
|
+
return _HAS_PTK and console.is_terminal and os.isatty(1)
|
|
309
308
|
|
|
310
|
-
|
|
309
|
+
|
|
310
|
+
def _build_unique_labels(
|
|
311
|
+
rows: list[dict[str, Any]], columns: list[tuple]
|
|
312
|
+
) -> tuple[list[str], dict[str, dict[str, Any]]]:
|
|
313
|
+
"""Build unique display labels and reverse mapping."""
|
|
311
314
|
labels = []
|
|
312
315
|
by_label: dict[str, dict[str, Any]] = {}
|
|
316
|
+
|
|
313
317
|
for r in rows:
|
|
314
318
|
label = _row_display(r, columns)
|
|
315
319
|
# Ensure uniqueness: if duplicate, suffix with …#n
|
|
@@ -322,64 +326,49 @@ def _fuzzy_pick(
|
|
|
322
326
|
labels.append(label)
|
|
323
327
|
by_label[label] = r
|
|
324
328
|
|
|
325
|
-
|
|
326
|
-
class FuzzyCompleter:
|
|
327
|
-
def __init__(self, words: list[str]):
|
|
328
|
-
self.words = words
|
|
329
|
-
|
|
330
|
-
def get_completions(self, document, complete_event):
|
|
331
|
-
word = document.get_word_before_cursor()
|
|
332
|
-
if not word:
|
|
333
|
-
return
|
|
334
|
-
|
|
335
|
-
word_lower = word.lower()
|
|
336
|
-
for label in self.words:
|
|
337
|
-
label_lower = label.lower()
|
|
338
|
-
# Check if all characters in the search word appear in order in the label
|
|
339
|
-
if self._fuzzy_match(word_lower, label_lower):
|
|
340
|
-
yield Completion(label, start_position=-len(word))
|
|
341
|
-
|
|
342
|
-
def _fuzzy_match(self, search: str, target: str) -> bool:
|
|
343
|
-
"""
|
|
344
|
-
True fuzzy matching: checks if all characters in search appear in order in target.
|
|
345
|
-
Examples:
|
|
346
|
-
- "aws" matches "aws_calculator_agent" ✓
|
|
347
|
-
- "calc" matches "aws_calculator_agent" ✓
|
|
348
|
-
- "gent" matches "aws_calculator_agent" ✓
|
|
349
|
-
- "agent" matches "aws_calculator_agent" ✓
|
|
350
|
-
- "aws_calc" matches "aws_calculator_agent" ✓
|
|
351
|
-
"""
|
|
352
|
-
if not search:
|
|
353
|
-
return True
|
|
354
|
-
|
|
355
|
-
search_idx = 0
|
|
356
|
-
for char in target:
|
|
357
|
-
if search_idx < len(search) and search[search_idx] == char:
|
|
358
|
-
search_idx += 1
|
|
359
|
-
if search_idx == len(search):
|
|
360
|
-
return True
|
|
361
|
-
return False
|
|
362
|
-
|
|
363
|
-
completer = FuzzyCompleter(labels)
|
|
329
|
+
return labels, by_label
|
|
364
330
|
|
|
365
|
-
try:
|
|
366
|
-
answer = prompt(
|
|
367
|
-
message=f"Find {title.rstrip('s')}: ",
|
|
368
|
-
completer=completer,
|
|
369
|
-
complete_in_thread=True,
|
|
370
|
-
complete_while_typing=True,
|
|
371
|
-
)
|
|
372
|
-
except (KeyboardInterrupt, EOFError):
|
|
373
|
-
return None
|
|
374
331
|
|
|
375
|
-
|
|
376
|
-
|
|
332
|
+
class _FuzzyCompleter:
|
|
333
|
+
"""Fuzzy completer for prompt_toolkit."""
|
|
377
334
|
|
|
378
|
-
|
|
335
|
+
def __init__(self, words: list[str]):
|
|
336
|
+
self.words = words
|
|
337
|
+
|
|
338
|
+
def get_completions(self, document, _complete_event):
|
|
339
|
+
word = document.get_word_before_cursor()
|
|
340
|
+
if not word:
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
word_lower = word.lower()
|
|
344
|
+
for label in self.words:
|
|
345
|
+
label_lower = label.lower()
|
|
346
|
+
if self._fuzzy_match(word_lower, label_lower):
|
|
347
|
+
yield Completion(label, start_position=-len(word))
|
|
348
|
+
|
|
349
|
+
def _fuzzy_match(self, search: str, target: str) -> bool:
|
|
350
|
+
"""True fuzzy matching: checks if all characters in search appear in order in target."""
|
|
351
|
+
if not search:
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
search_idx = 0
|
|
355
|
+
for char in target:
|
|
356
|
+
if search_idx < len(search) and search[search_idx] == char:
|
|
357
|
+
search_idx += 1
|
|
358
|
+
if search_idx == len(search):
|
|
359
|
+
return True
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _perform_fuzzy_search(
|
|
364
|
+
answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]
|
|
365
|
+
) -> dict[str, Any] | None:
|
|
366
|
+
"""Perform fuzzy search fallback and return best match."""
|
|
367
|
+
# Exact label match
|
|
379
368
|
if answer in by_label:
|
|
380
369
|
return by_label[answer]
|
|
381
370
|
|
|
382
|
-
# Fuzzy search fallback
|
|
371
|
+
# Fuzzy search fallback
|
|
383
372
|
best_match = None
|
|
384
373
|
best_score = -1
|
|
385
374
|
|
|
@@ -389,11 +378,36 @@ def _fuzzy_pick(
|
|
|
389
378
|
best_score = score
|
|
390
379
|
best_match = label
|
|
391
380
|
|
|
392
|
-
if best_match and best_score > 0
|
|
393
|
-
|
|
381
|
+
return by_label[best_match] if best_match and best_score > 0 else None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _fuzzy_pick(
|
|
385
|
+
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
386
|
+
) -> dict[str, Any] | None: # pragma: no cover - requires interactive prompt toolkit
|
|
387
|
+
"""
|
|
388
|
+
Open a minimal fuzzy palette using prompt_toolkit.
|
|
389
|
+
Returns the selected row (dict) or None if cancelled/missing deps.
|
|
390
|
+
"""
|
|
391
|
+
if not _check_fuzzy_pick_requirements():
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
# Build display labels and mapping
|
|
395
|
+
labels, by_label = _build_unique_labels(rows, columns)
|
|
396
|
+
|
|
397
|
+
# Create fuzzy completer
|
|
398
|
+
completer = _FuzzyCompleter(labels)
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
answer = prompt(
|
|
402
|
+
message=f"Find {title.rstrip('s')}: ",
|
|
403
|
+
completer=completer,
|
|
404
|
+
complete_in_thread=True,
|
|
405
|
+
complete_while_typing=True,
|
|
406
|
+
)
|
|
407
|
+
except (KeyboardInterrupt, EOFError):
|
|
408
|
+
return None
|
|
394
409
|
|
|
395
|
-
|
|
396
|
-
return None
|
|
410
|
+
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
397
411
|
|
|
398
412
|
|
|
399
413
|
def _fuzzy_score(search: str, target: str) -> int:
|
|
@@ -450,6 +464,52 @@ def _fuzzy_score(search: str, target: str) -> int:
|
|
|
450
464
|
# ----------------------------- Pretty outputs ---------------------------- #
|
|
451
465
|
|
|
452
466
|
|
|
467
|
+
def _coerce_result_payload(result: Any) -> Any:
|
|
468
|
+
try:
|
|
469
|
+
to_dict = getattr(result, "to_dict", None)
|
|
470
|
+
if callable(to_dict):
|
|
471
|
+
return to_dict()
|
|
472
|
+
except Exception:
|
|
473
|
+
return result
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _apply_mask_if_configured(payload: Any) -> Any:
|
|
478
|
+
mask_fields = _resolve_mask_fields()
|
|
479
|
+
if not mask_fields:
|
|
480
|
+
return payload
|
|
481
|
+
try:
|
|
482
|
+
return _mask_any(payload, mask_fields)
|
|
483
|
+
except Exception:
|
|
484
|
+
return payload
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _ensure_displayable(payload: Any) -> Any:
|
|
488
|
+
if isinstance(payload, dict | list | str | int | float | bool) or payload is None:
|
|
489
|
+
return payload
|
|
490
|
+
|
|
491
|
+
if hasattr(payload, "__dict__"):
|
|
492
|
+
try:
|
|
493
|
+
return dict(payload)
|
|
494
|
+
except Exception:
|
|
495
|
+
try:
|
|
496
|
+
return dict(payload.__dict__)
|
|
497
|
+
except Exception:
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
return str(payload)
|
|
502
|
+
except Exception:
|
|
503
|
+
return repr(payload)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _render_markdown_output(data: Any) -> None:
|
|
507
|
+
try:
|
|
508
|
+
console.print(Markdown(str(data)))
|
|
509
|
+
except ImportError:
|
|
510
|
+
click.echo(str(data))
|
|
511
|
+
|
|
512
|
+
|
|
453
513
|
def output_result(
|
|
454
514
|
ctx,
|
|
455
515
|
result: Any,
|
|
@@ -458,15 +518,10 @@ def output_result(
|
|
|
458
518
|
success_message: str | None = None,
|
|
459
519
|
):
|
|
460
520
|
fmt = _get_view(ctx)
|
|
461
|
-
data = result.to_dict() if hasattr(result, "to_dict") else result
|
|
462
521
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
try:
|
|
467
|
-
data = _mask_any(data, mask_fields)
|
|
468
|
-
except Exception:
|
|
469
|
-
pass # Continue with unmasked data if masking fails
|
|
522
|
+
data = _coerce_result_payload(result)
|
|
523
|
+
data = _apply_mask_if_configured(data)
|
|
524
|
+
data = _ensure_displayable(data)
|
|
470
525
|
|
|
471
526
|
if fmt == "json":
|
|
472
527
|
click.echo(json.dumps(data, indent=2, default=str))
|
|
@@ -477,18 +532,20 @@ def output_result(
|
|
|
477
532
|
return
|
|
478
533
|
|
|
479
534
|
if fmt == "md":
|
|
480
|
-
|
|
481
|
-
console.print(Markdown(str(data)))
|
|
482
|
-
except ImportError:
|
|
483
|
-
# Fallback to plain if markdown not available
|
|
484
|
-
click.echo(str(data))
|
|
535
|
+
_render_markdown_output(data)
|
|
485
536
|
return
|
|
486
537
|
|
|
487
538
|
if success_message:
|
|
488
539
|
console.print(Text(f"[green]✅ {success_message}[/green]"))
|
|
489
540
|
|
|
490
541
|
if panel_title:
|
|
491
|
-
console.print(
|
|
542
|
+
console.print(
|
|
543
|
+
AIPPanel(
|
|
544
|
+
Pretty(data),
|
|
545
|
+
title=panel_title,
|
|
546
|
+
border_style="blue",
|
|
547
|
+
)
|
|
548
|
+
)
|
|
492
549
|
else:
|
|
493
550
|
console.print(Text(f"[cyan]{title}:[/cyan]"))
|
|
494
551
|
console.print(Pretty(data))
|
|
@@ -500,17 +557,7 @@ def output_result(
|
|
|
500
557
|
# _PICK_THRESHOLD = int(os.getenv("AIP_PICK_THRESHOLD", "5") or "5")
|
|
501
558
|
|
|
502
559
|
|
|
503
|
-
def
|
|
504
|
-
ctx,
|
|
505
|
-
items: list[Any],
|
|
506
|
-
title: str,
|
|
507
|
-
columns: list[tuple],
|
|
508
|
-
transform_func=None,
|
|
509
|
-
):
|
|
510
|
-
"""Display a list with fuzzy palette by default on TTY, Rich table as fallback."""
|
|
511
|
-
fmt = _get_view(ctx)
|
|
512
|
-
|
|
513
|
-
# Normalize rows
|
|
560
|
+
def _normalise_rows(items: list[Any], transform_func) -> list[dict[str, Any]]:
|
|
514
561
|
try:
|
|
515
562
|
rows: list[dict[str, Any]] = []
|
|
516
563
|
for item in items:
|
|
@@ -524,106 +571,139 @@ def output_list(
|
|
|
524
571
|
rows.append(item)
|
|
525
572
|
else:
|
|
526
573
|
rows.append({"value": item})
|
|
574
|
+
return rows
|
|
527
575
|
except Exception:
|
|
528
|
-
|
|
576
|
+
return []
|
|
577
|
+
|
|
529
578
|
|
|
530
|
-
|
|
579
|
+
def _mask_rows_if_configured(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
531
580
|
mask_fields = _resolve_mask_fields()
|
|
532
|
-
if mask_fields:
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
581
|
+
if not mask_fields:
|
|
582
|
+
return rows
|
|
583
|
+
try:
|
|
584
|
+
return [_maybe_mask_row(row, mask_fields) for row in rows]
|
|
585
|
+
except Exception:
|
|
586
|
+
return rows
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _render_plain_list(
|
|
590
|
+
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
591
|
+
) -> None:
|
|
592
|
+
if not rows:
|
|
593
|
+
click.echo(f"No {title.lower()} found.")
|
|
594
|
+
return
|
|
595
|
+
for row in rows:
|
|
596
|
+
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
597
|
+
click.echo(row_str)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _render_markdown_list(
|
|
601
|
+
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
602
|
+
) -> None:
|
|
603
|
+
if not rows:
|
|
604
|
+
click.echo(f"No {title.lower()} found.")
|
|
605
|
+
return
|
|
606
|
+
headers = [header for _, header, _, _ in columns]
|
|
607
|
+
click.echo(f"| {' | '.join(headers)} |")
|
|
608
|
+
click.echo(f"| {' | '.join('---' for _ in headers)} |")
|
|
609
|
+
for row in rows:
|
|
610
|
+
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
611
|
+
click.echo(f"| {row_str} |")
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
|
|
615
|
+
return (
|
|
616
|
+
os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
|
|
617
|
+
and rows
|
|
618
|
+
and isinstance(rows[0], dict)
|
|
619
|
+
and "name" in rows[0]
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _create_table(columns: list[tuple], title: str):
|
|
624
|
+
table = AIPTable(title=title, expand=True)
|
|
625
|
+
for _key, header, style, width in columns:
|
|
626
|
+
table.add_column(header, style=style, width=width)
|
|
627
|
+
return table
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _build_table_group(
|
|
631
|
+
rows: list[dict[str, Any]], columns: list[tuple], title: str
|
|
632
|
+
) -> Group:
|
|
633
|
+
table = _create_table(columns, title)
|
|
634
|
+
for row in rows:
|
|
635
|
+
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
636
|
+
footer = Text(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
637
|
+
return Group(table, footer)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _should_page_output(row_count: int, is_tty: bool) -> bool:
|
|
641
|
+
pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
|
|
642
|
+
if pager_env in ("0", "off", "false"):
|
|
643
|
+
return False
|
|
644
|
+
if pager_env in ("1", "on", "true"):
|
|
645
|
+
return is_tty
|
|
646
|
+
try:
|
|
647
|
+
term_h = console.size.height or 24
|
|
648
|
+
approx_lines = 5 + row_count
|
|
649
|
+
return is_tty and (approx_lines >= term_h * 0.5)
|
|
650
|
+
except Exception:
|
|
651
|
+
return is_tty
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def output_list(
|
|
655
|
+
ctx,
|
|
656
|
+
items: list[Any],
|
|
657
|
+
title: str,
|
|
658
|
+
columns: list[tuple],
|
|
659
|
+
transform_func=None,
|
|
660
|
+
):
|
|
661
|
+
"""Display a list with fuzzy palette by default on TTY, Rich table as fallback."""
|
|
662
|
+
fmt = _get_view(ctx)
|
|
663
|
+
rows = _normalise_rows(items, transform_func)
|
|
664
|
+
rows = _mask_rows_if_configured(rows)
|
|
537
665
|
|
|
538
|
-
# JSON view bypasses any UI
|
|
539
666
|
if fmt == "json":
|
|
540
|
-
data =
|
|
667
|
+
data = (
|
|
668
|
+
rows
|
|
669
|
+
if rows
|
|
670
|
+
else [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
|
|
671
|
+
)
|
|
541
672
|
click.echo(json.dumps(data, indent=2, default=str))
|
|
542
673
|
return
|
|
543
674
|
|
|
544
|
-
# Plain view - simple text output
|
|
545
675
|
if fmt == "plain":
|
|
546
|
-
|
|
547
|
-
click.echo(f"No {title.lower()} found.")
|
|
548
|
-
return
|
|
549
|
-
for row in rows:
|
|
550
|
-
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
551
|
-
click.echo(row_str)
|
|
676
|
+
_render_plain_list(rows, title, columns)
|
|
552
677
|
return
|
|
553
678
|
|
|
554
|
-
# Markdown view - table format
|
|
555
679
|
if fmt == "md":
|
|
556
|
-
|
|
557
|
-
click.echo(f"No {title.lower()} found.")
|
|
558
|
-
return
|
|
559
|
-
# Create markdown table
|
|
560
|
-
headers = [header for _, header, _, _ in columns]
|
|
561
|
-
click.echo(f"| {' | '.join(headers)} |")
|
|
562
|
-
click.echo(f"| {' | '.join('---' for _ in headers)} |")
|
|
563
|
-
for row in rows:
|
|
564
|
-
row_str = " | ".join(str(row.get(key, "N/A")) for key, _, _, _ in columns)
|
|
565
|
-
click.echo(f"| {row_str} |")
|
|
680
|
+
_render_markdown_list(rows, title, columns)
|
|
566
681
|
return
|
|
567
682
|
|
|
568
683
|
if not items:
|
|
569
684
|
console.print(Text(f"[yellow]No {title.lower()} found.[/yellow]"))
|
|
570
685
|
return
|
|
571
686
|
|
|
572
|
-
|
|
573
|
-
if (
|
|
574
|
-
os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
|
|
575
|
-
and rows
|
|
576
|
-
and isinstance(rows[0], dict)
|
|
577
|
-
and "name" in rows[0]
|
|
578
|
-
):
|
|
687
|
+
if _should_sort_rows(rows):
|
|
579
688
|
try:
|
|
580
689
|
rows = sorted(rows, key=lambda r: str(r.get("name", "")).lower())
|
|
581
690
|
except Exception:
|
|
582
691
|
pass
|
|
583
692
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
693
|
+
picked = (
|
|
694
|
+
_fuzzy_pick(rows, columns, title)
|
|
695
|
+
if console.is_terminal and os.isatty(1)
|
|
696
|
+
else None
|
|
697
|
+
)
|
|
589
698
|
if picked:
|
|
590
|
-
|
|
591
|
-
table = Table(title=title, box=box.ROUNDED, expand=True)
|
|
592
|
-
for _key, header, style, width in columns:
|
|
593
|
-
table.add_column(header, style=style, width=width)
|
|
699
|
+
table = _create_table(columns, title)
|
|
594
700
|
table.add_row(*[str(picked.get(key, "N/A")) for key, _, _, _ in columns])
|
|
595
|
-
|
|
596
701
|
console.print(table)
|
|
597
702
|
console.print(Text("\n[dim]Tip: use `aip agents get <ID>` for details[/dim]"))
|
|
598
703
|
return
|
|
599
704
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
for _key, header, style, width in columns:
|
|
603
|
-
table.add_column(header, style=style, width=width)
|
|
604
|
-
for row in rows:
|
|
605
|
-
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
606
|
-
|
|
607
|
-
footer = Text(f"\n[dim]Total {len(rows)} items[/dim]")
|
|
608
|
-
content = Group(table, footer)
|
|
609
|
-
|
|
610
|
-
# Auto paging when long
|
|
611
|
-
is_tty = console.is_terminal and os.isatty(1)
|
|
612
|
-
pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
|
|
613
|
-
|
|
614
|
-
if pager_env in ("0", "off", "false"):
|
|
615
|
-
should_page = False
|
|
616
|
-
elif pager_env in ("1", "on", "true"):
|
|
617
|
-
should_page = is_tty
|
|
618
|
-
else:
|
|
619
|
-
try:
|
|
620
|
-
term_h = console.size.height or 24
|
|
621
|
-
approx_lines = 5 + len(rows)
|
|
622
|
-
should_page = is_tty and (approx_lines >= term_h * 0.5)
|
|
623
|
-
except Exception:
|
|
624
|
-
should_page = is_tty
|
|
625
|
-
|
|
626
|
-
if should_page:
|
|
705
|
+
content = _build_table_group(rows, columns, title)
|
|
706
|
+
if _should_page_output(len(rows), console.is_terminal and os.isatty(1)):
|
|
627
707
|
ansi = _render_ansi(content)
|
|
628
708
|
if not _page_with_system_pager(ansi):
|
|
629
709
|
with console.pager(styles=True):
|
|
@@ -701,12 +781,12 @@ def coerce_to_row(item, keys: list[str]) -> dict[str, Any]:
|
|
|
701
781
|
|
|
702
782
|
|
|
703
783
|
def build_renderer(
|
|
704
|
-
|
|
784
|
+
_ctx,
|
|
705
785
|
*,
|
|
706
786
|
save_path,
|
|
707
787
|
theme="dark",
|
|
708
788
|
verbose=False,
|
|
709
|
-
|
|
789
|
+
_tty_enabled=True,
|
|
710
790
|
live=None,
|
|
711
791
|
snapshots=None,
|
|
712
792
|
):
|
|
@@ -755,30 +835,16 @@ def build_renderer(
|
|
|
755
835
|
return renderer, working_console
|
|
756
836
|
|
|
757
837
|
|
|
758
|
-
def
|
|
759
|
-
|
|
760
|
-
) -> Any | None:
|
|
761
|
-
"""
|
|
762
|
-
Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
|
|
763
|
-
|
|
764
|
-
Args:
|
|
765
|
-
resources: List of resource objects to choose from
|
|
766
|
-
resource_type: Type of resource (e.g., "agent", "tool")
|
|
767
|
-
search_term: The search term that led to multiple matches
|
|
768
|
-
|
|
769
|
-
Returns:
|
|
770
|
-
Selected resource object or None if cancelled/no selection
|
|
771
|
-
"""
|
|
772
|
-
if not (_HAS_PTK and console.is_terminal and os.isatty(1)):
|
|
773
|
-
return None
|
|
774
|
-
|
|
775
|
-
# Build display corpus and a reverse map
|
|
838
|
+
def _build_resource_labels(resources: list[Any]) -> tuple[list[str], dict[str, Any]]:
|
|
839
|
+
"""Build unique display labels for resources."""
|
|
776
840
|
labels = []
|
|
777
841
|
by_label: dict[str, Any] = {}
|
|
842
|
+
|
|
778
843
|
for resource in resources:
|
|
779
844
|
name = getattr(resource, "name", "Unknown")
|
|
780
845
|
_id = getattr(resource, "id", "Unknown")
|
|
781
|
-
|
|
846
|
+
|
|
847
|
+
# Create display label
|
|
782
848
|
label_parts = []
|
|
783
849
|
if name and name != "Unknown":
|
|
784
850
|
label_parts.append(name)
|
|
@@ -792,39 +858,35 @@ def _fuzzy_pick_for_resources(
|
|
|
792
858
|
while f"{base} #{i}" in by_label:
|
|
793
859
|
i += 1
|
|
794
860
|
label = f"{base} #{i}"
|
|
861
|
+
|
|
795
862
|
labels.append(label)
|
|
796
863
|
by_label[label] = resource
|
|
797
864
|
|
|
865
|
+
return labels, by_label
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _fuzzy_pick_for_resources(
|
|
869
|
+
resources: list[Any], resource_type: str, _search_term: str
|
|
870
|
+
) -> Any | None: # pragma: no cover - interactive selection helper
|
|
871
|
+
"""
|
|
872
|
+
Fuzzy picker for resource objects, similar to _fuzzy_pick but without column dependencies.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
resources: List of resource objects to choose from
|
|
876
|
+
resource_type: Type of resource (e.g., "agent", "tool")
|
|
877
|
+
search_term: The search term that led to multiple matches
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
Selected resource object or None if cancelled/no selection
|
|
881
|
+
"""
|
|
882
|
+
if not _check_fuzzy_pick_requirements():
|
|
883
|
+
return None
|
|
884
|
+
|
|
885
|
+
# Build labels and mapping
|
|
886
|
+
labels, by_label = _build_resource_labels(resources)
|
|
887
|
+
|
|
798
888
|
# Create fuzzy completer
|
|
799
|
-
|
|
800
|
-
def __init__(self, words: list[str]):
|
|
801
|
-
self.words = words
|
|
802
|
-
|
|
803
|
-
def get_completions(self, document, complete_event):
|
|
804
|
-
word = document.get_word_before_cursor()
|
|
805
|
-
if not word:
|
|
806
|
-
return
|
|
807
|
-
|
|
808
|
-
word_lower = word.lower()
|
|
809
|
-
for label in self.words:
|
|
810
|
-
label_lower = label.lower()
|
|
811
|
-
# Fuzzy match logic
|
|
812
|
-
if self._fuzzy_match(word_lower, label_lower):
|
|
813
|
-
yield Completion(label, start_position=-len(word))
|
|
814
|
-
|
|
815
|
-
def _fuzzy_match(self, search: str, target: str) -> bool:
|
|
816
|
-
if not search:
|
|
817
|
-
return True
|
|
818
|
-
|
|
819
|
-
search_idx = 0
|
|
820
|
-
for char in target:
|
|
821
|
-
if search_idx < len(search) and search[search_idx] == char:
|
|
822
|
-
search_idx += 1
|
|
823
|
-
if search_idx == len(search):
|
|
824
|
-
return True
|
|
825
|
-
return False
|
|
826
|
-
|
|
827
|
-
completer = FuzzyCompleter(labels)
|
|
889
|
+
completer = _FuzzyCompleter(labels)
|
|
828
890
|
|
|
829
891
|
try:
|
|
830
892
|
answer = prompt(
|
|
@@ -836,27 +898,7 @@ def _fuzzy_pick_for_resources(
|
|
|
836
898
|
except (KeyboardInterrupt, EOFError):
|
|
837
899
|
return None
|
|
838
900
|
|
|
839
|
-
if
|
|
840
|
-
return None
|
|
841
|
-
|
|
842
|
-
# Exact label match
|
|
843
|
-
if answer in by_label:
|
|
844
|
-
return by_label[answer]
|
|
845
|
-
|
|
846
|
-
# Fuzzy search fallback
|
|
847
|
-
best_match = None
|
|
848
|
-
best_score = -1
|
|
849
|
-
|
|
850
|
-
for label in labels:
|
|
851
|
-
score = _fuzzy_score(answer.lower(), label.lower())
|
|
852
|
-
if score > best_score:
|
|
853
|
-
best_score = score
|
|
854
|
-
best_match = label
|
|
855
|
-
|
|
856
|
-
if best_match and best_score > 0:
|
|
857
|
-
return by_label[best_match]
|
|
858
|
-
|
|
859
|
-
return None
|
|
901
|
+
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
860
902
|
|
|
861
903
|
|
|
862
904
|
def resolve_resource(
|
|
@@ -945,9 +987,8 @@ def handle_ambiguous_resource(
|
|
|
945
987
|
f"[yellow]Multiple {resource_type.replace('{', '{{').replace('}', '}}')}s found matching '{ref.replace('{', '{{').replace('}', '}}')}':[/yellow]"
|
|
946
988
|
)
|
|
947
989
|
)
|
|
948
|
-
table =
|
|
990
|
+
table = AIPTable(
|
|
949
991
|
title=f"Select {resource_type.replace('{', '{{').replace('}', '}}').title()}",
|
|
950
|
-
box=box.ROUNDED,
|
|
951
992
|
)
|
|
952
993
|
table.add_column("#", style="dim", width=3)
|
|
953
994
|
table.add_column("ID", style="dim", width=36)
|