glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__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 (216) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1250 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +271 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  12. glaip_sdk/cli/commands/agents/_common.py +561 -0
  13. glaip_sdk/cli/commands/agents/create.py +151 -0
  14. glaip_sdk/cli/commands/agents/delete.py +64 -0
  15. glaip_sdk/cli/commands/agents/get.py +89 -0
  16. glaip_sdk/cli/commands/agents/list.py +129 -0
  17. glaip_sdk/cli/commands/agents/run.py +264 -0
  18. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  19. glaip_sdk/cli/commands/agents/update.py +112 -0
  20. glaip_sdk/cli/commands/common_config.py +104 -0
  21. glaip_sdk/cli/commands/configure.py +734 -143
  22. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  23. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  24. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  25. glaip_sdk/cli/commands/mcps/create.py +152 -0
  26. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  27. glaip_sdk/cli/commands/mcps/get.py +212 -0
  28. glaip_sdk/cli/commands/mcps/list.py +69 -0
  29. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  30. glaip_sdk/cli/commands/mcps/update.py +190 -0
  31. glaip_sdk/cli/commands/models.py +14 -12
  32. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  33. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  34. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  35. glaip_sdk/cli/commands/tools/_common.py +80 -0
  36. glaip_sdk/cli/commands/tools/create.py +228 -0
  37. glaip_sdk/cli/commands/tools/delete.py +61 -0
  38. glaip_sdk/cli/commands/tools/get.py +103 -0
  39. glaip_sdk/cli/commands/tools/list.py +69 -0
  40. glaip_sdk/cli/commands/tools/script.py +49 -0
  41. glaip_sdk/cli/commands/tools/update.py +102 -0
  42. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  43. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  44. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  45. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  46. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  47. glaip_sdk/cli/commands/update.py +164 -23
  48. glaip_sdk/cli/config.py +49 -7
  49. glaip_sdk/cli/constants.py +38 -0
  50. glaip_sdk/cli/context.py +8 -0
  51. glaip_sdk/cli/core/__init__.py +79 -0
  52. glaip_sdk/cli/core/context.py +124 -0
  53. glaip_sdk/cli/core/output.py +851 -0
  54. glaip_sdk/cli/core/prompting.py +649 -0
  55. glaip_sdk/cli/core/rendering.py +187 -0
  56. glaip_sdk/cli/display.py +45 -32
  57. glaip_sdk/cli/entrypoint.py +20 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +14 -17
  60. glaip_sdk/cli/main.py +344 -167
  61. glaip_sdk/cli/masking.py +21 -33
  62. glaip_sdk/cli/mcp_validators.py +5 -15
  63. glaip_sdk/cli/pager.py +15 -22
  64. glaip_sdk/cli/parsers/__init__.py +1 -3
  65. glaip_sdk/cli/parsers/json_input.py +11 -22
  66. glaip_sdk/cli/resolution.py +5 -10
  67. glaip_sdk/cli/rich_helpers.py +1 -3
  68. glaip_sdk/cli/slash/__init__.py +0 -9
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +65 -29
  72. glaip_sdk/cli/slash/prompt.py +24 -10
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +827 -232
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +12 -52
  91. glaip_sdk/cli/transcript/cache.py +258 -60
  92. glaip_sdk/cli/transcript/capture.py +72 -21
  93. glaip_sdk/cli/transcript/history.py +815 -0
  94. glaip_sdk/cli/transcript/launcher.py +1 -3
  95. glaip_sdk/cli/transcript/viewer.py +79 -329
  96. glaip_sdk/cli/update_notifier.py +385 -24
  97. glaip_sdk/cli/validators.py +16 -18
  98. glaip_sdk/client/__init__.py +3 -1
  99. glaip_sdk/client/_schedule_payloads.py +89 -0
  100. glaip_sdk/client/agent_runs.py +147 -0
  101. glaip_sdk/client/agents.py +370 -100
  102. glaip_sdk/client/base.py +78 -35
  103. glaip_sdk/client/hitl.py +136 -0
  104. glaip_sdk/client/main.py +25 -10
  105. glaip_sdk/client/mcps.py +166 -27
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +583 -79
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +214 -56
  113. glaip_sdk/client/validators.py +20 -48
  114. glaip_sdk/config/constants.py +11 -0
  115. glaip_sdk/exceptions.py +1 -3
  116. glaip_sdk/hitl/__init__.py +48 -0
  117. glaip_sdk/hitl/base.py +64 -0
  118. glaip_sdk/hitl/callback.py +43 -0
  119. glaip_sdk/hitl/local.py +121 -0
  120. glaip_sdk/hitl/remote.py +523 -0
  121. glaip_sdk/icons.py +9 -3
  122. glaip_sdk/mcps/__init__.py +21 -0
  123. glaip_sdk/mcps/base.py +345 -0
  124. glaip_sdk/models/__init__.py +107 -0
  125. glaip_sdk/models/agent.py +47 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/mcp.py +33 -0
  129. glaip_sdk/models/schedule.py +224 -0
  130. glaip_sdk/models/tool.py +33 -0
  131. glaip_sdk/payload_schemas/__init__.py +1 -13
  132. glaip_sdk/payload_schemas/agent.py +1 -3
  133. glaip_sdk/registry/__init__.py +55 -0
  134. glaip_sdk/registry/agent.py +164 -0
  135. glaip_sdk/registry/base.py +139 -0
  136. glaip_sdk/registry/mcp.py +253 -0
  137. glaip_sdk/registry/tool.py +445 -0
  138. glaip_sdk/rich_components.py +58 -2
  139. glaip_sdk/runner/__init__.py +76 -0
  140. glaip_sdk/runner/base.py +84 -0
  141. glaip_sdk/runner/deps.py +112 -0
  142. glaip_sdk/runner/langgraph.py +872 -0
  143. glaip_sdk/runner/logging_config.py +77 -0
  144. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  145. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  146. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  147. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  148. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  149. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  150. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  151. glaip_sdk/schedules/__init__.py +22 -0
  152. glaip_sdk/schedules/base.py +291 -0
  153. glaip_sdk/tools/__init__.py +22 -0
  154. glaip_sdk/tools/base.py +468 -0
  155. glaip_sdk/utils/__init__.py +59 -12
  156. glaip_sdk/utils/a2a/__init__.py +34 -0
  157. glaip_sdk/utils/a2a/event_processor.py +188 -0
  158. glaip_sdk/utils/agent_config.py +4 -14
  159. glaip_sdk/utils/bundler.py +403 -0
  160. glaip_sdk/utils/client.py +111 -0
  161. glaip_sdk/utils/client_utils.py +46 -28
  162. glaip_sdk/utils/datetime_helpers.py +58 -0
  163. glaip_sdk/utils/discovery.py +78 -0
  164. glaip_sdk/utils/display.py +25 -21
  165. glaip_sdk/utils/export.py +143 -0
  166. glaip_sdk/utils/general.py +1 -36
  167. glaip_sdk/utils/import_export.py +15 -16
  168. glaip_sdk/utils/import_resolver.py +524 -0
  169. glaip_sdk/utils/instructions.py +101 -0
  170. glaip_sdk/utils/rendering/__init__.py +115 -1
  171. glaip_sdk/utils/rendering/formatting.py +38 -23
  172. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  173. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  174. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  175. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  176. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  177. glaip_sdk/utils/rendering/models.py +18 -8
  178. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  179. glaip_sdk/utils/rendering/renderer/base.py +534 -882
  180. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  181. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  182. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  183. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  184. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  185. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  186. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  187. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  188. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  189. glaip_sdk/utils/rendering/state.py +204 -0
  190. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  191. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  192. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  193. glaip_sdk/utils/rendering/steps/format.py +176 -0
  194. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  195. glaip_sdk/utils/rendering/timing.py +36 -0
  196. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  197. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  198. glaip_sdk/utils/resource_refs.py +29 -26
  199. glaip_sdk/utils/runtime_config.py +425 -0
  200. glaip_sdk/utils/serialization.py +32 -46
  201. glaip_sdk/utils/sync.py +162 -0
  202. glaip_sdk/utils/tool_detection.py +301 -0
  203. glaip_sdk/utils/tool_storage_provider.py +140 -0
  204. glaip_sdk/utils/validation.py +20 -28
  205. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
  206. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  207. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  208. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  209. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  210. glaip_sdk/cli/commands/agents.py +0 -1412
  211. glaip_sdk/cli/commands/mcps.py +0 -1225
  212. glaip_sdk/cli/commands/tools.py +0 -597
  213. glaip_sdk/cli/utils.py +0 -1330
  214. glaip_sdk/models.py +0 -259
  215. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  216. glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
