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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Shared helpers for configuration/account flows."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from glaip_sdk import Client
|
|
8
|
+
from glaip_sdk.branding import PRIMARY, SUCCESS_STYLE, WARNING_STYLE, AIPBranding
|
|
9
|
+
from glaip_sdk.cli.utils import sdk_version
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_branding_header(console: Console, rule_text: str) -> None:
|
|
13
|
+
"""Render the standard CLI branding header with a custom rule text."""
|
|
14
|
+
branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
|
|
15
|
+
heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
|
|
16
|
+
console.print(heading)
|
|
17
|
+
console.print()
|
|
18
|
+
console.print(branding.get_welcome_banner())
|
|
19
|
+
console.rule(rule_text, style=PRIMARY)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_connection(
|
|
23
|
+
api_url: str,
|
|
24
|
+
api_key: str,
|
|
25
|
+
console: Console,
|
|
26
|
+
*,
|
|
27
|
+
abort_on_error: bool = False,
|
|
28
|
+
extra_hint: str | None = None,
|
|
29
|
+
) -> bool:
|
|
30
|
+
"""Test connectivity and report results.
|
|
31
|
+
|
|
32
|
+
Returns True on success, False on handled failures. Raises click.Abort when
|
|
33
|
+
abort_on_error is True and a fatal error occurs.
|
|
34
|
+
"""
|
|
35
|
+
console.print("\n🔌 Testing connection...")
|
|
36
|
+
client: Client | None = None
|
|
37
|
+
try:
|
|
38
|
+
# Import lazily so test patches targeting glaip_sdk.Client are honored
|
|
39
|
+
from importlib import import_module # noqa: PLC0415
|
|
40
|
+
|
|
41
|
+
client_module = import_module("glaip_sdk")
|
|
42
|
+
client = client_module.Client(api_url=api_url, api_key=api_key)
|
|
43
|
+
try:
|
|
44
|
+
agents = client.list_agents()
|
|
45
|
+
console.print(Text(f"✅ Connection successful! Found {len(agents)} agents", style=SUCCESS_STYLE))
|
|
46
|
+
return True
|
|
47
|
+
except Exception as exc: # pragma: no cover - API failures depend on network
|
|
48
|
+
console.print(Text(f"⚠️ Connection established but API call failed: {exc}", style=WARNING_STYLE))
|
|
49
|
+
console.print(" You may need to check your API permissions or network access")
|
|
50
|
+
if extra_hint:
|
|
51
|
+
console.print(extra_hint)
|
|
52
|
+
if abort_on_error:
|
|
53
|
+
raise click.Abort() from exc
|
|
54
|
+
return False
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
console.print(Text(f"❌ Connection failed: {exc}"))
|
|
57
|
+
console.print(" Please check your API URL and key")
|
|
58
|
+
if extra_hint:
|
|
59
|
+
console.print(extra_hint)
|
|
60
|
+
if abort_on_error:
|
|
61
|
+
raise click.Abort() from exc
|
|
62
|
+
return False
|
|
63
|
+
finally:
|
|
64
|
+
if client is not None:
|
|
65
|
+
client.close()
|
|
@@ -10,42 +10,53 @@ import click
|
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
from rich.text import Text
|
|
12
12
|
|
|
13
|
-
from glaip_sdk import
|
|
14
|
-
from glaip_sdk.
|
|
15
|
-
|
|
16
|
-
ERROR_STYLE,
|
|
17
|
-
INFO,
|
|
18
|
-
PRIMARY,
|
|
19
|
-
SUCCESS,
|
|
20
|
-
SUCCESS_STYLE,
|
|
21
|
-
WARNING_STYLE,
|
|
22
|
-
AIPBranding,
|
|
23
|
-
)
|
|
13
|
+
from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
|
|
14
|
+
from glaip_sdk.cli.account_store import get_account_store
|
|
15
|
+
from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
|
|
24
16
|
from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
|
|
17
|
+
from glaip_sdk.cli.hints import format_command_hint
|
|
18
|
+
from glaip_sdk.cli.masking import mask_api_key_display
|
|
25
19
|
from glaip_sdk.cli.rich_helpers import markup_text
|
|
26
|
-
from glaip_sdk.cli.utils import command_hint
|
|
20
|
+
from glaip_sdk.cli.utils import command_hint
|
|
27
21
|
from glaip_sdk.icons import ICON_TOOL
|
|
28
22
|
from glaip_sdk.rich_components import AIPTable
|
|
29
23
|
|
|
30
24
|
console = Console()
|
|
31
25
|
|
|
26
|
+
# Shared deprecation banner for legacy config commands
|
|
27
|
+
CONFIG_DEPRECATION_MSG = (
|
|
28
|
+
f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in a future release. "
|
|
29
|
+
"Use 'aip accounts ...' (list/add/use/remove) or 'aip configure' for the wizard.[/]"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _print_config_deprecation() -> None:
|
|
34
|
+
"""Print a standardized deprecation warning for legacy config commands."""
|
|
35
|
+
console.print(CONFIG_DEPRECATION_MSG)
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
@click.group()
|
|
34
39
|
def config_group() -> None:
|
|
35
40
|
"""Configuration management operations."""
|
|
36
|
-
|
|
41
|
+
_print_config_deprecation()
|
|
37
42
|
|
|
38
43
|
|
|
39
44
|
@config_group.command("list")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
46
|
+
@click.pass_context
|
|
47
|
+
def list_config(ctx: click.Context, output_json: bool) -> None:
|
|
48
|
+
"""List current configuration.
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
Deprecated: run 'aip accounts list' for profile-aware output.
|
|
51
|
+
"""
|
|
52
|
+
console.print(f"[{WARNING_STYLE}]Deprecated: run 'aip accounts list' for profile-aware output.[/]")
|
|
53
|
+
|
|
54
|
+
# Delegate to accounts list by invoking the command
|
|
55
|
+
from glaip_sdk.cli.commands.accounts import accounts_group # noqa: PLC0415
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
list_cmd = accounts_group.get_command(ctx, "list")
|
|
58
|
+
if list_cmd:
|
|
59
|
+
ctx.invoke(list_cmd, output_json=output_json)
|
|
49
60
|
|
|
50
61
|
|
|
51
62
|
CONFIG_VALUE_TYPES: dict[str, str] = {
|
|
@@ -103,14 +114,49 @@ def _coerce_config_value(key: str, raw_value: str) -> str | bool | int | float:
|
|
|
103
114
|
@config_group.command("set")
|
|
104
115
|
@click.argument("key")
|
|
105
116
|
@click.argument("value")
|
|
106
|
-
|
|
107
|
-
""
|
|
108
|
-
|
|
117
|
+
@click.option(
|
|
118
|
+
"--account",
|
|
119
|
+
"account_name",
|
|
120
|
+
help="Account name to set value for (defaults to active account)",
|
|
121
|
+
)
|
|
122
|
+
def set_config(key: str, value: str, account_name: str | None) -> None:
|
|
123
|
+
"""Set a configuration value.
|
|
109
124
|
|
|
125
|
+
For api_url and api_key, this operates on the specified account (or active account).
|
|
126
|
+
Other keys (timeout, history_default_limit) are global settings.
|
|
127
|
+
"""
|
|
128
|
+
# For other keys, use legacy config
|
|
129
|
+
valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
|
|
110
130
|
if key not in valid_keys:
|
|
111
131
|
console.print(f"[{ERROR_STYLE}]Error: Invalid key '{key}'. Valid keys are: {', '.join(valid_keys)}[/]")
|
|
112
132
|
raise click.ClickException(f"Invalid configuration key: {key}")
|
|
113
133
|
|
|
134
|
+
store = get_account_store()
|
|
135
|
+
# For api_url and api_key, update account profile but also mirror to legacy config
|
|
136
|
+
if key in ("api_url", "api_key"):
|
|
137
|
+
target_account = account_name or store.get_active_account() or "default"
|
|
138
|
+
try:
|
|
139
|
+
account = store.get_account(target_account) or {}
|
|
140
|
+
account[key] = value
|
|
141
|
+
store.add_account(
|
|
142
|
+
target_account,
|
|
143
|
+
account.get("api_url", ""),
|
|
144
|
+
account.get("api_key", ""),
|
|
145
|
+
overwrite=True,
|
|
146
|
+
)
|
|
147
|
+
except Exception:
|
|
148
|
+
# If account store persistence fails (e.g., mocked I/O), continue with legacy config
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
# Always update legacy config for backward compatibility and test isolation
|
|
152
|
+
legacy_config = load_config()
|
|
153
|
+
legacy_config[key] = value
|
|
154
|
+
save_config(legacy_config)
|
|
155
|
+
|
|
156
|
+
display_value = _mask_api_key(value) if key == "api_key" else value
|
|
157
|
+
console.print(Text(f"✅ Set {key} = {display_value} for account '{target_account}'", style=SUCCESS_STYLE))
|
|
158
|
+
return
|
|
159
|
+
|
|
114
160
|
coerced_value = _coerce_config_value(key, value)
|
|
115
161
|
config = load_config()
|
|
116
162
|
config[key] = coerced_value
|
|
@@ -126,12 +172,19 @@ def get_config(key: str) -> None:
|
|
|
126
172
|
"""Get a configuration value."""
|
|
127
173
|
config = load_config()
|
|
128
174
|
|
|
129
|
-
|
|
175
|
+
value = config.get(key)
|
|
176
|
+
|
|
177
|
+
# Fallback to account store for api_url/api_key when legacy config lacks the key
|
|
178
|
+
if value is None and key in {"api_url", "api_key"}:
|
|
179
|
+
store = get_account_store()
|
|
180
|
+
active = store.get_active_account() or "default"
|
|
181
|
+
account = store.get_account(active) or {}
|
|
182
|
+
value = account.get(key)
|
|
183
|
+
|
|
184
|
+
if value is None:
|
|
130
185
|
console.print(markup_text(f"[{WARNING_STYLE}]Configuration key '{key}' not found.[/]"))
|
|
131
186
|
raise click.ClickException(f"Configuration key not found: {key}")
|
|
132
187
|
|
|
133
|
-
value = config[key]
|
|
134
|
-
|
|
135
188
|
if key == "api_key":
|
|
136
189
|
console.print(_mask_api_key(value))
|
|
137
190
|
else:
|
|
@@ -189,41 +242,73 @@ def reset_config(force: bool) -> None:
|
|
|
189
242
|
console.print(message)
|
|
190
243
|
|
|
191
244
|
|
|
192
|
-
def _configure_interactive() -> None:
|
|
245
|
+
def _configure_interactive(account_name: str | None = None) -> None:
|
|
193
246
|
"""Shared configuration logic for both configure commands."""
|
|
247
|
+
store = get_account_store()
|
|
248
|
+
|
|
249
|
+
# Determine account name (use provided, active, or default)
|
|
250
|
+
if not account_name:
|
|
251
|
+
account_name = store.get_active_account() or "default"
|
|
252
|
+
|
|
253
|
+
# Get existing account if it exists
|
|
254
|
+
existing = store.get_account(account_name)
|
|
255
|
+
|
|
194
256
|
_render_configuration_header()
|
|
195
|
-
config =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
257
|
+
config = _prompt_configuration_inputs_for_account(existing)
|
|
258
|
+
|
|
259
|
+
# Save to account store
|
|
260
|
+
api_url = config.get("api_url", "")
|
|
261
|
+
api_key = config.get("api_key", "")
|
|
262
|
+
if api_url and api_key:
|
|
263
|
+
store.add_account(account_name, api_url, api_key, overwrite=True)
|
|
264
|
+
console.print(Text(f"\n✅ Configuration saved to account '{account_name}'", style=SUCCESS_STYLE))
|
|
265
|
+
|
|
266
|
+
_test_and_report_connection_for_account(account_name)
|
|
199
267
|
_print_post_configuration_hints()
|
|
268
|
+
# Show active account footer
|
|
269
|
+
from glaip_sdk.cli.commands.accounts import _print_active_account_footer # noqa: PLC0415
|
|
270
|
+
|
|
271
|
+
_print_active_account_footer(store)
|
|
200
272
|
|
|
201
273
|
|
|
202
274
|
@config_group.command()
|
|
203
|
-
|
|
204
|
-
""
|
|
205
|
-
|
|
275
|
+
@click.option(
|
|
276
|
+
"--account",
|
|
277
|
+
"account_name",
|
|
278
|
+
help="Account name to configure (defaults to active account)",
|
|
279
|
+
)
|
|
280
|
+
def configure(account_name: str | None) -> None:
|
|
281
|
+
"""Configure AIP CLI credentials and settings interactively.
|
|
282
|
+
|
|
283
|
+
This command is an alias for 'aip accounts add <name>' and will
|
|
284
|
+
configure the specified account (or active account if not specified).
|
|
285
|
+
"""
|
|
286
|
+
_configure_interactive(account_name)
|
|
206
287
|
|
|
207
288
|
|
|
208
289
|
# Alias command for backward compatibility
|
|
209
290
|
@click.command()
|
|
210
|
-
|
|
291
|
+
@click.option(
|
|
292
|
+
"--account",
|
|
293
|
+
"account_name",
|
|
294
|
+
help="Account name to configure (defaults to active account)",
|
|
295
|
+
)
|
|
296
|
+
def configure_command(account_name: str | None) -> None:
|
|
211
297
|
"""Configure AIP CLI credentials and settings interactively.
|
|
212
298
|
|
|
213
299
|
This is an alias for 'aip config configure' for backward compatibility.
|
|
300
|
+
For multi-account support, use 'aip accounts add <name>' instead.
|
|
214
301
|
"""
|
|
302
|
+
console.print(
|
|
303
|
+
f"[{WARNING_STYLE}]Setup tip:[/] Prefer 'aip accounts add <name>' or 'aip configure' from your terminal for "
|
|
304
|
+
"multi-account setup. Launching the interactive wizard now..."
|
|
305
|
+
)
|
|
215
306
|
# Delegate to the shared function
|
|
216
|
-
_configure_interactive()
|
|
307
|
+
_configure_interactive(account_name)
|
|
217
308
|
|
|
218
309
|
|
|
219
310
|
# Note: The config command group should be registered in main.py
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def _mask_api_key(value: str | None) -> str:
|
|
223
|
-
"""Return a redacted API key string suitable for display."""
|
|
224
|
-
if not value:
|
|
225
|
-
return ""
|
|
226
|
-
return "***" + value[-4:] if len(value) > 4 else "***"
|
|
311
|
+
_mask_api_key = mask_api_key_display
|
|
227
312
|
|
|
228
313
|
|
|
229
314
|
def _print_missing_config_hint() -> None:
|
|
@@ -250,23 +335,23 @@ def _render_config_table(config: dict[str, str]) -> None:
|
|
|
250
335
|
|
|
251
336
|
def _render_configuration_header() -> None:
|
|
252
337
|
"""Display the interactive configuration heading/banner."""
|
|
253
|
-
|
|
254
|
-
heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
|
|
255
|
-
console.print(heading)
|
|
256
|
-
console.print()
|
|
257
|
-
console.print(branding.get_welcome_banner())
|
|
258
|
-
console.rule("[bold]AIP Configuration[/bold]", style=PRIMARY)
|
|
338
|
+
render_branding_header(console, "[bold]AIP Configuration[/bold]")
|
|
259
339
|
|
|
260
340
|
|
|
261
|
-
def
|
|
262
|
-
"""Interactively prompt for configuration values."""
|
|
341
|
+
def _prompt_configuration_inputs_for_account(existing: dict[str, str] | None) -> dict[str, str]:
|
|
342
|
+
"""Interactively prompt for account configuration values."""
|
|
263
343
|
console.print("\n[bold]Enter your AIP configuration:[/bold]")
|
|
264
|
-
|
|
344
|
+
if existing:
|
|
345
|
+
console.print("(Leave blank to keep current values)")
|
|
265
346
|
console.print("─" * 50)
|
|
266
347
|
|
|
348
|
+
config = existing.copy() if existing else {}
|
|
349
|
+
|
|
267
350
|
_prompt_api_url(config)
|
|
268
351
|
_prompt_api_key(config)
|
|
269
352
|
|
|
353
|
+
return config
|
|
354
|
+
|
|
270
355
|
|
|
271
356
|
def _prompt_api_url(config: dict[str, str]) -> None:
|
|
272
357
|
"""Ask the user for the API URL, preserving existing values by default."""
|
|
@@ -290,43 +375,24 @@ def _prompt_api_key(config: dict[str, str]) -> None:
|
|
|
290
375
|
config["api_key"] = new_key
|
|
291
376
|
|
|
292
377
|
|
|
293
|
-
def
|
|
294
|
-
"""
|
|
295
|
-
|
|
296
|
-
|
|
378
|
+
def _test_and_report_connection_for_account(account_name: str) -> None:
|
|
379
|
+
"""Sanity-check the provided credentials against the backend."""
|
|
380
|
+
store = get_account_store()
|
|
381
|
+
account = store.get_account(account_name)
|
|
382
|
+
if not account:
|
|
383
|
+
return
|
|
297
384
|
|
|
385
|
+
api_url = account.get("api_url", "")
|
|
386
|
+
api_key = account.get("api_key", "")
|
|
387
|
+
if not api_url or not api_key:
|
|
388
|
+
return
|
|
298
389
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
try:
|
|
306
|
-
agents = client.list_agents()
|
|
307
|
-
console.print(
|
|
308
|
-
Text(
|
|
309
|
-
f"✅ Connection successful! Found {len(agents)} agents",
|
|
310
|
-
style=SUCCESS_STYLE,
|
|
311
|
-
)
|
|
312
|
-
)
|
|
313
|
-
except Exception as exc: # pragma: no cover - API failures depend on network
|
|
314
|
-
console.print(
|
|
315
|
-
Text(
|
|
316
|
-
f"⚠️ Connection established but API call failed: {exc}",
|
|
317
|
-
style=WARNING_STYLE,
|
|
318
|
-
)
|
|
319
|
-
)
|
|
320
|
-
console.print(" You may need to check your API permissions or network access")
|
|
321
|
-
except Exception as exc:
|
|
322
|
-
console.print(Text(f"❌ Connection failed: {exc}"))
|
|
323
|
-
console.print(" Please check your API URL and key")
|
|
324
|
-
hint_status = command_hint("status", slash_command="status")
|
|
325
|
-
if hint_status:
|
|
326
|
-
console.print(f" You can run {format_command_hint(hint_status) or hint_status} later to test again")
|
|
327
|
-
finally:
|
|
328
|
-
if client is not None:
|
|
329
|
-
client.close()
|
|
390
|
+
hint_status = command_hint("status", slash_command="status")
|
|
391
|
+
extra_hint = None
|
|
392
|
+
if hint_status:
|
|
393
|
+
extra_hint = f" You can run {format_command_hint(hint_status) or hint_status} later to test again"
|
|
394
|
+
|
|
395
|
+
check_connection(api_url, api_key, console, abort_on_error=False, extra_hint=extra_hint)
|
|
330
396
|
|
|
331
397
|
|
|
332
398
|
def _print_post_configuration_hints() -> None:
|
glaip_sdk/cli/commands/mcps.py
CHANGED
|
@@ -63,6 +63,7 @@ from glaip_sdk.utils.serialization import (
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
console = Console()
|
|
66
|
+
MAX_DESCRIPTION_LEN = 50
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
def _is_sensitive_data(val: Any) -> bool:
|
|
@@ -879,64 +880,210 @@ def get(
|
|
|
879
880
|
raise click.ClickException(str(e)) from e
|
|
880
881
|
|
|
881
882
|
|
|
883
|
+
def _get_tools_from_config(ctx: Any, client: Any, config_file: str) -> tuple[list[dict[str, Any]], str]:
|
|
884
|
+
"""Get tools from MCP config file.
|
|
885
|
+
|
|
886
|
+
Args:
|
|
887
|
+
ctx: Click context
|
|
888
|
+
client: GlaIP client instance
|
|
889
|
+
config_file: Path to config file
|
|
890
|
+
|
|
891
|
+
Returns:
|
|
892
|
+
Tuple of (tools list, title string)
|
|
893
|
+
"""
|
|
894
|
+
config_data = load_resource_from_file_with_validation(Path(config_file), "MCP config")
|
|
895
|
+
|
|
896
|
+
# Validate config structure
|
|
897
|
+
transport = config_data.get("transport")
|
|
898
|
+
if "config" not in config_data:
|
|
899
|
+
raise click.ClickException("Invalid MCP config: missing 'config' section in the file.")
|
|
900
|
+
config_data["config"] = validate_mcp_config_structure(
|
|
901
|
+
config_data["config"],
|
|
902
|
+
transport=transport,
|
|
903
|
+
source=config_file,
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Get tools from config without saving
|
|
907
|
+
with spinner_context(
|
|
908
|
+
ctx,
|
|
909
|
+
"[bold blue]Fetching tools from config…[/bold blue]",
|
|
910
|
+
console_override=console,
|
|
911
|
+
):
|
|
912
|
+
tools = client.mcps.get_mcp_tools_from_config(config_data)
|
|
913
|
+
|
|
914
|
+
title = f"{ICON_TOOL} Tools from config: {Path(config_file).name}"
|
|
915
|
+
return tools, title
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _get_tools_from_mcp(ctx: Any, client: Any, mcp_ref: str | None) -> tuple[list[dict[str, Any]], str]:
|
|
919
|
+
"""Get tools from saved MCP.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
ctx: Click context
|
|
923
|
+
client: GlaIP client instance
|
|
924
|
+
mcp_ref: MCP reference (ID or name)
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
Tuple of (tools list, title string)
|
|
928
|
+
"""
|
|
929
|
+
mcp = _resolve_mcp(ctx, client, mcp_ref)
|
|
930
|
+
|
|
931
|
+
with spinner_context(
|
|
932
|
+
ctx,
|
|
933
|
+
"[bold blue]Fetching MCP tools…[/bold blue]",
|
|
934
|
+
console_override=console,
|
|
935
|
+
):
|
|
936
|
+
tools = client.mcps.get_mcp_tools(mcp.id)
|
|
937
|
+
|
|
938
|
+
title = f"{ICON_TOOL} Tools from MCP: {mcp.name}"
|
|
939
|
+
return tools, title
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def _output_tool_names(ctx: Any, tools: list[dict[str, Any]]) -> None:
|
|
943
|
+
"""Output only tool names.
|
|
944
|
+
|
|
945
|
+
Args:
|
|
946
|
+
ctx: Click context
|
|
947
|
+
tools: List of tool dictionaries
|
|
948
|
+
"""
|
|
949
|
+
view = get_ctx_value(ctx, "view", "rich")
|
|
950
|
+
tool_names = [tool.get("name", "N/A") for tool in tools]
|
|
951
|
+
|
|
952
|
+
if view == "json":
|
|
953
|
+
handle_json_output(ctx, tool_names)
|
|
954
|
+
elif view == "plain":
|
|
955
|
+
if tool_names:
|
|
956
|
+
for name in tool_names:
|
|
957
|
+
console.print(name, markup=False)
|
|
958
|
+
console.print(f"Total: {len(tool_names)} tools", markup=False)
|
|
959
|
+
else:
|
|
960
|
+
console.print("No tools found", markup=False)
|
|
961
|
+
else:
|
|
962
|
+
if tool_names:
|
|
963
|
+
for name in tool_names:
|
|
964
|
+
console.print(name)
|
|
965
|
+
console.print(f"[dim]Total: {len(tool_names)} tools[/dim]")
|
|
966
|
+
else:
|
|
967
|
+
console.print("[yellow]No tools found[/yellow]")
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
|
|
971
|
+
"""Transform a tool dictionary to a display row dictionary.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
tool: Tool dictionary to transform.
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
Dictionary with name and description fields.
|
|
978
|
+
"""
|
|
979
|
+
description = tool.get("description", "N/A")
|
|
980
|
+
if len(description) > MAX_DESCRIPTION_LEN:
|
|
981
|
+
description = description[: MAX_DESCRIPTION_LEN - 3] + "..."
|
|
982
|
+
return {
|
|
983
|
+
"name": tool.get("name", "N/A"),
|
|
984
|
+
"description": description,
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def _output_tools_table(ctx: Any, tools: list[dict[str, Any]], title: str) -> None:
|
|
989
|
+
"""Output tools in table format.
|
|
990
|
+
|
|
991
|
+
Args:
|
|
992
|
+
ctx: Click context
|
|
993
|
+
tools: List of tool dictionaries
|
|
994
|
+
title: Table title
|
|
995
|
+
"""
|
|
996
|
+
columns = [
|
|
997
|
+
("name", "Name", ACCENT_STYLE, None),
|
|
998
|
+
("description", "Description", INFO, 50),
|
|
999
|
+
]
|
|
1000
|
+
|
|
1001
|
+
output_list(
|
|
1002
|
+
ctx,
|
|
1003
|
+
tools,
|
|
1004
|
+
title,
|
|
1005
|
+
columns,
|
|
1006
|
+
_transform_tool,
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def _validate_tool_command_args(mcp_ref: str | None, config_file: str | None) -> None:
|
|
1011
|
+
"""Validate that exactly one of mcp_ref or config_file is provided.
|
|
1012
|
+
|
|
1013
|
+
Args:
|
|
1014
|
+
mcp_ref: MCP reference (ID or name)
|
|
1015
|
+
config_file: Path to config file
|
|
1016
|
+
|
|
1017
|
+
Raises:
|
|
1018
|
+
ClickException: If validation fails
|
|
1019
|
+
"""
|
|
1020
|
+
if not mcp_ref and not config_file:
|
|
1021
|
+
raise click.ClickException(
|
|
1022
|
+
"Either MCP_REF or --from-config must be provided.\n"
|
|
1023
|
+
"Examples:\n"
|
|
1024
|
+
" aip mcps tools <MCP_ID>\n"
|
|
1025
|
+
" aip mcps tools --from-config mcp-config.json"
|
|
1026
|
+
)
|
|
1027
|
+
if mcp_ref and config_file:
|
|
1028
|
+
raise click.ClickException(
|
|
1029
|
+
"Cannot use both MCP_REF and --from-config at the same time.\n"
|
|
1030
|
+
"Use either:\n"
|
|
1031
|
+
" aip mcps tools <MCP_ID>\n"
|
|
1032
|
+
" aip mcps tools --from-config mcp-config.json"
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
|
|
882
1036
|
@mcps_group.command("tools")
|
|
883
|
-
@click.argument("mcp_ref")
|
|
1037
|
+
@click.argument("mcp_ref", required=False)
|
|
1038
|
+
@click.option(
|
|
1039
|
+
"--from-config",
|
|
1040
|
+
"--config",
|
|
1041
|
+
"config_file",
|
|
1042
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
1043
|
+
help="Get tools from MCP config file without saving to DB (JSON or YAML)",
|
|
1044
|
+
)
|
|
1045
|
+
@click.option(
|
|
1046
|
+
"--names-only",
|
|
1047
|
+
is_flag=True,
|
|
1048
|
+
help="Show only tool names (useful for allowed_tools config)",
|
|
1049
|
+
)
|
|
884
1050
|
@output_flags()
|
|
885
1051
|
@click.pass_context
|
|
886
|
-
def list_tools(ctx: Any, mcp_ref: str) -> None:
|
|
887
|
-
"""List tools available from a specific MCP.
|
|
1052
|
+
def list_tools(ctx: Any, mcp_ref: str | None, config_file: str | None, names_only: bool) -> None:
|
|
1053
|
+
"""List tools available from a specific MCP or config file.
|
|
888
1054
|
|
|
889
1055
|
Args:
|
|
890
1056
|
ctx: Click context containing output format preferences
|
|
891
|
-
mcp_ref: MCP reference (ID or name)
|
|
1057
|
+
mcp_ref: MCP reference (ID or name) - required if --from-config not used
|
|
1058
|
+
config_file: Path to MCP config file - alternative to mcp_ref
|
|
1059
|
+
names_only: Show only tool names instead of full table
|
|
892
1060
|
|
|
893
1061
|
Raises:
|
|
894
1062
|
ClickException: If MCP not found or tools fetch fails
|
|
895
|
-
"""
|
|
896
|
-
try:
|
|
897
|
-
client = get_client(ctx)
|
|
898
1063
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
# Get tools from MCP
|
|
903
|
-
with spinner_context(
|
|
904
|
-
ctx,
|
|
905
|
-
"[bold blue]Fetching MCP tools…[/bold blue]",
|
|
906
|
-
console_override=console,
|
|
907
|
-
):
|
|
908
|
-
tools = client.mcps.get_mcp_tools(mcp.id)
|
|
909
|
-
|
|
910
|
-
# Define table columns: (data_key, header, style, width)
|
|
911
|
-
columns = [
|
|
912
|
-
("name", "Name", ACCENT_STYLE, None),
|
|
913
|
-
("description", "Description", INFO, 50),
|
|
914
|
-
]
|
|
1064
|
+
Examples:
|
|
1065
|
+
Get tools from saved MCP:
|
|
1066
|
+
aip mcps tools <MCP_ID>
|
|
915
1067
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
"""Transform a tool dictionary to a display row dictionary.
|
|
1068
|
+
Get tools from config file (without saving to DB):
|
|
1069
|
+
aip mcps tools --from-config mcp-config.json
|
|
919
1070
|
|
|
920
|
-
|
|
921
|
-
|
|
1071
|
+
Get just tool names for allowed_tools config:
|
|
1072
|
+
aip mcps tools <MCP_ID> --names-only
|
|
1073
|
+
"""
|
|
1074
|
+
try:
|
|
1075
|
+
_validate_tool_command_args(mcp_ref, config_file)
|
|
1076
|
+
client = get_client(ctx)
|
|
922
1077
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
"name": tool.get("name", "N/A"),
|
|
928
|
-
"description": tool.get("description", "N/A")[:47] + "..."
|
|
929
|
-
if len(tool.get("description", "")) > 47
|
|
930
|
-
else tool.get("description", "N/A"),
|
|
931
|
-
}
|
|
1078
|
+
if config_file:
|
|
1079
|
+
tools, title = _get_tools_from_config(ctx, client, config_file)
|
|
1080
|
+
else:
|
|
1081
|
+
tools, title = _get_tools_from_mcp(ctx, client, mcp_ref)
|
|
932
1082
|
|
|
933
|
-
|
|
934
|
-
ctx,
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
columns,
|
|
938
|
-
transform_tool,
|
|
939
|
-
)
|
|
1083
|
+
if names_only:
|
|
1084
|
+
_output_tool_names(ctx, tools)
|
|
1085
|
+
else:
|
|
1086
|
+
_output_tools_table(ctx, tools, title)
|
|
940
1087
|
|
|
941
1088
|
except Exception as e:
|
|
942
1089
|
raise click.ClickException(str(e)) from e
|
|
@@ -36,8 +36,8 @@ from glaip_sdk.cli.transcript.history import (
|
|
|
36
36
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
37
37
|
from glaip_sdk.cli.utils import format_size, get_ctx_value, parse_json_line
|
|
38
38
|
from glaip_sdk.rich_components import AIPTable
|
|
39
|
+
from glaip_sdk.utils.rendering.layout.panels import create_final_panel
|
|
39
40
|
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
40
|
-
from glaip_sdk.utils.rendering.renderer.panels import create_final_panel
|
|
41
41
|
|
|
42
42
|
console = Console()
|
|
43
43
|
|