glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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/__init__.py +5 -2
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1191 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +265 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +251 -173
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +735 -143
- glaip_sdk/cli/commands/mcps.py +266 -134
- glaip_sdk/cli/commands/models.py +13 -9
- glaip_sdk/cli/commands/tools.py +67 -88
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +49 -7
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +45 -32
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +232 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +12 -19
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +807 -225
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -499
- glaip_sdk/cli/update_notifier.py +177 -24
- glaip_sdk/cli/utils.py +242 -1308
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +53 -37
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +320 -92
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +123 -15
- glaip_sdk/client/run_rendering.py +136 -101
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +163 -34
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +706 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +7 -35
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +3 -6
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
- glaip_sdk/utils/rendering/renderer/base.py +258 -1577
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +10 -51
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
- 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/step_tree_state.py +1 -3
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
- 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/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
- glaip_sdk-0.6.10.dist-info/RECORD +159 -0
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.1.0.dist-info/RECORD +0 -82
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,746 @@
|
|
|
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
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.branding import (
|
|
17
|
+
ACCENT_STYLE,
|
|
18
|
+
ERROR_STYLE,
|
|
19
|
+
INFO,
|
|
20
|
+
NEUTRAL,
|
|
21
|
+
SUCCESS,
|
|
22
|
+
SUCCESS_STYLE,
|
|
23
|
+
WARNING_STYLE,
|
|
24
|
+
)
|
|
25
|
+
from glaip_sdk.cli.account_store import (
|
|
26
|
+
AccountNotFoundError,
|
|
27
|
+
AccountStore,
|
|
28
|
+
AccountStoreError,
|
|
29
|
+
InvalidAccountNameError,
|
|
30
|
+
get_account_store,
|
|
31
|
+
)
|
|
32
|
+
from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
|
|
33
|
+
from glaip_sdk.cli.hints import format_command_hint
|
|
34
|
+
from glaip_sdk.cli.masking import mask_api_key_display
|
|
35
|
+
from glaip_sdk.cli.slash.accounts_shared import env_credentials_present
|
|
36
|
+
from glaip_sdk.cli.utils import command_hint
|
|
37
|
+
from glaip_sdk.icons import ICON_TOOL
|
|
38
|
+
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
39
|
+
|
|
40
|
+
console = Console()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group()
|
|
44
|
+
def accounts_group() -> None:
|
|
45
|
+
"""Manage multiple account profiles."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_mask_api_key = mask_api_key_display
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _print_active_account_footer(store: AccountStore) -> None:
|
|
52
|
+
"""Print footer showing active account."""
|
|
53
|
+
active = store.get_active_account()
|
|
54
|
+
if active:
|
|
55
|
+
account = store.get_account(active)
|
|
56
|
+
if account:
|
|
57
|
+
url = account.get("api_url", "")
|
|
58
|
+
masked_key = _mask_api_key(account.get("api_key"))
|
|
59
|
+
console.print(f"\n[{SUCCESS_STYLE}]Active account[/]: {active} · {url} · {masked_key}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@accounts_group.command("list")
|
|
63
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
64
|
+
def list_accounts(output_json: bool) -> None:
|
|
65
|
+
"""List all account profiles."""
|
|
66
|
+
store = get_account_store()
|
|
67
|
+
accounts = store.list_accounts()
|
|
68
|
+
active_account = store.get_active_account()
|
|
69
|
+
|
|
70
|
+
if output_json:
|
|
71
|
+
accounts_list = []
|
|
72
|
+
for name, account in accounts.items():
|
|
73
|
+
accounts_list.append(
|
|
74
|
+
{
|
|
75
|
+
"name": name,
|
|
76
|
+
"api_url": account.get("api_url", ""),
|
|
77
|
+
"has_key": bool(account.get("api_key")),
|
|
78
|
+
"active": name == active_account,
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
click.echo(json.dumps(accounts_list, indent=2))
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if not accounts:
|
|
85
|
+
console.print(f"[{WARNING_STYLE}]No accounts found.[/]")
|
|
86
|
+
hint = command_hint("accounts add", slash_command="login")
|
|
87
|
+
if hint:
|
|
88
|
+
console.print(f"Run {format_command_hint(hint) or hint} to add an account.")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Render table
|
|
92
|
+
table = AIPTable(title=f"{ICON_TOOL} AIP Accounts")
|
|
93
|
+
table.add_column("Name", style=INFO, width=20)
|
|
94
|
+
table.add_column("API URL", style=SUCCESS, width=40)
|
|
95
|
+
table.add_column("Key (masked)", style=NEUTRAL, width=20)
|
|
96
|
+
table.add_column("Status", style=SUCCESS_STYLE, width=10)
|
|
97
|
+
|
|
98
|
+
for name, account in sorted(accounts.items()):
|
|
99
|
+
url = account.get("api_url", "")
|
|
100
|
+
masked_key = _mask_api_key(account.get("api_key"))
|
|
101
|
+
is_active = name == active_account
|
|
102
|
+
status = "[bold green]●[/bold green] active" if is_active else ""
|
|
103
|
+
|
|
104
|
+
table.add_row(name, url, masked_key, status)
|
|
105
|
+
|
|
106
|
+
console.print(table)
|
|
107
|
+
|
|
108
|
+
if active_account:
|
|
109
|
+
console.print(f"\n[{SUCCESS_STYLE}]Active account[/]: {active_account}")
|
|
110
|
+
|
|
111
|
+
# Show hint for updating accounts
|
|
112
|
+
console.print(f"\n[{INFO}]💡 Tip[/]: To update an account's URL or key, use: [bold]aip accounts edit <name>[/bold]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _build_account_json_payload(
|
|
116
|
+
name: str,
|
|
117
|
+
api_url: str,
|
|
118
|
+
masked_key: str,
|
|
119
|
+
config_path: str,
|
|
120
|
+
is_active: bool,
|
|
121
|
+
env_lock: bool,
|
|
122
|
+
metadata: dict[str, str | None],
|
|
123
|
+
) -> dict[str, str | bool | None]:
|
|
124
|
+
"""Build JSON payload for account display.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name: Account name.
|
|
128
|
+
api_url: API URL.
|
|
129
|
+
masked_key: Masked API key.
|
|
130
|
+
config_path: Config file path.
|
|
131
|
+
is_active: Whether account is active.
|
|
132
|
+
env_lock: Whether env credentials are set.
|
|
133
|
+
metadata: Account metadata dict.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
JSON payload dict.
|
|
137
|
+
"""
|
|
138
|
+
payload: dict[str, str | bool | None] = {
|
|
139
|
+
"name": name,
|
|
140
|
+
"api_url": api_url,
|
|
141
|
+
"api_key_masked": masked_key,
|
|
142
|
+
"config_path": config_path,
|
|
143
|
+
"active": is_active,
|
|
144
|
+
"env_lock": env_lock,
|
|
145
|
+
}
|
|
146
|
+
for key, value in metadata.items():
|
|
147
|
+
if value:
|
|
148
|
+
payload[key] = value
|
|
149
|
+
return payload
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _format_config_path(config_path: str) -> str:
|
|
153
|
+
"""Format config path for display, shortening under home."""
|
|
154
|
+
path_obj = Path(config_path).expanduser()
|
|
155
|
+
try:
|
|
156
|
+
home = Path.home().expanduser()
|
|
157
|
+
resolved = path_obj.resolve(strict=False)
|
|
158
|
+
relative = resolved.relative_to(home).as_posix()
|
|
159
|
+
return f"~/{relative}"
|
|
160
|
+
except ValueError:
|
|
161
|
+
# Not under home; return expanded path
|
|
162
|
+
return str(path_obj)
|
|
163
|
+
except OSError:
|
|
164
|
+
# Fall back to original string on resolution errors
|
|
165
|
+
return config_path
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_account_display_lines(
|
|
169
|
+
name: str,
|
|
170
|
+
api_url: str,
|
|
171
|
+
masked_key: str,
|
|
172
|
+
config_path: str,
|
|
173
|
+
is_active: bool,
|
|
174
|
+
env_lock: bool,
|
|
175
|
+
metadata: dict[str, str | None],
|
|
176
|
+
) -> list[str]:
|
|
177
|
+
"""Build display lines for account information.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: Account name.
|
|
181
|
+
api_url: API URL.
|
|
182
|
+
masked_key: Masked API key.
|
|
183
|
+
config_path: Config file path.
|
|
184
|
+
is_active: Whether account is active.
|
|
185
|
+
env_lock: Whether env credentials are set.
|
|
186
|
+
metadata: Account metadata dict.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of formatted display lines.
|
|
190
|
+
"""
|
|
191
|
+
lines = [
|
|
192
|
+
f"[{SUCCESS_STYLE}]Name[/]: {name}{' (active)' if is_active else ''}",
|
|
193
|
+
f"[{SUCCESS_STYLE}]API URL[/]: {api_url or 'not set'}",
|
|
194
|
+
f"[{SUCCESS_STYLE}]Key[/]: {masked_key or 'not set'}",
|
|
195
|
+
f"[{SUCCESS_STYLE}]Config[/]: {config_path}",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
label_map = {
|
|
199
|
+
"notes": "Notes",
|
|
200
|
+
"last_used_at": "Last used",
|
|
201
|
+
"last_validated_at": "Last validated",
|
|
202
|
+
"created_with": "Created with",
|
|
203
|
+
}
|
|
204
|
+
for key, label in label_map.items():
|
|
205
|
+
value = metadata.get(key)
|
|
206
|
+
if value:
|
|
207
|
+
lines.append(f"[{SUCCESS_STYLE}]{label}[/]: {value}")
|
|
208
|
+
|
|
209
|
+
if env_lock:
|
|
210
|
+
lines.append(
|
|
211
|
+
f"[{WARNING_STYLE}]Env credentials detected (AIP_API_URL/AIP_API_KEY); stored profile may be ignored.[/]"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return lines
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@accounts_group.command("show")
|
|
218
|
+
@click.argument("name")
|
|
219
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
220
|
+
def show_account(name: str, output_json: bool) -> None:
|
|
221
|
+
"""Show details for a single account profile."""
|
|
222
|
+
store = get_account_store()
|
|
223
|
+
account = store.get_account(name)
|
|
224
|
+
|
|
225
|
+
if not account:
|
|
226
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
|
|
227
|
+
raise click.Abort()
|
|
228
|
+
|
|
229
|
+
api_url = account.get("api_url", "")
|
|
230
|
+
api_key = account.get("api_key")
|
|
231
|
+
masked_key = _mask_api_key(api_key or "")
|
|
232
|
+
active_account = store.get_active_account()
|
|
233
|
+
is_active = active_account == name
|
|
234
|
+
env_lock = env_credentials_present(partial=True)
|
|
235
|
+
config_path_raw = str(store.config_file)
|
|
236
|
+
config_path_display = _format_config_path(config_path_raw)
|
|
237
|
+
|
|
238
|
+
metadata = {
|
|
239
|
+
"notes": account.get("notes"),
|
|
240
|
+
"last_used_at": account.get("last_used_at"),
|
|
241
|
+
"last_validated_at": account.get("last_validated_at"),
|
|
242
|
+
"created_with": account.get("created_with"),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if output_json:
|
|
246
|
+
payload = _build_account_json_payload(name, api_url, masked_key, config_path_raw, is_active, env_lock, metadata)
|
|
247
|
+
click.echo(json.dumps(payload, indent=2))
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
lines = _build_account_display_lines(name, api_url, masked_key, config_path_display, is_active, env_lock, metadata)
|
|
251
|
+
|
|
252
|
+
lock_badge = " 🔒 Env lock" if env_lock else ""
|
|
253
|
+
console.print(
|
|
254
|
+
AIPPanel(
|
|
255
|
+
"\n".join(lines),
|
|
256
|
+
title=f"AIP Account{lock_badge}",
|
|
257
|
+
border_style=ACCENT_STYLE,
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) -> dict[str, str] | None:
|
|
263
|
+
"""Check if account exists and handle overwrite logic.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
name: Account name.
|
|
267
|
+
store: Account store instance.
|
|
268
|
+
overwrite: Whether to allow overwrite.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Existing account dict or None.
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
click.Abort: If account exists and overwrite is False.
|
|
275
|
+
"""
|
|
276
|
+
existing = store.get_account(name)
|
|
277
|
+
if existing and not overwrite:
|
|
278
|
+
console.print(f"[{WARNING_STYLE}]Account '{name}' already exists.[/] Use --yes to overwrite.")
|
|
279
|
+
raise click.Abort()
|
|
280
|
+
return existing
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _get_credentials_non_interactive(
|
|
284
|
+
url: str,
|
|
285
|
+
read_key_from_stdin: bool,
|
|
286
|
+
name: str,
|
|
287
|
+
command_name: str = "aip accounts add",
|
|
288
|
+
) -> tuple[str, str]:
|
|
289
|
+
"""Get credentials in non-interactive mode.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
url: API URL from flag.
|
|
293
|
+
read_key_from_stdin: Whether to read key from stdin.
|
|
294
|
+
name: Account name (for error messages).
|
|
295
|
+
command_name: Command name for guidance text.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Tuple of (api_url, api_key).
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
click.Abort: If stdin is required but not available, or if --key used without --url.
|
|
302
|
+
"""
|
|
303
|
+
if read_key_from_stdin:
|
|
304
|
+
if not sys.stdin.isatty():
|
|
305
|
+
return url, sys.stdin.read().strip()
|
|
306
|
+
console.print(
|
|
307
|
+
f"[{ERROR_STYLE}]Error: --key expects stdin or an explicit value. "
|
|
308
|
+
f"Use '--key <value>' or pipe: cat key.txt | {command_name} {name} --url {url} --key[/]",
|
|
309
|
+
)
|
|
310
|
+
raise click.Abort()
|
|
311
|
+
# URL provided, prompt for key
|
|
312
|
+
console.print(f"\n[{ACCENT_STYLE}]AIP API Key[/]:")
|
|
313
|
+
return url, getpass.getpass("> ")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _get_credentials_interactive(read_key_from_stdin: bool, existing: dict[str, str] | None) -> tuple[str, str]:
|
|
317
|
+
"""Get credentials in interactive mode.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
read_key_from_stdin: Whether --key flag was used.
|
|
321
|
+
existing: Existing account data.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Tuple of (api_url, api_key).
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
click.Abort: If --key used without --url.
|
|
328
|
+
"""
|
|
329
|
+
if read_key_from_stdin:
|
|
330
|
+
console.print(
|
|
331
|
+
f"[{ERROR_STYLE}]Error: --key requires --url. "
|
|
332
|
+
f"Provide --url with --key <value|-> for non-interactive use or omit --key to be prompted.[/]",
|
|
333
|
+
)
|
|
334
|
+
raise click.Abort()
|
|
335
|
+
# Fully interactive
|
|
336
|
+
_render_configuration_header()
|
|
337
|
+
return _prompt_account_inputs(existing)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _handle_key_rotation(
|
|
341
|
+
name: str,
|
|
342
|
+
existing_url: str,
|
|
343
|
+
command_name: str,
|
|
344
|
+
) -> tuple[str, str]:
|
|
345
|
+
"""Handle key rotation using stored URL.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
name: Account name (for error messages).
|
|
349
|
+
existing_url: Existing account URL.
|
|
350
|
+
command_name: Command name for error messages.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Tuple of (api_url, api_key).
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
click.Abort: If existing URL is missing.
|
|
357
|
+
"""
|
|
358
|
+
if not existing_url:
|
|
359
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' is missing an API URL. Provide --url to set it.[/]")
|
|
360
|
+
raise click.Abort()
|
|
361
|
+
return _get_credentials_non_interactive(existing_url, True, name, command_name)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _preserve_existing_values(
|
|
365
|
+
api_url: str,
|
|
366
|
+
api_key: str,
|
|
367
|
+
existing_url: str,
|
|
368
|
+
existing_key: str,
|
|
369
|
+
) -> tuple[str, str]:
|
|
370
|
+
"""Preserve stored values when blank input is provided during edit.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
api_url: Collected API URL.
|
|
374
|
+
api_key: Collected API key.
|
|
375
|
+
existing_url: Existing account URL.
|
|
376
|
+
existing_key: Existing account key.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Tuple of (api_url, api_key) with preserved values.
|
|
380
|
+
"""
|
|
381
|
+
if not api_url and existing_url:
|
|
382
|
+
api_url = existing_url
|
|
383
|
+
if not api_key and existing_key:
|
|
384
|
+
api_key = existing_key
|
|
385
|
+
return api_url, api_key
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _collect_credentials_from_inputs(
|
|
389
|
+
url: str | None,
|
|
390
|
+
api_key_input: str | None,
|
|
391
|
+
name: str,
|
|
392
|
+
existing: dict[str, str] | None,
|
|
393
|
+
command_name: str,
|
|
394
|
+
existing_url: str,
|
|
395
|
+
) -> tuple[str, str]:
|
|
396
|
+
"""Collect credentials based on input flags and existing data.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
url: Optional URL from flag.
|
|
400
|
+
api_key_input: API key value from flag (or "-" when stdin requested).
|
|
401
|
+
name: Account name (for error messages).
|
|
402
|
+
existing: Existing account data.
|
|
403
|
+
command_name: Command name for error messages.
|
|
404
|
+
existing_url: Existing account URL.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Tuple of (api_url, api_key).
|
|
408
|
+
"""
|
|
409
|
+
provided_key = api_key_input if api_key_input not in (None, "-") else None
|
|
410
|
+
read_key_from_stdin = api_key_input == "-"
|
|
411
|
+
|
|
412
|
+
if provided_key and url:
|
|
413
|
+
# Fully non-interactive: URL and key provided via flags
|
|
414
|
+
return url, provided_key
|
|
415
|
+
|
|
416
|
+
if provided_key:
|
|
417
|
+
# Reuse stored URL if present; otherwise require --url
|
|
418
|
+
if existing_url:
|
|
419
|
+
return existing_url, provided_key
|
|
420
|
+
if existing:
|
|
421
|
+
console.print(
|
|
422
|
+
f"[{ERROR_STYLE}]Error: Account '{name}' is missing an API URL. "
|
|
423
|
+
f"Provide --url to set it when rotating the key.[/]"
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
console.print(
|
|
427
|
+
f"[{ERROR_STYLE}]Error: --key requires --url for new accounts. "
|
|
428
|
+
f"Run without --key for prompts or pass both flags for non-interactive setup.[/]",
|
|
429
|
+
)
|
|
430
|
+
raise click.Abort()
|
|
431
|
+
|
|
432
|
+
if url and read_key_from_stdin:
|
|
433
|
+
# Non-interactive: URL from flag, key from stdin
|
|
434
|
+
return _get_credentials_non_interactive(url, True, name, command_name)
|
|
435
|
+
if url:
|
|
436
|
+
# URL provided, prompt for key
|
|
437
|
+
return _get_credentials_non_interactive(url, False, name, command_name)
|
|
438
|
+
if read_key_from_stdin and existing:
|
|
439
|
+
# Key rotation using stored URL
|
|
440
|
+
return _handle_key_rotation(name, existing_url, command_name)
|
|
441
|
+
# Fully interactive or error case
|
|
442
|
+
return _get_credentials_interactive(read_key_from_stdin, existing)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _collect_account_credentials(
|
|
446
|
+
url: str | None,
|
|
447
|
+
api_key_input: str | None,
|
|
448
|
+
name: str,
|
|
449
|
+
existing: dict[str, str] | None,
|
|
450
|
+
) -> tuple[str, str]:
|
|
451
|
+
"""Collect account credentials from various input methods.
|
|
452
|
+
|
|
453
|
+
Examples:
|
|
454
|
+
# Inline key
|
|
455
|
+
aip accounts add prod --url https://api.example.com --key sk-abc123
|
|
456
|
+
|
|
457
|
+
# Stdin (useful for scripts)
|
|
458
|
+
echo "sk-abc123" | aip accounts add prod --url https://api.example.com --key
|
|
459
|
+
|
|
460
|
+
# Fully interactive
|
|
461
|
+
aip accounts add prod
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
url: Optional URL from flag.
|
|
465
|
+
api_key_input: API key value from flag (or "-" when stdin requested).
|
|
466
|
+
name: Account name (for error messages).
|
|
467
|
+
existing: Existing account data.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Tuple of (api_url, api_key).
|
|
471
|
+
|
|
472
|
+
Raises:
|
|
473
|
+
click.Abort: If credentials cannot be collected or are invalid.
|
|
474
|
+
"""
|
|
475
|
+
command_name = "aip accounts edit" if existing else "aip accounts add"
|
|
476
|
+
existing_url = existing.get("api_url", "") if existing else ""
|
|
477
|
+
existing_key = existing.get("api_key", "") if existing else ""
|
|
478
|
+
|
|
479
|
+
api_url, api_key = _collect_credentials_from_inputs(url, api_key_input, name, existing, command_name, existing_url)
|
|
480
|
+
|
|
481
|
+
# Preserve stored values when blank input is provided during edit
|
|
482
|
+
api_url, api_key = _preserve_existing_values(api_url, api_key, existing_url, existing_key)
|
|
483
|
+
|
|
484
|
+
if not api_url or not api_key:
|
|
485
|
+
console.print(f"[{ERROR_STYLE}]Error: Both API URL and API key are required.[/]")
|
|
486
|
+
raise click.Abort()
|
|
487
|
+
return api_url, api_key
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@accounts_group.command("add")
|
|
491
|
+
@click.argument("name")
|
|
492
|
+
@click.option("--url", help="API URL (required for non-interactive mode)")
|
|
493
|
+
@click.option(
|
|
494
|
+
"--key",
|
|
495
|
+
"api_key_input",
|
|
496
|
+
type=str,
|
|
497
|
+
is_flag=False,
|
|
498
|
+
flag_value="-",
|
|
499
|
+
default=None,
|
|
500
|
+
help="API key value. Pass without a value or '-' to read from stdin. Requires --url for non-interactive use.",
|
|
501
|
+
)
|
|
502
|
+
@click.option(
|
|
503
|
+
"--yes",
|
|
504
|
+
"overwrite",
|
|
505
|
+
is_flag=True,
|
|
506
|
+
help="Overwrite existing account without prompting",
|
|
507
|
+
)
|
|
508
|
+
def add_account(
|
|
509
|
+
name: str,
|
|
510
|
+
url: str | None,
|
|
511
|
+
api_key_input: str | None,
|
|
512
|
+
overwrite: bool,
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Add a new account profile.
|
|
515
|
+
|
|
516
|
+
NAME is the account name (1-32 chars, alphanumeric, dash, underscore).
|
|
517
|
+
|
|
518
|
+
By default, this command runs interactively, prompting for API URL and key.
|
|
519
|
+
For non-interactive use, provide --url with --key <value> or --key - (stdin).
|
|
520
|
+
|
|
521
|
+
If the account already exists, use --yes to overwrite without prompting.
|
|
522
|
+
To update an existing account, use [bold]aip accounts edit <name>[/bold] instead.
|
|
523
|
+
"""
|
|
524
|
+
store = get_account_store()
|
|
525
|
+
|
|
526
|
+
# Check account overwrite
|
|
527
|
+
existing = _check_account_overwrite(name, store, overwrite)
|
|
528
|
+
|
|
529
|
+
# Collect credentials
|
|
530
|
+
api_url, api_key = _collect_account_credentials(url, api_key_input, name, existing)
|
|
531
|
+
|
|
532
|
+
# Save account
|
|
533
|
+
try:
|
|
534
|
+
store.add_account(name, api_url, api_key, overwrite=True)
|
|
535
|
+
console.print(Text(f"✅ Account '{name}' saved successfully", style=SUCCESS_STYLE))
|
|
536
|
+
_print_active_account_footer(store)
|
|
537
|
+
except InvalidAccountNameError as e:
|
|
538
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
539
|
+
raise click.Abort() from e
|
|
540
|
+
except AccountStoreError as e:
|
|
541
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
542
|
+
raise click.Abort() from e
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@accounts_group.command("edit")
|
|
546
|
+
@click.argument("name")
|
|
547
|
+
@click.option("--url", help="API URL (optional, leave blank to keep current)")
|
|
548
|
+
@click.option(
|
|
549
|
+
"--key",
|
|
550
|
+
"api_key_input",
|
|
551
|
+
type=str,
|
|
552
|
+
is_flag=False,
|
|
553
|
+
flag_value="-",
|
|
554
|
+
default=None,
|
|
555
|
+
help="API key value. Pass without a value or '-' to read from stdin. Uses stored URL unless --url is provided.",
|
|
556
|
+
)
|
|
557
|
+
def edit_account(
|
|
558
|
+
name: str,
|
|
559
|
+
url: str | None,
|
|
560
|
+
api_key_input: str | None,
|
|
561
|
+
) -> None:
|
|
562
|
+
"""Edit an existing account profile's URL or key.
|
|
563
|
+
|
|
564
|
+
NAME is the account name to edit.
|
|
565
|
+
|
|
566
|
+
By default, this command runs interactively, showing current values and
|
|
567
|
+
prompting for new ones. Leave fields blank to keep current values.
|
|
568
|
+
|
|
569
|
+
For non-interactive use, provide --url to change the URL, --key <value> to rotate the key,
|
|
570
|
+
or --key - (stdin) for scripts. Stored values are reused for any fields not provided.
|
|
571
|
+
"""
|
|
572
|
+
store = get_account_store()
|
|
573
|
+
|
|
574
|
+
# Account must exist for edit
|
|
575
|
+
existing = store.get_account(name)
|
|
576
|
+
if not existing:
|
|
577
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
|
|
578
|
+
console.print(f"Use [bold]aip accounts add {name}[/bold] to create a new account.")
|
|
579
|
+
raise click.Abort()
|
|
580
|
+
|
|
581
|
+
# Collect credentials (will pre-fill existing values in interactive mode)
|
|
582
|
+
api_url, api_key = _collect_account_credentials(url, api_key_input, name, existing)
|
|
583
|
+
|
|
584
|
+
# Save account
|
|
585
|
+
try:
|
|
586
|
+
store.add_account(name, api_url, api_key, overwrite=True)
|
|
587
|
+
console.print(Text(f"✅ Account '{name}' updated successfully", style=SUCCESS_STYLE))
|
|
588
|
+
_print_active_account_footer(store)
|
|
589
|
+
except InvalidAccountNameError as e:
|
|
590
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
591
|
+
raise click.Abort() from e
|
|
592
|
+
except AccountStoreError as e:
|
|
593
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
594
|
+
raise click.Abort() from e
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
@accounts_group.command("use")
|
|
598
|
+
@click.argument("name")
|
|
599
|
+
def use_account(name: str) -> None:
|
|
600
|
+
"""Switch to a different account profile."""
|
|
601
|
+
store = get_account_store()
|
|
602
|
+
|
|
603
|
+
try:
|
|
604
|
+
account = store.get_account(name)
|
|
605
|
+
if not account:
|
|
606
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
|
|
607
|
+
raise click.Abort()
|
|
608
|
+
|
|
609
|
+
url = account.get("api_url", "")
|
|
610
|
+
masked_key = _mask_api_key(account.get("api_key"))
|
|
611
|
+
api_key = account.get("api_key", "")
|
|
612
|
+
|
|
613
|
+
if not url or not api_key:
|
|
614
|
+
console.print(
|
|
615
|
+
f"[{ERROR_STYLE}]Error: Account '{name}' is missing credentials. "
|
|
616
|
+
f"Use [bold]aip accounts edit {name}[/bold] to update credentials.[/]"
|
|
617
|
+
)
|
|
618
|
+
raise click.Abort()
|
|
619
|
+
|
|
620
|
+
# Always validate before switching
|
|
621
|
+
check_connection(url, api_key, console, abort_on_error=True)
|
|
622
|
+
|
|
623
|
+
store.set_active_account(name)
|
|
624
|
+
|
|
625
|
+
console.print(
|
|
626
|
+
AIPPanel(
|
|
627
|
+
f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {url}\nKey: {masked_key}",
|
|
628
|
+
title="✅ Account Switched",
|
|
629
|
+
border_style=SUCCESS,
|
|
630
|
+
),
|
|
631
|
+
)
|
|
632
|
+
except click.Abort:
|
|
633
|
+
# check_connection already printed the failure context; just propagate
|
|
634
|
+
raise
|
|
635
|
+
except AccountNotFoundError as e:
|
|
636
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
637
|
+
raise click.Abort() from e
|
|
638
|
+
except Exception as e:
|
|
639
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
640
|
+
raise click.Abort() from e
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@accounts_group.command("rename")
|
|
644
|
+
@click.argument("current_name")
|
|
645
|
+
@click.argument("new_name")
|
|
646
|
+
@click.option(
|
|
647
|
+
"--yes",
|
|
648
|
+
"overwrite",
|
|
649
|
+
is_flag=True,
|
|
650
|
+
help="Overwrite target account if it already exists",
|
|
651
|
+
)
|
|
652
|
+
def rename_account(current_name: str, new_name: str, overwrite: bool) -> None:
|
|
653
|
+
"""Rename an account profile."""
|
|
654
|
+
store = get_account_store()
|
|
655
|
+
|
|
656
|
+
if current_name == new_name:
|
|
657
|
+
console.print(f"[{WARNING_STYLE}]Source and target names are the same; nothing to rename.[/]")
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
try:
|
|
661
|
+
if not store.get_account(current_name):
|
|
662
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{current_name}' not found.[/]")
|
|
663
|
+
raise click.Abort()
|
|
664
|
+
|
|
665
|
+
# Guard before calling store.rename_account to keep consistent messaging with add --yes
|
|
666
|
+
if store.get_account(new_name) and not overwrite:
|
|
667
|
+
console.print(f"[{WARNING_STYLE}]Account '{new_name}' already exists.[/] Use --yes to overwrite.")
|
|
668
|
+
raise click.Abort()
|
|
669
|
+
|
|
670
|
+
store.rename_account(current_name, new_name, overwrite=overwrite)
|
|
671
|
+
console.print(Text(f"✅ Account '{current_name}' renamed to '{new_name}'", style=SUCCESS_STYLE))
|
|
672
|
+
_print_active_account_footer(store)
|
|
673
|
+
except AccountStoreError as e:
|
|
674
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
675
|
+
raise click.Abort() from e
|
|
676
|
+
except Exception as e: # pragma: no cover - defensive catch-all
|
|
677
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
678
|
+
raise click.Abort() from e
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@accounts_group.command("remove")
|
|
682
|
+
@click.argument("name")
|
|
683
|
+
@click.option("--yes", "force", is_flag=True, help="Skip confirmation prompt")
|
|
684
|
+
def remove_account(name: str, force: bool) -> None:
|
|
685
|
+
"""Remove an account profile."""
|
|
686
|
+
store = get_account_store()
|
|
687
|
+
|
|
688
|
+
account = store.get_account(name)
|
|
689
|
+
if not account:
|
|
690
|
+
console.print(f"[{WARNING_STYLE}]Account '{name}' not found.[/]")
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
if not force:
|
|
694
|
+
console.print(f"[{WARNING_STYLE}]This will remove account '{name}'.[/]")
|
|
695
|
+
confirm = input("Are you sure? (y/N): ").strip().lower()
|
|
696
|
+
if confirm not in ["y", "yes"]:
|
|
697
|
+
console.print("Cancelled.")
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
try:
|
|
701
|
+
store.remove_account(name)
|
|
702
|
+
console.print(Text(f"✅ Account '{name}' removed", style=SUCCESS_STYLE))
|
|
703
|
+
|
|
704
|
+
# Show new active account if it changed
|
|
705
|
+
active = store.get_active_account()
|
|
706
|
+
if active:
|
|
707
|
+
console.print(f"[{SUCCESS_STYLE}]Active account is now: {active}[/]")
|
|
708
|
+
except AccountStoreError as e:
|
|
709
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
710
|
+
raise click.Abort() from e
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _render_configuration_header() -> None:
|
|
714
|
+
"""Display the interactive configuration heading/banner."""
|
|
715
|
+
render_branding_header(console, "[bold]AIP Account Configuration[/bold]")
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _prompt_account_inputs(existing: dict[str, str] | None) -> tuple[str, str]:
|
|
719
|
+
"""Interactively prompt for account credentials."""
|
|
720
|
+
console.print("\n[bold]Enter your AIP configuration:[/bold]")
|
|
721
|
+
if existing:
|
|
722
|
+
console.print("(Leave blank to keep current values)")
|
|
723
|
+
console.print("─" * 50)
|
|
724
|
+
|
|
725
|
+
# Prompt for URL
|
|
726
|
+
current_url = existing.get("api_url", "") if existing else ""
|
|
727
|
+
suffix = f"(current: {current_url})" if current_url else ""
|
|
728
|
+
console.print(f"\n[{ACCENT_STYLE}]AIP API URL[/] {suffix}:")
|
|
729
|
+
new_url = input("> ").strip()
|
|
730
|
+
api_url = new_url if new_url else current_url
|
|
731
|
+
if not api_url:
|
|
732
|
+
api_url = "https://your-aip-instance.com"
|
|
733
|
+
|
|
734
|
+
# Prompt for key
|
|
735
|
+
current_key_masked = _mask_api_key(existing.get("api_key")) if existing else ""
|
|
736
|
+
suffix = f"(current: {current_key_masked})" if current_key_masked else ""
|
|
737
|
+
console.print(f"\n[{ACCENT_STYLE}]AIP API Key[/] {suffix}:")
|
|
738
|
+
new_key = getpass.getpass("> ")
|
|
739
|
+
if new_key:
|
|
740
|
+
api_key = new_key
|
|
741
|
+
elif existing:
|
|
742
|
+
api_key = existing.get("api_key", "")
|
|
743
|
+
else:
|
|
744
|
+
api_key = ""
|
|
745
|
+
|
|
746
|
+
return api_url, api_key
|