glaip_sdk/cli/auth.py CHANGED
@@ -1,24 +1,25 @@
1
- """Authentication export helpers for MCP CLI commands.
1
+ """Authentication export helpers for MCP CLI commands and credential resolution.
2
2
 
3
3
  This module provides utilities for preparing authentication data for export,
4
4
  including interactive secret capture and placeholder generation.
5
5
 
6
- These helpers are distinct from the AIP CLI's own authentication, which always
7
- relies on the API URL and API key managed via ``aip configure`` / `AIP_API_*`
8
- environment variables.
6
+ It also provides credential resolution for the AIP CLI, supporting multiple
7
+ account profiles and environment variable overrides.
9
8
 
10
9
  Authors:
11
10
  Raymond Christopher (raymond.christopher@gdplabs.id)
12
11
  """
13
12
 
14
- from collections.abc import Iterable, Mapping
13
+ import os
14
+ from collections.abc import Callable, Iterable, Mapping
15
15
  from typing import Any
16
16
 
17
17
  import click
18
18
  from rich.console import Console
19
19
 
20
20
  from glaip_sdk.branding import HINT_PREFIX_STYLE, WARNING_STYLE
21
- from glaip_sdk.cli.utils import command_hint, format_command_hint
21
+ from glaip_sdk.cli.account_store import AccountNotFoundError, AccountStoreError, get_account_store
22
+ from glaip_sdk.cli.hints import command_hint, format_command_hint
22
23
 
23
24
 
24
25
  def prepare_authentication_export(
@@ -65,9 +66,7 @@ def prepare_authentication_export(
65
66
 
66
67
  # Handle bearer-token authentication
67
68
  if auth_type == "bearer-token":
68
- return _prepare_bearer_token_auth(
69
- auth, prompt_for_secrets, placeholder, console
70
- )
69
+ return _prepare_bearer_token_auth(auth, prompt_for_secrets, placeholder, console)
71
70
 
72
71
  # Handle api-key authentication
73
72
  if auth_type == "api-key":
@@ -75,9 +74,7 @@ def prepare_authentication_export(
75
74
 
76
75
  # Handle custom-header authentication
77
76
  if auth_type == "custom-header":
78
- return _prepare_custom_header_auth(
79
- auth, prompt_for_secrets, placeholder, console
80
- )
77
+ return _prepare_custom_header_auth(auth, prompt_for_secrets, placeholder, console)
81
78
 
82
79
  # Unknown auth type - return as-is but strip helper metadata
83
80
  result = auth.copy()
@@ -85,9 +82,7 @@ def prepare_authentication_export(
85
82
  return result
86
83
 
87
84
 
88
- def _get_token_value(
89
- prompt_for_secrets: bool, placeholder: str, console: Console
90
- ) -> str:
85
+ def _get_token_value(prompt_for_secrets: bool, placeholder: str, console: Console) -> str:
91
86
  """Get bearer token value either by prompting or using a placeholder.
