glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__py3-none-any.whl

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