glaip-sdk 0.4.0__py3-none-any.whl → 0.5.1__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 +251 -13
- glaip_sdk/cli/commands/accounts.py +414 -0
- glaip_sdk/cli/commands/agents.py +8 -3
- glaip_sdk/cli/commands/common_config.py +65 -0
- glaip_sdk/cli/commands/configure.py +153 -88
- 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 +0 -1
- glaip_sdk/cli/main.py +180 -79
- glaip_sdk/cli/masking.py +14 -1
- glaip_sdk/cli/slash/agent_session.py +1 -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 +11 -17
- glaip_sdk/cli/utils.py +185 -76
- {glaip_sdk-0.4.0.dist-info → glaip_sdk-0.5.1.dist-info}/METADATA +1 -1
- {glaip_sdk-0.4.0.dist-info → glaip_sdk-0.5.1.dist-info}/RECORD +22 -19
- {glaip_sdk-0.4.0.dist-info → glaip_sdk-0.5.1.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.4.0.dist-info → glaip_sdk-0.5.1.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
)
|
|
@@ -59,7 +60,6 @@ from glaip_sdk.cli.transcript import (
|
|
|
59
60
|
maybe_launch_post_run_viewer,
|
|
60
61
|
store_transcript_for_session,
|
|
61
62
|
)
|
|
62
|
-
from glaip_sdk.cli.hints import in_slash_mode
|
|
63
63
|
from glaip_sdk.cli.utils import (
|
|
64
64
|
_fuzzy_pick_for_resources,
|
|
65
65
|
build_renderer,
|
|
@@ -576,7 +576,10 @@ def list_agents(
|
|
|
576
576
|
and len(agents) > 0
|
|
577
577
|
)
|
|
578
578
|
|
|
579
|
+
# Track picker attempt so the fallback table doesn't re-open the palette
|
|
580
|
+
picker_attempted = False
|
|
579
581
|
if interactive_enabled:
|
|
582
|
+
picker_attempted = True
|
|
580
583
|
picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
|
|
581
584
|
if picked_agent:
|
|
582
585
|
_display_agent_details(ctx, client, picked_agent)
|
|
@@ -591,7 +594,9 @@ def list_agents(
|
|
|
591
594
|
f"{ICON_AGENT} Available Agents",
|
|
592
595
|
columns,
|
|
593
596
|
transform_agent,
|
|
594
|
-
skip_picker=
|
|
597
|
+
skip_picker=picker_attempted
|
|
598
|
+
or simple
|
|
599
|
+
or any(param is not None for param in (agent_type, framework, name, version)),
|
|
595
600
|
use_pager=False,
|
|
596
601
|
)
|
|
597
602
|
|
|
@@ -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()
|