92
87
 
93
88
  Args:
@@ -109,13 +104,28 @@ def _get_token_value(
109
104
  )
110
105
 
111
106
  if not click.get_text_stream("stdin").isatty():
112
- console.print(
113
- f"[{WARNING_STYLE}]⚠️ Non-interactive mode: "
114
- "using placeholder for bearer token[/]"
115
- )
107
+ console.print(f"[{WARNING_STYLE}]⚠️ Non-interactive mode: using placeholder for bearer token[/]")
116
108
  return placeholder
117
109
 
118
110
 
111
+ def _normalize_header_keys(
112
+ header_keys: Iterable[str] | str | None,
113
+ *,
114
+ default: Iterable[str] | None = None,
115
+ ) -> list[str]:
116
+ """Normalize header_keys to a list, handling strings and None safely."""
117
+ if header_keys is None:
118
+ return list(default or [])
119
+ if isinstance(header_keys, str):
120
+ return [header_keys] if header_keys else list(default or [])
121
+ try:
122
+ return list(header_keys)
123
+ except TypeError:
124
+ raise click.ClickException(
125
+ f"Invalid header_keys type: expected string or iterable, got {type(header_keys).__name__}"
126
+ ) from None
127
+
128
+
119
129
  def _build_bearer_headers(auth: dict[str, Any], token_value: str) -> dict[str, str]:
