glaip-sdk 0.1.4__py3-none-any.whl → 0.2.1__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 (37) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/_version.py +1 -0
  3. glaip_sdk/cli/commands/__init__.py +2 -2
  4. glaip_sdk/cli/commands/agents.py +22 -37
  5. glaip_sdk/cli/commands/configure.py +67 -6
  6. glaip_sdk/cli/commands/mcps.py +19 -23
  7. glaip_sdk/cli/commands/tools.py +17 -41
  8. glaip_sdk/cli/commands/transcripts.py +747 -0
  9. glaip_sdk/cli/config.py +6 -1
  10. glaip_sdk/cli/display.py +1 -0
  11. glaip_sdk/cli/main.py +12 -31
  12. glaip_sdk/cli/parsers/__init__.py +1 -3
  13. glaip_sdk/cli/slash/__init__.py +0 -9
  14. glaip_sdk/cli/slash/prompt.py +2 -0
  15. glaip_sdk/cli/slash/session.py +259 -90
  16. glaip_sdk/cli/transcript/__init__.py +12 -52
  17. glaip_sdk/cli/transcript/cache.py +255 -44
  18. glaip_sdk/cli/transcript/capture.py +32 -0
  19. glaip_sdk/cli/transcript/history.py +815 -0
  20. glaip_sdk/cli/transcript/viewer.py +6 -2
  21. glaip_sdk/cli/update_notifier.py +5 -2
  22. glaip_sdk/cli/utils.py +170 -0
  23. glaip_sdk/client/_agent_payloads.py +5 -0
  24. glaip_sdk/payload_schemas/__init__.py +1 -13
  25. glaip_sdk/utils/__init__.py +12 -7
  26. glaip_sdk/utils/datetime_helpers.py +58 -0
  27. glaip_sdk/utils/general.py +0 -33
  28. glaip_sdk/utils/import_export.py +9 -1
  29. glaip_sdk/utils/rendering/renderer/__init__.py +0 -20
  30. glaip_sdk/utils/rendering/renderer/debug.py +3 -20
  31. glaip_sdk/utils/rendering/steps.py +5 -6
  32. glaip_sdk/utils/resource_refs.py +2 -1
  33. glaip_sdk/utils/serialization.py +2 -0
  34. {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/METADATA +1 -1
  35. {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/RECORD +37 -34
  36. {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/WHEEL +0 -0
  37. {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.1.dist-info}/entry_points.txt +0 -0
glaip_sdk/__init__.py CHANGED
@@ -9,4 +9,4 @@ from glaip_sdk.client import Client
9
9
  from glaip_sdk.exceptions import AIPError
10
10
  from glaip_sdk.models import MCP, Agent, Tool
11
11
 
12
- __all__ = ["MCP", "AIPError", "Agent", "Client", "Tool", "__version__"]
12
+ __all__ = ["Client", "Agent", "Tool", "MCP", "AIPError", "__version__"]
glaip_sdk/_version.py CHANGED
@@ -56,6 +56,7 @@ def _try_get_dev_version() -> str | None:
56
56
 
57
57
 
58
58
  def _get_version() -> str:
59
+ """Return the SDK version from install metadata or fallbacks."""
59
60
  # Try installed version first
60
61
  installed_version = _try_get_installed_version()
61
62
  if installed_version:
@@ -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"]
@@ -32,7 +32,7 @@ from glaip_sdk.cli.agent_config import (
32
32
  from glaip_sdk.cli.agent_config import (
33
33
  sanitize_agent_config_for_cli as sanitize_agent_config,
34
34
  )
35
- from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
35
+ from glaip_sdk.cli.context import get_ctx_value, output_flags
36
36
  from glaip_sdk.cli.display import (
37
37
  build_resource_result_data,
38
38
  display_agent_run_suggestions,
@@ -44,9 +44,6 @@ from glaip_sdk.cli.display import (
44
44
  handle_rich_output,
45
45
  print_api_error,
46
46
  )
47
- from glaip_sdk.cli.io import (
48
- export_resource_to_file_with_validation as export_resource_to_file,
49
- )
50
47
  from glaip_sdk.cli.io import (
51
48
  fetch_raw_resource_details,
52
49
  )
@@ -360,6 +357,7 @@ def agents_group() -> None:
360
357
  pass
361
358
 
362
359
 
360
+ # pylint: disable=duplicate-code
363
361
  def _resolve_agent(
364
362
  ctx: Any,
365
363
  client: Any,
@@ -493,8 +491,9 @@ def list_agents(
493
491
  @output_flags()
494
492
  @click.pass_context
495
493
  def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> None:
496
- """Get agent details.
494
+ r"""Get agent details.
497
495
 
496
+ \b
498
497
  Examples:
499
498
  aip agents get my-agent
500
499
  aip agents get my-agent --export agent.json # Exports complete configuration as JSON
@@ -508,35 +507,15 @@ def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> Non
508
507
 
509
508
  # Handle export option
510
509
  if export:
511
- export_path = Path(export)
512
- # Auto-detect format from file extension
513
- detected_format = detect_export_format(export_path)
514
-
515
- # Always export comprehensive data - re-fetch agent with full details
516
- try:
517
- with spinner_context(
518
- ctx,
519
- "[bold blue]Fetching complete agent data…[/bold blue]",
520
- console_override=console,
521
- ):
522
- agent = client.agents.get_agent_by_id(agent.id)
523
- except Exception as e:
524
- handle_rich_output(
525
- ctx,
526
- markup_text(f"[{WARNING_STYLE}]⚠️ Could not fetch full agent details: {e}[/]"),
527
- )
528
- handle_rich_output(
529
- ctx,
530
- markup_text(f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]"),
531
- )
532
-
533
- export_resource_to_file(agent, export_path, detected_format)
534
- handle_rich_output(
510
+ from glaip_sdk.cli.utils import handle_resource_export
511
+
512
+ handle_resource_export(
535
513
  ctx,
536
- markup_text(
537
- f"[{SUCCESS_STYLE}]✅ Complete agent configuration exported to: {export_path} "
538
- f"(format: {detected_format})[/]"
539
- ),
514
+ agent,
515
+ Path(export),
516
+ resource_type="agent",
517
+ get_by_id_func=client.agents.get_agent_by_id,
518
+ console_override=console,
540
519
  )
541
520
 
542
521
  # Display full agent details using the standardized helper
@@ -706,10 +685,11 @@ def run(
706
685
  files: tuple[str, ...] | None,
707
686
  verbose: bool,
708
687
  ) -> None:
709
- """Run an agent with input text.
688
+ r"""Run an agent with input text.
710
689
 
711
690
  Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
712
691
 
692
+ \b
713
693
  Examples:
714
694
  aip agents run my-agent "Hello world"
715
695
  aip agents run agent-123 "Process this data" --timeout 600
@@ -778,11 +758,13 @@ def run(
778
758
 
779
759
 
780
760
  def _running_in_slash_mode(ctx: Any) -> bool:
761
+ """Return True if the command is executing inside the slash session."""
781
762
  ctx_obj = getattr(ctx, "obj", None)
782
763
  return isinstance(ctx_obj, dict) and bool(ctx_obj.get("_slash_session"))
783
764
 
784
765
 
785
766
  def _emit_verbose_guidance(ctx: Any) -> None:
767
+ """Explain the modern alternative to the deprecated --verbose flag."""
786
768
  if _running_in_slash_mode(ctx):
787
769
  message = (
788
770
  "[dim]Tip:[/] Verbose streaming has been retired in the command palette. Run the agent normally and open "
@@ -1037,8 +1019,9 @@ def create(
1037
1019
  timeout: float | None,
1038
1020
  import_file: str | None,
1039
1021
  ) -> None:
1040
- """Create a new agent.
1022
+ r"""Create a new agent.
1041
1023
 
1024
+ \b
1042
1025
  Examples:
1043
1026
  aip agents create --name "My Agent" --instruction "You are a helpful assistant"
1044
1027
  aip agents create --import agent.json
@@ -1232,8 +1215,9 @@ def update(
1232
1215
  timeout: float | None,
1233
1216
  import_file: str | None,
1234
1217
  ) -> None:
1235
- """Update an existing agent.
1218
+ r"""Update an existing agent.
1236
1219
 
1220
+ \b
1237
1221
  Examples:
1238
1222
  aip agents update my-agent --instruction "New instruction"
1239
1223
  aip agents update my-agent --import agent.json
@@ -1327,7 +1311,7 @@ def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1327
1311
  @output_flags()
1328
1312
  @click.pass_context
1329
1313
  def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1330
- """Sync agents with LangFlow server flows.
1314
+ r"""Sync agents with LangFlow server flows.
1331
1315
 
1332
1316
  This command fetches all flows from the configured LangFlow server and
1333
1317
  creates/updates corresponding agents in the platform.
@@ -1336,6 +1320,7 @@ def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1336
1320
  - Command options (--base-url, --api-key)
1337
1321
  - Environment variables (LANGFLOW_BASE_URL, LANGFLOW_API_KEY)
1338
1322
 
1323
+ \b
1339
1324
  Examples:
1340
1325
  aip agents sync-langflow
1341
1326
  aip agents sync-langflow --base-url https://my-langflow.com --api-key my-key
@@ -48,25 +48,76 @@ def list_config() -> None:
48
48
  _render_config_table(config)
49
49
 
50
50
 
51
+ CONFIG_VALUE_TYPES: dict[str, str] = {
52
+ "api_url": "string",
53
+ "api_key": "string",
54
+ "timeout": "float",
55
+ "history_default_limit": "int",
56
+ }
57
+
58
+
59
+ def _parse_bool_config(value: str) -> bool:
60
+ """Parse boolean-like CLI input."""
61
+ normalized = value.strip().lower()
62
+ if normalized in {"1", "true", "yes", "on"}:
63
+ return True
64
+ if normalized in {"0", "false", "no", "off"}:
65
+ return False
66
+ raise click.ClickException("Invalid boolean value. Use one of: true, false, yes, no, 1, 0.")
67
+
68
+
69
+ def _parse_int_config(value: str) -> int:
70
+ """Parse integer CLI input with non-negative enforcement."""
71
+ try:
72
+ parsed = int(value, 10)
73
+ except ValueError as exc:
74
+ raise click.ClickException("Invalid integer value.") from exc
75
+ if parsed < 0:
76
+ raise click.ClickException("Value must be greater than or equal to 0.")
77
+ return parsed
78
+
79
+
80
+ def _parse_float_config(value: str) -> float:
81
+ """Parse float CLI input with non-negative enforcement."""
82
+ try:
83
+ parsed = float(value)
84
+ except ValueError as exc:
85
+ raise click.ClickException("Invalid float value.") from exc
86
+ if parsed < 0:
87
+ raise click.ClickException("Value must be greater than or equal to 0.")
88
+ return parsed
89
+
90
+
91
+ def _coerce_config_value(key: str, raw_value: str) -> str | bool | int | float:
92
+ """Convert CLI string values to their target config types."""
93
+ kind = CONFIG_VALUE_TYPES.get(key, "string")
94
+ if kind == "bool":
95
+ return _parse_bool_config(raw_value)
96
+ if kind == "int":
97
+ return _parse_int_config(raw_value)
98
+ if kind == "float":
99
+ return _parse_float_config(raw_value)
100
+ return raw_value
101
+
102
+
51
103
  @config_group.command("set")
52
104
  @click.argument("key")
53
105
  @click.argument("value")
54
106
  def set_config(key: str, value: str) -> None:
55
107
  """Set a configuration value."""
56
- valid_keys = ["api_url", "api_key"]
108
+ valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
57
109
 
58
110
  if key not in valid_keys:
59
111
  console.print(f"[{ERROR_STYLE}]Error: Invalid key '{key}'. Valid keys are: {', '.join(valid_keys)}[/]")
60
112
  raise click.ClickException(f"Invalid configuration key: {key}")
61
113
 
114
+ coerced_value = _coerce_config_value(key, value)
62
115
  config = load_config()
63
- config[key] = value
116
+ config[key] = coerced_value
64
117
  save_config(config)
65
118
 
66
- if key == "api_key":
67
- console.print(Text(f"✅ Set {key} = {_mask_api_key(value)}", style=SUCCESS_STYLE))
68
- else:
69
- console.print(Text(f"✅ Set {key} = {value}", style=SUCCESS_STYLE))
119
+ display_value = _mask_api_key(coerced_value) if key == "api_key" else str(coerced_value)
120
+ console.print(Text(f"✅ Set {key} = {display_value}", style=SUCCESS_STYLE))
70
121
 
71
122
 
72
123
  @config_group.command("get")
@@ -169,12 +220,14 @@ def configure_command() -> None:
169
220
 
170
221
 
171
222
  def _mask_api_key(value: str | None) -> str:
223
+ """Return a redacted API key string suitable for display."""
172
224
  if not value:
173
225
  return ""
174
226
  return "***" + value[-4:] if len(value) > 4 else "***"
175
227
 
176
228
 
177
229
  def _print_missing_config_hint() -> None:
230
+ """Show guidance when no configuration file exists."""
178
231
  hint = command_hint("config configure", slash_command="login")
179
232
  if hint:
180
233
  console.print(f"[{WARNING_STYLE}]No configuration found.[/] Run {format_command_hint(hint) or hint} to set up.")
@@ -183,6 +236,7 @@ def _print_missing_config_hint() -> None:
183
236
 
184
237
 
185
238
  def _render_config_table(config: dict[str, str]) -> None:
239
+ """Render the current configuration in a friendly table."""
186
240
  table = AIPTable(title=f"{ICON_TOOL} AIP Configuration")
187
241
  table.add_column("Setting", style=INFO, width=20)
188
242
  table.add_column("Value", style=SUCCESS)
@@ -195,6 +249,7 @@ def _render_config_table(config: dict[str, str]) -> None:
195
249
 
196
250
 
197
251
  def _render_configuration_header() -> None:
252
+ """Display the interactive configuration heading/banner."""
198
253
  branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
199
254
  heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
200
255
  console.print(heading)
@@ -204,6 +259,7 @@ def _render_configuration_header() -> None:
204
259
 
205
260
 
206
261
  def _prompt_configuration_inputs(config: dict[str, str]) -> None:
262
+ """Interactively prompt for configuration values."""
207
263
  console.print("\n[bold]Enter your AIP configuration:[/bold]")
208
264
  console.print("(Leave blank to keep current values)")
209
265
  console.print("─" * 50)
@@ -213,6 +269,7 @@ def _prompt_configuration_inputs(config: dict[str, str]) -> None:
213
269
 
214
270
 
215
271
  def _prompt_api_url(config: dict[str, str]) -> None:
272
+ """Ask the user for the API URL, preserving existing values by default."""
216
273
  current_url = config.get("api_url", "")
217
274
  suffix = f"(current: {current_url})" if current_url else ""
218
275
  console.print(f"\n[{ACCENT_STYLE}]AIP API URL[/] {suffix}:")
@@ -224,6 +281,7 @@ def _prompt_api_url(config: dict[str, str]) -> None:
224
281
 
225
282
 
226
283
  def _prompt_api_key(config: dict[str, str]) -> None:
284
+ """Prompt the user for the API key while masking previous input."""
227
285
  current_key_masked = _mask_api_key(config.get("api_key"))
228
286
  suffix = f"(current: {current_key_masked})" if current_key_masked else ""
229
287
  console.print(f"\n[{ACCENT_STYLE}]AIP API Key[/] {suffix}:")
@@ -233,11 +291,13 @@ def _prompt_api_key(config: dict[str, str]) -> None:
233
291
 
234
292
 
235
293
  def _save_configuration(config: dict[str, str]) -> None:
294
+ """Persist the collected configuration to disk."""
236
295
  save_config(config)
237
296
  console.print(Text(f"\n✅ Configuration saved to: {CONFIG_FILE}", style=SUCCESS_STYLE))
238
297
 
239
298
 
240
299
  def _test_and_report_connection(config: dict[str, str]) -> None:
300
+ """Sanity-check the provided credentials against the backend."""
241
301
  console.print("\n🔌 Testing connection...")
242
302
  client: Client | None = None
243
303
  try:
@@ -270,6 +330,7 @@ def _test_and_report_connection(config: dict[str, str]) -> None:
270
330
 
271
331
 
272
332
  def _print_post_configuration_hints() -> None:
333
+ """Offer next-step guidance after configuration completes."""
273
334
  console.print("\n💡 You can now use AIP CLI commands!")
274
335
  hint_status = command_hint("status", slash_command="status")
275
336
  if hint_status:
@@ -53,7 +53,6 @@ from glaip_sdk.config.constants import (
53
53
  )
54
54
  from glaip_sdk.icons import ICON_TOOL
55
55
  from glaip_sdk.rich_components import AIPPanel
56
- from glaip_sdk.utils import format_datetime
57
56
  from glaip_sdk.utils.import_export import convert_export_to_import_format
58
57
  from glaip_sdk.utils.serialization import (
59
58
  build_mcp_export_payload,
@@ -299,7 +298,7 @@ def mcps_group() -> None:
299
298
  pass
300
299
 
301
300
 
302
- def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
301
+ def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None: # pylint: disable=duplicate-code
303
302
  """Resolve MCP reference (ID or name) with ambiguity handling.
304
303
 
305
304
  Args:
@@ -575,7 +574,7 @@ def create(
575
574
  auth: str | None,
576
575
  import_file: str | None,
577
576
  ) -> None:
578
- """Create a new MCP with specified configuration.
577
+ r"""Create a new MCP with specified configuration.
579
578
 
580
579
  You can create an MCP by providing all parameters via CLI options, or by
581
580
  importing from a file and optionally overriding specific fields.
@@ -593,6 +592,7 @@ def create(
593
592
  Raises:
594
593
  ClickException: If JSON parsing fails or API request fails
595
594
 
595
+ \b
596
596
  Examples:
597
597
  Create from CLI options:
598
598
  aip mcps create --name my-mcp --transport http --config '{"url": "https://api.example.com"}'
@@ -696,19 +696,15 @@ def _handle_mcp_export(
696
696
  detected_format = detect_export_format(export_path)
697
697
 
698
698
  # Always export comprehensive data - re-fetch with full details
699
- try:
700
- with spinner_context(
701
- ctx,
702
- "[bold blue]Fetching complete MCP details…[/bold blue]",
703
- console_override=console,
704
- ):
705
- mcp = client.mcps.get_mcp_by_id(mcp.id)
706
- except Exception as e:
707
- print_markup(
708
- f"[{WARNING_STYLE}]⚠️ Could not fetch full MCP details: {e}[/]",
709
- console=console,
710
- )
711
- print_markup(f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]", console=console)
699
+ from glaip_sdk.cli.utils import fetch_resource_for_export
700
+
701
+ mcp = fetch_resource_for_export(
702
+ ctx,
703
+ mcp,
704
+ resource_type="MCP",
705
+ get_by_id_func=client.mcps.get_mcp_by_id,
706
+ console_override=console,
707
+ )
712
708
 
713
709
  # Determine if we should prompt for secrets
714
710
  prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
@@ -779,11 +775,9 @@ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
779
775
 
780
776
  if raw_mcp_data:
781
777
  # Use raw API data - this preserves ALL fields
782
- formatted_data = raw_mcp_data.copy()
783
- if "created_at" in formatted_data:
784
- formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
785
- if "updated_at" in formatted_data:
786
- formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
778
+ from glaip_sdk.cli.utils import format_datetime_fields
779
+
780
+ formatted_data = format_datetime_fields(raw_mcp_data)
787
781
 
788
782
  output_result(
789
783
  ctx,
@@ -832,7 +826,7 @@ def get(
832
826
  no_auth_prompt: bool,
833
827
  auth_placeholder: str,
834
828
  ) -> None:
835
- """Get MCP details and optionally export configuration to file.
829
+ r"""Get MCP details and optionally export configuration to file.
836
830
 
837
831
  Args:
838
832
  ctx: Click context containing output format preferences
@@ -844,6 +838,7 @@ def get(
844
838
  Raises:
845
839
  ClickException: If MCP not found or export fails
846
840
 
841
+ \b
847
842
  Examples:
848
843
  aip mcps get my-mcp
849
844
  aip mcps get my-mcp --export mcp.json # Export as JSON
@@ -1050,7 +1045,7 @@ def update(
1050
1045
  import_file: str | None,
1051
1046
  y: bool,
1052
1047
  ) -> None:
1053
- """Update an existing MCP with new configuration values.
1048
+ r"""Update an existing MCP with new configuration values.
1054
1049
 
1055
1050
  You can update an MCP by providing individual fields via CLI options, or by
1056
1051
  importing from a file and optionally overriding specific fields.
@@ -1075,6 +1070,7 @@ def update(
1075
1070
  CLI options override imported values when both are specified.
1076
1071
  Uses PATCH for import-based updates, PUT/PATCH for CLI-only updates.
1077
1072
 
1073
+ \b
1078
1074
  Examples:
1079
1075
  Update with CLI options:
1080
1076
  aip mcps update my-mcp --name new-name --transport sse
@@ -4,6 +4,7 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
+ # pylint: disable=duplicate-code
7
8
  import json
8
9
  import re
9
10
  from pathlib import Path
@@ -19,7 +20,7 @@ from glaip_sdk.branding import (
19
20
  SUCCESS_STYLE,
20
21
  WARNING_STYLE,
21
22
  )
22
- from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
23
+ from glaip_sdk.cli.context import get_ctx_value, output_flags
23
24
  from glaip_sdk.cli.display import (
24
25
  display_api_error,
25
26
  display_confirmation_prompt,
@@ -29,9 +30,6 @@ from glaip_sdk.cli.display import (
29
30
  handle_json_output,
30
31
  handle_rich_output,
31
32
  )
32
- from glaip_sdk.cli.io import (
33
- export_resource_to_file_with_validation as export_resource_to_file,
34
- )
35
33
  from glaip_sdk.cli.io import (
36
34
  fetch_raw_resource_details,
37
35
  )
@@ -48,7 +46,6 @@ from glaip_sdk.cli.utils import (
48
46
  spinner_context,
49
47
  )
50
48
  from glaip_sdk.icons import ICON_TOOL
51
- from glaip_sdk.utils import format_datetime
52
49
  from glaip_sdk.utils.import_export import merge_import_with_cli_args
53
50
 
54
51
  console = Console()
@@ -60,6 +57,7 @@ def tools_group() -> None:
60
57
  pass
61
58
 
62
59
 
60
+ # pylint: disable=duplicate-code
63
61
  def _resolve_tool(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
64
62
  """Resolve tool reference (ID or name) with ambiguity handling."""
65
63
  return resolve_resource_reference(
@@ -118,6 +116,7 @@ def _check_duplicate_name(client: Any, tool_name: str) -> None:
118
116
 
119
117
 
120
118
  def _parse_tags(tags: str | None) -> list[str]:
119
+ """Return a cleaned list of tag strings from a comma-separated input."""
121
120
  return [t.strip() for t in (tags.split(",") if tags else []) if t.strip()]
122
121
 
123
122
 
@@ -259,8 +258,9 @@ def create(
259
258
  tags: tuple[str, ...] | None,
260
259
  import_file: str | None,
261
260
  ) -> None:
262
- """Create a new tool.
261
+ r"""Create a new tool.
263
262
 
263
+ \b
264
264
  Examples:
265
265
  aip tools create tool.py # Create from file
266
266
  aip tools create --import tool.json # Create from exported configuration
@@ -325,8 +325,9 @@ def create(
325
325
  @output_flags()
326
326
  @click.pass_context
327
327
  def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None:
328
- """Get tool details.
328
+ r"""Get tool details.
329
329
 
330
+ \b
330
331
  Examples:
331
332
  aip tools get my-tool
332
333
  aip tools get my-tool --export tool.json # Exports complete configuration as JSON
@@ -340,38 +341,15 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
340
341
 
341
342
  # Handle export option
342
343
  if export:
343
- export_path = Path(export)
344
- # Auto-detect format from file extension
345
- detected_format = detect_export_format(export_path)
346
-
347
- # Always export comprehensive data - re-fetch tool with full details if needed
348
- try:
349
- with spinner_context(
350
- ctx,
351
- "[bold blue]Fetching complete tool details…[/bold blue]",
352
- console_override=console,
353
- ):
354
- tool = client.get_tool_by_id(tool.id)
355
- except Exception as e:
356
- print_markup(
357
- f"[{WARNING_STYLE}]⚠️ Could not fetch full tool details: {e}[/]",
358
- console=console,
359
- )
360
- print_markup(
361
- f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]",
362
- console=console,
363
- )
344
+ from glaip_sdk.cli.utils import handle_resource_export
364
345
 
365
- with spinner_context(
346
+ handle_resource_export(
366
347
  ctx,
367
- "[bold blue]Exporting tool configuration…[/bold blue]",
348
+ tool,
349
+ Path(export),
350
+ resource_type="tool",
351
+ get_by_id_func=client.get_tool_by_id,
368
352
  console_override=console,
369
- ):
370
- export_resource_to_file(tool, export_path, detected_format)
371
- print_markup(
372
- f"[{SUCCESS_STYLE}]✅ Complete tool configuration exported to: {export_path} "
373
- f"(format: {detected_format})[/]",
374
- console=console,
375
353
  )
376
354
 
377
355
  # Try to fetch raw API data first to preserve ALL fields
@@ -385,11 +363,9 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
385
363
  if raw_tool_data:
386
364
  # Use raw API data - this preserves ALL fields
387
365
  # Format dates for better display (minimal postprocessing)
388
- formatted_data = raw_tool_data.copy()
389
- if "created_at" in formatted_data:
390
- formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
391
- if "updated_at" in formatted_data:
392
- formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
366
+ from glaip_sdk.cli.utils import format_datetime_fields
367
+
368
+ formatted_data = format_datetime_fields(raw_tool_data)
393
369
 
394
370
  # Display using output_result with raw data
395
371
  output_result(