glaip-sdk 0.0.4__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 +18 -17
  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 +570 -673
  7. glaip_sdk/cli/commands/configure.py +2 -2
  8. glaip_sdk/cli/commands/mcps.py +148 -143
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +250 -179
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +14 -18
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +305 -264
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +631 -191
  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 +146 -11
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +42 -2
  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.4.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 -93
  45. glaip_sdk-0.0.4.dist-info/RECORD +0 -41
  46. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -6,20 +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
13
  from rich.text import Text
14
14
 
15
- from ..utils import (
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 (
16
35
  coerce_to_row,
17
36
  get_client,
18
37
  output_flags,
19
38
  output_list,
20
39
  output_result,
21
- resolve_resource,
22
40
  )
41
+ from glaip_sdk.utils import format_datetime
42
+ from glaip_sdk.utils.import_export import merge_import_with_cli_args
23
43
 
24
44
  console = Console()
25
45
 
@@ -32,12 +52,14 @@ def tools_group():
32
52
 
33
53
  def _resolve_tool(ctx, client, ref, select=None):
34
54
  """Resolve tool reference (ID or name) with ambiguity handling."""
35
- return resolve_resource(
55
+ return resolve_resource_reference(
36
56
  ctx,
57
+ client,
37
58
  ref,
38
- get_by_id=client.get_tool,
39
- find_by_name=client.find_tools,
40
- label="Tool",
59
+ "tool",
60
+ client.get_tool,
61
+ client.find_tools,
62
+ "Tool",
41
63
  select=select,
42
64
  )
43
65
 
@@ -91,12 +113,19 @@ def _parse_tags(tags: str | None) -> list[str]:
91
113
 
92
114
  @tools_group.command(name="list")
93
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
+ )
94
123
  @click.pass_context
95
- def list_tools(ctx):
124
+ def list_tools(ctx, tool_type):
96
125
  """List all tools."""
97
126
  try:
98
127
  client = get_client(ctx)
99
- tools = client.list_tools()
128
+ tools = client.list_tools(tool_type=tool_type)
100
129
 
101
130
  # Define table columns: (data_key, header, style, width)
102
131
  columns = [
@@ -137,19 +166,59 @@ def list_tools(ctx):
137
166
  "--tags",
138
167
  help="Comma-separated tags for the tool",
139
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
+ )
140
175
  @output_flags()
141
176
  @click.pass_context
142
- def create(ctx, file_arg, file, name, description, tags):
143
- """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
+ """
144
185
  try:
145
186
  client = get_client(ctx)
146
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
+
147
216
  # Allow positional file argument for better DX (matches examples)
148
217
  if not file and file_arg:
149
218
  file = file_arg
150
219
 
151
220
  # Validate required parameters based on creation method
152
- if not file:
221
+ if not file and not import_file:
153
222
  # Metadata-only tool creation
154
223
  if not name:
155
224
  raise click.ClickException(
@@ -168,14 +237,14 @@ def create(ctx, file_arg, file, name, description, tags):
168
237
 
169
238
  # Upload the plugin code as-is (no rewrite)
170
239
  tool = client.create_tool_from_code(
171
- tool_name,
172
- code_content,
240
+ name=tool_name,
241
+ code=code_content,
173
242
  framework="langchain", # Always langchain
174
243
  description=description,
175
- tags=_parse_tags(tags),
244
+ tags=_parse_tags(tags) if tags else None,
176
245
  )
177
246
  else:
178
- # Metadata-only tool creation
247
+ # Metadata-only tool creation or import from file
179
248
  tool_kwargs = {}
180
249
  if name:
181
250
  tool_kwargs["name"] = name
@@ -186,60 +255,141 @@ def create(ctx, file_arg, file, name, description, tags):
186
255
  if tags:
187
256
  tool_kwargs["tags"] = _parse_tags(tags)
188
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
+
189
277
  tool = client.create_tool(**tool_kwargs)
190
278
 
191
- if ctx.obj.get("view") == "json":
192
- click.echo(json.dumps(tool.model_dump(), indent=2))
193
- else:
194
- # Rich output
195
- creation_method = (
196
- "file upload (custom)" if file else "metadata only (native)"
197
- )
198
- panel = Panel(
199
- f"[green]✅ Tool '{tool.name}' created successfully via {creation_method}![/green]\n\n"
200
- f"ID: {tool.id}\n"
201
- f"Framework: {getattr(tool, 'framework', 'N/A')} (default)\n"
202
- f"Type: {getattr(tool, 'tool_type', 'N/A')} (auto-detected)\n"
203
- f"Description: {getattr(tool, 'description', 'No description')}",
204
- title="🔧 Tool Created",
205
- border_style="green",
206
- )
207
- 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)
208
294
 
209
295
  except Exception as e:
210
- if ctx.obj.get("view") == "json":
211
- click.echo(json.dumps({"error": str(e)}, indent=2))
212
- else:
213
- console.print(Text(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")
214
299
  raise click.ClickException(str(e))
215
300
 
216
301
 
217
302
  @tools_group.command()
218
303
  @click.argument("tool_ref")
219
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
+ )
220
310
  @output_flags()
221
311
  @click.pass_context
222
- def get(ctx, tool_ref, select):
223
- """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
+ """
224
320
  try:
225
321
  client = get_client(ctx)
226
322
 
227
323
  # Resolve tool with ambiguity handling
228
324
  tool = _resolve_tool(ctx, client, tool_ref, select)
229
325
 
230
- # Create result data with all available fields from backend
231
- result_data = {
232
- "id": str(getattr(tool, "id", "N/A")),
233
- "name": getattr(tool, "name", "N/A"),
234
- "tool_type": getattr(tool, "tool_type", "N/A"),
235
- "framework": getattr(tool, "framework", "N/A"),
236
- "version": getattr(tool, "version", "N/A"),
237
- "description": getattr(tool, "description", "N/A"),
238
- }
239
-
240
- output_result(
241
- ctx, result_data, title="Tool Details", panel_title=f"🔧 {tool.name}"
242
- )
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
+ )
243
393
 
244
394
  except Exception as e:
245
395
  raise click.ClickException(str(e))
@@ -248,7 +398,9 @@ def get(ctx, tool_ref, select):
248
398
  @tools_group.command()
249
399
  @click.argument("tool_id")
250
400
  @click.option(
251
- "--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)",
252
404
  )
253
405
  @click.option("--description", help="New description")
254
406
  @click.option("--tags", help="Comma-separated tags")
@@ -265,41 +417,44 @@ def update(ctx, tool_id, file, description, tags):
265
417
  except Exception as e:
266
418
  raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
267
419
 
420
+ # Prepare update data
268
421
  update_data = {}
269
-
270
422
  if description:
271
423
  update_data["description"] = description
272
-
273
424
  if tags:
274
425
  update_data["tags"] = [tag.strip() for tag in tags.split(",")]
275
426
 
276
427
  if file:
277
- # Update code
278
- updated_tool = tool.update(file_path=file)
279
- if ctx.obj.get("view") != "json":
280
- console.print(Text(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
+ )
281
439
  elif update_data:
282
- # 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
+ )
283
445
  updated_tool = tool.update(**update_data)
284
- if ctx.obj.get("view") != "json":
285
- console.print(Text("[green]✓[/green] Tool metadata updated"))
446
+ handle_rich_output(ctx, Text("[green]✓[/green] Tool metadata updated"))
286
447
  else:
287
- if ctx.obj.get("view") != "json":
288
- console.print(Text("[yellow]No updates specified[/yellow]"))
448
+ handle_rich_output(ctx, Text("[yellow]No updates specified[/yellow]"))
289
449
  return
290
450
 
291
- if ctx.obj.get("view") == "json":
292
- click.echo(json.dumps(updated_tool.model_dump(), indent=2))
293
- else:
294
- console.print(
295
- f"[green]✅ Tool '{updated_tool.name}' updated successfully[/green]"
296
- )
451
+ handle_json_output(ctx, updated_tool.model_dump())
452
+ handle_rich_output(ctx, display_update_success("Tool", updated_tool.name))
297
453
 
298
454
  except Exception as e:
299
- if ctx.obj.get("view") == "json":
300
- click.echo(json.dumps({"error": str(e)}, indent=2))
301
- else:
302
- console.print(Text(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")
303
458
  raise click.ClickException(str(e))
304
459
 
305
460
 
@@ -319,37 +474,29 @@ def delete(ctx, tool_id, yes):
319
474
  except Exception as e:
320
475
  raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
321
476
 
322
- # Confirm deletion
323
- if not yes and not click.confirm(
324
- f"Are you sure you want to delete tool '{tool.name}'?"
325
- ):
326
- if ctx.obj.get("view") != "json":
327
- console.print(Text("Deletion cancelled."))
477
+ # Confirm deletion via centralized display helper
478
+ if not yes and not display_confirmation_prompt("Tool", tool.name):
328
479
  return
329
480
 
330
481
  tool.delete()
331
482
 
332
- if ctx.obj.get("view") == "json":
333
- click.echo(
334
- json.dumps(
335
- {"success": True, "message": f"Tool '{tool.name}' deleted"},
336
- indent=2,
337
- )
338
- )
339
- else:
340
- console.print(
341
- Text(f"[green]✅ Tool '{tool.name}' deleted successfully[/green]")
342
- )
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))
343
491
 
344
492
  except Exception as e:
345
- if ctx.obj.get("view") == "json":
346
- click.echo(json.dumps({"error": str(e)}, indent=2))
347
- else:
348
- console.print(Text(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")
349
496
  raise click.ClickException(str(e))
350
497
 
351
498
 
352
- @tools_group.command()
499
+ @tools_group.command("script")
353
500
  @click.argument("tool_id")
354
501
  @output_flags()
355
502
  @click.pass_context
@@ -357,92 +504,16 @@ def script(ctx, tool_id):
357
504
  """Get tool script content."""
358
505
  try:
359
506
  client = get_client(ctx)
360
-
361
- # Get tool by ID (no ambiguity handling needed)
362
- try:
363
- tool = client.get_tool_by_id(tool_id)
364
- except Exception as e:
365
- raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
366
-
367
- # Get tool script content
368
- script_content = client.tools.get_tool_script(tool_id)
369
-
370
- if ctx.obj.get("view") == "json":
371
- click.echo(
372
- json.dumps(
373
- {
374
- "tool_id": tool_id,
375
- "tool_name": tool.name,
376
- "script": script_content,
377
- },
378
- indent=2,
379
- )
380
- )
381
- elif ctx.obj.get("output"):
382
- # Save to file
383
- output_file = ctx.obj.get("output")
384
- with open(output_file, "w", encoding="utf-8") as f:
385
- f.write(script_content)
386
- console.print(f"[green]✅ Tool script saved to {output_file}[/green]")
387
- else:
388
- # Display in terminal
389
- console.print(
390
- Panel(
391
- script_content,
392
- title=f"🔧 Tool Script: {tool.name}",
393
- border_style="cyan",
394
- )
395
- )
396
-
397
- except Exception as e:
398
- if ctx.obj.get("view") == "json":
399
- click.echo(json.dumps({"error": str(e)}, indent=2))
400
- else:
401
- console.print(f"[red]Error getting tool script: {e}[/red]")
402
- raise click.ClickException(str(e))
403
-
404
-
405
- @tools_group.command()
406
- @click.argument("tool_id")
407
- @click.option(
408
- "--file",
409
- type=click.Path(exists=True),
410
- required=True,
411
- help="New tool file for code update",
412
- )
413
- @click.option("--name", help="New tool name")
414
- @click.option("--description", help="New description")
415
- @click.option("--tags", help="Comma-separated tags")
416
- @output_flags()
417
- @click.pass_context
418
- def upload_update(ctx, tool_id, file, name, description, tags):
419
- """Update a tool plugin via file upload."""
420
- try:
421
- client = get_client(ctx)
422
-
423
- # Prepare update data
424
- update_data = {}
425
- if name:
426
- update_data["name"] = name
427
- if description:
428
- update_data["description"] = description
429
- if tags:
430
- update_data["tags"] = [tag.strip() for tag in tags.split(",")]
431
-
432
- # Update tool via file upload
433
- updated_tool = client.tools.update_tool_via_file(tool_id, file, **update_data)
507
+ script_content = client.get_tool_script(tool_id)
434
508
 
435
509
  if ctx.obj.get("view") == "json":
436
- click.echo(json.dumps(updated_tool.model_dump(), indent=2))
510
+ click.echo(json.dumps({"script": script_content}, indent=2))
437
511
  else:
438
- console.print(
439
- f"[green]✅ Tool '{updated_tool.name}' updated successfully via file upload[/green]"
440
- )
441
- console.print(f"[blue]📁 File: {file}[/blue]")
512
+ console.print(f"[green]📜 Tool Script for '{tool_id}':[/green]")
513
+ console.print(script_content)
442
514
 
443
515
  except Exception as e:
444
- if ctx.obj.get("view") == "json":
445
- click.echo(json.dumps({"error": str(e)}, indent=2))
446
- else:
447
- console.print(Text(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]"))
448
519
  raise click.ClickException(str(e))