120
130
  """Build headers for bearer token authentication.
121
131
 
@@ -127,7 +137,10 @@ def _build_bearer_headers(auth: dict[str, Any], token_value: str) -> dict[str, s
127
137
  A dictionary of HTTP headers including the Authorization header when
128
138
  applicable.
129
139
  """
130
- header_keys = auth.get("header_keys", ["Authorization"])
140
+ default_header_keys = ["Authorization"]
141
+ has_header_keys = "header_keys" in auth
142
+ header_keys_raw = auth.get("header_keys") if has_header_keys else default_header_keys
143
+ header_keys = _normalize_header_keys(header_keys_raw, default=None if has_header_keys else default_header_keys)
131
144
  headers = {}
132
145
  for key in header_keys:
133
146
  # Prepend "Bearer " if this is Authorization header
@@ -186,8 +199,8 @@ def _extract_api_key_name(auth: dict[str, Any]) -> str | None:
186
199
  """
187
200
  key_name = auth.get("key")
188
201
  if not key_name and "header_keys" in auth:
189
- header_keys = auth["header_keys"]
190
- if isinstance(header_keys, list) and header_keys:
202
+ header_keys = _normalize_header_keys(auth["header_keys"])
203
+ if header_keys:
191
204
  key_name = header_keys[0]
192
205
  return key_name
193
206
 
@@ -220,16 +233,11 @@ def _get_api_key_value(
220
233
  )
221
234
 
222
235
  if not click.get_text_stream("stdin").isatty():
223
- console.print(
224
- f"[{WARNING_STYLE}]⚠️ Non-interactive mode: "
225
- f"using placeholder for API key '{key_name}'[/]"
226
- )
236
+ console.print(f"[{WARNING_STYLE}]⚠️ Non-interactive mode: using placeholder for API key '{key_name}'[/]")
227
237
  return placeholder
228
238
 
229
239
 
230
- def _build_api_key_headers(
231
- auth: dict[str, Any], key_name: str | None, key_value: str
232
- ) -> dict[str, str]:
240
+ def _build_api_key_headers(auth: dict[str, Any], key_name: str | None, key_value: str) -> dict[str, str]:
233
241
  """Build headers for API key authentication.
234
242
 
235
243
  Args:
