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
@@ -0,0 +1,235 @@
1
+ """List MCP tools command.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import click
13
+
14
+ from glaip_sdk.branding import ACCENT_STYLE, INFO
15
+ from glaip_sdk.cli.context import get_ctx_value, output_flags
16
+ from glaip_sdk.cli.core.context import get_client
17
+ from glaip_sdk.cli.core.output import output_list
18
+ from glaip_sdk.cli.core.rendering import spinner_context
19
+ from glaip_sdk.cli.display import handle_json_output
20
+ from glaip_sdk.cli.io import load_resource_from_file_with_validation
21
+ from glaip_sdk.cli.mcp_validators import validate_mcp_config_structure
22
+ from glaip_sdk.icons import ICON_TOOL
23
+
24
+ from ._common import _resolve_mcp, console, mcps_group
25
+
26
+ MAX_DESCRIPTION_LEN = 50
27
+
28
+
29
+ def _get_tools_from_config(ctx: Any, client: Any, config_file: str) -> tuple[list[dict[str, Any]], str]:
30
+ """Get tools from MCP config file.
31
+
32
+ Args:
33
+ ctx: Click context
34
+ client: GlaIP client instance
35
+ config_file: Path to config file
36
+
37
+ Returns:
38
+ Tuple of (tools list, title string)
39
+ """
40
+ config_data = load_resource_from_file_with_validation(Path(config_file), "MCP config")
41
+
42
+ # Validate config structure
43
+ transport = config_data.get("transport")
44
+ if "config" not in config_data:
45
+ raise click.ClickException("Invalid MCP config: missing 'config' section in the file.")
46
+ config_data["config"] = validate_mcp_config_structure(
47
+ config_data["config"],
48
+ transport=transport,
49
+ source=config_file,
50
+ )
51
+
52
+ # Get tools from config without saving
53
+ with spinner_context(
54
+ ctx,
55
+ "[bold blue]Fetching tools from config…[/bold blue]",
56
+ console_override=console,
57
+ ):
58
+ tools = client.mcps.get_mcp_tools_from_config(config_data)
59
+
60
+ title = f"{ICON_TOOL} Tools from config: {Path(config_file).name}"
61
+ return tools, title
62
+
63
+
64
+ def _get_tools_from_mcp(ctx: Any, client: Any, mcp_ref: str | None) -> tuple[list[dict[str, Any]], str]:
65
+ """Get tools from saved MCP.
66
+
67
+ Args:
68
+ ctx: Click context
69
+ client: GlaIP client instance
70
+ mcp_ref: MCP reference (ID or name)
71
+
72
+ Returns:
73
+ Tuple of (tools list, title string)
74
+ """
75
+ mcp = _resolve_mcp(ctx, client, mcp_ref)
76
+
77
+ with spinner_context(
78
+ ctx,
79
+ "[bold blue]Fetching MCP tools…[/bold blue]",
80
+ console_override=console,
81
+ ):
82
+ tools = client.mcps.get_mcp_tools(mcp.id)
83
+
84
+ title = f"{ICON_TOOL} Tools from MCP: {mcp.name}"
85
+ return tools, title
86
+
87
+
88
+ def _output_tool_names(ctx: Any, tools: list[dict[str, Any]]) -> None:
89
+ """Output only tool names.
90
+
91
+ Args:
92
+ ctx: Click context
93
+ tools: List of tool dictionaries
94
+ """
95
+ view = get_ctx_value(ctx, "view", "rich")
96
+ tool_names = [tool.get("name", "N/A") for tool in tools]
97
+
98
+ if view == "json":
99
+ handle_json_output(ctx, tool_names)
100
+ elif view == "plain":
101
+ if tool_names:
102
+ for name in tool_names:
103
+ console.print(name, markup=False)
104
+ console.print(f"Total: {len(tool_names)} tools", markup=False)
105
+ else:
106
+ console.print("No tools found", markup=False)
107
+ else:
108
+ if tool_names:
109
+ for name in tool_names:
110
+ console.print(name)
111
+ console.print(f"[dim]Total: {len(tool_names)} tools[/dim]")
112
+ else:
113
+ console.print("[yellow]No tools found[/yellow]")
114
+
115
+
116
+ def _transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
117
+ """Transform a tool dictionary to a display row dictionary.
118
+
119
+ Args:
120
+ tool: Tool dictionary to transform.
121
+
122
+ Returns:
123
+ Dictionary with name and description fields.
124
+ """
125
+ description = tool.get("description", "N/A")
126
+ if len(description) > MAX_DESCRIPTION_LEN:
127
+ description = description[: MAX_DESCRIPTION_LEN - 3] + "..."
128
+ return {
129
+ "name": tool.get("name", "N/A"),
130
+ "description": description,
131
+ }
132
+
133
+
134
+ def _output_tools_table(ctx: Any, tools: list[dict[str, Any]], title: str) -> None:
135
+ """Output tools in table format.
136
+
137
+ Args:
138
+ ctx: Click context
139
+ tools: List of tool dictionaries
140
+ title: Table title
141
+ """
142
+ columns = [
143
+ ("name", "Name", ACCENT_STYLE, None),
144
+ ("description", "Description", INFO, 50),
145
+ ]
146
+
147
+ output_list(
148
+ ctx,
149
+ tools,
150
+ title,
151
+ columns,
152
+ _transform_tool,
153
+ )
154
+
155
+
156
+ def _validate_tool_command_args(mcp_ref: str | None, config_file: str | None) -> None:
157
+ """Validate that exactly one of mcp_ref or config_file is provided.
158
+
159
+ Args:
160
+ mcp_ref: MCP reference (ID or name)
161
+ config_file: Path to config file
162
+
163
+ Raises:
164
+ ClickException: If validation fails
165
+ """
166
+ if not mcp_ref and not config_file:
167
+ raise click.ClickException(
168
+ "Either MCP_REF or --from-config must be provided.\n"
169
+ "Examples:\n"
170
+ " aip mcps tools <MCP_ID>\n"
171
+ " aip mcps tools --from-config mcp-config.json"
172
+ )
173
+ if mcp_ref and config_file:
174
+ raise click.ClickException(
175
+ "Cannot use both MCP_REF and --from-config at the same time.\n"
176
+ "Use either:\n"
177
+ " aip mcps tools <MCP_ID>\n"
178
+ " aip mcps tools --from-config mcp-config.json"
179
+ )
180
+
181
+
182
+ @mcps_group.command("tools")
183
+ @click.argument("mcp_ref", required=False)
184
+ @click.option(
185
+ "--from-config",
186
+ "--config",
187
+ "config_file",
188
+ type=click.Path(exists=True, dir_okay=False),
189
+ help="Get tools from MCP config file without saving to DB (JSON or YAML)",
190
+ )
191
+ @click.option(
192
+ "--names-only",
193
+ is_flag=True,
194
+ help="Show only tool names (useful for allowed_tools config)",
195
+ )
196
+ @output_flags()
197
+ @click.pass_context
198
+ def list_tools(ctx: Any, mcp_ref: str | None, config_file: str | None, names_only: bool) -> None:
199
+ """List tools available from a specific MCP or config file.
200
+
201
+ Args:
202
+ ctx: Click context containing output format preferences
203
+ mcp_ref: MCP reference (ID or name) - required if --from-config not used
204
+ config_file: Path to MCP config file - alternative to mcp_ref
205
+ names_only: Show only tool names instead of full table
206
+
207
+ Raises:
208
+ ClickException: If MCP not found or tools fetch fails
209
+
210
+ Examples:
211
+ Get tools from saved MCP:
212
+ aip mcps tools <MCP_ID>
213
+
214
+ Get tools from config file (without saving to DB):
215
+ aip mcps tools --from-config mcp-config.json
216
+
217
+ Get just tool names for allowed_tools config:
218
+ aip mcps tools <MCP_ID> --names-only
219
+ """
220
+ try:
221
+ _validate_tool_command_args(mcp_ref, config_file)
222
+ client = get_client(ctx)
223
+
224
+ if config_file:
225
+ tools, title = _get_tools_from_config(ctx, client, config_file)
226
+ else:
227
+ tools, title = _get_tools_from_mcp(ctx, client, mcp_ref)
228
+
229
+ if names_only:
230
+ _output_tool_names(ctx, tools)
231
+ else:
232
+ _output_tools_table(ctx, tools, title)
233
+
234
+ except Exception as e:
235
+ raise click.ClickException(str(e)) from e
@@ -0,0 +1,190 @@
1
+ """Update MCP command.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import click
12
+
13
+ from glaip_sdk.cli.context import output_flags
14
+ from glaip_sdk.cli.display import display_update_success, handle_json_output, handle_rich_output
15
+ from glaip_sdk.cli.core.context import get_client
16
+ from glaip_sdk.cli.core.rendering import spinner_context
17
+
18
+ from ._common import (
19
+ _handle_cli_error,
20
+ _handle_update_preview_and_confirmation,
21
+ _load_import_ready_payload,
22
+ _parse_and_validate_config_auth,
23
+ _resolve_mcp,
24
+ _validate_import_payload_fields,
25
+ _validate_update_inputs,
26
+ console,
27
+ mcps_group,
28
+ )
29
+
30
+
31
+ def _merge_update_kwargs(
32
+ import_payload: dict[str, Any] | None,
33
+ name: str | None,
34
+ transport: str | None,
35
+ description: str | None,
36
+ config: str | None,
37
+ auth: str | None,
38
+ mcp: Any,
39
+ ) -> dict[str, Any]:
40
+ """Merge import payload and CLI options into kwargs for SDK builder.
41
+
42
+ Args:
43
+ import_payload: Import payload dictionary or None
44
+ name: MCP name option
45
+ transport: Transport option
46
+ description: Description option
47
+ config: Config option
48
+ auth: Auth option
49
+ mcp: Current MCP object
50
+
51
+ Returns:
52
+ Dictionary with merged update kwargs
53
+ """
54
+ update_kwargs: dict[str, Any] = {}
55
+
56
+ # Start with import payload fields
57
+ if import_payload:
58
+ for field in ("name", "transport", "description", "config", "authentication"):
59
+ if field in import_payload:
60
+ update_kwargs[field] = import_payload[field]
61
+
62
+ # Override with CLI options (CLI takes precedence)
63
+ if name is not None:
64
+ update_kwargs["name"] = name
65
+ if transport is not None:
66
+ update_kwargs["transport"] = transport
67
+ if description is not None:
68
+ update_kwargs["description"] = description
69
+ _parse_and_validate_config_auth(update_kwargs, config, auth, transport, import_payload, mcp)
70
+
71
+ return update_kwargs
72
+
73
+
74
+ @mcps_group.command()
75
+ @click.argument("mcp_ref")
76
+ @click.option("--name", help="New MCP name")
77
+ @click.option("--transport", type=click.Choice(["http", "sse"]), help="New transport protocol")
78
+ @click.option("--description", help="New description")
79
+ @click.option(
80
+ "--config",
81
+ help="JSON configuration string or @file reference (e.g., @config.json)",
82
+ )
83
+ @click.option(
84
+ "--auth",
85
+ "--authentication",
86
+ "auth",
87
+ help="JSON authentication object or @file reference (e.g., @auth.json)",
88
+ )
89
+ @click.option(
90
+ "--import",
91
+ "import_file",
92
+ type=click.Path(exists=True, dir_okay=False, readable=True),
93
+ help="Import MCP configuration from JSON or YAML export",
94
+ )
95
+ @click.option("-y", is_flag=True, help="Skip confirmation prompt when using --import")
96
+ @output_flags()
97
+ @click.pass_context
98
+ def update(
99
+ ctx: Any,
100
+ mcp_ref: str,
101
+ name: str | None,
102
+ transport: str | None,
103
+ description: str | None,
104
+ config: str | None,
105
+ auth: str | None,
106
+ import_file: str | None,
107
+ y: bool,
108
+ ) -> None:
109
+ r"""Update an existing MCP with new configuration values.
110
+
111
+ You can update an MCP by providing individual fields via CLI options, or by
112
+ importing from a file and optionally overriding specific fields.
113
+
114
+ Args:
115
+ ctx: Click context containing output format preferences
116
+ mcp_ref: MCP reference (ID or name)
117
+ name: New MCP name (optional)
118
+ transport: New transport protocol (optional)
119
+ description: New description (optional)
120
+ config: New JSON configuration string or @file reference (optional)
121
+ auth: New JSON authentication object or @file reference (optional)
122
+ import_file: Optional path to import configuration from export file.
123
+ CLI options override imported values.
124
+ y: Skip confirmation prompt when using --import
125
+
126
+ Raises:
127
+ ClickException: If MCP not found, JSON invalid, or no fields specified
128
+
129
+ Note:
130
+ Must specify either --import OR at least one CLI field.
131
+ CLI options override imported values when both are specified.
132
+ Method selection (PATCH vs PUT) is handled automatically by the SDK client
133
+ based on the fields provided.
134
+
135
+ \b
136
+ Examples:
137
+ Update with CLI options:
138
+ aip mcps update my-mcp --name new-name --transport sse
139
+
140
+ Import from file:
141
+ aip mcps update my-mcp --import mcp-export.json
142
+
143
+ Import with overrides:
144
+ aip mcps update my-mcp --import mcp-export.json --name new-name -y
145
+ """
146
+ try:
147
+ client = get_client(ctx)
148
+
149
+ # Validate that at least one update method is provided
150
+ _validate_update_inputs(name, transport, description, config, auth, import_file)
151
+
152
+ # Resolve MCP using helper function
153
+ mcp = _resolve_mcp(ctx, client, mcp_ref)
154
+
155
+ # Load and validate import data if provided
156
+ import_payload = None
157
+ if import_file:
158
+ import_payload = _load_import_ready_payload(import_file)
159
+ if not _validate_import_payload_fields(import_payload):
160
+ return
161
+
162
+ # Merge import payload and CLI options into kwargs for SDK builder
163
+ update_kwargs = _merge_update_kwargs(import_payload, name, transport, description, config, auth, mcp)
164
+
165
+ if not update_kwargs:
166
+ raise click.ClickException("No update fields specified")
167
+
168
+ # Build preview data for confirmation (using the same structure as before)
169
+ preview_data = update_kwargs.copy()
170
+
171
+ # Show confirmation preview for import-based updates (unless -y flag)
172
+ if not _handle_update_preview_and_confirmation(
173
+ import_payload, y, mcp, preview_data, name, transport, description, config, auth
174
+ ):
175
+ return
176
+
177
+ # Use SDK client method to update MCP
178
+ # Pass mcp object (not mcp.id) to avoid extra fetch; SDK accepts str | MCP
179
+ with spinner_context(
180
+ ctx,
181
+ "[bold blue]Updating MCP…[/bold blue]",
182
+ console_override=console,
183
+ ):
184
+ updated_mcp = client.mcps.update_mcp(mcp, **update_kwargs)
185
+
186
+ handle_json_output(ctx, updated_mcp.model_dump())
187
+ handle_rich_output(ctx, display_update_success("MCP", updated_mcp.name))
188
+
189
+ except Exception as e:
190
+ _handle_cli_error(ctx, e, "MCP update")
@@ -11,11 +11,8 @@ from rich.console import Console
11
11
 
