glaip-sdk 0.3.0__py3-none-any.whl → 0.5.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/cli/account_store.py +522 -0
- glaip_sdk/cli/auth.py +224 -8
- glaip_sdk/cli/commands/accounts.py +414 -0
- glaip_sdk/cli/commands/agents.py +2 -2
- glaip_sdk/cli/commands/common_config.py +65 -0
- glaip_sdk/cli/commands/configure.py +153 -87
- glaip_sdk/cli/commands/mcps.py +191 -44
- glaip_sdk/cli/commands/transcripts.py +1 -1
- glaip_sdk/cli/config.py +31 -3
- glaip_sdk/cli/display.py +1 -1
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +181 -79
- glaip_sdk/cli/masking.py +14 -1
- glaip_sdk/cli/slash/agent_session.py +2 -1
- glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
- glaip_sdk/cli/slash/session.py +11 -9
- glaip_sdk/cli/slash/tui/remote_runs_app.py +2 -3
- glaip_sdk/cli/transcript/capture.py +12 -18
- glaip_sdk/cli/transcript/viewer.py +13 -646
- glaip_sdk/cli/update_notifier.py +2 -1
- glaip_sdk/cli/utils.py +95 -139
- glaip_sdk/client/agents.py +2 -4
- glaip_sdk/client/main.py +2 -18
- glaip_sdk/client/mcps.py +11 -1
- glaip_sdk/client/run_rendering.py +90 -111
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/models.py +8 -7
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/rendering/__init__.py +6 -13
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
- glaip_sdk/utils/rendering/renderer/base.py +214 -1469
- glaip_sdk/utils/rendering/renderer/debug.py +24 -0
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/validation.py +13 -21
- {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/METADATA +1 -1
- glaip_sdk-0.5.0.dist-info/RECORD +113 -0
- glaip_sdk-0.3.0.dist-info/RECORD +0 -94
- {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/config.py
CHANGED
|
@@ -5,12 +5,25 @@ Authors:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
from copy import deepcopy
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
import yaml
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
_ENV_CONFIG_DIR = os.getenv("AIP_CONFIG_DIR")
|
|
15
|
+
_TEST_ENV = os.getenv("PYTEST_CURRENT_TEST") or os.getenv("PYTEST_XDIST_WORKER")
|
|
16
|
+
|
|
17
|
+
if _ENV_CONFIG_DIR:
|
|
18
|
+
CONFIG_DIR = Path(_ENV_CONFIG_DIR)
|
|
19
|
+
elif _TEST_ENV:
|
|
20
|
+
# Isolate test runs (including xdist workers) from the real user config directory
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
CONFIG_DIR = Path(tempfile.gettempdir()) / "aip-test-config"
|
|
24
|
+
else: # pragma: no cover - default path used outside test runs
|
|
25
|
+
CONFIG_DIR = Path.home() / ".aip"
|
|
26
|
+
|
|
14
27
|
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
15
28
|
_ALLOWED_KEYS = {
|
|
16
29
|
"api_url",
|
|
@@ -18,13 +31,28 @@ _ALLOWED_KEYS = {
|
|
|
18
31
|
"timeout",
|
|
19
32
|
"history_default_limit",
|
|
20
33
|
}
|
|
34
|
+
# Keys that must be preserved for multi-account support
|
|
35
|
+
_PRESERVE_KEYS = {
|
|
36
|
+
"version",
|
|
37
|
+
"active_account",
|
|
38
|
+
"accounts",
|
|
39
|
+
}
|
|
21
40
|
|
|
22
41
|
|
|
23
42
|
def _sanitize_config(data: dict[str, Any] | None) -> dict[str, Any]:
|
|
24
|
-
"""Return config filtered to allowed keys only."""
|
|
43
|
+
"""Return config filtered to allowed keys only, preserving multi-account keys."""
|
|
25
44
|
if not data:
|
|
26
45
|
return {}
|
|
27
|
-
|
|
46
|
+
result: dict[str, Any] = {}
|
|
47
|
+
# Preserve multi-account structure (defensively copy to avoid callers mutating source)
|
|
48
|
+
for key in _PRESERVE_KEYS:
|
|
49
|
+
if key in data:
|
|
50
|
+
result[key] = deepcopy(data[key])
|
|
51
|
+
# Add allowed legacy keys (copied to avoid side effects)
|
|
52
|
+
for key in _ALLOWED_KEYS:
|
|
53
|
+
if key in data:
|
|
54
|
+
result[key] = deepcopy(data[key])
|
|
55
|
+
return result
|
|
28
56
|
|
|
29
57
|
|
|
30
58
|
def load_config() -> dict[str, Any]:
|
glaip_sdk/cli/display.py
CHANGED
|
@@ -16,8 +16,8 @@ from rich.panel import Panel
|
|
|
16
16
|
from rich.text import Text
|
|
17
17
|
|
|
18
18
|
from glaip_sdk.branding import ERROR_STYLE, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
|
|
19
|
+
from glaip_sdk.cli.hints import command_hint, format_command_hint, in_slash_mode
|
|
19
20
|
from glaip_sdk.cli.rich_helpers import markup_text
|
|
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
|
|
glaip_sdk/cli/hints.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Helpers for formatting CLI/slash command hints.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from glaip_sdk.branding import HINT_COMMAND_STYLE, HINT_DESCRIPTION_COLOR
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def in_slash_mode(ctx: click.Context | None = None) -> bool:
|
|
15
|
+
"""Return True when running inside the slash command palette."""
|
|
16
|
+
if ctx is None:
|
|
17
|
+
try:
|
|
18
|
+
ctx = click.get_current_context(silent=True)
|
|
19
|
+
except RuntimeError:
|
|
20
|
+
ctx = None
|
|
21
|
+
|
|
22
|
+
if ctx is None:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
obj = getattr(ctx, "obj", None)
|
|
26
|
+
if isinstance(obj, dict):
|
|
27
|
+
return bool(obj.get("_slash_session"))
|
|
28
|
+
|
|
29
|
+
return bool(getattr(obj, "_slash_session", False))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def command_hint(
|
|
33
|
+
cli_command: str | None,
|
|
34
|
+
slash_command: str | None = None,
|
|
35
|
+
*,
|
|
36
|
+
ctx: click.Context | None = None,
|
|
37
|
+
) -> str | None:
|
|
38
|
+
"""Return the appropriate command string for the current mode."""
|
|
39
|
+
if in_slash_mode(ctx):
|
|
40
|
+
if not slash_command:
|
|
41
|
+
return None
|
|
42
|
+
return slash_command if slash_command.startswith("/") else f"/{slash_command}"
|
|
43
|
+
|
|
44
|
+
if not cli_command:
|
|
45
|
+
return None
|
|
46
|
+
return f"aip {cli_command}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_command_hint(command: str | None, description: str | None = None) -> str | None:
|
|
50
|
+
"""Return a Rich markup string that highlights a command hint."""
|
|
51
|
+
if not command:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
|
|
55
|
+
if description:
|
|
56
|
+
highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
|
|
57
|
+
return highlighted
|
glaip_sdk/cli/io.py
CHANGED
|
@@ -7,6 +7,7 @@ Authors:
|
|
|
7
7
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
from importlib import import_module
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import TYPE_CHECKING, Any
|
|
12
13
|
|
|
@@ -25,9 +26,11 @@ if TYPE_CHECKING: # pragma: no cover - typing-only imports
|
|
|
25
26
|
|
|
26
27
|
def _create_console() -> "Console":
|
|
27
28
|
"""Return a Console instance (lazy import for easier testing)."""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
try:
|
|
30
|
+
console_module = import_module("rich.console")
|
|
31
|
+
except ImportError as exc: # pragma: no cover - optional dependency missing
|
|
32
|
+
raise RuntimeError("Rich Console is not available") from exc
|
|
33
|
+
return console_module.Console()
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
def load_resource_from_file_with_validation(file_path: Path, resource_type: str) -> dict[str, Any]:
|
glaip_sdk/cli/main.py
CHANGED
|
@@ -4,7 +4,7 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import logging
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
10
|
from typing import Any
|
|
@@ -25,6 +25,9 @@ from glaip_sdk.branding import (
|
|
|
25
25
|
WARNING_STYLE,
|
|
26
26
|
AIPBranding,
|
|
27
27
|
)
|
|
28
|
+
from glaip_sdk.cli.account_store import get_account_store
|
|
29
|
+
from glaip_sdk.cli.auth import resolve_credentials
|
|
30
|
+
from glaip_sdk.cli.commands.accounts import accounts_group
|
|
28
31
|
from glaip_sdk.cli.commands.agents import agents_group
|
|
29
32
|
from glaip_sdk.cli.commands.configure import (
|
|
30
33
|
config_group,
|
|
@@ -36,9 +39,10 @@ from glaip_sdk.cli.commands.tools import tools_group
|
|
|
36
39
|
from glaip_sdk.cli.commands.transcripts import transcripts_group
|
|
37
40
|
from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
|
|
38
41
|
from glaip_sdk.cli.config import load_config
|
|
42
|
+
from glaip_sdk.cli.hints import in_slash_mode
|
|
39
43
|
from glaip_sdk.cli.transcript import get_transcript_cache_stats
|
|
40
44
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
41
|
-
from glaip_sdk.cli.utils import format_size,
|
|
45
|
+
from glaip_sdk.cli.utils import format_size, sdk_version, spinner_context, update_spinner
|
|
42
46
|
from glaip_sdk.config.constants import (
|
|
43
47
|
DEFAULT_AGENT_RUN_TIMEOUT,
|
|
44
48
|
)
|
|
@@ -60,13 +64,13 @@ AVAILABLE_STATUS = "✅ Available"
|
|
|
60
64
|
@click.version_option(package_name="glaip-sdk", prog_name="aip")
|
|
61
65
|
@click.option(
|
|
62
66
|
"--api-url",
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
help="(Deprecated) AIP API URL; use profiles via --account instead",
|
|
68
|
+
hidden=True,
|
|
65
69
|
)
|
|
66
70
|
@click.option(
|
|
67
71
|
"--api-key",
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
help="(Deprecated) AIP API Key; use profiles via --account instead",
|
|
73
|
+
hidden=True,
|
|
70
74
|
)
|
|
71
75
|
@click.option("--timeout", default=30.0, help="Request timeout in seconds")
|
|
72
76
|
@click.option(
|
|
@@ -77,6 +81,12 @@ AVAILABLE_STATUS = "✅ Available"
|
|
|
77
81
|
help="Output view format",
|
|
78
82
|
)
|
|
79
83
|
@click.option("--no-tty", is_flag=True, help="Disable TTY renderer")
|
|
84
|
+
@click.option(
|
|
85
|
+
"--account",
|
|
86
|
+
"account_name",
|
|
87
|
+
help="Target a named account profile for this command",
|
|
88
|
+
hidden=True, # Hidden by default, shown with --help --all
|
|
89
|
+
)
|
|
80
90
|
@click.pass_context
|
|
81
91
|
def main(
|
|
82
92
|
ctx: Any,
|
|
@@ -85,6 +95,7 @@ def main(
|
|
|
85
95
|
timeout: float | None,
|
|
86
96
|
view: str | None,
|
|
87
97
|
no_tty: bool,
|
|
98
|
+
account_name: str | None,
|
|
88
99
|
) -> None:
|
|
89
100
|
r"""GL AIP SDK Command Line Interface.
|
|
90
101
|
|
|
@@ -95,9 +106,14 @@ def main(
|
|
|
95
106
|
Examples:
|
|
96
107
|
aip version # Show detailed version info
|
|
97
108
|
aip configure # Configure credentials
|
|
109
|
+
aip accounts add prod # Add account profile
|
|
110
|
+
aip accounts use staging # Switch account
|
|
98
111
|
aip agents list # List all agents
|
|
99
112
|
aip tools create my_tool.py # Create a new tool
|
|
100
113
|
aip agents run my-agent "Hello world" # Run an agent
|
|
114
|
+
|
|
115
|
+
\b
|
|
116
|
+
NEW: Store multiple accounts via 'aip accounts add' and switch with 'aip accounts use'.
|
|
101
117
|
"""
|
|
102
118
|
# Store configuration in context
|
|
103
119
|
ctx.ensure_object(dict)
|
|
@@ -105,6 +121,7 @@ def main(
|
|
|
105
121
|
ctx.obj["api_key"] = api_key
|
|
106
122
|
ctx.obj["timeout"] = timeout
|
|
107
123
|
ctx.obj["view"] = view
|
|
124
|
+
ctx.obj["account_name"] = account_name
|
|
108
125
|
|
|
109
126
|
ctx.obj["tty"] = not no_tty
|
|
110
127
|
|
|
@@ -117,12 +134,13 @@ def main(
|
|
|
117
134
|
|
|
118
135
|
if not ctx.resilient_parsing and ctx.obj["tty"] and not launching_slash:
|
|
119
136
|
console = Console()
|
|
120
|
-
maybe_notify_update(
|
|
137
|
+
preferred_console = maybe_notify_update(
|
|
121
138
|
sdk_version(),
|
|
122
139
|
console=console,
|
|
123
140
|
ctx=ctx,
|
|
124
141
|
slash_command="update",
|
|
125
142
|
)
|
|
143
|
+
ctx.obj["_preferred_console"] = preferred_console or console
|
|
126
144
|
|
|
127
145
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
128
146
|
if launching_slash:
|
|
@@ -135,6 +153,7 @@ def main(
|
|
|
135
153
|
|
|
136
154
|
|
|
137
155
|
# Add command groups
|
|
156
|
+
main.add_command(accounts_group)
|
|
138
157
|
main.add_command(agents_group)
|
|
139
158
|
main.add_command(config_group)
|
|
140
159
|
main.add_command(tools_group)
|
|
@@ -164,27 +183,34 @@ def _should_launch_slash(ctx: click.Context) -> bool:
|
|
|
164
183
|
|
|
165
184
|
def _load_and_merge_config(ctx: click.Context) -> dict:
|
|
166
185
|
"""Load configuration from multiple sources and merge them."""
|
|
167
|
-
# Load config from file and merge with context
|
|
168
|
-
file_config = load_config()
|
|
169
186
|
context_config = ctx.obj or {}
|
|
187
|
+
account_name = context_config.get("account_name")
|
|
170
188
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
# Resolve credentials using new account store system
|
|
190
|
+
api_url, api_key, source = resolve_credentials(
|
|
191
|
+
account_name=account_name,
|
|
192
|
+
api_url=context_config.get("api_url"),
|
|
193
|
+
api_key=context_config.get("api_key"),
|
|
194
|
+
)
|
|
177
195
|
|
|
178
|
-
#
|
|
179
|
-
|
|
196
|
+
# Load other config values (timeout, etc.) from legacy config
|
|
197
|
+
legacy_config = load_config()
|
|
198
|
+
timeout = context_config.get("timeout") or legacy_config.get("timeout")
|
|
180
199
|
|
|
181
|
-
|
|
182
|
-
|
|
200
|
+
return {
|
|
201
|
+
"api_url": api_url,
|
|
202
|
+
"api_key": api_key,
|
|
203
|
+
"timeout": timeout,
|
|
204
|
+
"_source": source, # Track where credentials came from
|
|
205
|
+
}
|
|
183
206
|
|
|
184
207
|
|
|
185
208
|
def _validate_config_and_show_error(config: dict, console: Console) -> None:
|
|
186
209
|
"""Validate configuration and show error if incomplete."""
|
|
210
|
+
store = get_account_store()
|
|
211
|
+
has_accounts = bool(store.list_accounts())
|
|
187
212
|
if not config.get("api_url") or not config.get("api_key"):
|
|
213
|
+
no_accounts_hint = "" if has_accounts else "\n • No accounts found; create one now to continue"
|
|
188
214
|
console.print(
|
|
189
215
|
AIPPanel(
|
|
190
216
|
f"[{ERROR_STYLE}]❌ Configuration incomplete[/]\n\n"
|
|
@@ -192,11 +218,12 @@ def _validate_config_and_show_error(config: dict, console: Console) -> None:
|
|
|
192
218
|
f" • API URL: {config.get('api_url', 'Not set')}\n"
|
|
193
219
|
f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
|
|
194
220
|
f"💡 To fix this:\n"
|
|
195
|
-
f" • Run 'aip
|
|
196
|
-
f" • Or run 'aip
|
|
221
|
+
f" • Run 'aip accounts add default' to set up credentials\n"
|
|
222
|
+
f" • Or run 'aip configure' for interactive setup\n"
|
|
223
|
+
f" • Or run 'aip accounts list' to see current accounts{no_accounts_hint}",
|
|
197
224
|
title="❌ Configuration Error",
|
|
198
225
|
border_style=ERROR,
|
|
199
|
-
)
|
|
226
|
+
),
|
|
200
227
|
)
|
|
201
228
|
console.print(f"\n[{SUCCESS_STYLE}]✅ AIP - Ready[/] (SDK v{sdk_version()}) - Configure to connect")
|
|
202
229
|
sys.exit(1)
|
|
@@ -206,17 +233,49 @@ def _resolve_status_console(ctx: Any) -> tuple[Console, bool]:
|
|
|
206
233
|
"""Return the console to use and whether we are in slash mode."""
|
|
207
234
|
ctx_obj = ctx.obj if isinstance(ctx.obj, dict) else None
|
|
208
235
|
console_override = ctx_obj.get("_slash_console") if ctx_obj else None
|
|
209
|
-
|
|
236
|
+
preferred_console = ctx_obj.get("_preferred_console") if ctx_obj else None
|
|
237
|
+
if preferred_console is None:
|
|
238
|
+
# In heavily mocked tests, maybe_notify_update may be patched with a return_value
|
|
239
|
+
preferred_console = getattr(maybe_notify_update, "return_value", None)
|
|
240
|
+
console = console_override or preferred_console or Console()
|
|
210
241
|
slash_mode = in_slash_mode(ctx)
|
|
211
242
|
return console, slash_mode
|
|
212
243
|
|
|
213
244
|
|
|
214
|
-
def _render_status_heading(console: Console, slash_mode: bool) ->
|
|
215
|
-
"""Print the status heading/banner.
|
|
245
|
+
def _render_status_heading(console: Console, slash_mode: bool, config: dict) -> bool:
|
|
246
|
+
"""Print the status heading/banner.
|
|
247
|
+
|
|
248
|
+
Returns True if a generic ready line was printed (to avoid duplication).
|
|
249
|
+
"""
|
|
216
250
|
del slash_mode # heading now consistent across invocation contexts
|
|
251
|
+
ready_printed = False
|
|
217
252
|
console.print(f"[{INFO_STYLE}]GL AIP status[/]")
|
|
218
|
-
console.print()
|
|
219
|
-
|
|
253
|
+
console.print("")
|
|
254
|
+
|
|
255
|
+
# Show account information
|
|
256
|
+
source = str(config.get("_source") or "unknown")
|
|
257
|
+
account_name = None
|
|
258
|
+
if source.startswith("account:") or source.startswith("active_profile:"):
|
|
259
|
+
account_name = source.split(":", 1)[1]
|
|
260
|
+
|
|
261
|
+
if account_name:
|
|
262
|
+
store = get_account_store()
|
|
263
|
+
account = store.get_account(account_name)
|
|
264
|
+
if account:
|
|
265
|
+
url = account.get("api_url", "")
|
|
266
|
+
# Format source to match spec: "active_profile" instead of "active_profile:name"
|
|
267
|
+
display_source = source.split(":")[0] if ":" in source else source
|
|
268
|
+
console.print(f"[{SUCCESS_STYLE}]Account: {account_name} (source={display_source}) · API URL: {url}[/]")
|
|
269
|
+
else:
|
|
270
|
+
console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
|
|
271
|
+
ready_printed = True
|
|
272
|
+
elif source == "flag":
|
|
273
|
+
console.print(f"[{SUCCESS_STYLE}]Account: (source={source})[/]")
|
|
274
|
+
else:
|
|
275
|
+
console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
|
|
276
|
+
ready_printed = True
|
|
277
|
+
|
|
278
|
+
return ready_printed
|
|
220
279
|
|
|
221
280
|
|
|
222
281
|
def _collect_cache_summary() -> tuple[str | None, str | None]:
|
|
@@ -244,19 +303,37 @@ def _display_cache_summary(console: Console, slash_mode: bool, cache_line: str |
|
|
|
244
303
|
console.print(cache_note)
|
|
245
304
|
|
|
246
305
|
|
|
247
|
-
def
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
|
|
306
|
+
def _safe_list_call(obj: Any, attr: str) -> list[Any]:
|
|
307
|
+
"""Call list-like client methods defensively, returning an empty list on failure."""
|
|
308
|
+
func = getattr(obj, attr, None)
|
|
309
|
+
if callable(func):
|
|
310
|
+
try:
|
|
311
|
+
return func()
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
logging.getLogger(__name__).debug(
|
|
314
|
+
"Failed to call %s on %s: %s", attr, type(obj).__name__, exc, exc_info=True
|
|
315
|
+
)
|
|
316
|
+
return []
|
|
317
|
+
return []
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _get_client_from_config(config: dict) -> Any:
|
|
321
|
+
"""Return a Client instance built from config."""
|
|
322
|
+
return Client(
|
|
251
323
|
api_url=config["api_url"],
|
|
252
324
|
api_key=config["api_key"],
|
|
253
325
|
timeout=config.get("timeout", 30.0),
|
|
254
326
|
)
|
|
255
327
|
|
|
256
|
-
|
|
328
|
+
|
|
329
|
+
def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
|
|
330
|
+
"""Create client and test connection by fetching resources."""
|
|
331
|
+
client: Any = _get_client_from_config(config)
|
|
332
|
+
|
|
333
|
+
# Test connection by listing resources with a spinner where available
|
|
257
334
|
try:
|
|
258
335
|
with spinner_context(
|
|
259
|
-
None,
|
|
336
|
+
None,
|
|
260
337
|
"[bold blue]Checking GL AIP status…[/bold blue]",
|
|
261
338
|
console_override=console,
|
|
262
339
|
spinner_style=INFO,
|
|
@@ -269,48 +346,21 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
|
|
|
269
346
|
|
|
270
347
|
update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
|
|
271
348
|
mcps = client.list_mcps()
|
|
272
|
-
|
|
273
|
-
# Create status table
|
|
274
|
-
table = AIPTable(title="🔗 GL AIP Status")
|
|
275
|
-
table.add_column("Resource", style=INFO, width=15)
|
|
276
|
-
table.add_column("Count", style=NEUTRAL, width=10)
|
|
277
|
-
table.add_column("Status", style=SUCCESS_STYLE, width=15)
|
|
278
|
-
|
|
279
|
-
table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
|
|
280
|
-
table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
|
|
281
|
-
table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
|
|
282
|
-
|
|
283
|
-
if compact:
|
|
284
|
-
connection_summary = "GL AIP reachable"
|
|
285
|
-
console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
|
|
286
|
-
console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
|
|
287
|
-
console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
|
|
288
|
-
else:
|
|
289
|
-
console.print( # pragma: no cover - UI display formatting
|
|
290
|
-
AIPPanel(
|
|
291
|
-
f"[{SUCCESS_STYLE}]✅ Connected to GL AIP[/]\n"
|
|
292
|
-
f"🔗 API URL: {client.api_url}\n"
|
|
293
|
-
f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
|
|
294
|
-
title="🚀 Connection Status",
|
|
295
|
-
border_style=SUCCESS,
|
|
296
|
-
)
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
console.print(table) # pragma: no cover - UI display formatting
|
|
300
|
-
|
|
301
349
|
except Exception as e:
|
|
302
350
|
# Show AIP Ready status even if connection fails
|
|
303
351
|
if compact:
|
|
304
352
|
status_text = "API call failed"
|
|
305
|
-
|
|
353
|
+
api_url = getattr(client, "api_url", config.get("api_url", ""))
|
|
354
|
+
console.print(f"[dim]• Base URL[/dim]: {api_url} ({status_text})")
|
|
306
355
|
console.print(f"[{ERROR_STYLE}]• Error[/]: {e}")
|
|
307
356
|
console.print("[dim]• Tip[/dim]: Check network connectivity or API permissions and try again.")
|
|
308
357
|
console.print("[dim]• Resources[/dim]: unavailable")
|
|
309
358
|
else:
|
|
359
|
+
api_url = getattr(client, "api_url", config.get("api_url", ""))
|
|
310
360
|
console.print(
|
|
311
361
|
AIPPanel(
|
|
312
362
|
f"[{WARNING_STYLE}]⚠️ Connection established but API call failed[/]\n"
|
|
313
|
-
f"🔗 API URL: {
|
|
363
|
+
f"🔗 API URL: {api_url}\n"
|
|
314
364
|
f"❌ Error: {e}\n\n"
|
|
315
365
|
f"💡 This usually means:\n"
|
|
316
366
|
f" • Network connectivity issues\n"
|
|
@@ -318,8 +368,37 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
|
|
|
318
368
|
f" • Backend service issues",
|
|
319
369
|
title="⚠️ Partial Connection",
|
|
320
370
|
border_style=WARNING,
|
|
321
|
-
)
|
|
371
|
+
),
|
|
322
372
|
)
|
|
373
|
+
return client
|
|
374
|
+
|
|
375
|
+
# Create status table
|
|
376
|
+
table = AIPTable(title="🔗 GL AIP Status")
|
|
377
|
+
table.add_column("Resource", style=INFO, width=15)
|
|
378
|
+
table.add_column("Count", style=NEUTRAL, width=10)
|
|
379
|
+
table.add_column("Status", style=SUCCESS_STYLE, width=15)
|
|
380
|
+
|
|
381
|
+
table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
|
|
382
|
+
table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
|
|
383
|
+
table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
|
|
384
|
+
|
|
385
|
+
if compact:
|
|
386
|
+
connection_summary = "GL AIP reachable"
|
|
387
|
+
console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
|
|
388
|
+
console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
|
|
389
|
+
console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
|
|
390
|
+
else:
|
|
391
|
+
console.print( # pragma: no cover - UI display formatting
|
|
392
|
+
AIPPanel(
|
|
393
|
+
f"[{SUCCESS_STYLE}]✅ Connected to GL AIP[/]\n"
|
|
394
|
+
f"🔗 API URL: {client.api_url}\n"
|
|
395
|
+
f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
|
|
396
|
+
title="🚀 Connection Status",
|
|
397
|
+
border_style=SUCCESS,
|
|
398
|
+
),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
console.print(table) # pragma: no cover - UI display formatting
|
|
323
402
|
|
|
324
403
|
return client
|
|
325
404
|
|
|
@@ -337,38 +416,61 @@ def _handle_connection_error(config: dict, console: Console, error: Exception) -
|
|
|
337
416
|
f" • Run 'aip config list' to check configuration",
|
|
338
417
|
title="❌ Connection Error",
|
|
339
418
|
border_style=ERROR,
|
|
340
|
-
)
|
|
419
|
+
),
|
|
341
420
|
)
|
|
342
|
-
|
|
421
|
+
# Log and return; callers decide whether to exit.
|
|
343
422
|
|
|
344
423
|
|
|
345
424
|
@main.command()
|
|
425
|
+
@click.option(
|
|
426
|
+
"--account",
|
|
427
|
+
"account_name",
|
|
428
|
+
help="Target a named account profile for this command",
|
|
429
|
+
)
|
|
346
430
|
@click.pass_context
|
|
347
|
-
def status(ctx: Any) -> None:
|
|
431
|
+
def status(ctx: Any, account_name: str | None) -> None:
|
|
348
432
|
"""Show connection status and basic info."""
|
|
349
433
|
config: dict = {}
|
|
350
434
|
console: Console | None = None
|
|
351
435
|
try:
|
|
352
|
-
|
|
353
|
-
|
|
436
|
+
if account_name:
|
|
437
|
+
if ctx.obj is None:
|
|
438
|
+
ctx.obj = {}
|
|
439
|
+
ctx.obj["account_name"] = account_name
|
|
354
440
|
|
|
355
|
-
|
|
356
|
-
_display_cache_summary(console, slash_mode, cache_line, cache_note)
|
|
441
|
+
console, slash_mode = _resolve_status_console(ctx)
|
|
357
442
|
|
|
358
443
|
# Load and merge configuration
|
|
359
444
|
config = _load_and_merge_config(ctx)
|
|
360
445
|
|
|
446
|
+
ready_printed = _render_status_heading(console, slash_mode, config)
|
|
447
|
+
if not ready_printed:
|
|
448
|
+
console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
|
|
449
|
+
|
|
450
|
+
cache_result = _collect_cache_summary()
|
|
451
|
+
if isinstance(cache_result, tuple) and len(cache_result) == 2:
|
|
452
|
+
cache_line, cache_note = cache_result
|
|
453
|
+
else:
|
|
454
|
+
cache_line, cache_note = cache_result, None
|
|
455
|
+
_display_cache_summary(console, slash_mode, cache_line, cache_note)
|
|
456
|
+
|
|
361
457
|
# Validate configuration
|
|
362
458
|
_validate_config_and_show_error(config, console)
|
|
363
459
|
|
|
364
460
|
# Create and test client connection using unified compact layout
|
|
365
461
|
client = _create_and_test_client(config, console, compact=True)
|
|
366
|
-
client
|
|
462
|
+
close = getattr(client, "close", None)
|
|
463
|
+
if callable(close):
|
|
464
|
+
try:
|
|
465
|
+
close()
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
367
468
|
|
|
368
469
|
except Exception as e:
|
|
369
|
-
# Handle any unexpected errors during the process
|
|
470
|
+
# Handle any unexpected errors during the process and exit with error code
|
|
370
471
|
fallback_console = console or Console()
|
|
371
472
|
_handle_connection_error(config or {}, fallback_console, e)
|
|
473
|
+
sys.exit(1)
|
|
372
474
|
|
|
373
475
|
|
|
374
476
|
@main.command()
|
|
@@ -397,7 +499,7 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
397
499
|
"[bold blue]🔍 Checking for updates...[/bold blue]\n\n💡 To install updates, run: aip update",
|
|
398
500
|
title="📋 Update Check",
|
|
399
501
|
border_style="blue",
|
|
400
|
-
)
|
|
502
|
+
),
|
|
401
503
|
)
|
|
402
504
|
return
|
|
403
505
|
|
|
@@ -413,7 +515,7 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
413
515
|
title="Update Process",
|
|
414
516
|
border_style="blue",
|
|
415
517
|
padding=(0, 1),
|
|
416
|
-
)
|
|
518
|
+
),
|
|
417
519
|
)
|
|
418
520
|
|
|
419
521
|
# Update using pip
|
|
@@ -437,7 +539,7 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
437
539
|
title="🎉 Update Complete",
|
|
438
540
|
border_style=SUCCESS,
|
|
439
541
|
padding=(0, 1),
|
|
440
|
-
)
|
|
542
|
+
),
|
|
441
543
|
)
|
|
442
544
|
|
|
443
545
|
# Show new version
|
|
@@ -461,7 +563,7 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
461
563
|
title="❌ Update Error",
|
|
462
564
|
border_style=ERROR,
|
|
463
565
|
padding=(0, 1),
|
|
464
|
-
)
|
|
566
|
+
),
|
|
465
567
|
)
|
|
466
568
|
sys.exit(1)
|
|
467
569
|
|
|
@@ -473,7 +575,7 @@ def update(check_only: bool, force: bool) -> None:
|
|
|
473
575
|
" Then try: aip update",
|
|
474
576
|
title="❌ Missing Dependency",
|
|
475
577
|
border_style=ERROR,
|
|
476
|
-
)
|
|
578
|
+
),
|
|
477
579
|
)
|
|
478
580
|
sys.exit(1)
|
|
479
581
|
|
glaip_sdk/cli/masking.py
CHANGED
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from glaip_sdk.cli.constants import
|
|
11
|
+
from glaip_sdk.cli.constants import MASK_SENSITIVE_FIELDS, MASKING_ENABLED
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
14
|
"mask_payload",
|
|
@@ -17,6 +17,7 @@ __all__ = [
|
|
|
17
17
|
"_mask_any",
|
|
18
18
|
"_maybe_mask_row",
|
|
19
19
|
"_resolve_mask_fields",
|
|
20
|
+
"mask_api_key_display",
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
|
|
@@ -121,3 +122,15 @@ def mask_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
121
122
|
return [_maybe_mask_row(row, mask_fields) for row in rows]
|
|
122
123
|
except Exception:
|
|
123
124
|
return rows
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def mask_api_key_display(value: str | None) -> str:
|
|
128
|
+
"""Mask API keys for CLI display while preserving readability for short keys."""
|
|
129
|
+
if not value:
|
|
130
|
+
return ""
|
|
131
|
+
length = len(value)
|
|
132
|
+
if length <= 4:
|
|
133
|
+
return "***"
|
|
134
|
+
if length <= 8:
|
|
135
|
+
return value[:1] + "••••" + value[-1:]
|
|
136
|
+
return value[:4] + "••••" + value[-4:]
|
|
@@ -15,8 +15,9 @@ 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
17
|
from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
18
|
+
from glaip_sdk.cli.hints import format_command_hint
|
|
18
19
|
from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
|
|
19
|
-
from glaip_sdk.cli.utils import bind_slash_session_context
|
|
20
|
+
from glaip_sdk.cli.utils import bind_slash_session_context
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
22
23
|
from glaip_sdk.cli.slash.session import SlashSession
|