@@ -240,8 +248,12 @@ def _build_api_key_headers(
240
248
  Returns:
241
249
  A dictionary of HTTP headers for API key authentication.
242
250
  """
243
- header_keys = auth.get("header_keys", [key_name] if key_name else [])
244
- return {key: key_value for key in header_keys}
251
+ default_header_keys = [key_name] if key_name else []
252
+ has_header_keys = "header_keys" in auth
253
+ header_keys_raw = auth.get("header_keys") if has_header_keys else default_header_keys
254
+ header_keys_list = _normalize_header_keys(header_keys_raw, default=None if has_header_keys else default_header_keys)
255
+ filtered_keys = [k for k in header_keys_list if k]
256
+ return dict.fromkeys(filtered_keys, key_value)
245
257
 
246
258
 
247
259
  def _prepare_api_key_auth(
@@ -270,9 +282,7 @@ def _prepare_api_key_auth(
270
282
 
271
283
  # Capture or use placeholder for value
272
284
  if not has_valid_value:
273
- key_value = _get_api_key_value(
274
- key_name, prompt_for_secrets, placeholder, console
275
- )
285
+ key_value = _get_api_key_value(key_name, prompt_for_secrets, placeholder, console)
276
286
 
277
287
  # Check if original had headers structure
278
288
  if "headers" in auth or "header_keys" in auth:
@@ -317,9 +327,7 @@ def _prepare_custom_header_auth(
317
327
  return {"type": "custom-header", "headers": headers}
318
328
 
319
329
 
320
- def _extract_header_names(
321
- existing_headers: Mapping[str, Any] | None, header_keys: Iterable[str] | None
322
- ) -> list[str]:
330
+ def _extract_header_names(existing_headers: Mapping[str, Any] | None, header_keys: Iterable[str] | None) -> list[str]:
323
331
  """Extract the list of header names to process.
324
332
 
325
333
  Args:
@@ -331,9 +339,7 @@ def _extract_header_names(
331
339
  """
332
340
  if existing_headers:
333
341
  return list(existing_headers.keys())
334
- if header_keys:
335
- return list(header_keys)
336
- return []
342
+ return _normalize_header_keys(header_keys)
337
343
 
338
344
 
339
345
  def _is_valid_secret(value: Any) -> bool:
@@ -376,9 +382,7 @@ def _prompt_or_placeholder(
376
382
  )
377
383
 
378
384
  if not click.get_text_stream("stdin").isatty():
379
- console.print(
380
- f"[{WARNING_STYLE}]⚠️ Non-interactive mode: using placeholder for header '{name}'[/]"
381
- )
385
+ console.print(f"[{WARNING_STYLE}]⚠️ Non-interactive mode: using placeholder for header '{name}'[/]")
382
386
  return placeholder
383
387
 
384
388
 
@@ -450,11 +454,12 @@ def _prompt_secret_with_placeholder(
450
454
  tip = command_hint(tip_cli_command, tip_slash_command)
451
455
  if tip:
452
456
  console.print(
453
- f"[{HINT_PREFIX_STYLE}]Tip:[/] use {format_command_hint(tip) or tip} later if you want to update these credentials."
457
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] use {format_command_hint(tip) or tip} later "
458
+ "if you want to update these credentials."
454
459
  )
455
460
 
456
461
  attempts = 0
457
- while attempts <= retry_limit:
462
+ while attempts <= retry_limit: # pragma: no cover
458
463
  response = click.prompt(
459
464
  prompt_message,
460
465
  default="",
@@ -477,3 +482,224 @@ def _prompt_secret_with_placeholder(
477
482
 
478
483
  # This line is unreachable as the loop always returns
479
484
  # return placeholder
485
+
486
+
487
+ # ----------------------------- Credential Resolution ----------------------------- #
488
+
489
+
490
+ def resolve_api_url_from_context(
491
+ ctx: Any,
492
+ *,
493
+ get_api_url: Callable[[Any], str | None] | None = None,
494
+ get_account_name: Callable[[Any], str | None] | None = None,
495
+ ) -> str | None:
496
+ """Resolve API URL from context using account store (CLI/palette ignores env creds).
497
+
498
+ Helper function to extract API URL from various context formats.
499
+ Used by transcript capture and slash session to avoid code duplication.
500
+
501
+ Args:
502
+ ctx: Context object (can be dict, click.Context, or any object with attributes).
503
+ get_api_url: Optional function to extract api_url from context.
504
+ If None, tries ctx.obj.get("api_url") or ctx.get("api_url").
505
+ get_account_name: Optional function to extract account_name from context.
506
+ If None, tries ctx.obj.get("account_name") or ctx.get("account_name").
507
+
508
+ Returns:
509
+ Resolved API URL or None.
510
+ """
511
+ api_url = None
512
+ account_name = None
513
+
514
+ if get_api_url:
515
+ api_url = get_api_url(ctx)
516
+ elif isinstance(ctx, dict):
517
+ api_url = ctx.get("api_url")
518
+ elif hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
519
+ api_url = ctx.obj.get("api_url")
520
+
521
+ if get_account_name:
522
+ account_name = get_account_name(ctx)
523
+ elif isinstance(ctx, dict):
524
+ account_name = ctx.get("account_name")
525
+ elif hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
526
+ account_name = ctx.obj.get("account_name")
527
+
528
+ if isinstance(api_url, str) and api_url.strip():
529
+ return api_url.strip()
530
+
531
+ try:
532
+ resolved_url, _, _ = resolve_credentials(
533
+ account_name=account_name,
534
+ api_url=None,
535
+ api_key=None,
536
+ ignore_env_creds=True,
537
+ )
538
+ except Exception:
539
+ return None
540
+
541
+ return resolved_url
542
+
543
+
544
+ def _resolve_account_name(account_name: str | None) -> str | None:
545
+ """Resolve account name from parameter (env var removed for CLI/palette)."""
546
+ return account_name
547
+
548
+
549
+ def _validate_account_exists(account_name: str | None, store: Any) -> None:
550
+ """Validate that the specified account exists.
551
+
552
+ Raises:
553
+ AccountNotFoundError: If account_name is specified but account doesn't exist.
554
+ """
555
+ if account_name:
556
+ account = store.get_account(account_name)
557
+ if not account:
558
+ raise AccountNotFoundError(
559
+ f"Account '{account_name}' not found. Run 'aip accounts list' to see available accounts."
560
+ )
561
+
562
+
563
+ def _merge_credentials(
564
+ api_url: str | None,
565
+ api_key: str | None,
566
+ profile_url: str | None,
567
+ profile_key: str | None,
568
+ ignore_env_creds: bool,
569
+ ) -> tuple[str | None, str | None]:
570
+ """Merge credentials from multiple sources.
571
+
572
+ Args:
573
+ api_url: Explicit API URL override.
574
+ api_key: Explicit API key override.
575
+ profile_url: Profile API URL.
576
+ profile_key: Profile API key.
577
+ ignore_env_creds: If True, ignore env vars.
578
+
579
+ Returns:
580
+ Tuple of (final_url, final_key).
581
+ """
582
+ if not ignore_env_creds:
583
+ env_url = os.getenv("AIP_API_URL")
584
+ env_key = os.getenv("AIP_API_KEY")
585
+ final_url = api_url or env_url or profile_url
586
+ final_key = api_key or env_key or profile_key
587
+ else:
588
+ final_url = api_url or profile_url
589
+ final_key = api_key or profile_key
590
+ return final_url, final_key
591
+
592
+
593
+ def _determine_source(
594
+ api_url: str | None,
595
+ api_key: str | None,
596
+ account_name: str | None,
597
+ store: Any,
598
+ ) -> str:
599
+ """Determine the source of credentials.
600
+
601
+ Returns:
602
+ Source string describing where credentials came from.
603
+ """
604
+ if api_url or api_key:
605
+ return "flag"
606
+ if account_name:
607
+ return f"account:{account_name}"
608
+ active = store.get_active_account()
609
+ return f"active_profile:{active}" if active else "none"
610
+
611
+
612
+ _ENV_WARNING_EMITTED = False
613
+
614
+
615
+ def _maybe_warn_env_creds_ignored(ignore_env_creds: bool) -> None:
616
+ """Emit a one-time warning when env credentials are present but ignored."""
617
+ global _ENV_WARNING_EMITTED
618
+
619
+ if _ENV_WARNING_EMITTED or not ignore_env_creds:
620
+ return
621
+
622
+ if os.getenv("AIP_API_URL") or os.getenv("AIP_API_KEY"):
623
+ click.echo(
624
+ "Warning: CLI ignores AIP_API_URL/AIP_API_KEY; use account profiles via 'aip accounts add/use'. "
625
+ "Python SDK callers can opt in with ignore_env_creds=False.",
626
+ err=True,
627
+ )
628
+ _ENV_WARNING_EMITTED = True
629
+
630
+
631
+ def resolve_credentials(
632
+ account_name: str | None = None,
633
+ api_url: str | None = None,
634
+ api_key: str | None = None,
635
+ *,
636
+ ignore_env_creds: bool = True,
637
+ ) -> tuple[str | None, str | None, str]:
638
+ """Resolve credentials from multiple sources with precedence.
639
+
640
+ For CLI/palette: ignores raw credential env vars (AIP_API_URL/AIP_API_KEY),
641
+ and only uses explicit account selection (no AIP_ACCOUNT env). Python SDK can use
642
+ ignore_env_creds=False to honor env vars if needed.
643
+
644
+ Precedence order (CLI/palette):
645
+ 1. Explicit parameters (api_url, api_key)
646
+ 2. Account profile (from account_name or active_account)
647
+
648
+ Args:
649
+ account_name: Account name to use, or None for active account.
650
+ api_url: Explicit API URL override.
651
+ api_key: Explicit API key override.
652
+ ignore_env_creds: If True (default), ignore AIP_API_URL/AIP_API_KEY env vars.
653
+
654
+ Returns:
655
+ Tuple of (api_url, api_key, source) where source describes where
656
+ credentials came from (e.g., "flag", "active_profile", "account:name").
657
+
658
+ Raises:
659
+ click.ClickException: If a requested account does not exist.
660
+ """
661
+ _maybe_warn_env_creds_ignored(ignore_env_creds)
662
+
663
+ # 1. Explicit parameters take highest precedence
664
+ if api_url and api_key:
665
+ return api_url, api_key, "flag"
666
+
667
+ # 2. Account profile resolution
668
+ account_name = _resolve_account_name(account_name)
669
+ store = get_account_store()
670
+ try:
671
+ _validate_account_exists(account_name, store)
672
+ except AccountNotFoundError as exc:
673
+ raise click.ClickException(str(exc)) from exc
674
+
675
+ try:
676
+ profile_url, profile_key = store.get_credentials(account_name)
677
+ except AccountStoreError:
678
+ profile_url, profile_key = None, None
679
+
680
+ final_url, final_key = _merge_credentials(api_url, api_key, profile_url, profile_key, ignore_env_creds)
681
+ source = _determine_source(api_url, api_key, account_name, store)
682
+
683
+ return final_url, final_key, source
684
+
685
+
686
+ def get_credentials(
687
+ account_name: str | None = None,
688
+ api_url: str | None = None,
689
+ api_key: str | None = None,
690
+ ) -> tuple[str | None, str | None]:
691
+ """Get credentials for CLI commands (backward compatible wrapper).
692
+
693
+ This function maintains backward compatibility with existing code that
694
+ expects (url, key) tuple. For source information, use resolve_credentials.
695
+
696
+ Args:
697
+ account_name: Account name to use, or None for active account.
698
+ api_url: Explicit API URL override.
699
+ api_key: Explicit API key override.
700
+
701
+ Returns:
702
+ Tuple of (api_url, api_key).
703
+ """
704
+ url, key, _ = resolve_credentials(account_name, api_url, api_key)
705
+ return url, key
@@ -1,5 +1,5 @@
1
1
  """CLI commands package exports."""
2
2
 
3
- from glaip_sdk.cli.commands import agents, configure, mcps, models, tools, update
3
+ from glaip_sdk.cli.commands import agents, configure, mcps, models, tools, transcripts, update
4
4
 
5
- __all__ = ["agents", "configure", "mcps", "models", "tools", "update"]
5
+ __all__ = ["agents", "configure", "mcps", "models", "tools", "transcripts", "update"]