glaip-sdk 0.6.12__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.12.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.12.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.12.dist-info/RECORD +0 -159
  156. glaip_sdk-0.6.12.dist-info/entry_points.txt +0 -3
@@ -1,578 +0,0 @@
1
- """Accounts controller for the /accounts slash command.
2
-
3
- Provides a lightweight Textual list with fallback Rich snapshot to switch
4
- between stored accounts using the shared AccountStore and CLI validation.
5
-
6
- Authors:
7
- Raymond Christopher (raymond.christopher@gdplabs.id)
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import sys
13
- from collections.abc import Iterable
14
- from getpass import getpass
15
- from typing import TYPE_CHECKING, Any
16
-
17
- from rich.console import Console
18
- from rich.prompt import Prompt
19
-
20
- from glaip_sdk.branding import ERROR_STYLE, INFO_STYLE, SUCCESS_STYLE, WARNING_STYLE
21
- from glaip_sdk.cli.account_store import AccountStore, AccountStoreError, get_account_store
22
- from glaip_sdk.cli.commands.common_config import check_connection_with_reason
23
- from glaip_sdk.cli.masking import mask_api_key_display
24
- from glaip_sdk.cli.validators import validate_api_key
25
- from glaip_sdk.cli.slash.accounts_shared import (
26
- build_account_rows,
27
- build_account_status_string,
28
- env_credentials_present,
29
- )
30
- from glaip_sdk.cli.slash.tui.accounts_app import TEXTUAL_SUPPORTED, AccountsTUICallbacks, run_accounts_textual
31
- from glaip_sdk.rich_components import AIPPanel, AIPTable
32
- from glaip_sdk.utils.validation import validate_url
33
-
34
- if TYPE_CHECKING: # pragma: no cover
35
- from glaip_sdk.cli.slash.session import SlashSession
36
-
37
- TEXTUAL_AVAILABLE = bool(TEXTUAL_SUPPORTED)
38
-
39
-
40
- class AccountsController:
41
- """Controller for listing and switching accounts inside the palette."""
42
-
43
- def __init__(self, session: SlashSession) -> None:
44
- """Initialize the accounts controller.
45
-
46
- Args:
47
- session: The slash session context.
48
- """
49
- self.session = session
50
- self.console: Console = session.console
51
- self.ctx = session.ctx
52
-
53
- def handle_accounts_command(self, args: list[str]) -> bool:
54
- """Handle `/accounts` with optional `/accounts <name>` quick switch."""
55
- store = get_account_store()
56
- env_lock = env_credentials_present(partial=True)
57
- accounts = store.list_accounts()
58
-
59
- if not accounts:
60
- self.console.print(f"[{WARNING_STYLE}]No accounts found. Use `/login` to add credentials.[/]")
61
- return self.session._continue_session()
62
-
63
- if args:
64
- name = args[0]
65
- self._switch_account(store, name, env_lock)
66
- return self.session._continue_session()
67
-
68
- rows = self._build_rows(accounts, store.get_active_account(), env_lock)
69
-
70
- if self._should_use_textual():
71
- self._render_textual(rows, store, env_lock)
72
- else:
73
- self._render_rich_interactive(store, env_lock)
74
-
75
- return self.session._continue_session()
76
-
77
- def _should_use_textual(self) -> bool:
78
- """Return whether Textual UI should be used."""
79
- if not TEXTUAL_AVAILABLE:
80
- return False
81
-
82
- def _is_tty(stream: Any) -> bool:
83
- isatty = getattr(stream, "isatty", None)
84
- if not callable(isatty):
85
- return False
86
- try:
87
- return bool(isatty())
88
- except Exception:
89
- return False
90
-
91
- return _is_tty(sys.stdin) and _is_tty(sys.stdout)
92
-
93
- def _build_rows(
94
- self,
95
- accounts: dict[str, dict[str, str]],
96
- active_account: str | None,
97
- env_lock: bool,
98
- ) -> list[dict[str, str | bool]]:
99
- """Normalize account rows for display."""
100
- return build_account_rows(accounts, active_account, env_lock)
101
-
102
- def _render_rich(self, rows: Iterable[dict[str, str | bool]], env_lock: bool) -> None:
103
- """Render a Rich snapshot with columns matching TUI."""
104
- if env_lock:
105
- self.console.print(
106
- f"[{WARNING_STYLE}]Env credentials detected (AIP_API_URL/AIP_API_KEY); add/edit/delete are disabled.[/]"
107
- )
108
-
109
- table = AIPTable(title="AIP Accounts")
110
- table.add_column("Name", style=INFO_STYLE, width=20)
111
- table.add_column("API URL", style=SUCCESS_STYLE, width=40)
112
- table.add_column("Key (masked)", style="dim", width=20)
113
- table.add_column("Status", style=SUCCESS_STYLE, width=14)
114
-
115
- for row in rows:
116
- status = build_account_status_string(row, use_markup=True)
117
- # pylint: disable=duplicate-code
118
- # Similar to accounts_app.py but uses Rich AIPTable API
119
- table.add_row(
120
- str(row.get("name", "")),
121
- str(row.get("api_url", "")),
122
- str(row.get("masked_key", "")),
123
- status,
124
- )
125
-
126
- self.console.print(table)
127
-
128
- def _render_rich_interactive(self, store: AccountStore, env_lock: bool) -> None:
129
- """Render Rich snapshot and run linear add/edit/delete prompts."""
130
- if env_lock:
131
- rows = self._build_rows(store.list_accounts(), store.get_active_account(), env_lock)
132
- self._render_rich(rows, env_lock)
133
- return
134
-
135
- while True: # pragma: no cover - interactive prompt loop
136
- rows = self._build_rows(store.list_accounts(), store.get_active_account(), env_lock)
137
- self._render_rich(rows, env_lock)
138
- action = self._prompt_action()
139
- if action == "q":
140
- break
141
- if action == "a":
142
- self._rich_add_flow(store)
143
- elif action == "e":
144
- self._rich_edit_flow(store)
145
- elif action == "d":
146
- self._rich_delete_flow(store)
147
- elif action == "s":
148
- self._rich_switch_flow(store, env_lock)
149
- else:
150
- self.console.print(f"[{WARNING_STYLE}]Invalid choice. Use a/e/d/s/q.[/]")
151
-
152
- def _render_textual(self, rows: list[dict[str, str | bool]], store: AccountStore, env_lock: bool) -> None:
153
- """Launch the Textual accounts browser."""
154
- active_before = store.get_active_account()
155
- notified = False
156
-
157
- def _switch_in_textual(name: str) -> tuple[bool, str]:
158
- nonlocal notified
159
- switched, message = self._switch_account(
160
- store,
161
- name,
162
- env_lock,
163
- emit_console=False,
164
- invalidate_session=True,
165
- )
166
- if switched:
167
- notified = True
168
- return switched, message
169
-
170
- callbacks = AccountsTUICallbacks(switch_account=_switch_in_textual)
171
- active = next((row["name"] for row in rows if row.get("active")), None)
172
- try:
173
- run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks)
174
- except Exception as exc: # pragma: no cover - defensive around Textual failures
175
- self.console.print(f"[{WARNING_STYLE}]Accounts browser exited unexpectedly: {exc}[/]")
176
-
177
- # Exit snapshot: surface a success banner when a switch occurred inside the TUI.
178
- # Always notify when the active account changed, even if Textual raised.
179
- active_after = store.get_active_account()
180
- if active_after != active_before and not notified:
181
- self._notify_account_switched(active_after)
182
- if active_after != active:
183
- host_after = ""
184
- display_account = active_after or "default"
185
- account_after = store.get_account(display_account) if hasattr(store, "get_account") else None
186
- if account_after:
187
- host_after = account_after.get("api_url", "")
188
- host_suffix = f" • {host_after}" if host_after else ""
189
- self.console.print(
190
- AIPPanel(
191
- f"[{SUCCESS_STYLE}]Active account ➜ {display_account}[/]{host_suffix}",
192
- title="✅ Account Switched",
193
- border_style=SUCCESS_STYLE,
194
- )
195
- )
196
-
197
- def _format_connection_error_message(self, error_reason: str, account_name: str, api_url: str) -> str:
198
- """Format error message for connection validation failures."""
199
- code, detail = self._parse_error_reason(error_reason)
200
- if code == "connection_failed":
201
- return f"Switch aborted: cannot reach {api_url}. Check URL or network."
202
- if code == "api_failed":
203
- return f"Switch aborted: API error for '{account_name}'. Check credentials."
204
- detail_suffix = f": {detail}" if detail else ""
205
- return f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
206
-
207
- def _emit_error_message(self, msg: str, style: str = ERROR_STYLE) -> None:
208
- """Emit an error or warning message to the console."""
209
- self.console.print(f"[{style}]{msg}[/]")
210
-
211
- def _validate_account_switch(
212
- self, store: AccountStore, name: str, env_lock: bool, emit_console: bool
213
- ) -> tuple[bool, str, dict[str, str] | None]:
214
- """Validate account switch prerequisites; returns (is_valid, error_msg, account_dict)."""
215
- if env_lock:
216
- msg = "Env credentials detected (AIP_API_URL/AIP_API_KEY); switching is disabled."
217
- if emit_console:
218
- self._emit_error_message(msg, WARNING_STYLE)
219
- return False, msg, None
220
-
221
- account = store.get_account(name)
222
- if not account:
223
- msg = f"Account '{name}' not found."
224
- if emit_console:
225
- self._emit_error_message(msg)
226
- return False, msg, None
227
-
228
- api_url = account.get("api_url", "")
229
- api_key = account.get("api_key", "")
230
- if not api_url or not api_key:
231
- edit_cmd = f"aip accounts edit {name}"
232
- msg = f"Account '{name}' is missing credentials. Use `/login` or `{edit_cmd}`."
233
- if emit_console:
234
- self._emit_error_message(msg)
235
- return False, msg, None
236
-
237
- ok, error_reason = check_connection_with_reason(api_url, api_key, abort_on_error=False)
238
- if not ok:
239
- msg = self._format_connection_error_message(error_reason, name, api_url)
240
- if emit_console:
241
- self._emit_error_message(msg, WARNING_STYLE)
242
- return False, msg, None
243
-
244
- return True, "", account
245
-
246
- def _execute_account_switch(
247
- self, store: AccountStore, name: str, account: dict[str, str], invalidate_session: bool, emit_console: bool
248
- ) -> tuple[bool, str]:
249
- """Execute the account switch and emit success message."""
250
- try:
251
- store.set_active_account(name)
252
- api_url = account.get("api_url", "")
253
- api_key = account.get("api_key", "")
254
- masked_key = mask_api_key_display(api_key)
255
- if invalidate_session:
256
- self._notify_account_switched(name)
257
- if emit_console:
258
- self.console.print(
259
- AIPPanel(
260
- f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {api_url}\nKey: {masked_key}",
261
- title="✅ Account Switched",
262
- border_style=SUCCESS_STYLE,
263
- )
264
- )
265
- return True, f"Switched to '{name}'."
266
- except AccountStoreError as exc:
267
- msg = f"Failed to set active account: {exc}"
268
- if emit_console:
269
- self._emit_error_message(msg)
270
- return False, msg
271
- except Exception as exc: # NOSONAR(S1045) - catch-all needed for unexpected errors
272
- msg = f"Unexpected error while switching to '{name}': {exc}"
273
- if emit_console:
274
- self._emit_error_message(msg)
275
- return False, msg
276
-
277
- def _switch_account(
278
- self,
279
- store: AccountStore,
280
- name: str,
281
- env_lock: bool,
282
- *,
283
- emit_console: bool = True,
284
- invalidate_session: bool = True,
285
- ) -> tuple[bool, str]:
286
- """Validate and switch active account; returns (success, message)."""
287
- is_valid, error_msg, account = self._validate_account_switch(store, name, env_lock, emit_console)
288
- if not is_valid:
289
- return False, error_msg
290
-
291
- if account is None: # Defensive – should never happen, but avoid crashing in production
292
- return False, "Unable to locate account after validation."
293
- return self._execute_account_switch(store, name, account, invalidate_session, emit_console)
294
-
295
- @staticmethod
296
- def _parse_error_reason(reason: str | None) -> tuple[str, str]:
297
- """Parse error reason into (code, detail) to avoid fragile substring checks."""
298
- if not reason:
299
- return "", ""
300
- if ":" in reason:
301
- code, _, detail = reason.partition(":")
302
- return code.strip(), detail.strip()
303
- return reason.strip(), ""
304
-
305
- def _prompt_action(self) -> str:
306
- """Prompt for add/edit/delete/quit action."""
307
- try:
308
- choice = Prompt.ask("(a)dd / (e)dit / (d)elete / (s)witch / (q)uit", default="q")
309
- except Exception: # pragma: no cover - defensive around prompt failures
310
- return "q"
311
- return (choice or "").strip().lower()[:1]
312
-
313
- def _prompt_yes_no(self, prompt: str, *, default: bool = True) -> bool:
314
- """Prompt a yes/no question with a default."""
315
- default_str = "Y/n" if default else "y/N"
316
- try:
317
- answer = Prompt.ask(f"{prompt} ({default_str})", default="y" if default else "n")
318
- except Exception: # pragma: no cover - defensive around prompt failures
319
- return default
320
- normalized = (answer or "").strip().lower()
321
- if not normalized:
322
- return default
323
- return normalized in {"y", "yes"}
324
-
325
- def _prompt_account_name(self, store: AccountStore, *, for_edit: bool) -> str | None:
326
- """Prompt for an account name, validating per store rules."""
327
- while True: # pragma: no cover - interactive prompt loop
328
- name = self._get_name_input(for_edit)
329
- if name is None:
330
- return None
331
- if not name:
332
- self.console.print(f"[{WARNING_STYLE}]Name is required.[/]")
333
- continue
334
- if not self._validate_name_format(store, name):
335
- continue
336
- if not self._validate_name_existence(store, name, for_edit):
337
- continue
338
- return name
339
-
340
- def _get_name_input(self, for_edit: bool) -> str | None:
341
- """Get account name input from user."""
342
- try:
343
- prompt_text = "Account name" + (" (existing)" if for_edit else "")
344
- name = Prompt.ask(prompt_text)
345
- return name.strip() if name else None
346
- except Exception:
347
- return None
348
-
349
- def _validate_name_format(self, store: AccountStore, name: str) -> bool:
350
- """Validate account name format."""
351
- try:
352
- store.validate_account_name(name)
353
- return True
354
- except Exception as exc:
355
- self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
356
- return False
357
-
358
- def _validate_name_existence(self, store: AccountStore, name: str, for_edit: bool) -> bool:
359
- """Validate account name existence based on mode."""
360
- account_exists = store.get_account(name) is not None
361
- if not for_edit and account_exists:
362
- self.console.print(
363
- f"[{WARNING_STYLE}]Account '{name}' already exists. Use edit instead or choose a new name.[/]"
364
- )
365
- return False
366
- if for_edit and not account_exists:
367
- self.console.print(f"[{WARNING_STYLE}]Account '{name}' not found. Try again or quit.[/]")
368
- return False
369
- return True
370
-
371
- def _prompt_api_url(self, existing_url: str | None = None) -> str | None:
372
- """Prompt for API URL with HTTPS validation."""
373
- placeholder = existing_url or "https://your-aip-instance.com"
374
- while True: # pragma: no cover - interactive prompt loop
375
- try:
376
- entered = Prompt.ask("API URL", default=placeholder)
377
- except Exception:
378
- return None
379
- url = (entered or "").strip()
380
- if not url and existing_url:
381
- return existing_url
382
- if not url:
383
- self.console.print(f"[{WARNING_STYLE}]API URL is required.[/]")
384
- continue
385
- try:
386
- return validate_url(url)
387
- except Exception as exc:
388
- self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
389
-
390
- def _prompt_api_key(self, existing_key: str | None = None) -> str | None:
391
- """Prompt for API key (masked)."""
392
- mask_hint = "leave blank to keep current" if existing_key else None
393
- while True: # pragma: no cover - interactive prompt loop
394
- try:
395
- entered = getpass(f"API key ({mask_hint or 'input hidden'}): ")
396
- except Exception:
397
- return None
398
- if not entered and existing_key:
399
- return existing_key
400
- if not entered:
401
- self.console.print(f"[{WARNING_STYLE}]API key is required.[/]")
402
- continue
403
- try:
404
- return validate_api_key(entered)
405
- except Exception as exc:
406
- self.console.print(f"[{ERROR_STYLE}]{exc}[/]")
407
-
408
- def _rich_add_flow(self, store: AccountStore) -> None:
409
- """Run Rich add prompts and save."""
410
- name = self._prompt_account_name(store, for_edit=False)
411
- if not name:
412
- return
413
- api_url = self._prompt_api_url()
414
- if not api_url:
415
- return
416
- api_key = self._prompt_api_key()
417
- if not api_key:
418
- return
419
- should_test = self._prompt_yes_no("Test connection before save?", default=True)
420
- self._save_account(store, name, api_url, api_key, should_test, True, is_edit=False)
421
-
422
- def _rich_edit_flow(self, store: AccountStore) -> None:
423
- """Run Rich edit prompts and save."""
424
- name = self._prompt_account_name(store, for_edit=True)
425
- if not name:
426
- return
427
- existing = store.get_account(name) or {}
428
- api_url = self._prompt_api_url(existing.get("api_url"))
429
- if not api_url:
430
- return
431
- api_key = self._prompt_api_key(existing.get("api_key"))
432
- if not api_key:
433
- return
434
- should_test = self._prompt_yes_no("Test connection before save?", default=True)
435
- self._save_account(store, name, api_url, api_key, should_test, False, is_edit=True)
436
-
437
- def _rich_switch_flow(self, store: AccountStore, env_lock: bool) -> None:
438
- """Run Rich switch prompt and set active account."""
439
- name = self._prompt_account_name(store, for_edit=True)
440
- if not name:
441
- return
442
- self._switch_account(store, name, env_lock)
443
-
444
- def _save_account(
445
- self,
446
- store: AccountStore,
447
- name: str,
448
- api_url: str,
449
- api_key: str,
450
- should_test: bool,
451
- set_active: bool,
452
- *,
453
- is_edit: bool,
454
- ) -> None:
455
- """Validate, optionally test, and persist account changes."""
456
- if should_test and not self._run_connection_test_with_retry(api_url, api_key):
457
- return
458
-
459
- try:
460
- store.add_account(name, api_url, api_key, overwrite=is_edit)
461
- except AccountStoreError as exc:
462
- self.console.print(f"[{ERROR_STYLE}]Save failed: {exc}[/]")
463
- return
464
- except Exception as exc:
465
- self.console.print(f"[{ERROR_STYLE}]Unexpected error while saving: {exc}[/]")
466
- return
467
-
468
- self.console.print(f"[{SUCCESS_STYLE}]Account '{name}' saved.[/]")
469
- if set_active:
470
- try:
471
- store.set_active_account(name)
472
- except Exception as exc:
473
- self.console.print(f"[{WARNING_STYLE}]Account saved but could not set active: {exc}[/]")
474
- else:
475
- self._notify_account_switched(name)
476
- self._announce_active_change(store, name)
477
-
478
- def _notify_account_switched(self, name: str | None) -> None:
479
- """Best-effort notify the hosting session that the active account changed."""
480
- notify = getattr(self.session, "on_account_switched", None)
481
- if callable(notify):
482
- try:
483
- notify(name)
484
- except Exception: # pragma: no cover - best-effort callback
485
- pass
486
-
487
- def _confirm_delete_prompt(self, name: str) -> bool:
488
- """Ask for delete confirmation; return True when confirmed."""
489
- self.console.print(f"[{WARNING_STYLE}]Type '{name}' to confirm deletion. This cannot be undone.[/]")
490
- while True: # pragma: no cover - interactive prompt loop
491
- confirmation = Prompt.ask("Confirm name (or blank to cancel)", default="")
492
- if confirmation is None or not confirmation.strip():
493
- self.console.print(f"[{WARNING_STYLE}]Deletion cancelled.[/]")
494
- return False
495
- if confirmation.strip() != name:
496
- self.console.print(f"[{WARNING_STYLE}]Name does not match; type '{name}' to confirm.[/]")
497
- continue
498
- return True
499
-
500
- def _delete_account_and_notify(self, store: AccountStore, name: str, active_before: str | None) -> None:
501
- """Remove account with error handling and announce active change."""
502
- try:
503
- store.remove_account(name)
504
- except AccountStoreError as exc:
505
- self.console.print(f"[{ERROR_STYLE}]Delete failed: {exc}[/]")
506
- return
507
- except Exception as exc:
508
- self.console.print(f"[{ERROR_STYLE}]Unexpected error while deleting: {exc}[/]")
509
- return
510
-
511
- self.console.print(f"[{SUCCESS_STYLE}]Account '{name}' deleted.[/]")
512
- # Announce active account change if it changed
513
- active_after = store.get_active_account()
514
- if active_after is not None and active_after != active_before:
515
- self._announce_active_change(store, active_after)
516
- elif active_after is None and active_before == name:
517
- self.console.print(f"[{WARNING_STYLE}]No account is currently active. Select an account to activate it.[/]")
518
-
519
- def _rich_delete_flow(self, store: AccountStore) -> None:
520
- """Run Rich delete prompts with name confirmation."""
521
- name = self._prompt_account_name(store, for_edit=True)
522
- if not name:
523
- return
524
-
525
- # Check if this is the last remaining account before prompting for confirmation
526
- accounts = store.list_accounts()
527
- if len(accounts) <= 1 and name in accounts:
528
- self.console.print(f"[{WARNING_STYLE}]Cannot remove the last remaining account.[/]")
529
- return
530
-
531
- if not self._confirm_delete_prompt(name):
532
- return
533
-
534
- # Re-check after confirmation prompt (race condition guard)
535
- accounts = store.list_accounts()
536
- if len(accounts) <= 1 and name in accounts:
537
- self.console.print(f"[{WARNING_STYLE}]Cannot remove the last remaining account.[/]")
538
- return
539
-
540
- active_before = store.get_active_account()
541
- self._delete_account_and_notify(store, name, active_before)
542
-
543
- def _format_connection_failure(self, code: str, detail: str, api_url: str) -> str:
544
- """Build a user-facing connection failure message."""
545
- detail_suffix = f": {detail}" if detail else ""
546
- if code == "connection_failed":
547
- return f"Connection test failed: cannot reach {api_url}{detail_suffix}"
548
- if code == "api_failed":
549
- return f"Connection test failed: API error{detail_suffix}"
550
- return f"Connection test failed{detail_suffix}"
551
-
552
- def _run_connection_test_with_retry(self, api_url: str, api_key: str) -> bool:
553
- """Run connection test with retry/skip prompts."""
554
- skip_prompt_shown = False
555
- while True:
556
- ok, reason = check_connection_with_reason(api_url, api_key, abort_on_error=False)
557
- if ok:
558
- return True
559
- code, detail = self._parse_error_reason(reason)
560
- message = self._format_connection_failure(code, detail, api_url)
561
- self.console.print(f"[{WARNING_STYLE}]{message}[/]")
562
- retry = self._prompt_yes_no("Retry connection test?", default=True)
563
- if retry:
564
- continue
565
- if not skip_prompt_shown:
566
- skip_prompt_shown = True
567
- skip = self._prompt_yes_no("Skip connection test and save?", default=False)
568
- if skip:
569
- return True
570
- self.console.print(f"[{WARNING_STYLE}]Cancelled save after failed connection test.[/]")
571
- return False
572
-
573
- def _announce_active_change(self, store: AccountStore, name: str) -> None:
574
- """Print active account change announcement."""
575
- account = store.get_account(name) or {}
576
- host = account.get("api_url", "")
577
- host_suffix = f" • {host}" if host else ""
578
- self.console.print(f"[{SUCCESS_STYLE}]Active account ➜ {name}{host_suffix}[/]")
@@ -1,75 +0,0 @@
1
- """Shared helpers for palette `/accounts`.
2
-
3
- Authors:
4
- Raymond Christopher (raymond.christopher@gdplabs.id)
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import os
10
- from typing import Any
11
-
12
- from glaip_sdk.cli.masking import mask_api_key_display
13
-
14
-
15
- def build_account_status_string(row: dict[str, Any], *, use_markup: bool = False) -> str:
16
- """Build status string for an account row (active/env-lock).
17
-
18
- When `use_markup` is True, returns Rich markup strings for Textual/Rich rendering;
19
- when False, returns plain text for console output.
20
-
21
- Example:
22
- build_account_status_string({"active": True, "env_lock": True}, use_markup=True)
23
- returns "[bold green]● active[/] · [yellow]🔒 env-lock[/]"
24
- use_markup=False returns "● active · 🔒 env-lock"
25
- """
26
- status_parts: list[str] = []
27
- if row.get("active"):
28
- status_parts.append("[bold green]● active[/]" if use_markup else "● active")
29
- if row.get("env_lock"):
30
- status_parts.append("[yellow]🔒 env-lock[/]" if use_markup else "🔒 env-lock")
31
- return " · ".join(status_parts)
32
-
33
-
34
- def env_credentials_present(*, partial: bool = False) -> bool:
35
- """Return True when env credentials are present.
36
-
37
- Args:
38
- partial: When True, treat either AIP_API_URL or AIP_API_KEY as present
39
- (used by UIs that should lock on any env override). When False,
40
- require both to be non-empty (used for context display).
41
- """
42
- api_url = (os.getenv("AIP_API_URL") or "").strip()
43
- api_key = (os.getenv("AIP_API_KEY") or "").strip()
44
- if partial:
45
- return bool(api_url or api_key)
46
- return bool(api_url and api_key)
47
-
48
-
49
- def build_account_rows(
50
- accounts: dict[str, dict[str, str]],
51
- active_account: str | None,
52
- env_lock: bool,
53
- ) -> list[dict[str, str | bool]]:
54
- """Build account rows for display from accounts dict.
55
-
56
- Args:
57
- accounts: Dictionary mapping account names to account data.
58
- active_account: Name of the currently active account.
59
- env_lock: Whether environment credentials are locking account switching.
60
-
61
- Returns:
62
- List of account row dictionaries with name, api_url, masked_key, active, and env_lock.
63
- """
64
- rows: list[dict[str, str | bool]] = []
65
- for name, account in sorted(accounts.items()):
66
- rows.append(
67
- {
68
- "name": name,
69
- "api_url": account.get("api_url", ""),
70
- "masked_key": mask_api_key_display(account.get("api_key", "")),
71
- "active": name == active_account,
72
- "env_lock": env_lock,
73
- }
74
- )
75
- return rows