glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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 (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -6,9 +6,12 @@ Author:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import importlib.util
10
+ import os
9
11
  import subprocess
10
12
  import sys
11
13
  from collections.abc import Sequence
14
+ from pathlib import Path
12
15
 
13
16
  import click
14
17
  from rich.console import Console
@@ -18,19 +21,132 @@ from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO_STYLE, SUCCESS_ST
18
21
  PACKAGE_NAME = "glaip-sdk"
19
22
 
20
23
 
21
- def _build_upgrade_command(include_prerelease: bool) -> Sequence[str]:
22
- """Return the pip command used to upgrade the SDK."""
23
- command = [
24
- sys.executable,
25
- "-m",
26
- "pip",
27
- "install",
28
- "--upgrade",
29
- PACKAGE_NAME,
30
- ]
24
+ def _is_uv_managed_environment() -> bool:
25
+ """Check if running in a uv-managed tool environment.
26
+
27
+ Uses a path-based heuristic against sys.executable, sys.prefix, and UV_TOOL_DIR
28
+ or UV_TOOL_BIN to detect a case-insensitive "uv/tools" segment. Update if uv
29
+ changes its layout.
30
+ """
31
+ if _has_uv_tool_path(sys.executable):
32
+ return True
33
+ if _has_uv_tool_path(sys.prefix):
34
+ return True
35
+ uv_tool_dir = os.environ.get("UV_TOOL_DIR") or os.environ.get("UV_TOOL_BIN")
36
+ if uv_tool_dir and _has_uv_tool_path(uv_tool_dir):
37
+ return True
38
+ return False
39
+
40
+
41
+ def _has_uv_tool_path(path: str) -> bool:
42
+ """Return True when a path contains a case-insensitive uv/tools segment."""
43
+ parts = [part.lower() for part in Path(path).parts]
44
+ for idx, part in enumerate(parts[:-1]):
45
+ if part == "uv" and parts[idx + 1] == "tools":
46
+ return True
47
+ return False
48
+
49
+
50
+ def _is_pip_available() -> bool:
51
+ """Return True when pip can be imported in the current interpreter."""
52
+ return importlib.util.find_spec("pip") is not None
53
+
54
+
55
+ def _build_missing_pip_guidance(
56
+ *,
57
+ include_prerelease: bool,
58
+ package_name: str = PACKAGE_NAME,
59
+ force_reinstall: bool = False,
60
+ ) -> tuple[str, str]:
61
+ """Return error and troubleshooting guidance when pip is unavailable."""
62
+ manual_cmd = _build_manual_upgrade_command(
63
+ include_prerelease,
64
+ package_name=package_name,
65
+ is_uv=True,
66
+ force_reinstall=force_reinstall,
67
+ )
68
+ error_detail = "pip is not available in this environment."
69
+ troubleshooting = (
70
+ "💡 Troubleshooting:\n"
71
+ f" • If you installed via uv tool, run: {manual_cmd}\n"
72
+ " • Otherwise install pip: python -m ensurepip"
73
+ )
74
+ return error_detail, troubleshooting
75
+
76
+
77
+ def _build_command_parts(
78
+ *,
79
+ package_name: str = PACKAGE_NAME,
80
+ is_uv: bool | None = None,
81
+ force_reinstall: bool = False,
82
+ include_prerelease: bool = False,
83
+ ) -> tuple[list[str], str]:
84
+ """Build the common parts of upgrade commands.
85
+
86
+ Returns:
87
+ Tuple of (command parts list, force_reinstall flag name).
88
+ For uv: (["uv", "tool", "install", "--upgrade", package_name], "--reinstall")
89
+ For pip: (["pip", "install", "--upgrade", package_name], "--force-reinstall")
90
+ """
91
+ if is_uv is None:
92
+ is_uv = _is_uv_managed_environment()
93
+
94
+ if is_uv:
95
+ command = ["uv", "tool", "install", "--upgrade", package_name]
96
+ reinstall_flag = "--reinstall"
97
+ else:
98
+ command = ["pip", "install", "--upgrade", package_name]
99
+ reinstall_flag = "--force-reinstall"
100
+
101
+ if force_reinstall:
102
+ command.insert(-1, reinstall_flag)
103
+
31
104
  if include_prerelease:
32
105
  command.append("--pre")
33
- return command
106
+
107
+ return command, reinstall_flag
108
+
109
+
110
+ def _build_upgrade_command(
111
+ include_prerelease: bool,
112
+ *,
113
+ package_name: str = PACKAGE_NAME,
114
+ is_uv: bool | None = None,
115
+ force_reinstall: bool = False,
116
+ ) -> Sequence[str]:
117
+ """Return the command used to upgrade the SDK (pip or uv tool install)."""
118
+ if is_uv is None:
119
+ is_uv = _is_uv_managed_environment()
120
+
121
+ command_parts, _ = _build_command_parts(
122
+ package_name=package_name,
123
+ is_uv=is_uv,
124
+ force_reinstall=force_reinstall,
125
+ include_prerelease=include_prerelease,
126
+ )
127
+
128
+ # For pip, prepend sys.executable and -m
129
+ if not is_uv:
130
+ command_parts = [sys.executable, "-m"] + command_parts
131
+
132
+ return command_parts
133
+
134
+
135
+ def _build_manual_upgrade_command(
136
+ include_prerelease: bool,
137
+ *,
138
+ package_name: str = PACKAGE_NAME,
139
+ is_uv: bool | None = None,
140
+ force_reinstall: bool = False,
141
+ ) -> str:
142
+ """Return a manual upgrade command string matching the active environment."""
143
+ command_parts, _ = _build_command_parts(
144
+ package_name=package_name,
145
+ is_uv=is_uv,
146
+ force_reinstall=force_reinstall,
147
+ include_prerelease=include_prerelease,
148
+ )
149
+ return " ".join(command_parts)
34
150
 
35
151
 
36
152
  @click.command(name="update")
@@ -40,22 +156,52 @@ def _build_upgrade_command(include_prerelease: bool) -> Sequence[str]:
40
156
  is_flag=True,
41
157
  help="Include pre-release versions when upgrading.",
42
158
  )
