glaip-sdk 0.2.1__py3-none-any.whl → 0.3.0__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/_version.py +8 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/commands/agents.py +180 -39
- glaip_sdk/cli/commands/mcps.py +44 -18
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +35 -16
- glaip_sdk/cli/commands/transcripts.py +8 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/display.py +34 -19
- glaip_sdk/cli/main.py +14 -7
- glaip_sdk/cli/masking.py +8 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/slash/agent_session.py +57 -20
- glaip_sdk/cli/slash/prompt.py +8 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +341 -46
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
- glaip_sdk/cli/transcript/viewer.py +232 -32
- glaip_sdk/cli/update_notifier.py +2 -2
- glaip_sdk/cli/utils.py +266 -35
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +30 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +186 -22
- glaip_sdk/client/main.py +23 -6
- glaip_sdk/client/mcps.py +2 -4
- glaip_sdk/client/run_rendering.py +66 -0
- glaip_sdk/client/tools.py +2 -3
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/models/__init__.py +56 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/utils/client_utils.py +13 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/import_export.py +6 -9
- glaip_sdk/utils/rendering/__init__.py +122 -1
- glaip_sdk/utils/rendering/renderer/base.py +3 -7
- glaip_sdk/utils/rendering/renderer/debug.py +0 -1
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/steps.py +1 -0
- glaip_sdk/utils/resource_refs.py +26 -15
- glaip_sdk/utils/serialization.py +16 -0
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
- glaip_sdk-0.3.0.dist-info/RECORD +94 -0
- glaip_sdk-0.2.1.dist-info/RECORD +0 -86
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/commands/tools.py
CHANGED
|
@@ -4,7 +4,6 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
# pylint: disable=duplicate-code
|
|
8
7
|
import json
|
|
9
8
|
import re
|
|
10
9
|
from pathlib import Path
|
|
@@ -40,7 +39,10 @@ from glaip_sdk.cli.resolution import resolve_resource_reference
|
|
|
40
39
|
from glaip_sdk.cli.rich_helpers import markup_text, print_markup
|
|
41
40
|
from glaip_sdk.cli.utils import (
|
|
42
41
|
coerce_to_row,
|
|
42
|
+
format_datetime_fields,
|
|
43
43
|
get_client,
|
|
44
|
+
handle_best_effort_check,
|
|
45
|
+
handle_resource_export,
|
|
44
46
|
output_list,
|
|
45
47
|
output_result,
|
|
46
48
|
spinner_context,
|
|
@@ -57,16 +59,32 @@ def tools_group() -> None:
|
|
|
57
59
|
pass
|
|
58
60
|
|
|
59
61
|
|
|
60
|
-
# pylint: disable=duplicate-code
|
|
61
62
|
def _resolve_tool(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
|
|
62
|
-
"""Resolve tool
|
|
63
|
+
"""Resolve a tool by ID or name, handling ambiguous matches interactively.
|
|
64
|
+
|
|
65
|
+
This function provides tool-specific resolution logic. It uses
|
|
66
|
+
resolve_resource_reference to find tools by UUID or name, with interactive
|
|
67
|
+
selection when multiple matches are found.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
ctx: Click context for CLI operations.
|
|
71
|
+
client: API client instance.
|
|
72
|
+
ref: Tool reference (UUID string or name).
|
|
73
|
+
select: Pre-selected index for non-interactive mode (1-based).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Tool object if found, None otherwise.
|
|
77
|
+
"""
|
|
78
|
+
# Configure tool-specific resolution with standard fuzzy matching
|
|
79
|
+
get_by_id = client.get_tool
|
|
80
|
+
find_by_name = client.find_tools
|
|
63
81
|
return resolve_resource_reference(
|
|
64
82
|
ctx,
|
|
65
83
|
client,
|
|
66
84
|
ref,
|
|
67
85
|
"tool",
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
get_by_id,
|
|
87
|
+
find_by_name,
|
|
70
88
|
"Tool",
|
|
71
89
|
select=select,
|
|
72
90
|
)
|
|
@@ -100,19 +118,16 @@ def _validate_name_match(provided: str | None, internal: str) -> str:
|
|
|
100
118
|
|
|
101
119
|
def _check_duplicate_name(client: Any, tool_name: str) -> None:
|
|
102
120
|
"""Raise if a tool with the same name already exists."""
|
|
103
|
-
|
|
121
|
+
|
|
122
|
+
def _check_duplicate() -> None:
|
|
104
123
|
existing = client.find_tools(name=tool_name)
|
|
105
124
|
if existing:
|
|
106
125
|
raise click.ClickException(
|
|
107
126
|
f"A tool named '{tool_name}' already exists. "
|
|
108
127
|
"Please change your plugin's 'name' to a unique value, then re-run."
|
|
109
128
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
raise
|
|
113
|
-
except Exception:
|
|
114
|
-
# Non-fatal: best-effort duplicate check for other errors
|
|
115
|
-
pass
|
|
129
|
+
|
|
130
|
+
handle_best_effort_check(_check_duplicate)
|
|
116
131
|
|
|
117
132
|
|
|
118
133
|
def _parse_tags(tags: str | None) -> list[str]:
|
|
@@ -211,6 +226,14 @@ def list_tools(ctx: Any, tool_type: str | None) -> None:
|
|
|
211
226
|
|
|
212
227
|
# Transform function for safe dictionary access
|
|
213
228
|
def transform_tool(tool: Any) -> dict[str, Any]:
|
|
229
|
+
"""Transform a tool object to a display row dictionary.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
tool: Tool object to transform.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dictionary with id, name, and framework fields.
|
|
236
|
+
"""
|
|
214
237
|
row = coerce_to_row(tool, ["id", "name", "framework"])
|
|
215
238
|
# Ensure id is always a string
|
|
216
239
|
row["id"] = str(row["id"])
|
|
@@ -341,8 +364,6 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
|
|
|
341
364
|
|
|
342
365
|
# Handle export option
|
|
343
366
|
if export:
|
|
344
|
-
from glaip_sdk.cli.utils import handle_resource_export
|
|
345
|
-
|
|
346
367
|
handle_resource_export(
|
|
347
368
|
ctx,
|
|
348
369
|
tool,
|
|
@@ -363,8 +384,6 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
|
|
|
363
384
|
if raw_tool_data:
|
|
364
385
|
# Use raw API data - this preserves ALL fields
|
|
365
386
|
# Format dates for better display (minimal postprocessing)
|
|
366
|
-
from glaip_sdk.cli.utils import format_datetime_fields
|
|
367
|
-
|
|
368
387
|
formatted_data = format_datetime_fields(raw_tool_data)
|
|
369
388
|
|
|
370
389
|
# Display using output_result with raw data
|
|
@@ -156,6 +156,14 @@ def _launch_transcript_viewer(
|
|
|
156
156
|
viewer_ctx = _build_viewer_context(entry, meta, events)
|
|
157
157
|
|
|
158
158
|
def _export(destination: Path) -> Path:
|
|
159
|
+
"""Export cached transcript to destination.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
destination: Path to export transcript to.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Path to exported transcript file.
|
|
166
|
+
"""
|
|
159
167
|
return export_cached_transcript(destination=destination, run_id=entry.run_id)
|
|
160
168
|
|
|
161
169
|
run_viewer_session(target_console, viewer_ctx, _export, initial_view=initial_view)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""CLI-specific constants for glaip-sdk.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Minimum length that forces multiline YAML strings to be rendered using the literal
|
|
8
|
+
# block style. This prevents long prompts and instructions from being inlined.
|
|
9
|
+
LITERAL_STRING_THRESHOLD = 200
|
|
10
|
+
|
|
11
|
+
# Masking configuration
|
|
12
|
+
MASKING_ENABLED = True
|
|
13
|
+
MASK_SENSITIVE_FIELDS = {
|
|
14
|
+
"api_key",
|
|
15
|
+
"apikey",
|
|
16
|
+
"token",
|
|
17
|
+
"access_token",
|
|
18
|
+
"secret",
|
|
19
|
+
"client_secret",
|
|
20
|
+
"password",
|
|
21
|
+
"private_key",
|
|
22
|
+
"bearer",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Table + pager behaviour
|
|
26
|
+
TABLE_SORT_ENABLED = True
|
|
27
|
+
PAGER_MODE = "auto" # valid values: "auto", "on", "off"
|
|
28
|
+
PAGER_WRAP_LINES = False
|
|
29
|
+
PAGER_HEADER_ENABLED = True
|
|
30
|
+
|
|
31
|
+
# Update notification toggle
|
|
32
|
+
UPDATE_CHECK_ENABLED = True
|
|
33
|
+
|
|
34
|
+
# Agent instruction preview defaults
|
|
35
|
+
DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT = 800
|
|
36
|
+
|
|
37
|
+
# Remote runs defaults
|
|
38
|
+
DEFAULT_REMOTE_RUNS_PAGE_LIMIT = 20
|
glaip_sdk/cli/context.py
CHANGED
|
@@ -106,6 +106,14 @@ def output_flags() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
|
106
106
|
"""
|
|
107
107
|
|
|
108
108
|
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
109
|
+
"""Apply output flags to a click command.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
f: Click command function to decorate.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Decorated command function.
|
|
116
|
+
"""
|
|
109
117
|
f = click.option(
|
|
110
118
|
"--json",
|
|
111
119
|
"json_mode",
|
glaip_sdk/cli/display.py
CHANGED
|
@@ -17,7 +17,7 @@ from rich.text import Text
|
|
|
17
17
|
|
|
18
18
|
from glaip_sdk.branding import ERROR_STYLE, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
|
|
19
19
|
from glaip_sdk.cli.rich_helpers import markup_text
|
|
20
|
-
from glaip_sdk.cli.utils import command_hint, format_command_hint
|
|
20
|
+
from glaip_sdk.cli.utils import command_hint, format_command_hint, in_slash_mode
|
|
21
21
|
from glaip_sdk.icons import ICON_AGENT, ICON_TOOL
|
|
22
22
|
from glaip_sdk.rich_components import AIPPanel
|
|
23
23
|
|
|
@@ -304,6 +304,7 @@ def display_agent_run_suggestions(agent: Any) -> Panel:
|
|
|
304
304
|
"""Return a panel with post-creation suggestions for an agent."""
|
|
305
305
|
agent_id = getattr(agent, "id", "")
|
|
306
306
|
agent_name = getattr(agent, "name", "")
|
|
307
|
+
slash_mode = in_slash_mode()
|
|
307
308
|
run_hint_id = command_hint(
|
|
308
309
|
f'agents run {agent_id} "Your message here"',
|
|
309
310
|
slash_command=None,
|
|
@@ -313,27 +314,41 @@ def display_agent_run_suggestions(agent: Any) -> Panel:
|
|
|
313
314
|
slash_command=None,
|
|
314
315
|
)
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
f" {format_command_hint(
|
|
321
|
-
|
|
317
|
+
content_parts: list[str] = ["[bold blue]💡 Next Steps:[/bold blue]\n\n"]
|
|
318
|
+
|
|
319
|
+
if slash_mode:
|
|
320
|
+
slash_shortcuts = "\n".join(
|
|
321
|
+
f" {format_command_hint(command, description) or command}"
|
|
322
|
+
for command, description in (
|
|
323
|
+
("/details", "Show configuration (toggle preview)"),
|
|
324
|
+
("/help", "Show command palette menu"),
|
|
325
|
+
("/exit", "Return to the palette"),
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
content_parts.append(
|
|
329
|
+
f"🚀 Start chatting with [bold]{agent_name}[/bold] right here:\n"
|
|
330
|
+
f" Type your message below and press Enter to run it immediately.\n\n"
|
|
331
|
+
f"{ICON_TOOL} Slash shortcuts:\n"
|
|
332
|
+
f"{slash_shortcuts}"
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
cli_hint_lines = [format_command_hint(hint) or hint for hint in (run_hint_id, run_hint_name) if hint]
|
|
336
|
+
if cli_hint_lines:
|
|
337
|
+
joined_hints = "\n".join(f" {hint}" for hint in cli_hint_lines)
|
|
338
|
+
content_parts.append(f"🚀 Run this agent from the CLI:\n{joined_hints}\n\n")
|
|
339
|
+
content_parts.append(
|
|
340
|
+
f"{ICON_TOOL} Available options:\n"
|
|
341
|
+
f" [dim]--chat-history[/dim] Include previous conversation\n"
|
|
342
|
+
f" [dim]--file[/dim] Attach files\n"
|
|
343
|
+
f" [dim]--input[/dim] Alternative input method\n"
|
|
344
|
+
f" [dim]--timeout[/dim] Set execution timeout\n"
|
|
345
|
+
f" [dim]--save[/dim] Save transcript to file\n"
|
|
346
|
+
f" [dim]--verbose[/dim] Show detailed execution\n\n"
|
|
347
|
+
f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]"
|
|
322
348
|
)
|
|
323
349
|
|
|
324
350
|
return AIPPanel(
|
|
325
|
-
|
|
326
|
-
f"🚀 Start chatting with [bold]{agent_name}[/bold] right here:\n"
|
|
327
|
-
f" Type your message below and press Enter to run it immediately.\n\n"
|
|
328
|
-
f"{cli_section}"
|
|
329
|
-
f"{ICON_TOOL} Available options:\n"
|
|
330
|
-
f" [dim]--chat-history[/dim] Include previous conversation\n"
|
|
331
|
-
f" [dim]--file[/dim] Attach files\n"
|
|
332
|
-
f" [dim]--input[/dim] Alternative input method\n"
|
|
333
|
-
f" [dim]--timeout[/dim] Set execution timeout\n"
|
|
334
|
-
f" [dim]--save[/dim] Save transcript to file\n"
|
|
335
|
-
f" [dim]--verbose[/dim] Show detailed execution\n\n"
|
|
336
|
-
f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]",
|
|
351
|
+
"".join(content_parts),
|
|
337
352
|
title=f"{ICON_AGENT} Ready to Run Agent",
|
|
338
353
|
border_style="blue",
|
|
339
354
|
padding=(0, 1),
|
glaip_sdk/cli/main.py
CHANGED
|
@@ -34,7 +34,7 @@ from glaip_sdk.cli.commands.mcps import mcps_group
|
|
|
34
34
|
from glaip_sdk.cli.commands.models import models_group
|
|
35
35
|
from glaip_sdk.cli.commands.tools import tools_group
|
|
36
36
|
from glaip_sdk.cli.commands.transcripts import transcripts_group
|
|
37
|
-
from glaip_sdk.cli.commands.update import update_command
|
|
37
|
+
from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
|
|
38
38
|
from glaip_sdk.cli.config import load_config
|
|
39
39
|
from glaip_sdk.cli.transcript import get_transcript_cache_stats
|
|
40
40
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
@@ -387,6 +387,7 @@ def version() -> None:
|
|
|
387
387
|
)
|
|
388
388
|
def update(check_only: bool, force: bool) -> None:
|
|
389
389
|
"""Update AIP SDK to the latest version from PyPI."""
|
|
390
|
+
slash_mode = in_slash_mode()
|
|
390
391
|
try:
|
|
391
392
|
console = Console()
|
|
392
393
|
|
|
@@ -400,11 +401,15 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
400
401
|
)
|
|
401
402
|
return
|
|
402
403
|
|
|
404
|
+
update_hint = ""
|
|
405
|
+
if not slash_mode:
|
|
406
|
+
update_hint = "\n💡 Use --check-only to just check for updates"
|
|
407
|
+
|
|
403
408
|
console.print(
|
|
404
409
|
AIPPanel(
|
|
405
410
|
"[bold blue]🔄 Updating AIP SDK...[/bold blue]\n\n"
|
|
406
|
-
"📦 This will update the package from PyPI
|
|
407
|
-
"
|
|
411
|
+
"📦 This will update the package from PyPI"
|
|
412
|
+
f"{update_hint}",
|
|
408
413
|
title="Update Process",
|
|
409
414
|
border_style="blue",
|
|
410
415
|
padding=(0, 1),
|
|
@@ -413,8 +418,6 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
413
418
|
|
|
414
419
|
# Update using pip
|
|
415
420
|
try:
|
|
416
|
-
from glaip_sdk.cli.commands.update import _build_upgrade_command
|
|
417
|
-
|
|
418
421
|
cmd = list(_build_upgrade_command(include_prerelease=False))
|
|
419
422
|
# Replace package name with "glaip-sdk" (main.py uses different name)
|
|
420
423
|
cmd[-1] = "glaip-sdk"
|
|
@@ -422,11 +425,15 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
422
425
|
cmd.insert(5, "--force-reinstall")
|
|
423
426
|
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
424
427
|
|
|
428
|
+
verify_hint = ""
|
|
429
|
+
if not slash_mode:
|
|
430
|
+
verify_hint = "\n💡 Restart your terminal or run 'aip --version' to verify"
|
|
431
|
+
|
|
425
432
|
console.print(
|
|
426
433
|
AIPPanel(
|
|
427
434
|
f"[{SUCCESS_STYLE}]✅ Update successful![/]\n\n"
|
|
428
|
-
"🔄 AIP SDK has been updated to the latest version
|
|
429
|
-
"
|
|
435
|
+
"🔄 AIP SDK has been updated to the latest version"
|
|
436
|
+
f"{verify_hint}",
|
|
430
437
|
title="🎉 Update Complete",
|
|
431
438
|
border_style=SUCCESS,
|
|
432
439
|
padding=(0, 1),
|
glaip_sdk/cli/masking.py
CHANGED
|
@@ -6,9 +6,10 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import os
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
11
|
+
from glaip_sdk.cli.constants import MASKING_ENABLED, MASK_SENSITIVE_FIELDS
|
|
12
|
+
|
|
12
13
|
__all__ = [
|
|
13
14
|
"mask_payload",
|
|
14
15
|
"mask_rows",
|
|
@@ -18,18 +19,6 @@ __all__ = [
|
|
|
18
19
|
"_resolve_mask_fields",
|
|
19
20
|
]
|
|
20
21
|
|
|
21
|
-
_DEFAULT_MASK_FIELDS = {
|
|
22
|
-
"api_key",
|
|
23
|
-
"apikey",
|
|
24
|
-
"token",
|
|
25
|
-
"access_token",
|
|
26
|
-
"secret",
|
|
27
|
-
"client_secret",
|
|
28
|
-
"password",
|
|
29
|
-
"private_key",
|
|
30
|
-
"bearer",
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
|
|
34
23
|
def _mask_value(raw: Any) -> str:
|
|
35
24
|
"""Return a masked representation of the provided value.
|
|
@@ -90,22 +79,10 @@ def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any
|
|
|
90
79
|
|
|
91
80
|
|
|
92
81
|
def _resolve_mask_fields() -> set[str]:
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
set[str]: Set of field names to mask. Empty set if masking is disabled
|
|
97
|
-
via AIP_MASK_OFF environment variable, custom fields from
|
|
98
|
-
AIP_MASK_FIELDS, or default fields if neither is set.
|
|
99
|
-
"""
|
|
100
|
-
if os.getenv("AIP_MASK_OFF", "0") in {"1", "true", "on", "yes"}:
|
|
82
|
+
"""Return the configured set of fields that should be masked."""
|
|
83
|
+
if not MASKING_ENABLED:
|
|
101
84
|
return set()
|
|
102
|
-
|
|
103
|
-
env_fields = (os.getenv("AIP_MASK_FIELDS") or "").strip()
|
|
104
|
-
if env_fields:
|
|
105
|
-
parts = [part.strip().lower() for part in env_fields.split(",") if part.strip()]
|
|
106
|
-
return set(parts)
|
|
107
|
-
|
|
108
|
-
return set(_DEFAULT_MASK_FIELDS)
|
|
85
|
+
return set(MASK_SENSITIVE_FIELDS)
|
|
109
86
|
|
|
110
87
|
|
|
111
88
|
def mask_payload(payload: Any) -> Any:
|
|
@@ -115,9 +92,7 @@ def mask_payload(payload: Any) -> Any:
|
|
|
115
92
|
payload: Any data structure (dict, list, or primitive) to mask.
|
|
116
93
|
|
|
117
94
|
Returns:
|
|
118
|
-
Any: The payload with sensitive fields masked based on
|
|
119
|
-
configuration. Returns original payload if masking is disabled
|
|
120
|
-
or if an error occurs during masking.
|
|
95
|
+
Any: The payload with sensitive fields masked based on configuration.
|
|
121
96
|
"""
|
|
122
97
|
mask_fields = _resolve_mask_fields()
|
|
123
98
|
if not mask_fields:
|
|
@@ -136,8 +111,8 @@ def mask_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
136
111
|
|
|
137
112
|
Returns:
|
|
138
113
|
list[dict[str, Any]]: List of rows with sensitive fields masked based
|
|
139
|
-
on
|
|
140
|
-
|
|
114
|
+
on configuration. Returns original rows if
|
|
115
|
+
masking is disabled or if an error occurs.
|
|
141
116
|
"""
|
|
142
117
|
mask_fields = _resolve_mask_fields()
|
|
143
118
|
if not mask_fields:
|
glaip_sdk/cli/pager.py
CHANGED
|
@@ -19,6 +19,8 @@ from typing import Any
|
|
|
19
19
|
|
|
20
20
|
from rich.console import Console
|
|
21
21
|
|
|
22
|
+
from glaip_sdk.cli.constants import PAGER_HEADER_ENABLED, PAGER_MODE, PAGER_WRAP_LINES
|
|
23
|
+
|
|
22
24
|
__all__ = [
|
|
23
25
|
"console",
|
|
24
26
|
"_prepare_pager_env",
|
|
@@ -64,8 +66,7 @@ def _prepare_pager_env(clear_on_exit: bool = True) -> None:
|
|
|
64
66
|
-R : pass ANSI color escapes
|
|
65
67
|
-S : chop long lines (horizontal scroll with ←/→)
|
|
66
68
|
(No -F, no -X) so we open a full-screen pager and clear on exit.
|
|
67
|
-
Toggle wrapping
|
|
68
|
-
Power users can override via AIP_LESS_FLAGS.
|
|
69
|
+
Toggle wrapping via `PAGER_WRAP_LINES` (True drops -S).
|
|
69
70
|
|
|
70
71
|
Args:
|
|
71
72
|
clear_on_exit: Whether to clear the pager on exit (default: True)
|
|
@@ -75,10 +76,9 @@ def _prepare_pager_env(clear_on_exit: bool = True) -> None:
|
|
|
75
76
|
"""
|
|
76
77
|
os.environ.pop("LESSSECURE", None)
|
|
77
78
|
if os.getenv("LESS") is None:
|
|
78
|
-
|
|
79
|
-
base = "-R" if want_wrap else "-RS"
|
|
79
|
+
base = "-R" if PAGER_WRAP_LINES else "-RS"
|
|
80
80
|
default_flags = base if clear_on_exit else (base + "FX")
|
|
81
|
-
os.environ["LESS"] =
|
|
81
|
+
os.environ["LESS"] = default_flags
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def _render_ansi(renderable: Any) -> str:
|
|
@@ -111,8 +111,7 @@ def _pager_header() -> str:
|
|
|
111
111
|
Returns:
|
|
112
112
|
str: Header text containing navigation help, or empty string if disabled
|
|
113
113
|
"""
|
|
114
|
-
|
|
115
|
-
if v in {"0", "false", "off"}:
|
|
114
|
+
if not PAGER_HEADER_ENABLED:
|
|
116
115
|
return ""
|
|
117
116
|
return "\n".join(
|
|
118
117
|
[
|
|
@@ -254,10 +253,10 @@ def _should_page_output(row_count: int, is_tty: bool) -> bool:
|
|
|
254
253
|
bool: True if output should be paginated, False otherwise
|
|
255
254
|
"""
|
|
256
255
|
active_console = _get_console()
|
|
257
|
-
|
|
258
|
-
if
|
|
256
|
+
pager_mode = (PAGER_MODE or "auto").lower()
|
|
257
|
+
if pager_mode in ("0", "off", "false"):
|
|
259
258
|
return False
|
|
260
|
-
if
|
|
259
|
+
if pager_mode in ("1", "on", "true"):
|
|
261
260
|
return is_tty
|
|
262
261
|
try:
|
|
263
262
|
term_h = active_console.size.height or 24
|
|
@@ -14,8 +14,9 @@ import click
|
|
|
14
14
|
from glaip_sdk.branding import ERROR_STYLE, HINT_PREFIX_STYLE
|
|
15
15
|
from glaip_sdk.cli.commands.agents import get as agents_get_command
|
|
16
16
|
from glaip_sdk.cli.commands.agents import run as agents_run_command
|
|
17
|
+
from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
17
18
|
from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
|
|
18
|
-
from glaip_sdk.cli.utils import format_command_hint
|
|
19
|
+
from glaip_sdk.cli.utils import bind_slash_session_context, format_command_hint
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
21
22
|
from glaip_sdk.cli.slash.session import SlashSession
|
|
@@ -38,11 +39,13 @@ class AgentRunSession:
|
|
|
38
39
|
self._agent_name = getattr(agent, "name", "") or self._agent_id
|
|
39
40
|
self._prompt_placeholder: str = "Chat with this agent here; use / for shortcuts. Alt+Enter inserts a newline."
|
|
40
41
|
self._contextual_completion_help: dict[str, str] = {
|
|
41
|
-
"details": "Show this agent's
|
|
42
|
+
"details": "Show this agent's configuration (+ expands prompt).",
|
|
42
43
|
"help": "Display this context-aware menu.",
|
|
44
|
+
"runs": "✨ NEW · Browse remote run history for this agent.",
|
|
43
45
|
"exit": "Return to the command palette.",
|
|
44
46
|
"q": "Return to the command palette.",
|
|
45
47
|
}
|
|
48
|
+
self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
46
49
|
|
|
47
50
|
def run(self) -> None:
|
|
48
51
|
"""Run the interactive agent session loop."""
|
|
@@ -86,6 +89,7 @@ class AgentRunSession:
|
|
|
86
89
|
try:
|
|
87
90
|
|
|
88
91
|
def _prompt_message() -> Any:
|
|
92
|
+
"""Get formatted prompt message for agent session."""
|
|
89
93
|
prompt_prefix = f"{self._agent_name} ({self._agent_id}) "
|
|
90
94
|
|
|
91
95
|
# Use FormattedText if prompt_toolkit is available, otherwise use simple string
|
|
@@ -122,7 +126,7 @@ class AgentRunSession:
|
|
|
122
126
|
if raw in {"/exit", "/back", "/q"}:
|
|
123
127
|
return self._handle_exit_command()
|
|
124
128
|
|
|
125
|
-
if raw
|
|
129
|
+
if raw == "/details":
|
|
126
130
|
return self._handle_details_command(agent_id)
|
|
127
131
|
|
|
128
132
|
if raw in {"/help", "/?"}:
|
|
@@ -151,16 +155,59 @@ class AgentRunSession:
|
|
|
151
155
|
self.session.handle_command(raw, invoked_from_agent=True)
|
|
152
156
|
return not self.session._should_exit
|
|
153
157
|
|
|
154
|
-
def _show_details(self, agent_id: str) -> None:
|
|
158
|
+
def _show_details(self, agent_id: str, *, enable_prompt: bool = True) -> None:
|
|
159
|
+
"""Render the agent's configuration export inside the command palette."""
|
|
155
160
|
try:
|
|
156
|
-
self.session.ctx.invoke(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
self.session.ctx.invoke(
|
|
162
|
+
agents_get_command,
|
|
163
|
+
agent_ref=agent_id,
|
|
164
|
+
instruction_preview=self._instruction_preview_limit,
|
|
160
165
|
)
|
|
166
|
+
if enable_prompt:
|
|
167
|
+
self._prompt_instruction_view_toggle(agent_id)
|
|
168
|
+
self.console.print(
|
|
169
|
+
f"[{HINT_PREFIX_STYLE}]Tip:[/] Continue the conversation in this prompt, or use "
|
|
170
|
+
f"{format_command_hint('/help') or '/help'} for shortcuts."
|
|
171
|
+
)
|
|
161
172
|
except click.ClickException as exc:
|
|
162
173
|
self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
|
|
163
174
|
|
|
175
|
+
def _prompt_instruction_view_toggle(self, agent_id: str) -> None:
|
|
176
|
+
"""Offer a prompt to expand or collapse the instruction preview after details."""
|
|
177
|
+
if not getattr(self.console, "is_terminal", False):
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
while True:
|
|
181
|
+
mode = "expanded" if self._instruction_preview_limit == 0 else "trimmed"
|
|
182
|
+
self.console.print(f"[dim]Instruction view is {mode}. Press Ctrl+T to toggle, Enter to continue.[/dim]")
|
|
183
|
+
try:
|
|
184
|
+
ch = click.getchar()
|
|
185
|
+
except (EOFError, KeyboardInterrupt): # pragma: no cover - defensive guard
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if not self._handle_instruction_toggle_input(agent_id, ch):
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if self._instruction_preview_limit == 0:
|
|
192
|
+
self._instruction_preview_limit = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
193
|
+
self.console.print("")
|
|
194
|
+
|
|
195
|
+
def _handle_instruction_toggle_input(self, agent_id: str, ch: str) -> bool:
|
|
196
|
+
"""Process a single toggle keypress; return False when the loop should exit."""
|
|
197
|
+
if ch in {"\r", "\n"}:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
lowered = ch.lower()
|
|
201
|
+
if lowered == "t" or ch == "\x14": # support literal 't' or Ctrl+T
|
|
202
|
+
self._instruction_preview_limit = (
|
|
203
|
+
DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT if self._instruction_preview_limit == 0 else 0
|
|
204
|
+
)
|
|
205
|
+
self._show_details(agent_id, enable_prompt=False)
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
# Ignore other keys and continue prompting.
|
|
209
|
+
return True
|
|
210
|
+
|
|
164
211
|
def _after_agent_run(self) -> None:
|
|
165
212
|
"""Handle transcript viewer behaviour after a successful run."""
|
|
166
213
|
payload, manifest = self.session._get_last_transcript()
|
|
@@ -206,21 +253,11 @@ class AgentRunSession:
|
|
|
206
253
|
@contextmanager
|
|
207
254
|
def _bind_session_context(self) -> Any:
|
|
208
255
|
"""Temporarily attach this slash session to the Click context."""
|
|
209
|
-
|
|
210
|
-
has_context = isinstance(ctx_obj, dict)
|
|
211
|
-
previous_session = ctx_obj.get("_slash_session") if has_context else None
|
|
212
|
-
if has_context:
|
|
213
|
-
ctx_obj["_slash_session"] = self.session
|
|
214
|
-
try:
|
|
256
|
+
with bind_slash_session_context(self.session.ctx, self.session):
|
|
215
257
|
yield
|
|
216
|
-
finally:
|
|
217
|
-
if has_context:
|
|
218
|
-
if previous_session is None:
|
|
219
|
-
ctx_obj.pop("_slash_session", None)
|
|
220
|
-
else:
|
|
221
|
-
ctx_obj["_slash_session"] = previous_session
|
|
222
258
|
|
|
223
259
|
def _run_agent(self, agent_id: str, message: str) -> None:
|
|
260
|
+
"""Execute the agents run command for the active agent."""
|
|
224
261
|
if not message:
|
|
225
262
|
return
|
|
226
263
|
|
glaip_sdk/cli/slash/prompt.py
CHANGED
|
@@ -123,6 +123,11 @@ def _create_key_bindings(_session: SlashSession) -> Any:
|
|
|
123
123
|
bindings = KeyBindings()
|
|
124
124
|
|
|
125
125
|
def _refresh_completions(buffer: Any) -> None: # type: ignore[no-any-return]
|
|
126
|
+
"""Refresh completions when slash command is typed.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
buffer: Prompt buffer instance.
|
|
130
|
+
"""
|
|
126
131
|
text = buffer.document.text_before_cursor or ""
|
|
127
132
|
if text.startswith("/") and " " not in text:
|
|
128
133
|
buffer.start_completion(select_first=False)
|
|
@@ -172,8 +177,11 @@ def _iter_command_completions(
|
|
|
172
177
|
return []
|
|
173
178
|
|
|
174
179
|
commands = sorted(session._unique_commands.values(), key=lambda c: c.name)
|
|
180
|
+
agent_context = bool(getattr(session, "_current_agent", None))
|
|
175
181
|
|
|
176
182
|
for cmd in commands:
|
|
183
|
+
if getattr(cmd, "agent_only", False) and not agent_context:
|
|
184
|
+
continue
|
|
177
185
|
yield from _generate_command_completions(cmd, prefix, text, seen)
|
|
178
186
|
|
|
179
187
|
|