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/auth.py
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
"""Authentication export helpers for MCP CLI commands.
|
|
1
|
+
"""Authentication export helpers for MCP CLI commands and credential resolution.
|
|
2
2
|
|
|
3
3
|
This module provides utilities for preparing authentication data for export,
|
|
4
4
|
including interactive secret capture and placeholder generation.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
environment variables.
|
|
6
|
+
It also provides credential resolution for the AIP CLI, supporting multiple
|
|
7
|
+
account profiles and environment variable overrides.
|
|
9
8
|
|
|
10
9
|
Authors:
|
|
11
10
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
12
11
|
"""
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
17
|
import click
|
|
18
18
|
from rich.console import Console
|
|
19
19
|
|
|
20
20
|
from glaip_sdk.branding import HINT_PREFIX_STYLE, WARNING_STYLE
|
|
21
|
-
from glaip_sdk.cli.
|
|
21
|
+
from glaip_sdk.cli.account_store import AccountNotFoundError, AccountStoreError, get_account_store
|
|
22
|
+
from glaip_sdk.cli.hints import format_command_hint
|
|
23
|
+
from glaip_sdk.cli.utils import command_hint
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def prepare_authentication_export(
|
|
@@ -226,8 +228,8 @@ def _build_api_key_headers(auth: dict[str, Any], key_name: str | None, key_value
|
|
|
226
228
|
Returns:
|
|
227
229
|
A dictionary of HTTP headers for API key authentication.
|
|
228
230
|
"""
|
|
229
|
-
header_keys = auth.get("header_keys", [key_name] if key_name else [])
|
|
230
|
-
return
|
|
231
|
+
header_keys = [k for k in auth.get("header_keys", [key_name] if key_name else []) if k]
|
|
232
|
+
return dict.fromkeys(header_keys, key_value)
|
|
231
233
|
|
|
232
234
|
|
|
233
235
|
def _prepare_api_key_auth(
|
|
@@ -458,3 +460,217 @@ def _prompt_secret_with_placeholder(
|
|
|
458
460
|
|
|
459
461
|
# This line is unreachable as the loop always returns
|
|
460
462
|
# return placeholder
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ----------------------------- Credential Resolution ----------------------------- #
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def resolve_api_url_from_context(
|
|
469
|
+
ctx: Any,
|
|
470
|
+
*,
|
|
471
|
+
get_api_url: Callable[[Any], str | None] | None = None,
|
|
472
|
+
get_account_name: Callable[[Any], str | None] | None = None,
|
|
473
|
+
) -> str | None:
|
|
474
|
+
"""Resolve API URL from context using account store (CLI/palette ignores env creds).
|
|
475
|
+
|
|
476
|
+
Helper function to extract API URL from various context formats.
|
|
477
|
+
Used by transcript capture and slash session to avoid code duplication.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
ctx: Context object (can be dict, click.Context, or any object with attributes).
|
|
481
|
+
get_api_url: Optional function to extract api_url from context.
|
|
482
|
+
If None, tries ctx.obj.get("api_url") or ctx.get("api_url").
|
|
483
|
+
get_account_name: Optional function to extract account_name from context.
|
|
484
|
+
If None, tries ctx.obj.get("account_name") or ctx.get("account_name").
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Resolved API URL or None.
|
|
488
|
+
"""
|
|
489
|
+
api_url = None
|
|
490
|
+
account_name = None
|
|
491
|
+
|
|
492
|
+
if get_api_url:
|
|
493
|
+
api_url = get_api_url(ctx)
|
|
494
|
+
elif isinstance(ctx, dict):
|
|
495
|
+
api_url = ctx.get("api_url")
|
|
496
|
+
elif hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
|
|
497
|
+
api_url = ctx.obj.get("api_url")
|
|
498
|
+
|
|
499
|
+
if get_account_name:
|
|
500
|
+
account_name = get_account_name(ctx)
|
|
501
|
+
elif isinstance(ctx, dict):
|
|
502
|
+
account_name = ctx.get("account_name")
|
|
503
|
+
elif hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
|
|
504
|
+
account_name = ctx.obj.get("account_name")
|
|
505
|
+
|
|
506
|
+
resolved_url, _, _ = resolve_credentials(
|
|
507
|
+
account_name=account_name,
|
|
508
|
+
api_url=api_url,
|
|
509
|
+
api_key=None,
|
|
510
|
+
ignore_env_creds=True,
|
|
511
|
+
)
|
|
512
|
+
return resolved_url
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _resolve_account_name(account_name: str | None) -> str | None:
|
|
516
|
+
"""Resolve account name from parameter (env var removed for CLI/palette)."""
|
|
517
|
+
return account_name
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _validate_account_exists(account_name: str | None, store: Any) -> None:
|
|
521
|
+
"""Validate that the specified account exists.
|
|
522
|
+
|
|
523
|
+
Raises:
|
|
524
|
+
AccountNotFoundError: If account_name is specified but account doesn't exist.
|
|
525
|
+
"""
|
|
526
|
+
if account_name:
|
|
527
|
+
account = store.get_account(account_name)
|
|
528
|
+
if not account:
|
|
529
|
+
raise AccountNotFoundError(
|
|
530
|
+
f"Account '{account_name}' not found. Run 'aip accounts list' to see available accounts."
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _merge_credentials(
|
|
535
|
+
api_url: str | None,
|
|
536
|
+
api_key: str | None,
|
|
537
|
+
profile_url: str | None,
|
|
538
|
+
profile_key: str | None,
|
|
539
|
+
ignore_env_creds: bool,
|
|
540
|
+
) -> tuple[str | None, str | None]:
|
|
541
|
+
"""Merge credentials from multiple sources.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
api_url: Explicit API URL override.
|
|
545
|
+
api_key: Explicit API key override.
|
|
546
|
+
profile_url: Profile API URL.
|
|
547
|
+
profile_key: Profile API key.
|
|
548
|
+
ignore_env_creds: If True, ignore env vars.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Tuple of (final_url, final_key).
|
|
552
|
+
"""
|
|
553
|
+
if not ignore_env_creds:
|
|
554
|
+
env_url = os.getenv("AIP_API_URL")
|
|
555
|
+
env_key = os.getenv("AIP_API_KEY")
|
|
556
|
+
final_url = api_url or env_url or profile_url
|
|
557
|
+
final_key = api_key or env_key or profile_key
|
|
558
|
+
else:
|
|
559
|
+
final_url = api_url or profile_url
|
|
560
|
+
final_key = api_key or profile_key
|
|
561
|
+
return final_url, final_key
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _determine_source(
|
|
565
|
+
api_url: str | None,
|
|
566
|
+
api_key: str | None,
|
|
567
|
+
account_name: str | None,
|
|
568
|
+
store: Any,
|
|
569
|
+
) -> str:
|
|
570
|
+
"""Determine the source of credentials.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Source string describing where credentials came from.
|
|
574
|
+
"""
|
|
575
|
+
if api_url or api_key:
|
|
576
|
+
return "flag"
|
|
577
|
+
if account_name:
|
|
578
|
+
return f"account:{account_name}"
|
|
579
|
+
active = store.get_active_account()
|
|
580
|
+
return f"active_profile:{active}" if active else "none"
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
_ENV_WARNING_EMITTED = False
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _maybe_warn_env_creds_ignored(ignore_env_creds: bool) -> None:
|
|
587
|
+
"""Emit a one-time warning when env credentials are present but ignored."""
|
|
588
|
+
global _ENV_WARNING_EMITTED
|
|
589
|
+
|
|
590
|
+
if _ENV_WARNING_EMITTED or not ignore_env_creds:
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
if os.getenv("AIP_API_URL") or os.getenv("AIP_API_KEY"):
|
|
594
|
+
click.echo(
|
|
595
|
+
"Warning: CLI ignores AIP_API_URL/AIP_API_KEY; use account profiles via 'aip accounts add/use'. "
|
|
596
|
+
"Python SDK callers can opt in with ignore_env_creds=False.",
|
|
597
|
+
err=True,
|
|
598
|
+
)
|
|
599
|
+
_ENV_WARNING_EMITTED = True
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def resolve_credentials(
|
|
603
|
+
account_name: str | None = None,
|
|
604
|
+
api_url: str | None = None,
|
|
605
|
+
api_key: str | None = None,
|
|
606
|
+
*,
|
|
607
|
+
ignore_env_creds: bool = True,
|
|
608
|
+
) -> tuple[str | None, str | None, str]:
|
|
609
|
+
"""Resolve credentials from multiple sources with precedence.
|
|
610
|
+
|
|
611
|
+
For CLI/palette: ignores raw credential env vars (AIP_API_URL/AIP_API_KEY),
|
|
612
|
+
and only uses explicit account selection (no AIP_ACCOUNT env). Python SDK can use
|
|
613
|
+
ignore_env_creds=False to honor env vars if needed.
|
|
614
|
+
|
|
615
|
+
Precedence order (CLI/palette):
|
|
616
|
+
1. Explicit parameters (api_url, api_key)
|
|
617
|
+
2. Account profile (from account_name or active_account)
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
account_name: Account name to use, or None for active account.
|
|
621
|
+
api_url: Explicit API URL override.
|
|
622
|
+
api_key: Explicit API key override.
|
|
623
|
+
ignore_env_creds: If True (default), ignore AIP_API_URL/AIP_API_KEY env vars.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Tuple of (api_url, api_key, source) where source describes where
|
|
627
|
+
credentials came from (e.g., "flag", "active_profile", "account:name").
|
|
628
|
+
|
|
629
|
+
Raises:
|
|
630
|
+
click.ClickException: If a requested account does not exist.
|
|
631
|
+
"""
|
|
632
|
+
_maybe_warn_env_creds_ignored(ignore_env_creds)
|
|
633
|
+
|
|
634
|
+
# 1. Explicit parameters take highest precedence
|
|
635
|
+
if api_url and api_key:
|
|
636
|
+
return api_url, api_key, "flag"
|
|
637
|
+
|
|
638
|
+
# 2. Account profile resolution
|
|
639
|
+
account_name = _resolve_account_name(account_name)
|
|
640
|
+
store = get_account_store()
|
|
641
|
+
try:
|
|
642
|
+
_validate_account_exists(account_name, store)
|
|
643
|
+
except AccountNotFoundError as exc:
|
|
644
|
+
raise click.ClickException(str(exc)) from exc
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
profile_url, profile_key = store.get_credentials(account_name)
|
|
648
|
+
except AccountStoreError:
|
|
649
|
+
profile_url, profile_key = None, None
|
|
650
|
+
|
|
651
|
+
final_url, final_key = _merge_credentials(api_url, api_key, profile_url, profile_key, ignore_env_creds)
|
|
652
|
+
source = _determine_source(api_url, api_key, account_name, store)
|
|
653
|
+
|
|
654
|
+
return final_url, final_key, source
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def get_credentials(
|
|
658
|
+
account_name: str | None = None,
|
|
659
|
+
api_url: str | None = None,
|
|
660
|
+
api_key: str | None = None,
|
|
661
|
+
) -> tuple[str | None, str | None]:
|
|
662
|
+
"""Get credentials for CLI commands (backward compatible wrapper).
|
|
663
|
+
|
|
664
|
+
This function maintains backward compatibility with existing code that
|
|
665
|
+
expects (url, key) tuple. For source information, use resolve_credentials.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
account_name: Account name to use, or None for active account.
|
|
669
|
+
api_url: Explicit API URL override.
|
|
670
|
+
api_key: Explicit API key override.
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
Tuple of (api_url, api_key).
|
|
674
|
+
"""
|
|
675
|
+
url, key, _ = resolve_credentials(account_name, api_url, api_key)
|
|
676
|
+
return url, key
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Account management commands for multi-account profiles.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import getpass
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from glaip_sdk.branding import (
|
|
16
|
+
ACCENT_STYLE,
|
|
17
|
+
ERROR_STYLE,
|
|
18
|
+
INFO,
|
|
19
|
+
NEUTRAL,
|
|
20
|
+
SUCCESS,
|
|
21
|
+
SUCCESS_STYLE,
|
|
22
|
+
WARNING_STYLE,
|
|
23
|
+
)
|
|
24
|
+
from glaip_sdk.cli.account_store import (
|
|
25
|
+
AccountNotFoundError,
|
|
26
|
+
AccountStore,
|
|
27
|
+
AccountStoreError,
|
|
28
|
+
InvalidAccountNameError,
|
|
29
|
+
get_account_store,
|
|
30
|
+
)
|
|
31
|
+
from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
|
|
32
|
+
from glaip_sdk.cli.hints import format_command_hint
|
|
33
|
+
from glaip_sdk.cli.masking import mask_api_key_display
|
|
34
|
+
from glaip_sdk.cli.utils import command_hint
|
|
35
|
+
from glaip_sdk.icons import ICON_TOOL
|
|
36
|
+
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group()
|
|
42
|
+
def accounts_group() -> None:
|
|
43
|
+
"""Manage multiple account profiles."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_mask_api_key = mask_api_key_display
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _print_active_account_footer(store: AccountStore) -> None:
|
|
50
|
+
"""Print footer showing active account."""
|
|
51
|
+
active = store.get_active_account()
|
|
52
|
+
if active:
|
|
53
|
+
account = store.get_account(active)
|
|
54
|
+
if account:
|
|
55
|
+
url = account.get("api_url", "")
|
|
56
|
+
masked_key = _mask_api_key(account.get("api_key"))
|
|
57
|
+
console.print(f"\n[{SUCCESS_STYLE}]Active account[/]: {active} · {url} · {masked_key}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@accounts_group.command("list")
|
|
61
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
62
|
+
def list_accounts(output_json: bool) -> None:
|
|
63
|
+
"""List all account profiles."""
|
|
64
|
+
store = get_account_store()
|
|
65
|
+
accounts = store.list_accounts()
|
|
66
|
+
active_account = store.get_active_account()
|
|
67
|
+
|
|
68
|
+
if output_json:
|
|
69
|
+
accounts_list = []
|
|
70
|
+
for name, account in accounts.items():
|
|
71
|
+
accounts_list.append(
|
|
72
|
+
{
|
|
73
|
+
"name": name,
|
|
74
|
+
"api_url": account.get("api_url", ""),
|
|
75
|
+
"has_key": bool(account.get("api_key")),
|
|
76
|
+
"active": name == active_account,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
click.echo(json.dumps(accounts_list, indent=2))
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if not accounts:
|
|
83
|
+
console.print(f"[{WARNING_STYLE}]No accounts found.[/]")
|
|
84
|
+
hint = command_hint("accounts add", slash_command="login")
|
|
85
|
+
if hint:
|
|
86
|
+
console.print(f"Run {format_command_hint(hint) or hint} to add an account.")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Render table
|
|
90
|
+
table = AIPTable(title=f"{ICON_TOOL} AIP Accounts")
|
|
91
|
+
table.add_column("Name", style=INFO, width=20)
|
|
92
|
+
table.add_column("API URL", style=SUCCESS, width=40)
|
|
93
|
+
table.add_column("Key (masked)", style=NEUTRAL, width=20)
|
|
94
|
+
table.add_column("Status", style=SUCCESS_STYLE, width=10)
|
|
95
|
+
|
|
96
|
+
for name, account in sorted(accounts.items()):
|
|
97
|
+
url = account.get("api_url", "")
|
|
98
|
+
masked_key = _mask_api_key(account.get("api_key"))
|
|
99
|
+
is_active = name == active_account
|
|
100
|
+
status = "[bold green]●[/bold green] active" if is_active else ""
|
|
101
|
+
|
|
102
|
+
table.add_row(name, url, masked_key, status)
|
|
103
|
+
|
|
104
|
+
console.print(table)
|
|
105
|
+
|
|
106
|
+
if active_account:
|
|
107
|
+
console.print(f"\n[{SUCCESS_STYLE}]Active account[/]: {active_account}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) -> dict[str, str] | None:
|
|
111
|
+
"""Check if account exists and handle overwrite logic.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
name: Account name.
|
|
115
|
+
store: Account store instance.
|
|
116
|
+
overwrite: Whether to allow overwrite.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Existing account dict or None.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
click.Abort: If account exists and overwrite is False.
|
|
123
|
+
"""
|
|
124
|
+
existing = store.get_account(name)
|
|
125
|
+
if existing and not overwrite:
|
|
126
|
+
console.print(f"[{WARNING_STYLE}]Account '{name}' already exists.[/] Use --yes to overwrite.")
|
|
127
|
+
raise click.Abort()
|
|
128
|
+
return existing
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _get_credentials_non_interactive(url: str, read_key_from_stdin: bool, name: str) -> tuple[str, str]:
|
|
132
|
+
"""Get credentials in non-interactive mode.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
url: API URL from flag.
|
|
136
|
+
read_key_from_stdin: Whether to read key from stdin.
|
|
137
|
+
name: Account name (for error messages).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Tuple of (api_url, api_key).
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
click.Abort: If stdin is required but not available, or if --key used without --url.
|
|
144
|
+
"""
|
|
145
|
+
if read_key_from_stdin:
|
|
146
|
+
if not sys.stdin.isatty():
|
|
147
|
+
return url, sys.stdin.read().strip()
|
|
148
|
+
console.print(
|
|
149
|
+
f"[{ERROR_STYLE}]Error: --key requires stdin input. "
|
|
150
|
+
f"Use: cat key.txt | aip accounts add {name} --url {url} --key[/]",
|
|
151
|
+
)
|
|
152
|
+
raise click.Abort()
|
|
153
|
+
# URL provided, prompt for key
|
|
154
|
+
console.print(f"\n[{ACCENT_STYLE}]AIP API Key[/]:")
|
|
155
|
+
return url, getpass.getpass("> ")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_credentials_interactive(read_key_from_stdin: bool, existing: dict[str, str] | None) -> tuple[str, str]:
|
|
159
|
+
"""Get credentials in interactive mode.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
read_key_from_stdin: Whether --key flag was used.
|
|
163
|
+
existing: Existing account data.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Tuple of (api_url, api_key).
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
click.Abort: If --key used without --url.
|
|
170
|
+
"""
|
|
171
|
+
if read_key_from_stdin:
|
|
172
|
+
console.print(
|
|
173
|
+
f"[{ERROR_STYLE}]Error: --key requires --url. For non-interactive mode, provide both: --url <url> --key[/]",
|
|
174
|
+
)
|
|
175
|
+
raise click.Abort()
|
|
176
|
+
# Fully interactive
|
|
177
|
+
_render_configuration_header()
|
|
178
|
+
return _prompt_account_inputs(existing)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _collect_account_credentials(
|
|
182
|
+
url: str | None,
|
|
183
|
+
read_key_from_stdin: bool,
|
|
184
|
+
name: str,
|
|
185
|
+
existing: dict[str, str] | None,
|
|
186
|
+
) -> tuple[str, str]:
|
|
187
|
+
"""Collect account credentials from various input methods.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
url: Optional URL from flag.
|
|
191
|
+
read_key_from_stdin: Whether to read key from stdin.
|
|
192
|
+
name: Account name (for error messages).
|
|
193
|
+
existing: Existing account data.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Tuple of (api_url, api_key).
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
click.Abort: If credentials cannot be collected or are invalid.
|
|
200
|
+
"""
|
|
201
|
+
if url and read_key_from_stdin:
|
|
202
|
+
# Non-interactive: URL from flag, key from stdin
|
|
203
|
+
api_url, api_key = _get_credentials_non_interactive(url, True, name)
|
|
204
|
+
elif url:
|
|
205
|
+
# URL provided, prompt for key
|
|
206
|
+
api_url, api_key = _get_credentials_non_interactive(url, False, name)
|
|
207
|
+
else:
|
|
208
|
+
# Fully interactive or error case
|
|
209
|
+
api_url, api_key = _get_credentials_interactive(read_key_from_stdin, existing)
|
|
210
|
+
|
|
211
|
+
if not api_url or not api_key:
|
|
212
|
+
console.print(f"[{ERROR_STYLE}]Error: Both API URL and API key are required.[/]")
|
|
213
|
+
raise click.Abort()
|
|
214
|
+
return api_url, api_key
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@accounts_group.command("add")
|
|
218
|
+
@click.argument("name")
|
|
219
|
+
@click.option("--url", help="API URL (required for non-interactive mode)")
|
|
220
|
+
@click.option(
|
|
221
|
+
"--key",
|
|
222
|
+
"read_key_from_stdin",
|
|
223
|
+
is_flag=True,
|
|
224
|
+
help="Read API key from stdin (secure, for scripts). Requires --url.",
|
|
225
|
+
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--yes",
|
|
228
|
+
"overwrite",
|
|
229
|
+
is_flag=True,
|
|
230
|
+
help="Overwrite existing account without prompting",
|
|
231
|
+
)
|
|
232
|
+
def add_account(
|
|
233
|
+
name: str,
|
|
234
|
+
url: str | None,
|
|
235
|
+
read_key_from_stdin: bool,
|
|
236
|
+
overwrite: bool,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Add or update an account profile.
|
|
239
|
+
|
|
240
|
+
NAME is the account name (1-32 chars, alphanumeric, dash, underscore).
|
|
241
|
+
|
|
242
|
+
By default, this command runs interactively, prompting for API URL and key.
|
|
243
|
+
For non-interactive use, both --url and --key (stdin) are required.
|
|
244
|
+
"""
|
|
245
|
+
store = get_account_store()
|
|
246
|
+
|
|
247
|
+
# Check account overwrite
|
|
248
|
+
existing = _check_account_overwrite(name, store, overwrite)
|
|
249
|
+
|
|
250
|
+
# Collect credentials
|
|
251
|
+
api_url, api_key = _collect_account_credentials(url, read_key_from_stdin, name, existing)
|
|
252
|
+
|
|
253
|
+
# Save account
|
|
254
|
+
try:
|
|
255
|
+
store.add_account(name, api_url, api_key, overwrite=True)
|
|
256
|
+
console.print(Text(f"✅ Account '{name}' saved successfully", style=SUCCESS_STYLE))
|
|
257
|
+
_print_active_account_footer(store)
|
|
258
|
+
except InvalidAccountNameError as e:
|
|
259
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
260
|
+
raise click.Abort() from e
|
|
261
|
+
except AccountStoreError as e:
|
|
262
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
263
|
+
raise click.Abort() from e
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@accounts_group.command("use")
|
|
267
|
+
@click.argument("name")
|
|
268
|
+
def use_account(name: str) -> None:
|
|
269
|
+
"""Switch to a different account profile."""
|
|
270
|
+
store = get_account_store()
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
account = store.get_account(name)
|
|
274
|
+
if not account:
|
|
275
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
|
|
276
|
+
raise click.Abort()
|
|
277
|
+
|
|
278
|
+
url = account.get("api_url", "")
|
|
279
|
+
masked_key = _mask_api_key(account.get("api_key"))
|
|
280
|
+
api_key = account.get("api_key", "")
|
|
281
|
+
|
|
282
|
+
if not url or not api_key:
|
|
283
|
+
console.print(
|
|
284
|
+
f"[{ERROR_STYLE}]Error: Account '{name}' is missing credentials. Re-run 'aip accounts add {name}'.[/]"
|
|
285
|
+
)
|
|
286
|
+
raise click.Abort()
|
|
287
|
+
|
|
288
|
+
# Always validate before switching
|
|
289
|
+
check_connection(url, api_key, console, abort_on_error=True)
|
|
290
|
+
|
|
291
|
+
store.set_active_account(name)
|
|
292
|
+
|
|
293
|
+
console.print(
|
|
294
|
+
AIPPanel(
|
|
295
|
+
f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {url}\nKey: {masked_key}",
|
|
296
|
+
title="✅ Account Switched",
|
|
297
|
+
border_style=SUCCESS,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
except click.Abort:
|
|
301
|
+
# check_connection already printed the failure context; just propagate
|
|
302
|
+
raise
|
|
303
|
+
except AccountNotFoundError as e:
|
|
304
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
305
|
+
raise click.Abort() from e
|
|
306
|
+
except Exception as e:
|
|
307
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
308
|
+
raise click.Abort() from e
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@accounts_group.command("rename")
|
|
312
|
+
@click.argument("current_name")
|
|
313
|
+
@click.argument("new_name")
|
|
314
|
+
@click.option(
|
|
315
|
+
"--yes",
|
|
316
|
+
"overwrite",
|
|
317
|
+
is_flag=True,
|
|
318
|
+
help="Overwrite target account if it already exists",
|
|
319
|
+
)
|
|
320
|
+
def rename_account(current_name: str, new_name: str, overwrite: bool) -> None:
|
|
321
|
+
"""Rename an account profile."""
|
|
322
|
+
store = get_account_store()
|
|
323
|
+
|
|
324
|
+
if current_name == new_name:
|
|
325
|
+
console.print(f"[{WARNING_STYLE}]Source and target names are the same; nothing to rename.[/]")
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
if not store.get_account(current_name):
|
|
330
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{current_name}' not found.[/]")
|
|
331
|
+
raise click.Abort()
|
|
332
|
+
|
|
333
|
+
# Guard before calling store.rename_account to keep consistent messaging with add --yes
|
|
334
|
+
if store.get_account(new_name) and not overwrite:
|
|
335
|
+
console.print(f"[{WARNING_STYLE}]Account '{new_name}' already exists.[/] Use --yes to overwrite.")
|
|
336
|
+
raise click.Abort()
|
|
337
|
+
|
|
338
|
+
store.rename_account(current_name, new_name, overwrite=overwrite)
|
|
339
|
+
console.print(Text(f"✅ Account '{current_name}' renamed to '{new_name}'", style=SUCCESS_STYLE))
|
|
340
|
+
_print_active_account_footer(store)
|
|
341
|
+
except AccountStoreError as e:
|
|
342
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
343
|
+
raise click.Abort() from e
|
|
344
|
+
except Exception as e: # pragma: no cover - defensive catch-all
|
|
345
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
346
|
+
raise click.Abort() from e
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@accounts_group.command("remove")
|
|
350
|
+
@click.argument("name")
|
|
351
|
+
@click.option("--yes", "force", is_flag=True, help="Skip confirmation prompt")
|
|
352
|
+
def remove_account(name: str, force: bool) -> None:
|
|
353
|
+
"""Remove an account profile."""
|
|
354
|
+
store = get_account_store()
|
|
355
|
+
|
|
356
|
+
account = store.get_account(name)
|
|
357
|
+
if not account:
|
|
358
|
+
console.print(f"[{WARNING_STYLE}]Account '{name}' not found.[/]")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if not force:
|
|
362
|
+
console.print(f"[{WARNING_STYLE}]This will remove account '{name}'.[/]")
|
|
363
|
+
confirm = input("Are you sure? (y/N): ").strip().lower()
|
|
364
|
+
if confirm not in ["y", "yes"]:
|
|
365
|
+
console.print("Cancelled.")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
store.remove_account(name)
|
|
370
|
+
console.print(Text(f"✅ Account '{name}' removed", style=SUCCESS_STYLE))
|
|
371
|
+
|
|
372
|
+
# Show new active account if it changed
|
|
373
|
+
active = store.get_active_account()
|
|
374
|
+
if active:
|
|
375
|
+
console.print(f"[{SUCCESS_STYLE}]Active account is now: {active}[/]")
|
|
376
|
+
except AccountStoreError as e:
|
|
377
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
378
|
+
raise click.Abort() from e
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _render_configuration_header() -> None:
|
|
382
|
+
"""Display the interactive configuration heading/banner."""
|
|
383
|
+
render_branding_header(console, "[bold]AIP Account Configuration[/bold]")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _prompt_account_inputs(existing: dict[str, str] | None) -> tuple[str, str]:
|
|
387
|
+
"""Interactively prompt for account credentials."""
|
|
388
|
+
console.print("\n[bold]Enter your AIP configuration:[/bold]")
|
|
389
|
+
if existing:
|
|
390
|
+
console.print("(Leave blank to keep current values)")
|
|
391
|
+
console.print("─" * 50)
|
|
392
|
+
|
|
393
|
+
# Prompt for URL
|
|
394
|
+
current_url = existing.get("api_url", "") if existing else ""
|
|
395
|
+
suffix = f"(current: {current_url})" if current_url else ""
|
|
396
|
+
console.print(f"\n[{ACCENT_STYLE}]AIP API URL[/] {suffix}:")
|
|
397
|
+
new_url = input("> ").strip()
|
|
398
|
+
api_url = new_url if new_url else current_url
|
|
399
|
+
if not api_url:
|
|
400
|
+
api_url = "https://your-aip-instance.com"
|
|
401
|
+
|
|
402
|
+
# Prompt for key
|
|
403
|
+
current_key_masked = _mask_api_key(existing.get("api_key")) if existing else ""
|
|
404
|
+
suffix = f"(current: {current_key_masked})" if current_key_masked else ""
|
|
405
|
+
console.print(f"\n[{ACCENT_STYLE}]AIP API Key[/] {suffix}:")
|
|
406
|
+
new_key = getpass.getpass("> ")
|
|
407
|
+
if new_key:
|
|
408
|
+
api_key = new_key
|
|
409
|
+
elif existing:
|
|
410
|
+
api_key = existing.get("api_key", "")
|
|
411
|
+
else:
|
|
412
|
+
api_key = ""
|
|
413
|
+
|
|
414
|
+
return api_url, api_key
|
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -34,8 +34,8 @@ from glaip_sdk.cli.agent_config import (
|
|
|
34
34
|
from glaip_sdk.cli.agent_config import (
|
|
35
35
|
sanitize_agent_config_for_cli as sanitize_agent_config,
|
|
36
36
|
)
|
|
37
|
-
from glaip_sdk.cli.context import get_ctx_value, output_flags
|
|
38
37
|
from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
38
|
+
from glaip_sdk.cli.context import get_ctx_value, output_flags
|
|
39
39
|
from glaip_sdk.cli.display import (
|
|
40
40
|
build_resource_result_data,
|
|
41
41
|
display_agent_run_suggestions,
|
|
@@ -47,6 +47,7 @@ from glaip_sdk.cli.display import (
|
|
|
47
47
|
handle_rich_output,
|
|
48
48
|
print_api_error,
|
|
49
49
|
)
|
|
50
|
+
from glaip_sdk.cli.hints import in_slash_mode
|
|
50
51
|
from glaip_sdk.cli.io import (
|
|
51
52
|
fetch_raw_resource_details,
|
|
52
53
|
)
|
|
@@ -65,7 +66,6 @@ from glaip_sdk.cli.utils import (
|
|
|
65
66
|
coerce_to_row,
|
|
66
67
|
get_client,
|
|
67
68
|
handle_resource_export,
|
|
68
|
-
in_slash_mode,
|
|
69
69
|
output_list,
|
|
70
70
|
output_result,
|
|
71
71
|
spinner_context,
|