glaip-sdk 0.0.3__py3-none-any.whl → 0.0.5__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 (47) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +146 -0
  3. glaip_sdk/cli/__init__.py +1 -1
  4. glaip_sdk/cli/agent_config.py +82 -0
  5. glaip_sdk/cli/commands/__init__.py +3 -3
  6. glaip_sdk/cli/commands/agents.py +786 -271
  7. glaip_sdk/cli/commands/configure.py +19 -19
  8. glaip_sdk/cli/commands/mcps.py +151 -141
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +252 -178
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +27 -20
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +372 -213
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +632 -171
  19. glaip_sdk/client/base.py +66 -4
  20. glaip_sdk/client/main.py +226 -0
  21. glaip_sdk/client/mcps.py +143 -18
  22. glaip_sdk/client/tools.py +327 -104
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +43 -3
  25. glaip_sdk/rich_components.py +29 -0
  26. glaip_sdk/utils/__init__.py +18 -171
  27. glaip_sdk/utils/agent_config.py +181 -0
  28. glaip_sdk/utils/client_utils.py +159 -79
  29. glaip_sdk/utils/display.py +100 -0
  30. glaip_sdk/utils/general.py +94 -0
  31. glaip_sdk/utils/import_export.py +140 -0
  32. glaip_sdk/utils/rendering/formatting.py +6 -1
  33. glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
  34. glaip_sdk/utils/rendering/renderer/base.py +340 -247
  35. glaip_sdk/utils/rendering/renderer/debug.py +3 -2
  36. glaip_sdk/utils/rendering/renderer/panels.py +11 -10
  37. glaip_sdk/utils/rendering/steps.py +1 -1
  38. glaip_sdk/utils/resource_refs.py +192 -0
  39. glaip_sdk/utils/rich_utils.py +29 -0
  40. glaip_sdk/utils/serialization.py +285 -0
  41. glaip_sdk/utils/validation.py +273 -0
  42. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
  43. glaip_sdk-0.0.5.dist-info/RECORD +55 -0
  44. glaip_sdk/cli/commands/init.py +0 -177
  45. glaip_sdk-0.0.3.dist-info/RECORD +0 -40
  46. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -6,19 +6,40 @@ Authors:
6
6
 
7
7
  import json
8
8
  import re
9
+ from pathlib import Path
9
10
 
10
11
  import click
11
12
  from rich.console import Console
