glaip-sdk 0.0.18__py3-none-any.whl → 0.0.20__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 (50) hide show
  1. glaip_sdk/_version.py +2 -2
  2. glaip_sdk/branding.py +27 -2
  3. glaip_sdk/cli/auth.py +93 -28
  4. glaip_sdk/cli/commands/__init__.py +2 -2
  5. glaip_sdk/cli/commands/agents.py +108 -21
  6. glaip_sdk/cli/commands/configure.py +141 -90
  7. glaip_sdk/cli/commands/mcps.py +371 -48
  8. glaip_sdk/cli/commands/models.py +4 -3
  9. glaip_sdk/cli/commands/tools.py +27 -14
  10. glaip_sdk/cli/commands/update.py +66 -0
  11. glaip_sdk/cli/config.py +13 -2
  12. glaip_sdk/cli/display.py +35 -26
  13. glaip_sdk/cli/io.py +14 -5
  14. glaip_sdk/cli/main.py +185 -73
  15. glaip_sdk/cli/pager.py +2 -1
  16. glaip_sdk/cli/parsers/json_input.py +62 -14
  17. glaip_sdk/cli/resolution.py +4 -1
  18. glaip_sdk/cli/slash/__init__.py +3 -4
  19. glaip_sdk/cli/slash/agent_session.py +88 -36
  20. glaip_sdk/cli/slash/prompt.py +20 -48
  21. glaip_sdk/cli/slash/session.py +440 -189
  22. glaip_sdk/cli/transcript/__init__.py +71 -0
  23. glaip_sdk/cli/transcript/cache.py +338 -0
  24. glaip_sdk/cli/transcript/capture.py +278 -0
  25. glaip_sdk/cli/transcript/export.py +38 -0
  26. glaip_sdk/cli/transcript/launcher.py +79 -0
  27. glaip_sdk/cli/transcript/viewer.py +624 -0
  28. glaip_sdk/cli/update_notifier.py +29 -5
  29. glaip_sdk/cli/utils.py +256 -74
  30. glaip_sdk/client/agents.py +3 -1
  31. glaip_sdk/client/run_rendering.py +2 -2
  32. glaip_sdk/icons.py +19 -0
  33. glaip_sdk/models.py +6 -0
  34. glaip_sdk/rich_components.py +29 -1
  35. glaip_sdk/utils/__init__.py +1 -1
  36. glaip_sdk/utils/client_utils.py +6 -4
  37. glaip_sdk/utils/display.py +61 -32
  38. glaip_sdk/utils/rendering/formatting.py +6 -5
  39. glaip_sdk/utils/rendering/renderer/base.py +213 -66
  40. glaip_sdk/utils/rendering/renderer/debug.py +73 -16
  41. glaip_sdk/utils/rendering/renderer/panels.py +27 -15
  42. glaip_sdk/utils/rendering/renderer/progress.py +61 -38
  43. glaip_sdk/utils/serialization.py +5 -2
  44. glaip_sdk/utils/validation.py +1 -2
  45. {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/METADATA +1 -1
  46. glaip_sdk-0.0.20.dist-info/RECORD +80 -0
  47. glaip_sdk/utils/rich_utils.py +0 -29
  48. glaip_sdk-0.0.18.dist-info/RECORD +0 -73
  49. {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
5
6
  """
6
7
 
7
8
  import json
@@ -12,6 +13,13 @@ from typing import Any
12
13
  import click
13
14
  from rich.console import Console
14
15
 
16
+ from glaip_sdk.branding import (
17
+ ACCENT_STYLE,
18
+ INFO,
19
+ SUCCESS,
20
+ SUCCESS_STYLE,
21
+ WARNING_STYLE,
22
+ )
15
23
  from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
16
24
  from glaip_sdk.cli.display import (
17
25
  display_api_error,
@@ -43,6 +51,7 @@ from glaip_sdk.cli.utils import (
43
51
  from glaip_sdk.config.constants import (
44
52
  DEFAULT_MCP_TYPE,
45
53
  )
54
+ from glaip_sdk.icons import ICON_TOOL
46
55
  from glaip_sdk.rich_components import AIPPanel
47
56
  from glaip_sdk.utils import format_datetime
48
57
  from glaip_sdk.utils.import_export import convert_export_to_import_format
@@ -54,6 +63,241 @@ from glaip_sdk.utils.serialization import (
54
63
  console = Console()
55
64
 
56
65
 
66
+ def _is_sensitive_data(val: Any) -> bool:
67
+ """Check if value contains sensitive authentication data.
68
+
69
+ Args:
70
+ val: Value to check for sensitive information
71
+
72
+ Returns:
73
+ True if the value appears to contain sensitive data
74
+ """
75
+ if not isinstance(val, dict):
76
+ return False
77
+
78
+ sensitive_patterns = {"token", "password", "secret", "key", "credential"}
79
+ return any(
80
+ pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns
81
+ )
82
+
83
+
84
+ def _redact_sensitive_dict(val: dict[str, Any]) -> dict[str, Any]:
85
+ """Redact sensitive fields from a dictionary.
86
+
87
+ Args:
88
+ val: Dictionary to redact
89
+
90
+ Returns:
91
+ Redacted dictionary
92
+ """
93
+ redacted = val.copy()
94
+ sensitive_patterns = {"token", "password", "secret", "key", "credential"}
95
+ for k in redacted.keys():
96
+ if any(pattern in k.lower() for pattern in sensitive_patterns):
97
+ redacted[k] = "<REDACTED>"
98
+ return redacted
99
+
100
+
101
+ def _format_dict_value(val: dict[str, Any]) -> str:
102
+ """Format a dictionary value for display.
103
+
104
+ Args:
105
+ val: Dictionary to format
106
+
107
+ Returns:
108
+ Formatted string representation
109
+ """
110
+ if _is_sensitive_data(val):
111
+ redacted = _redact_sensitive_dict(val)
112
+ return json.dumps(redacted, indent=2)
113
+ return json.dumps(val, indent=2)
114
+
115
+
116
+ def _format_preview_value(val: Any) -> str:
117
+ """Format a value for display in update preview with sensitive data redaction.
118
+
119
+ Args:
120
+ val: Value to format
121
+
122
+ Returns:
123
+ Formatted string representation of the value
124
+ """
125
+ if val is None:
126
+ return "[dim]None[/dim]"
127
+ if isinstance(val, dict):
128
+ return _format_dict_value(val)
129
+ if isinstance(val, str):
130
+ return f'"{val}"' if val else '""'
131
+ return str(val)
132
+
133
+
134
+ def _build_empty_override_warnings(empty_fields: list[str]) -> list[str]:
135
+ """Build warning lines for empty CLI overrides.
136
+
137
+ Args:
138
+ empty_fields: List of field names with empty string overrides
139
+
140
+ Returns:
141
+ List of formatted warning lines
142
+ """
143
+ if not empty_fields:
144
+ return []
145
+
146
+ warnings = [
147
+ "\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"
148
+ ]
149
+ warnings.extend(
150
+ f"- [yellow]{field}: will be set to empty string[/yellow]"
151
+ for field in empty_fields
152
+ )
153
+ return warnings
154
+
155
+
156
+ def _validate_import_payload_fields(import_payload: dict[str, Any]) -> bool:
157
+ """Validate that import payload contains updatable fields.
158
+
159
+ Args:
160
+ import_payload: Import payload to validate
161
+
162
+ Returns:
163
+ True if payload has updatable fields, False otherwise
164
+ """
165
+ updatable_fields = {"name", "transport", "description", "config", "authentication"}
166
+ has_updatable = any(field in import_payload for field in updatable_fields)
167
+
168
+ if not has_updatable:
169
+ available_fields = set(import_payload.keys())
170
+ print_markup(
171
+ "[yellow]⚠️ No updatable fields found in import file.[/yellow]\n"
172
+ f"[dim]Found fields: {', '.join(sorted(available_fields))}[/dim]\n"
173
+ f"[dim]Updatable fields: {', '.join(sorted(updatable_fields))}[/dim]"
174
+ )
175
+ return has_updatable
176
+
177
+
178
+ def _get_config_transport(
179
+ transport: str | None,
180
+ import_payload: dict[str, Any] | None,
181
+ mcp: Any,
182
+ ) -> str | None:
183
+ """Get the transport value for config validation.
184
+
185
+ Args:
186
+ transport: CLI transport flag
187
+ import_payload: Optional import payload
188
+ mcp: Current MCP object
189
+
190
+ Returns:
191
+ Transport value or None
192
+ """
193
+ if import_payload:
194
+ return transport or import_payload.get("transport")
195
+ return transport or getattr(mcp, "transport", None)
196
+
197
+
198
+ def _build_update_data_from_sources(
199
+ import_payload: dict[str, Any] | None,
200
+ mcp: Any,
201
+ name: str | None,
202
+ transport: str | None,
203
+ description: str | None,
204
+ config: str | None,
205
+ auth: str | None,
206
+ ) -> dict[str, Any]:
207
+ """Build update data from import payload and CLI flags.
208
+
209
+ Args:
210
+ import_payload: Optional import payload
211
+ mcp: Current MCP object
212
+ name: CLI name flag
213
+ transport: CLI transport flag
214
+ description: CLI description flag
215
+ config: CLI config flag
216
+ auth: CLI auth flag
217
+
218
+ Returns:
219
+ Dictionary with update data
220
+ """
221
+ update_data = {}
222
+
223
+ # Start with import data if available
224
+ if import_payload:
225
+ updatable_fields = [
226
+ "name",
227
+ "transport",
228
+ "description",
229
+ "config",
230
+ "authentication",
231
+ ]
232
+ for field in updatable_fields:
233
+ if field in import_payload:
234
+ update_data[field] = import_payload[field]
235
+
236
+ # CLI flags override import values
237
+ if name is not None:
238
+ update_data["name"] = name
239
+ if transport is not None:
240
+ update_data["transport"] = transport
241
+ if description is not None:
242
+ update_data["description"] = description
243
+ if config is not None:
244
+ parsed_config = parse_json_input(config)
245
+ config_transport = _get_config_transport(transport, import_payload, mcp)
246
+ update_data["config"] = validate_mcp_config_structure(
247
+ parsed_config,
248
+ transport=config_transport,
249
+ source="--config",
250
+ )
251
+ if auth is not None:
252
+ parsed_auth = parse_json_input(auth)
253
+ update_data["authentication"] = validate_mcp_auth_structure(
254
+ parsed_auth, source="--auth"
255
+ )
256
+
257
+ return update_data
258
+
259
+
260
+ def _collect_cli_overrides(
261
+ name: str | None,
262
+ transport: str | None,
263
+ description: str | None,
264
+ config: str | None,
265
+ auth: str | None,
266
+ ) -> dict[str, Any]:
267
+ """Collect CLI flags that were explicitly provided.
268
+
269
+ Args:
270
+ name: CLI name flag
271
+ transport: CLI transport flag
272
+ description: CLI description flag
273
+ config: CLI config flag
274
+ auth: CLI auth flag
275
+
276
+ Returns:
277
+ Dictionary of provided CLI overrides
278
+ """
279
+ cli_overrides = {}
280
+ if name is not None:
281
+ cli_overrides["name"] = name
282
+ if transport is not None:
283
+ cli_overrides["transport"] = transport
284
+ if description is not None:
285
+ cli_overrides["description"] = description
286
+ if config is not None:
287
+ cli_overrides["config"] = config
288
+ if auth is not None:
289
+ cli_overrides["auth"] = auth
290
+ return cli_overrides
291
+
292
+
293
+ def _handle_cli_error(ctx: Any, error: Exception, operation: str) -> None:
294
+ """Render CLI error once and exit with non-zero status."""
295
+ handle_json_output(ctx, error=error)
296
+ if get_ctx_value(ctx, "view") != "json":
297
+ display_api_error(error, operation)
298
+ ctx.exit(1)
299
+
300
+
57
301
  @click.group(name="mcps", no_args_is_help=True)
58
302
  def mcps_group() -> None:
59
303
  """MCP management operations.
@@ -291,8 +535,8 @@ def list_mcps(ctx: Any) -> None:
291
535
  # Define table columns: (data_key, header, style, width)
292
536
  columns = [
293
537
  ("id", "ID", "dim", 36),
294
- ("name", "Name", "cyan", None),
295
- ("config", "Config", "blue", None),
538
+ ("name", "Name", ACCENT_STYLE, None),
539
+ ("config", "Config", INFO, None),
296
540
  ]
297
541
 
298
542
  # Transform function for safe dictionary access
@@ -440,10 +684,7 @@ def create(
440
684
  handle_rich_output(ctx, rich_panel)
441
685
 
442
686
  except Exception as e:
443
- handle_json_output(ctx, error=e)
444
- if get_ctx_value(ctx, "view") != "json":
445
- display_api_error(e, "MCP creation")
446
- raise click.ClickException(str(e))
687
+ _handle_cli_error(ctx, e, "MCP creation")
447
688
 
448
689
 
449
690
  def _handle_mcp_export(
@@ -482,11 +723,11 @@ def _handle_mcp_export(
482
723
  mcp = client.mcps.get_mcp_by_id(mcp.id)
483
724
  except Exception as e:
484
725
  print_markup(
485
- f"[yellow]⚠️ Could not fetch full MCP details: {e}[/yellow]",
726
+ f"[{WARNING_STYLE}]⚠️ Could not fetch full MCP details: {e}[/]",
486
727
  console=console,
487
728
  )
488
729
  print_markup(
489
- "[yellow]⚠️ Proceeding with available data[/yellow]", console=console
730
+ f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]", console=console
490
731
  )
491
732
 
492
733
  # Determine if we should prompt for secrets
@@ -495,8 +736,8 @@ def _handle_mcp_export(
495
736
  # Warn user if non-interactive mode forces placeholder usage
496
737
  if not no_auth_prompt and not sys.stdin.isatty():
497
738
  print_markup(
498
- "[yellow]⚠️ Non-interactive mode detected. "
499
- "Using placeholder values for secrets.[/yellow]",
739
+ f"[{WARNING_STYLE}]⚠️ Non-interactive mode detected. "
740
+ "Using placeholder values for secrets.[/]",
500
741
  console=console,
501
742
  )
502
743
 
@@ -531,8 +772,8 @@ def _handle_mcp_export(
531
772
  write_resource_export(export_path, export_payload, detected_format)
532
773
 
533
774
  print_markup(
534
- f"[green]✅ Complete MCP configuration exported to: "
535
- f"{export_path} (format: {detected_format})[/green]",
775
+ f"[{SUCCESS_STYLE}]✅ Complete MCP configuration exported to: "
776
+ f"{export_path} (format: {detected_format})[/]",
536
777
  console=console,
537
778
  )
538
779
 
@@ -574,7 +815,7 @@ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
574
815
  )
575
816
  else:
576
817
  # Fall back to Pydantic model data
577
- console.print("[yellow]Falling back to Pydantic model data[/yellow]")
818
+ console.print(f"[{WARNING_STYLE}]Falling back to Pydantic model data[/]")
578
819
  result_data = {
579
820
  "id": str(getattr(mcp, "id", "N/A")),
580
821
  "name": getattr(mcp, "name", "N/A"),
@@ -680,8 +921,8 @@ def list_tools(ctx: Any, mcp_ref: str) -> None:
680
921
 
681
922
  # Define table columns: (data_key, header, style, width)
682
923
  columns = [
683
- ("name", "Name", "cyan", None),
684
- ("description", "Description", "green", 50),
924
+ ("name", "Name", ACCENT_STYLE, None),
925
+ ("description", "Description", INFO, 50),
685
926
  ]
686
927
 
687
928
  # Transform function for safe dictionary access
@@ -694,7 +935,11 @@ def list_tools(ctx: Any, mcp_ref: str) -> None:
694
935
  }
695
936
 
696
937
  output_list(
697
- ctx, tools, f"🔧 Tools from MCP: {mcp.name}", columns, transform_tool
938
+ ctx,
939
+ tools,
940
+ f"{ICON_TOOL} Tools from MCP: {mcp.name}",
941
+ columns,
942
+ transform_tool,
698
943
  )
699
944
 
700
945
  except Exception as e:
@@ -734,7 +979,7 @@ def connect(ctx: Any, config_file: str) -> None:
734
979
  view = get_ctx_value(ctx, "view", "rich")
735
980
  if view != "json":
736
981
  print_markup(
737
- f"[yellow]Connecting to MCP with config from {config_file}...[/yellow]",
982
+ f"[{WARNING_STYLE}]Connecting to MCP with config from {config_file}...[/]",
738
983
  console=console,
739
984
  )
740
985
 
@@ -751,10 +996,10 @@ def connect(ctx: Any, config_file: str) -> None:
751
996
  handle_json_output(ctx, result)
752
997
  else:
753
998
  success_panel = AIPPanel(
754
- f"[green]✓[/green] MCP connection successful!\n\n"
999
+ f"[{SUCCESS_STYLE}]✓[/] MCP connection successful!\n\n"
755
1000
  f"[bold]Result:[/bold] {result}",
756
1001
  title="🔌 Connection",
757
- border_style="green",
1002
+ border_style=SUCCESS,
758
1003
  )
759
1004
  console.print(success_panel)
760
1005
 
@@ -762,9 +1007,50 @@ def connect(ctx: Any, config_file: str) -> None:
762
1007
  raise click.ClickException(str(e))
763
1008
 
764
1009
 
1010
+ def _generate_update_preview(
1011
+ mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]
1012
+ ) -> str:
1013
+ """Generate formatted preview of changes for user confirmation.
1014
+
1015
+ Args:
1016
+ mcp: Current MCP object
1017
+ update_data: Data that will be sent in update request
1018
+ cli_overrides: CLI flags that were explicitly provided
1019
+
1020
+ Returns:
1021
+ Formatted preview string showing old→new values
1022
+ """
1023
+ lines = [
1024
+ f"\n[bold]The following fields will be updated for MCP '{mcp.name}':[/bold]\n"
1025
+ ]
1026
+
1027
+ empty_overrides = []
1028
+
1029
+ # Show each field that will be updated
1030
+ for field, new_value in update_data.items():
1031
+ old_value = getattr(mcp, field, None)
1032
+
1033
+ # Track empty CLI overrides
1034
+ if field in cli_overrides and cli_overrides[field] == "":
1035
+ empty_overrides.append(field)
1036
+
1037
+ old_display = _format_preview_value(old_value)
1038
+ new_display = _format_preview_value(new_value)
1039
+
1040
+ lines.append(f"- [cyan]{field}[/cyan]: {old_display} → {new_display}")
1041
+
1042
+ # Add warnings for empty CLI overrides
1043
+ lines.extend(_build_empty_override_warnings(empty_overrides))
1044
+
1045
+ return "\n".join(lines)
1046
+
1047
+
765
1048
  @mcps_group.command()
766
1049
  @click.argument("mcp_ref")
767
1050
  @click.option("--name", help="New MCP name")
1051
+ @click.option(
1052
+ "--transport", type=click.Choice(["http", "sse"]), help="New transport protocol"
1053
+ )
768
1054
  @click.option("--description", help="New description")
769
1055
  @click.option(
770
1056
  "--config",
@@ -776,62 +1062,105 @@ def connect(ctx: Any, config_file: str) -> None:
776
1062
  "auth",
777
1063
  help="JSON authentication object or @file reference (e.g., @auth.json)",
778
1064
  )
1065
+ @click.option(
1066
+ "--import",
1067
+ "import_file",
1068
+ type=click.Path(exists=True, dir_okay=False, readable=True),
1069
+ help="Import MCP configuration from JSON or YAML export",
1070
+ )
1071
+ @click.option("-y", is_flag=True, help="Skip confirmation prompt when using --import")
779
1072
  @output_flags()
780
1073
  @click.pass_context
781
1074
  def update(
782
1075
  ctx: Any,
783
1076
  mcp_ref: str,
784
1077
  name: str | None,
1078
+ transport: str | None,
785
1079
  description: str | None,
786
1080
  config: str | None,
787
1081
  auth: str | None,
1082
+ import_file: str | None,
1083
+ y: bool,
788
1084
  ) -> None:
789
1085
  """Update an existing MCP with new configuration values.
790
1086
 
1087
+ You can update an MCP by providing individual fields via CLI options, or by
1088
+ importing from a file and optionally overriding specific fields.
1089
+
791
1090
  Args:
792
1091
  ctx: Click context containing output format preferences
793
1092
  mcp_ref: MCP reference (ID or name)
794
1093
  name: New MCP name (optional)
1094
+ transport: New transport protocol (optional)
795
1095
  description: New description (optional)
796
1096
  config: New JSON configuration string or @file reference (optional)
797
1097
  auth: New JSON authentication object or @file reference (optional)
1098
+ import_file: Optional path to import configuration from export file.
1099
+ CLI options override imported values.
1100
+ y: Skip confirmation prompt when using --import
798
1101
 
799
1102
  Raises:
800
1103
  ClickException: If MCP not found, JSON invalid, or no fields specified
801
1104
 
802
1105
  Note:
803
- At least one field must be specified for update.
804
- Uses PUT for complete updates or PATCH for partial updates.
1106
+ Must specify either --import OR at least one CLI field.
1107
+ CLI options override imported values when both are specified.
1108
+ Uses PATCH for import-based updates, PUT/PATCH for CLI-only updates.
1109
+
1110
+ Examples:
1111
+ Update with CLI options:
1112
+ aip mcps update my-mcp --name new-name --transport sse
1113
+
1114
+ Import from file:
1115
+ aip mcps update my-mcp --import mcp-export.json
1116
+
1117
+ Import with overrides:
1118
+ aip mcps update my-mcp --import mcp-export.json --name new-name -y
805
1119
  """
806
1120
  try:
807
1121
  client = get_client(ctx)
808
1122
 
1123
+ # Validate that at least one update method is provided
1124
+ cli_flags_provided = any(
1125
+ v is not None for v in [name, transport, description, config, auth]
1126
+ )
1127
+ if not import_file and not cli_flags_provided:
1128
+ raise click.ClickException(
1129
+ "No update fields specified. Use --import file or at least one of: "
1130
+ "--name, --transport, --description, --config, --auth"
1131
+ )
1132
+
809
1133
  # Resolve MCP using helper function
810
1134
  mcp = _resolve_mcp(ctx, client, mcp_ref)
811
1135
 
812
- # Build update data
813
- update_data = {}
814
- if name is not None:
815
- update_data["name"] = name
816
- if description is not None:
817
- update_data["description"] = description
818
- if config is not None:
819
- parsed_config = parse_json_input(config)
820
- update_data["config"] = validate_mcp_config_structure(
821
- parsed_config,
822
- transport=getattr(mcp, "transport", None),
823
- source="--config",
824
- )
825
- if auth is not None:
826
- parsed_auth = parse_json_input(auth)
827
- update_data["authentication"] = validate_mcp_auth_structure(
828
- parsed_auth, source="--auth"
829
- )
1136
+ # Load and validate import data if provided
1137
+ import_payload = None
1138
+ if import_file:
1139
+ import_payload = _load_import_ready_payload(import_file)
1140
+ if not _validate_import_payload_fields(import_payload):
1141
+ return
1142
+
1143
+ # Build update data from import and CLI flags
1144
+ update_data = _build_update_data_from_sources(
1145
+ import_payload, mcp, name, transport, description, config, auth
1146
+ )
830
1147
 
831
1148
  if not update_data:
832
1149
  raise click.ClickException("No update fields specified")
833
1150
 
834
- # Update MCP (automatically chooses PUT or PATCH based on provided fields)
1151
+ # Show confirmation preview for import-based updates (unless -y flag)
1152
+ if import_payload and not y:
1153
+ cli_overrides = _collect_cli_overrides(
1154
+ name, transport, description, config, auth
1155
+ )
1156
+ preview = _generate_update_preview(mcp, update_data, cli_overrides)
1157
+ print_markup(preview)
1158
+
1159
+ if not click.confirm("\nContinue with update?", default=False):
1160
+ print_markup("[yellow]Update cancelled.[/yellow]")
1161
+ return
1162
+
1163
+ # Update MCP
835
1164
  with spinner_context(
836
1165
  ctx,
837
1166
  "[bold blue]Updating MCP…[/bold blue]",
@@ -843,10 +1172,7 @@ def update(
843
1172
  handle_rich_output(ctx, display_update_success("MCP", updated_mcp.name))
844
1173
 
845
1174
  except Exception as e:
846
- handle_json_output(ctx, error=e)
847
- if get_ctx_value(ctx, "view") != "json":
848
- display_api_error(e, "MCP update")
849
- raise click.ClickException(str(e))
1175
+ _handle_cli_error(ctx, e, "MCP update")
850
1176
 
851
1177
 
852
1178
  @mcps_group.command()
@@ -896,7 +1222,4 @@ def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
896
1222
  handle_rich_output(ctx, display_deletion_success("MCP", mcp.name))
897
1223
 
898
1224
  except Exception as e:
899
- handle_json_output(ctx, error=e)
900
- if get_ctx_value(ctx, "view") != "json":
901
- display_api_error(e, "MCP deletion")
902
- raise click.ClickException(str(e))
1225
+ _handle_cli_error(ctx, e, "MCP deletion")
@@ -9,6 +9,7 @@ from typing import Any
9
9
  import click
10
10
  from rich.console import Console
11
11
 
12
+ from glaip_sdk.branding import ACCENT_STYLE, INFO, SUCCESS
12
13
  from glaip_sdk.cli.context import output_flags
13
14
  from glaip_sdk.cli.utils import (
14
15
  get_client,
@@ -42,9 +43,9 @@ def list_models(ctx: Any) -> None:
42
43
  # Define table columns: (data_key, header, style, width)
43
44
  columns = [
44
45
  ("id", "ID", "dim", 36),
45
- ("provider", "Provider", "cyan", None),
46
- ("name", "Model", "green", None),
47
- ("base_url", "Base URL", "yellow", None),
46
+ ("provider", "Provider", ACCENT_STYLE, None),
47
+ ("name", "Model", SUCCESS, None),
48
+ ("base_url", "Base URL", INFO, None),
48
49
  ]
49
50
 
50
51
  # Transform function for safe dictionary access