43
- def update_command(include_prerelease: bool) -> None:
44
- """Upgrade the glaip-sdk package using pip."""
159
+ @click.option(
160
+ "--reinstall",
161
+ "force_reinstall",
162
+ is_flag=True,
163
+ help="Force reinstall even if already up-to-date.",
164
+ )
165
+ def update_command(include_prerelease: bool, force_reinstall: bool) -> None:
166
+ """Upgrade the glaip-sdk package using pip or uv tool install."""
45
167
  console = Console()
46
- upgrade_cmd = _build_upgrade_command(include_prerelease)
168
+ # Call _is_uv_managed_environment() once and pass explicitly to avoid redundant calls
169
+ is_uv = _is_uv_managed_environment()
170
+ if not is_uv and not _is_pip_available():
171
+ error_detail, troubleshooting = _build_missing_pip_guidance(
172
+ include_prerelease=include_prerelease,
173
+ force_reinstall=force_reinstall,
174
+ )
175
+ raise click.ClickException(f"{error_detail}\n{troubleshooting}")
176
+ upgrade_cmd = _build_upgrade_command(
177
+ include_prerelease,
178
+ is_uv=is_uv,
179
+ force_reinstall=force_reinstall,
180
+ )
181
+
182
+ # Determine the appropriate manual command for error messages
183
+ manual_cmd = _build_manual_upgrade_command(
184
+ include_prerelease,
185
+ is_uv=is_uv,
186
+ force_reinstall=force_reinstall,
187
+ )
188
+
47
189
  console.print(f"[{ACCENT_STYLE}]Upgrading {PACKAGE_NAME} using[/] [{INFO_STYLE}]{' '.join(upgrade_cmd)}[/]")
48
190
 
49
191
  try:
50
192
  subprocess.run(upgrade_cmd, check=True)
51
193
  except FileNotFoundError as exc:
194
+ if is_uv:
195
+ raise click.ClickException(
196
+ f"Unable to locate uv executable. Please ensure uv is installed and on your PATH.\n"
197
+ f"Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh\n"
198
+ f"Or run manually: {manual_cmd}"
199
+ ) from exc
52
200
  raise click.ClickException(
53
201
  "Unable to locate Python executable to run pip. Please ensure Python is installed and try again."
54
202
  ) from exc
