glaip-sdk 0.6.11__py3-none-any.whl → 0.6.14__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.
Files changed (156) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. {glaip_sdk-0.6.11.dist-info → glaip_sdk-0.6.14.dist-info}/METADATA +31 -37
  3. glaip_sdk-0.6.14.dist-info/RECORD +12 -0
  4. {glaip_sdk-0.6.11.dist-info → glaip_sdk-0.6.14.dist-info}/WHEEL +2 -1
  5. glaip_sdk-0.6.14.dist-info/entry_points.txt +2 -0
  6. glaip_sdk-0.6.14.dist-info/top_level.txt +1 -0
  7. glaip_sdk/agents/__init__.py +0 -27
  8. glaip_sdk/agents/base.py +0 -1191
  9. glaip_sdk/cli/__init__.py +0 -9
  10. glaip_sdk/cli/account_store.py +0 -540
  11. glaip_sdk/cli/agent_config.py +0 -78
  12. glaip_sdk/cli/auth.py +0 -699
  13. glaip_sdk/cli/commands/__init__.py +0 -5
  14. glaip_sdk/cli/commands/accounts.py +0 -746
  15. glaip_sdk/cli/commands/agents.py +0 -1509
  16. glaip_sdk/cli/commands/common_config.py +0 -101
  17. glaip_sdk/cli/commands/configure.py +0 -896
  18. glaip_sdk/cli/commands/mcps.py +0 -1356
  19. glaip_sdk/cli/commands/models.py +0 -69
  20. glaip_sdk/cli/commands/tools.py +0 -576
  21. glaip_sdk/cli/commands/transcripts.py +0 -755
  22. glaip_sdk/cli/commands/update.py +0 -61
  23. glaip_sdk/cli/config.py +0 -95
  24. glaip_sdk/cli/constants.py +0 -38
  25. glaip_sdk/cli/context.py +0 -150
  26. glaip_sdk/cli/core/__init__.py +0 -79
  27. glaip_sdk/cli/core/context.py +0 -124
  28. glaip_sdk/cli/core/output.py +0 -846
  29. glaip_sdk/cli/core/prompting.py +0 -649
  30. glaip_sdk/cli/core/rendering.py +0 -187
  31. glaip_sdk/cli/display.py +0 -355
  32. glaip_sdk/cli/hints.py +0 -57
  33. glaip_sdk/cli/io.py +0 -112
  34. glaip_sdk/cli/main.py +0 -604
  35. glaip_sdk/cli/masking.py +0 -136
  36. glaip_sdk/cli/mcp_validators.py +0 -287
  37. glaip_sdk/cli/pager.py +0 -266
  38. glaip_sdk/cli/parsers/__init__.py +0 -7
  39. glaip_sdk/cli/parsers/json_input.py +0 -177
  40. glaip_sdk/cli/resolution.py +0 -67
  41. glaip_sdk/cli/rich_helpers.py +0 -27
  42. glaip_sdk/cli/slash/__init__.py +0 -15
  43. glaip_sdk/cli/slash/accounts_controller.py +0 -578
  44. glaip_sdk/cli/slash/accounts_shared.py +0 -75
  45. glaip_sdk/cli/slash/agent_session.py +0 -285
  46. glaip_sdk/cli/slash/prompt.py +0 -256
  47. glaip_sdk/cli/slash/remote_runs_controller.py +0 -566
  48. glaip_sdk/cli/slash/session.py +0 -1708
  49. glaip_sdk/cli/slash/tui/__init__.py +0 -9
  50. glaip_sdk/cli/slash/tui/accounts_app.py +0 -876
  51. glaip_sdk/cli/slash/tui/background_tasks.py +0 -72
  52. glaip_sdk/cli/slash/tui/loading.py +0 -58
  53. glaip_sdk/cli/slash/tui/remote_runs_app.py +0 -628
  54. glaip_sdk/cli/transcript/__init__.py +0 -31
  55. glaip_sdk/cli/transcript/cache.py +0 -536
  56. glaip_sdk/cli/transcript/capture.py +0 -329
  57. glaip_sdk/cli/transcript/export.py +0 -38
  58. glaip_sdk/cli/transcript/history.py +0 -815
  59. glaip_sdk/cli/transcript/launcher.py +0 -77
  60. glaip_sdk/cli/transcript/viewer.py +0 -374
  61. glaip_sdk/cli/update_notifier.py +0 -290
  62. glaip_sdk/cli/utils.py +0 -263
  63. glaip_sdk/cli/validators.py +0 -238
  64. glaip_sdk/client/__init__.py +0 -11
  65. glaip_sdk/client/_agent_payloads.py +0 -520
  66. glaip_sdk/client/agent_runs.py +0 -147
  67. glaip_sdk/client/agents.py +0 -1335
  68. glaip_sdk/client/base.py +0 -502
  69. glaip_sdk/client/main.py +0 -249
  70. glaip_sdk/client/mcps.py +0 -370
  71. glaip_sdk/client/run_rendering.py +0 -700
  72. glaip_sdk/client/shared.py +0 -21
  73. glaip_sdk/client/tools.py +0 -661
  74. glaip_sdk/client/validators.py +0 -198
  75. glaip_sdk/config/constants.py +0 -52
  76. glaip_sdk/mcps/__init__.py +0 -21
  77. glaip_sdk/mcps/base.py +0 -345
  78. glaip_sdk/models/__init__.py +0 -90
  79. glaip_sdk/models/agent.py +0 -47
  80. glaip_sdk/models/agent_runs.py +0 -116
  81. glaip_sdk/models/common.py +0 -42
  82. glaip_sdk/models/mcp.py +0 -33
  83. glaip_sdk/models/tool.py +0 -33
  84. glaip_sdk/payload_schemas/__init__.py +0 -7
  85. glaip_sdk/payload_schemas/agent.py +0 -85
  86. glaip_sdk/registry/__init__.py +0 -55
  87. glaip_sdk/registry/agent.py +0 -164
  88. glaip_sdk/registry/base.py +0 -139
  89. glaip_sdk/registry/mcp.py +0 -253
  90. glaip_sdk/registry/tool.py +0 -232
  91. glaip_sdk/runner/__init__.py +0 -59
  92. glaip_sdk/runner/base.py +0 -84
  93. glaip_sdk/runner/deps.py +0 -115
  94. glaip_sdk/runner/langgraph.py +0 -782
  95. glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -257
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
  99. glaip_sdk/runner/tool_adapter/__init__.py +0 -18
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -219
  102. glaip_sdk/tools/__init__.py +0 -22
  103. glaip_sdk/tools/base.py +0 -435
  104. glaip_sdk/utils/__init__.py +0 -86
  105. glaip_sdk/utils/a2a/__init__.py +0 -34
  106. glaip_sdk/utils/a2a/event_processor.py +0 -188
  107. glaip_sdk/utils/agent_config.py +0 -194
  108. glaip_sdk/utils/bundler.py +0 -267
  109. glaip_sdk/utils/client.py +0 -111
  110. glaip_sdk/utils/client_utils.py +0 -486
  111. glaip_sdk/utils/datetime_helpers.py +0 -58
  112. glaip_sdk/utils/discovery.py +0 -78
  113. glaip_sdk/utils/display.py +0 -135
  114. glaip_sdk/utils/export.py +0 -143
  115. glaip_sdk/utils/general.py +0 -61
  116. glaip_sdk/utils/import_export.py +0 -168
  117. glaip_sdk/utils/import_resolver.py +0 -492
  118. glaip_sdk/utils/instructions.py +0 -101
  119. glaip_sdk/utils/rendering/__init__.py +0 -115
  120. glaip_sdk/utils/rendering/formatting.py +0 -264
  121. glaip_sdk/utils/rendering/layout/__init__.py +0 -64
  122. glaip_sdk/utils/rendering/layout/panels.py +0 -156
  123. glaip_sdk/utils/rendering/layout/progress.py +0 -202
  124. glaip_sdk/utils/rendering/layout/summary.py +0 -74
  125. glaip_sdk/utils/rendering/layout/transcript.py +0 -606
  126. glaip_sdk/utils/rendering/models.py +0 -85
  127. glaip_sdk/utils/rendering/renderer/__init__.py +0 -55
  128. glaip_sdk/utils/rendering/renderer/base.py +0 -1024
  129. glaip_sdk/utils/rendering/renderer/config.py +0 -27
  130. glaip_sdk/utils/rendering/renderer/console.py +0 -55
  131. glaip_sdk/utils/rendering/renderer/debug.py +0 -178
  132. glaip_sdk/utils/rendering/renderer/factory.py +0 -138
  133. glaip_sdk/utils/rendering/renderer/stream.py +0 -202
  134. glaip_sdk/utils/rendering/renderer/summary_window.py +0 -79
  135. glaip_sdk/utils/rendering/renderer/thinking.py +0 -273
  136. glaip_sdk/utils/rendering/renderer/toggle.py +0 -182
  137. glaip_sdk/utils/rendering/renderer/tool_panels.py +0 -442
  138. glaip_sdk/utils/rendering/renderer/transcript_mode.py +0 -162
  139. glaip_sdk/utils/rendering/state.py +0 -204
  140. glaip_sdk/utils/rendering/step_tree_state.py +0 -100
  141. glaip_sdk/utils/rendering/steps/__init__.py +0 -34
  142. glaip_sdk/utils/rendering/steps/event_processor.py +0 -778
  143. glaip_sdk/utils/rendering/steps/format.py +0 -176
  144. glaip_sdk/utils/rendering/steps/manager.py +0 -387
  145. glaip_sdk/utils/rendering/timing.py +0 -36
  146. glaip_sdk/utils/rendering/viewer/__init__.py +0 -21
  147. glaip_sdk/utils/rendering/viewer/presenter.py +0 -184
  148. glaip_sdk/utils/resource_refs.py +0 -195
  149. glaip_sdk/utils/run_renderer.py +0 -41
  150. glaip_sdk/utils/runtime_config.py +0 -425
  151. glaip_sdk/utils/serialization.py +0 -424
  152. glaip_sdk/utils/sync.py +0 -142
  153. glaip_sdk/utils/tool_detection.py +0 -33
  154. glaip_sdk/utils/validation.py +0 -264
  155. glaip_sdk-0.6.11.dist-info/RECORD +0 -159
  156. glaip_sdk-0.6.11.dist-info/entry_points.txt +0 -3
@@ -1,746 +0,0 @@
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