12
- from rich.panel import Panel
13
-
14
- from ..utils import (
13
+ from rich.text import Text
14
+
15
+ from glaip_sdk.cli.display import (
16
+ display_api_error,
17
+ display_confirmation_prompt,
18
+ display_creation_success,
19
+ display_deletion_success,
20
+ display_update_success,
21
+ handle_json_output,
22
+ handle_rich_output,
23
+ )
24
+ from glaip_sdk.cli.io import (
25
+ export_resource_to_file_with_validation as export_resource_to_file,
26
+ )
27
+ from glaip_sdk.cli.io import (
28
+ fetch_raw_resource_details,
29
+ )
30
+ from glaip_sdk.cli.io import (
31
+ load_resource_from_file_with_validation as load_resource_from_file,
32
+ )
33
+ from glaip_sdk.cli.resolution import resolve_resource_reference
34
+ from glaip_sdk.cli.utils import (
15
35
  coerce_to_row,
16
36
  get_client,
17
37
  output_flags,
18
38
  output_list,
19
39
  output_result,
20
- resolve_resource,
21
40
  )
41
+ from glaip_sdk.utils import format_datetime
42
+ from glaip_sdk.utils.import_export import merge_import_with_cli_args
22
43
 
23
44
  console = Console()
24
45
 
@@ -31,12 +52,14 @@ def tools_group():
31
52
 
32
53
  def _resolve_tool(ctx, client, ref, select=None):
33
54
  """Resolve tool reference (ID or name) with ambiguity handling."""
34
- return resolve_resource(
55
+ return resolve_resource_reference(
35
56
  ctx,
57
+ client,
36
58
  ref,
37
- get_by_id=client.get_tool,
38
- find_by_name=client.find_tools,
39
- label="Tool",
59
+ "tool",
60
+ client.get_tool,
61
+ client.find_tools,
62
+ "Tool",
40
63
  select=select,
41
64
  )
42
65
 
@@ -90,12 +113,19 @@ def _parse_tags(tags: str | None) -> list[str]:
90
113
 
91
114
  @tools_group.command(name="list")
92
115
  @output_flags()
116
+ @click.option(
117
+ "--type",
118
+ "tool_type",
119
+ help="Filter tools by type (e.g., custom, native)",
120
+ type=str,
121
+ required=False,
122
+ )
93
123
  @click.pass_context
94
- def list_tools(ctx):
124
+ def list_tools(ctx, tool_type):
95
125
  """List all tools."""
96
126
  try:
97
127
  client = get_client(ctx)
98
- tools = client.list_tools()
128
+ tools = client.list_tools(tool_type=tool_type)
99
129
 
100
130
  # Define table columns: (data_key, header, style, width)
101
131
  columns = [
@@ -136,19 +166,59 @@ def list_tools(ctx):
136
166
  "--tags",
137
167
  help="Comma-separated tags for the tool",
138
168
  )
169
+ @click.option(
170
+ "--import",
171
+ "import_file",
172
+ type=click.Path(exists=True, dir_okay=False),
173
+ help="Import tool configuration from JSON file",
174
+ )
139
175
  @output_flags()
140
176
  @click.pass_context
141
- def create(ctx, file_arg, file, name, description, tags):
142
- """Create a new tool."""
177
+ def create(ctx, file_arg, file, name, description, tags, import_file):
178
+ """Create a new tool.
179
+
180
+ Examples:
181
+ aip tools create --name "My Tool" --description "A helpful tool"
182
+ aip tools create tool.py # Create from file
183
+ aip tools create --import tool.json # Create from exported configuration
184
+ """
143
185
  try:
144
186
  client = get_client(ctx)
145
187
 
188
+ # Initialize merged_data for cases without import_file
189
+ merged_data = {}
190
+
191
+ # Handle import from file
192
+ if import_file:
193
+ import_data = load_resource_from_file(Path(import_file), "tool")
194
+
195
+ # Merge CLI args with imported data
196
+ cli_args = {
197
+ "name": name,
198
+ "description": description,
199
+ "tags": tags,
200
+ }
201
+
202
+ merged_data = merge_import_with_cli_args(import_data, cli_args)
203
+ else:
204
+ # No import file - use CLI args directly
205
+ merged_data = {
206
+ "name": name,
207
+ "description": description,
208
+ "tags": tags,
209
+ }
210
+
211
+ # Extract merged values
212
+ name = merged_data.get("name")
213
+ description = merged_data.get("description")
214
+ tags = merged_data.get("tags")
215
+
146
216
  # Allow positional file argument for better DX (matches examples)
147
217
  if not file and file_arg:
148
218
  file = file_arg
149
219
 
150
220
  # Validate required parameters based on creation method
151
- if not file:
221
+ if not file and not import_file:
152
222
  # Metadata-only tool creation
153
223
  if not name:
154
224
  raise click.ClickException(
@@ -167,14 +237,14 @@ def create(ctx, file_arg, file, name, description, tags):
167
237
 
168
238
  # Upload the plugin code as-is (no rewrite)
169
239
  tool = client.create_tool_from_code(
170
- tool_name,
171
- code_content,
240
+ name=tool_name,
241
+ code=code_content,
172
242
  framework="langchain", # Always langchain
173
243
  description=description,
174
- tags=_parse_tags(tags),
244
+ tags=_parse_tags(tags) if tags else None,
175
245
  )
176
246
  else:
177
- # Metadata-only tool creation
247
+ # Metadata-only tool creation or import from file
178
248
  tool_kwargs = {}
179
249
  if name:
180
250
  tool_kwargs["name"] = name
@@ -185,60 +255,141 @@ def create(ctx, file_arg, file, name, description, tags):
185
255
  if tags:
186
256
  tool_kwargs["tags"] = _parse_tags(tags)
187
257
 
258
+ # If importing from file, include all other detected attributes
259
+ if import_file:
260
+ # Add all other attributes from import data (excluding already handled ones)
261
+ excluded_fields = {
262
+ "name",
263
+ "description",
264
+ "tags",
265
+ # System-only fields that shouldn't be passed to create_tool
266
+ "id",
267
+ "created_at",
268
+ "updated_at",
269
+ "tool_type",
270
+ "framework",
271
+ "version",
272
+ }
273
+ for key, value in merged_data.items():
274
+ if key not in excluded_fields and value is not None:
275
+ tool_kwargs[key] = value
276
+
188
277
  tool = client.create_tool(**tool_kwargs)
189
278
 
190
- if ctx.obj.get("view") == "json":
191
- click.echo(json.dumps(tool.model_dump(), indent=2))
192
- else:
193
- # Rich output
194
- creation_method = (
195
- "file upload (custom)" if file else "metadata only (native)"
196
- )
197
- panel = Panel(
198
- f"[green]✅ Tool '{tool.name}' created successfully via {creation_method}![/green]\n\n"
199
- f"ID: {tool.id}\n"
200
- f"Framework: {getattr(tool, 'framework', 'N/A')} (default)\n"
201
- f"Type: {getattr(tool, 'tool_type', 'N/A')} (auto-detected)\n"
202
- f"Description: {getattr(tool, 'description', 'No description')}",
203
- title="🔧 Tool Created",
204
- border_style="green",
205
- )
206
- console.print(panel)
279
+ # Handle JSON output
280
+ handle_json_output(ctx, tool.model_dump())
281
+
282
+ # Handle Rich output
283
+ creation_method = "file upload (custom)" if file else "metadata only (native)"
284
+ rich_panel = display_creation_success(
285
+ "Tool",
286
+ tool.name,
287
+ tool.id,
288
+ Framework=getattr(tool, "framework", "N/A"),
289
+ Type=getattr(tool, "tool_type", "N/A"),
290
+ Description=getattr(tool, "description", "No description"),
291
+ Method=creation_method,
292
+ )
293
+ handle_rich_output(ctx, rich_panel)
207
294
 
208
295
  except Exception as e:
209
- if ctx.obj.get("view") == "json":
210
- click.echo(json.dumps({"error": str(e)}, indent=2))
211
- else:
212
- console.print(f"[red]Error creating tool: {e}[/red]")
296
+ handle_json_output(ctx, error=e)
297
+ if ctx.obj.get("view") != "json":
298
+ display_api_error(e, "tool creation")
213
299
  raise click.ClickException(str(e))
214
300
 
215
301
 
216
302
  @tools_group.command()
217
303
  @click.argument("tool_ref")
218
304
  @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
305
+ @click.option(
306
+ "--export",
307
+ type=click.Path(dir_okay=False, writable=True),
308
+ help="Export complete tool configuration to file (format auto-detected from .json/.yaml extension)",
309
+ )
219
310
  @output_flags()
220
311
  @click.pass_context
221
- def get(ctx, tool_ref, select):
222
- """Get tool details."""
312
+ def get(ctx, tool_ref, select, export):
313
+ """Get tool details.
314
+
315
+ Examples:
316
+ aip tools get my-tool
317
+ aip tools get my-tool --export tool.json # Exports complete configuration as JSON
318
+ aip tools get my-tool --export tool.yaml # Exports complete configuration as YAML
319
+ """
223
320
  try:
224
321
  client = get_client(ctx)
225
322
 
226
323
  # Resolve tool with ambiguity handling
227
324
  tool = _resolve_tool(ctx, client, tool_ref, select)
228
325
 
229
- # Create result data with all available fields from backend
230
- result_data = {
231
- "id": str(getattr(tool, "id", "N/A")),
232
- "name": getattr(tool, "name", "N/A"),
233
- "tool_type": getattr(tool, "tool_type", "N/A"),
234
- "framework": getattr(tool, "framework", "N/A"),
235
- "version": getattr(tool, "version", "N/A"),
236
- "description": getattr(tool, "description", "N/A"),
237
- }
238
-
239
- output_result(
240
- ctx, result_data, title="Tool Details", panel_title=f"🔧 {tool.name}"
241
- )
326
+ # Handle export option
327
+ if export:
328
+ export_path = Path(export)
329
+ # Auto-detect format from file extension
330
+ if export_path.suffix.lower() in [".yaml", ".yml"]:
331
+ detected_format = "yaml"
332
+ else:
333
+ detected_format = "json"
334
+
335
+ # Always export comprehensive data - re-fetch tool with full details if needed
336
+ try:
337
+ tool = client.get_tool_by_id(tool.id)
338
+ except Exception as e:
339
+ console.print(
340
+ Text(f"[yellow]⚠️ Could not fetch full tool details: {e}[/yellow]")
341
+ )
342
+ console.print(
343
+ Text("[yellow]⚠️ Proceeding with available data[/yellow]")
344
+ )
345
+
346
+ export_resource_to_file(tool, export_path, detected_format)
347
+ console.print(
348
+ Text(
349
+ f"[green]✅ Complete tool configuration exported to: {export_path} (format: {detected_format})[/green]"
350
+ )
351
+ )
352
+
353
+ # Try to fetch raw API data first to preserve ALL fields
354
+ raw_tool_data = fetch_raw_resource_details(client, tool, "tools")
355
+
356
+ if raw_tool_data:
357
+ # Use raw API data - this preserves ALL fields
358
+ # Format dates for better display (minimal postprocessing)
359
+ formatted_data = raw_tool_data.copy()
360
+ if "created_at" in formatted_data:
361
+ formatted_data["created_at"] = format_datetime(
362
+ formatted_data["created_at"]
363
+ )
364
+ if "updated_at" in formatted_data:
365
+ formatted_data["updated_at"] = format_datetime(
366
+ formatted_data["updated_at"]
367
+ )
368
+
369
+ # Display using output_result with raw data
370
+ output_result(
371
+ ctx,
372
+ formatted_data,
373
+ title="Tool Details",
374
+ panel_title=f"🔧 {raw_tool_data.get('name', 'Unknown')}",
375
+ )
376
+ else:
377
+ # Fall back to original method if raw fetch fails
378
+ console.print("[yellow]Falling back to Pydantic model data[/yellow]")
379
+
380
+ # Create result data with all available fields from backend
381
+ result_data = {
382
+ "id": str(getattr(tool, "id", "N/A")),
383
+ "name": getattr(tool, "name", "N/A"),
384
+ "tool_type": getattr(tool, "tool_type", "N/A"),
385
+ "framework": getattr(tool, "framework", "N/A"),
386
+ "version": getattr(tool, "version", "N/A"),
387
+ "description": getattr(tool, "description", "N/A"),
388
+ }
389
+
390
+ output_result(
391
+ ctx, result_data, title="Tool Details", panel_title=f"🔧 {tool.name}"
392
+ )
242
393
 
243
394
  except Exception as e:
244
395
  raise click.ClickException(str(e))
@@ -247,7 +398,9 @@ def get(ctx, tool_ref, select):
247
398
  @tools_group.command()
248
399
  @click.argument("tool_id")
249
400
  @click.option(
250
- "--file", type=click.Path(exists=True), help="New tool file for code update"
401
+ "--file",
402
+ type=click.Path(exists=True),
403
+ help="New tool file for code update (custom tools only)",
251
404
  )
252
405
  @click.option("--description", help="New description")
253
406
  @click.option("--tags", help="Comma-separated tags")
@@ -264,41 +417,44 @@ def update(ctx, tool_id, file, description, tags):
264
417
  except Exception as e:
265
418
  raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
266
419
 
420
+ # Prepare update data
267
421
  update_data = {}
268
-
269
422
  if description:
270
423
  update_data["description"] = description
271
-
272
424
  if tags:
273
425
  update_data["tags"] = [tag.strip() for tag in tags.split(",")]
274
426
 
275
427
  if file:
276
- # Update code
277
- updated_tool = tool.update(file_path=file)
278
- if ctx.obj.get("view") != "json":
279
- console.print(f"[green]✓[/green] Tool code updated from {file}")
428
+ # Update code via file upload (custom tools only)
429
+ if tool.tool_type != "custom":
430
+ raise click.ClickException(
431
+ f"File updates are only supported for custom tools. Tool '{tool.name}' is of type '{tool.tool_type}'."
432
+ )
433
+ updated_tool = client.tools.update_tool_via_file(
434
+ tool.id, file, framework=tool.framework
435
+ )
436
+ handle_rich_output(
437
+ ctx, Text(f"[green]✓[/green] Tool code updated from {file}")
438
+ )
280
439
  elif update_data:
281
- # Update metadata
440
+ # Update metadata only (native tools only)
441
+ if tool.tool_type != "native":
442
+ raise click.ClickException(
443
+ f"Metadata updates are only supported for native tools. Tool '{tool.name}' is of type '{tool.tool_type}'."
444
+ )
282
445
  updated_tool = tool.update(**update_data)
283
- if ctx.obj.get("view") != "json":
284
- console.print("[green]✓[/green] Tool metadata updated")
446
+ handle_rich_output(ctx, Text("[green]✓[/green] Tool metadata updated"))
285
447
  else:
286
- if ctx.obj.get("view") != "json":
287
- console.print("[yellow]No updates specified[/yellow]")
448
+ handle_rich_output(ctx, Text("[yellow]No updates specified[/yellow]"))
288
449
  return
289
450
 
290
- if ctx.obj.get("view") == "json":
291
- click.echo(json.dumps(updated_tool.model_dump(), indent=2))
292
- else:
293
- console.print(
294
- f"[green]✅ Tool '{updated_tool.name}' updated successfully[/green]"
295
- )
451
+ handle_json_output(ctx, updated_tool.model_dump())
452
+ handle_rich_output(ctx, display_update_success("Tool", updated_tool.name))
296
453
 
297
454
  except Exception as e:
298
- if ctx.obj.get("view") == "json":
299
- click.echo(json.dumps({"error": str(e)}, indent=2))
300
- else:
301
- console.print(f"[red]Error updating tool: {e}[/red]")
455
+ handle_json_output(ctx, error=e)
456
+ if ctx.obj.get("view") != "json":
457
+ display_api_error(e, "tool update")
302
458
  raise click.ClickException(str(e))
303
459
 
304
460
 
@@ -318,35 +474,29 @@ def delete(ctx, tool_id, yes):
318
474
  except Exception as e:
319
475
  raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
320
476
 
321
- # Confirm deletion
322
- if not yes and not click.confirm(
323
- f"Are you sure you want to delete tool '{tool.name}'?"
324
- ):
325
- if ctx.obj.get("view") != "json":
326
- console.print("Deletion cancelled.")
477
+ # Confirm deletion via centralized display helper
478
+ if not yes and not display_confirmation_prompt("Tool", tool.name):
327
479
  return
328
480
 
329
481
  tool.delete()
330
482
 
331
- if ctx.obj.get("view") == "json":
332
- click.echo(
333
- json.dumps(
334
- {"success": True, "message": f"Tool '{tool.name}' deleted"},
335
- indent=2,
336
- )
337
- )
338
- else:
339
- console.print(f"[green]✅ Tool '{tool.name}' deleted successfully[/green]")
483
+ handle_json_output(
484
+ ctx,
485
+ {
486
+ "success": True,
487
+ "message": f"Tool '{tool.name}' deleted",
488
+ },
489
+ )
490
+ handle_rich_output(ctx, display_deletion_success("Tool", tool.name))
340
491
 
341
492
  except Exception as e:
342
- if ctx.obj.get("view") == "json":
343
- click.echo(json.dumps({"error": str(e)}, indent=2))
344
- else:
345
- console.print(f"[red]Error deleting tool: {e}[/red]")
493
+ handle_json_output(ctx, error=e)
494
+ if ctx.obj.get("view") != "json":
495
+ display_api_error(e, "tool deletion")
346
496
  raise click.ClickException(str(e))
347
497
 
348
498
 
349
- @tools_group.command()
499
+ @tools_group.command("script")
350
500
  @click.argument("tool_id")
351
501
  @output_flags()
352
502
  @click.pass_context
@@ -354,92 +504,16 @@ def script(ctx, tool_id):
354
504
  """Get tool script content."""
355
505
  try:
356
506
  client = get_client(ctx)
357
-
358
- # Get tool by ID (no ambiguity handling needed)
359
- try:
360
- tool = client.get_tool_by_id(tool_id)
361
- except Exception as e:
362
- raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
363
-
364
- # Get tool script content
365
- script_content = client.tools.get_tool_script(tool_id)
507
+ script_content = client.get_tool_script(tool_id)
366
508
 
367
509
  if ctx.obj.get("view") == "json":
368
- click.echo(
369
- json.dumps(
370
- {
371
- "tool_id": tool_id,
372
- "tool_name": tool.name,
373
- "script": script_content,
374
- },
375
- indent=2,
376
- )
377
- )
378
- elif ctx.obj.get("output"):
379
- # Save to file
380
- output_file = ctx.obj.get("output")
381
- with open(output_file, "w", encoding="utf-8") as f:
382
- f.write(script_content)
383
- console.print(f"[green]✅ Tool script saved to {output_file}[/green]")
510
+ click.echo(json.dumps({"script": script_content}, indent=2))
384
511
  else:
385
- # Display in terminal
386
- console.print(
387
- Panel(
388
- script_content,
389
- title=f"🔧 Tool Script: {tool.name}",
390
- border_style="cyan",
391
- )
392
- )
512
+ console.print(f"[green]📜 Tool Script for '{tool_id}':[/green]")
513
+ console.print(script_content)
393
514
 
394
515
  except Exception as e:
395
- if ctx.obj.get("view") == "json":
396
- click.echo(json.dumps({"error": str(e)}, indent=2))
397
- else:
398
- console.print(f"[red]Error getting tool script: {e}[/red]")
399
- raise click.ClickException(str(e))
400
-
401
-
402
- @tools_group.command()
403
- @click.argument("tool_id")
404
- @click.option(
405
- "--file",
406
- type=click.Path(exists=True),
407
- required=True,
408
- help="New tool file for code update",
409
- )
410
- @click.option("--name", help="New tool name")
411
- @click.option("--description", help="New description")
412
- @click.option("--tags", help="Comma-separated tags")
413
- @output_flags()
414
- @click.pass_context
415
- def upload_update(ctx, tool_id, file, name, description, tags):
416
- """Update a tool plugin via file upload."""
417
- try:
418
- client = get_client(ctx)
419
-
420
- # Prepare update data
421
- update_data = {}
422
- if name:
423
- update_data["name"] = name
424
- if description:
425
- update_data["description"] = description
426
- if tags:
427
- update_data["tags"] = [tag.strip() for tag in tags.split(",")]
428
-
429
- # Update tool via file upload
430
- updated_tool = client.tools.update_tool_via_file(tool_id, file, **update_data)
431
-
432
- if ctx.obj.get("view") == "json":
433
- click.echo(json.dumps(updated_tool.model_dump(), indent=2))
434
- else:
435
- console.print(
436
- f"[green]✅ Tool '{updated_tool.name}' updated successfully via file upload[/green]"
437
- )
438
- console.print(f"[blue]📁 File: {file}[/blue]")
439
-
440
- except Exception as e:
441
- if ctx.obj.get("view") == "json":
442
- click.echo(json.dumps({"error": str(e)}, indent=2))
443
- else:
444
- console.print(f"[red]Error updating tool: {e}[/red]")
516
+ handle_json_output(ctx, error=e)
517
+ if ctx.obj.get("view") != "json":
518
+ console.print(Text(f"[red]Error getting tool script: {e}[/red]"))
445
519
  raise click.ClickException(str(e))