12
12
  from glaip_sdk.branding import ACCENT_STYLE, INFO, SUCCESS
13
13
  from glaip_sdk.cli.context import output_flags
14
- from glaip_sdk.cli.utils import (
15
- get_client,
16
- output_list,
17
- spinner_context,
18
- )
14
+ from glaip_sdk.cli.core.output import output_list
15
+ from glaip_sdk.cli.core.rendering import with_client_and_spinner
19
16
 
20
17
  console = Console()
21
18
 
@@ -32,12 +29,11 @@ def models_group() -> None:
32
29
  def list_models(ctx: Any) -> None:
33
30
  """List available language models."""
34
31
  try:
35
- client = get_client(ctx)
36
- with spinner_context(
32
+ with with_client_and_spinner(
37
33
  ctx,
38
34
  "[bold blue]Fetching language models…[/bold blue]",
39
35
  console_override=console,
40
- ):
36
+ ) as client:
41
37
  models = client.list_language_models()
42
38
 
43
39
  # Define table columns: (data_key, header, style, width)
@@ -50,6 +46,14 @@ def list_models(ctx: Any) -> None:
50
46
 
51
47
  # Transform function for safe dictionary access
52
48
  def transform_model(model: dict[str, Any]) -> dict[str, Any]:
49
+ """Transform a model dictionary to a display row dictionary.
50
+
51
+ Args:
52
+ model: Model dictionary to transform.
53
+
54
+ Returns:
55
+ Dictionary with id, provider, name, and base_url fields.
56
+ """
53
57
  return {
54
58
  "id": str(model.get("id", "N/A")),
55
59
  "provider": model.get("provider", "N/A"),
@@ -0,0 +1,21 @@
1
+ """Shared CLI command helpers.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.cli.commands.shared.formatters import (
8
+ _format_empty_override_warnings,
9
+ _format_dict_value,
10
+ _format_preview_value,
11
+ _is_sensitive_data,
12
+ _redact_sensitive_dict,
13
+ )
14
+
15
+ __all__ = [
16
+ "_format_empty_override_warnings",
17
+ "_format_dict_value",
18
+ "_format_preview_value",
19
+ "_is_sensitive_data",
20
+ "_redact_sensitive_dict",
21
+ ]
@@ -0,0 +1,91 @@
1
+ """Shared formatting helpers for CLI commands.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import json
8
+ from typing import Any
9
+
10
+
11
+ def _is_sensitive_data(val: Any) -> bool:
12
+ """Check if value contains sensitive authentication data.
13
+
14
+ Args:
15
+ val: Value to check for sensitive information
16
+
17
+ Returns:
18
+ True if the value appears to contain sensitive data
19
+ """
20
+ if not isinstance(val, dict):
21
+ return False
22
+
23
+ sensitive_patterns = {"token", "password", "secret", "key", "credential"}
24
+ return any(pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns)
25
+
26
+
27
+ def _redact_sensitive_dict(val: dict[str, Any]) -> dict[str, Any]:
28
+ """Redact sensitive fields from a dictionary.
29
+
30
+ Args:
31
+ val: Dictionary to redact
32
+
33
+ Returns:
34
+ Redacted dictionary
35
+ """
36
+ redacted = val.copy()
37
+ sensitive_patterns = {"token", "password", "secret", "key", "credential"}
38
+ for k in redacted.keys():
39
+ if any(pattern in k.lower() for pattern in sensitive_patterns):
40
+ redacted[k] = "<REDACTED>"
41
+ return redacted
42
+
43
+
44
+ def _format_dict_value(val: dict[str, Any]) -> str:
45
+ """Format a dictionary value for display.
46
+
47
+ Args:
48
+ val: Dictionary to format
49
+
50
+ Returns:
51
+ Formatted string representation
52
+ """
53
+ if _is_sensitive_data(val):
54
+ redacted = _redact_sensitive_dict(val)
55
+ return json.dumps(redacted, indent=2)
56
+ return json.dumps(val, indent=2)
57
+
58
+
59
+ def _format_preview_value(val: Any) -> str:
60
+ """Format a value for display in update preview with sensitive data redaction.
61
+
62
+ Args:
63
+ val: Value to format
64
+
65
+ Returns:
66
+ Formatted string representation of the value
67
+ """
68
+ if val is None:
69
+ return "[dim]None[/dim]"
70
+ if isinstance(val, dict):
71
+ return _format_dict_value(val)
72
+ if isinstance(val, str):
73
+ return f'"{val}"' if val else '""'
74
+ return str(val)
75
+
76
+
77
+ def _format_empty_override_warnings(empty_fields: list[str]) -> list[str]:
78
+ """Format warning lines for empty CLI overrides.
79
+
80
+ Args:
81
+ empty_fields: List of field names with empty string overrides
82
+
83
+ Returns:
84
+ List of formatted warning lines
85
+ """
86
+ if not empty_fields:
87
+ return []
88
+
89
+ warnings = ["\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"]
90
+ warnings.extend(f"- [yellow]{field}: will be set to empty string[/yellow]" for field in empty_fields)
91
+ return warnings
@@ -0,0 +1,69 @@
1
+ """Tool CLI commands package.
2
+
3
+ This package contains tool management commands split by operation.
4
+ The package is the canonical import surface.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ # pylint: disable=duplicate-code
11
+ from glaip_sdk.cli.commands.tools._common import tools_group, _resolve_tool, console
12
+ from glaip_sdk.cli.commands.tools.create import create # noqa: E402
13
+ from glaip_sdk.cli.commands.tools.delete import delete # noqa: E402
14
+ from glaip_sdk.cli.commands.tools.get import get # noqa: E402
15
+ from glaip_sdk.cli.commands.tools.list import list_tools # noqa: E402
16
+ from glaip_sdk.cli.commands.tools.script import script # noqa: E402
17
+ from glaip_sdk.cli.commands.tools.update import update # noqa: E402
18
+
19
+ # Import helper functions from create module for backward compatibility
20
+ from glaip_sdk.cli.commands.tools.create import ( # noqa: E402
21
+ _check_duplicate_name,
22
+ _create_tool_from_file,
23
+ _extract_internal_name,
24
+ _handle_import_file,
25
+ _parse_tags,
26
+ _validate_name_match,
27
+ )
28
+
29
+ # Import core functions for test compatibility
30
+ from glaip_sdk.cli.core.context import get_client # noqa: E402
31
+ from glaip_sdk.cli.core.output import ( # noqa: E402
32
+ output_list,
33
+ output_result,
34
+ )
35
+ from glaip_sdk.cli.core.rendering import spinner_context # noqa: E402
36
+
37
+ # Import IO functions for test compatibility
38
+ from glaip_sdk.cli.io import ( # noqa: E402
39
+ fetch_raw_resource_details,
40
+ load_resource_from_file_with_validation,
41
+ )
42
+
43
+ # Alias for backward compatibility (used in create.py)
44
+ load_resource_from_file = load_resource_from_file_with_validation
45
+
46
+ __all__ = [
47
+ "tools_group",
48
+ "create",
49
+ "delete",
50
+ "get",
51
+ "list_tools",
52
+ "script",
53
+ "update",
54
+ "_check_duplicate_name",
55
+ "_create_tool_from_file",
56
+ "_extract_internal_name",
57
+ "_handle_import_file",
58
+ "_parse_tags",
59
+ "_resolve_tool",
60
+ "_validate_name_match",
61
+ "console",
62
+ "get_client",
63
+ "output_list",
64
+ "output_result",
65
+ "spinner_context",
66
+ "fetch_raw_resource_details",
67
+ "load_resource_from_file_with_validation",
68
+ "load_resource_from_file",
69
+ ]