55
203
  except subprocess.CalledProcessError as exc:
56
- console.print(
57
- f"[{ERROR_STYLE}]Automatic upgrade failed.[/] Please run `pip install -U {PACKAGE_NAME}` manually."
58
- )
204
+ console.print(f"[{ERROR_STYLE}]Automatic upgrade failed.[/] Please run `{manual_cmd}` manually.")
59
205
  raise click.ClickException("Automatic upgrade failed.") from exc
60
206
 
61
207
  console.print(f"[{SUCCESS_STYLE}]✅ {PACKAGE_NAME} upgraded successfully.[/]")
glaip_sdk/cli/config.py CHANGED
@@ -5,21 +5,66 @@ Authors:
5
5
  """
6
6
 
7
7
  import os
8
+ from copy import deepcopy
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
11
12
  import yaml
12
13
 
13
- CONFIG_DIR = Path.home() / ".aip"
14
+ _ENV_CONFIG_DIR = os.getenv("AIP_CONFIG_DIR")
15
+ # Detect pytest environment: check for pytest markers or test session
16
+ # This provides automatic test isolation even if conftest.py doesn't set AIP_CONFIG_DIR
17
+ # Note: conftest.py sets AIP_CONFIG_DIR before imports, which takes precedence
18
+ _TEST_ENV = os.getenv("PYTEST_CURRENT_TEST") or os.getenv("PYTEST_XDIST_WORKER") or os.getenv("_PYTEST_RAISE")
19
+
20
+ if _ENV_CONFIG_DIR:
21
+ # Explicit override via environment variable (highest priority)
22
+ # This is set by conftest.py before imports, ensuring test isolation
23
+ CONFIG_DIR = Path(_ENV_CONFIG_DIR)
24
+ elif _TEST_ENV:
25
+ # Isolate test runs (including xdist workers) from the real user config directory
26
+ # Use a per-process unique temp directory to avoid conflicts in parallel test runs
27
+ import tempfile
28
+ import uuid
29
+
30
+ # Create a unique temp dir per test process to avoid conflicts
31
+ temp_base = Path(tempfile.gettempdir())
32
+ test_config_dir = temp_base / f"aip-test-config-{os.getpid()}-{uuid.uuid4().hex[:8]}"
33
+ CONFIG_DIR = test_config_dir
34
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
35
+ else: # pragma: no cover - default path used outside test runs
36
+ CONFIG_DIR = Path.home() / ".aip"
37
+
14
38
  CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
- _ALLOWED_KEYS = {"api_url", "api_key", "timeout"}
39
+ _ALLOWED_KEYS = {
40
+ "api_url",
41
+ "api_key",
42
+ "timeout",
43
+ "history_default_limit",
44
+ }
45
+ # Keys that must be preserved for multi-account support
46
+ _PRESERVE_KEYS = {
47
+ "version",
48
+ "active_account",
49
+ "accounts",
50
+ "tui",
51
+ }
16
52
 
17
53
 
18
54
  def _sanitize_config(data: dict[str, Any] | None) -> dict[str, Any]:
19
- """Return config filtered to allowed keys only."""
55
+ """Return config filtered to allowed keys only, preserving multi-account keys."""
20
56
  if not data:
21
57
  return {}
22
- return {k: v for k, v in data.items() if k in _ALLOWED_KEYS}
58
+ result: dict[str, Any] = {}
59
+ # Preserve multi-account structure (defensively copy to avoid callers mutating source)
60
+ for key in _PRESERVE_KEYS:
61
+ if key in data:
62
+ result[key] = deepcopy(data[key])
63
+ # Add allowed legacy keys (copied to avoid side effects)
64
+ for key in _ALLOWED_KEYS:
65
+ if key in data:
66
+ result[key] = deepcopy(data[key])
67
+ return result
23
68
 
24
69
 
25
70
  def load_config() -> dict[str, Any]:
@@ -0,0 +1,38 @@
1
+ """CLI-specific constants for glaip-sdk.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ # Minimum length that forces multiline YAML strings to be rendered using the literal
8
+ # block style. This prevents long prompts and instructions from being inlined.
9
+ LITERAL_STRING_THRESHOLD = 200
10
+
11
+ # Masking configuration
12
+ MASKING_ENABLED = True
13
+ MASK_SENSITIVE_FIELDS = {
14
+ "api_key",
15
+ "apikey",
16
+ "token",
17
+ "access_token",
18
+ "secret",
19
+ "client_secret",
20
+ "password",
21
+ "private_key",
22
+ "bearer",
23
+ }
24
+
25
+ # Table + pager behaviour
26
+ TABLE_SORT_ENABLED = True
27
+ PAGER_MODE = "auto" # valid values: "auto", "on", "off"
28
+ PAGER_WRAP_LINES = False
29
+ PAGER_HEADER_ENABLED = True
30
+
31
+ # Update notification toggle
32
+ UPDATE_CHECK_ENABLED = True
33
+
34
+ # Agent instruction preview defaults
35
+ DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT = 800
36
+
37
+ # Remote runs defaults
38
+ DEFAULT_REMOTE_RUNS_PAGE_LIMIT = 20
glaip_sdk/cli/context.py CHANGED
@@ -106,6 +106,14 @@ def output_flags() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
106
106
  """
107
107
 
108
108
  def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
109
+ """Apply output flags to a click command.
110
+
111
+ Args:
112
+ f: Click command function to decorate.
113
+
114
+ Returns:
115
+ Decorated command function.
116
+ """
109
117
  f = click.option(
110
118
  "--json",
111
119
  "json_mode",
@@ -0,0 +1,79 @@
1
+ """CLI core modules for glaip-sdk.
2
+
3
+ This package contains focused modules extracted from the monolithic cli/utils.py:
4
+ - context: Click context helpers, config loading, credential resolution
5
+ - prompting: prompt_toolkit + questionary wrappers, validators
6
+ - rendering: Rich console helpers, viewer launchers, renderer builders
7
+ - output: Table/console output utilities, list rendering
8
+
9
+ Authors:
10
+ Raymond Christopher (raymond.christopher@gdplabs.id)
11
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
12
+ """ # pylint: disable=duplicate-code
13
+
14
+ from __future__ import annotations
15
+
16
+ # Re-export all public APIs from submodules for convenience
17
+ from glaip_sdk.cli.core.context import (
18
+ bind_slash_session_context,
19
+ get_client,
20
+ handle_best_effort_check,
21
+ restore_slash_session_context,
22
+ )
23
+ from glaip_sdk.cli.core.output import (
24
+ coerce_to_row,
25
+ detect_export_format,
26
+ fetch_resource_for_export,
27
+ format_datetime_fields,
28
+ format_size,
29
+ handle_resource_export,
30
+ output_list,
31
+ output_result,
32
+ parse_json_line,
33
+ resolve_resource,
34
+ handle_ambiguous_resource,
35
+ sdk_version,
36
+ )
37
+ from glaip_sdk.cli.core.prompting import (
38
+ _fuzzy_pick_for_resources,
39
+ prompt_export_choice_questionary,
40
+ questionary_safe_ask,
41
+ )
42
+ from glaip_sdk.cli.core.rendering import (
43
+ build_renderer,
44
+ spinner_context,
45
+ stop_spinner,
46
+ update_spinner,
47
+ with_client_and_spinner,
48
+ )
49
+
50
+ __all__ = [
51
+ # Context
52
+ "bind_slash_session_context",
53
+ "get_client",
54
+ "handle_best_effort_check",
55
+ "restore_slash_session_context",
56
+ # Prompting
57
+ "_fuzzy_pick_for_resources",
58
+ "prompt_export_choice_questionary",
59
+ "questionary_safe_ask",
60
+ # Rendering
61
+ "build_renderer",
62
+ "spinner_context",
63
+ "stop_spinner",
64
+ "update_spinner",
65
+ "with_client_and_spinner",
66
+ # Output
67
+ "coerce_to_row",
68
+ "detect_export_format",
69
+ "fetch_resource_for_export",
70
+ "format_datetime_fields",
71
+ "format_size",
72
+ "handle_resource_export",
73
+ "output_list",
74
+ "output_result",
75
+ "parse_json_line",
76
+ "resolve_resource",
77
+ "handle_ambiguous_resource",
78
+ "sdk_version",
79
+ ]
@@ -0,0 +1,124 @@
1
+ """CLI context helpers, config loading, and credential resolution.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import os
12
+ from collections.abc import Mapping
13
+ from contextlib import contextmanager
14
+ from typing import TYPE_CHECKING, Any, cast
15
+
16
+ import click
17
+
18
+ from glaip_sdk.cli.config import load_config
19
+ from glaip_sdk.cli.hints import command_hint
20
+
21
+ if TYPE_CHECKING: # pragma: no cover - import-only during type checking
22
+ from glaip_sdk import Client
23
+
24
+
25
+ @contextmanager
26
+ def bind_slash_session_context(ctx: Any, session: Any) -> Any:
27
+ """Temporarily attach a slash session to the Click context.
28
+
29
+ Args:
30
+ ctx: Click context object.
31
+ session: SlashSession instance to bind.
32
+
33
+ Yields:
34
+ None - context manager for use in with statement.
35
+ """
36
+ ctx_obj = getattr(ctx, "obj", None)
37
+ has_context = isinstance(ctx_obj, dict)
38
+ previous_session = ctx_obj.get("_slash_session") if has_context else None
39
+ if has_context:
40
+ ctx_obj["_slash_session"] = session
41
+ try:
42
+ yield
43
+ finally:
44
+ if has_context:
45
+ if previous_session is None:
46
+ ctx_obj.pop("_slash_session", None)
47
+ else:
48
+ ctx_obj["_slash_session"] = previous_session
49
+
50
+
51
+ def restore_slash_session_context(ctx_obj: dict[str, Any], previous_session: Any | None) -> None:
52
+ """Restore slash session context after operation.
53
+
54
+ Args:
55
+ ctx_obj: Click context obj dictionary.
56
+ previous_session: Previous session to restore, or None to remove.
57
+ """
58
+ if previous_session is None:
59
+ ctx_obj.pop("_slash_session", None)
60
+ else:
61
+ ctx_obj["_slash_session"] = previous_session
62
+
63
+
64
+ def handle_best_effort_check(
65
+ check_func: Any,
66
+ ) -> None:
67
+ """Handle best-effort duplicate/existence checks with proper exception handling.
68
+
69
+ Args:
70
+ check_func: Function that performs the check and raises ClickException if duplicate found.
71
+ """
72
+ try:
73
+ check_func()
74
+ except click.ClickException:
75
+ raise
76
+ except Exception:
77
+ # Non-fatal: best-effort duplicate check
78
+ pass
79
+
80
+
81
+ def get_client(ctx: Any) -> Client: # pragma: no cover
82
+ """Get configured client from context and account store (ctx > account)."""
83
+ # Import here to avoid circular import
84
+ from glaip_sdk.cli.auth import resolve_credentials # noqa: PLC0415
85
+
86
+ module = importlib.import_module("glaip_sdk")
87
+ client_class = cast("type[Client]", module.Client)
88
+ context_config_obj = getattr(ctx, "obj", None)
89
+ context_config = context_config_obj if isinstance(context_config_obj, Mapping) else {}
90
+
91
+ account_name = context_config.get("account_name")
92
+ api_url, api_key, _ = resolve_credentials(
93
+ account_name=account_name,
94
+ api_url=context_config.get("api_url"),
95
+ api_key=context_config.get("api_key"),
96
+ )
97
+
98
+ if not api_url or not api_key:
99
+ configure_hint = command_hint("accounts add", slash_command="login", ctx=ctx)
100
+ actions: list[str] = []
101
+ if configure_hint:
102
+ actions.append(f"Run `{configure_hint}` to add an account profile")
103
+ else:
104
+ actions.append("add an account with 'aip accounts add'")
105
+ raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
106
+
107
+ # Get timeout from context or config
108
+ timeout = context_config.get("timeout")
109
+ if timeout is None:
110
+ raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
111
+ try:
112
+ timeout = float(raw_timeout) if raw_timeout != "0" else None
113
+ except ValueError:
114
+ timeout = None
115
+ if timeout is None:
116
+ # Fallback to legacy config
117
+ file_config = load_config() or {}
118
+ timeout = file_config.get("timeout")
119
+
120
+ return client_class(
121
+ api_url=api_url,
122
+ api_key=api_key,
123
+ timeout=float(timeout or 30.0),
124
+ )