griptape-nodes 0.52.1__py3-none-any.whl → 0.54.0__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 (71) hide show
  1. griptape_nodes/__init__.py +8 -942
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/app.py +48 -86
  4. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
  5. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
  6. griptape_nodes/cli/__init__.py +1 -0
  7. griptape_nodes/cli/commands/__init__.py +1 -0
  8. griptape_nodes/cli/commands/config.py +74 -0
  9. griptape_nodes/cli/commands/engine.py +80 -0
  10. griptape_nodes/cli/commands/init.py +550 -0
  11. griptape_nodes/cli/commands/libraries.py +96 -0
  12. griptape_nodes/cli/commands/models.py +504 -0
  13. griptape_nodes/cli/commands/self.py +120 -0
  14. griptape_nodes/cli/main.py +56 -0
  15. griptape_nodes/cli/shared.py +75 -0
  16. griptape_nodes/common/__init__.py +1 -0
  17. griptape_nodes/common/directed_graph.py +71 -0
  18. griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
  19. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
  20. griptape_nodes/drivers/storage/local_storage_driver.py +23 -14
  21. griptape_nodes/exe_types/core_types.py +60 -2
  22. griptape_nodes/exe_types/node_types.py +257 -38
  23. griptape_nodes/exe_types/param_components/__init__.py +1 -0
  24. griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
  25. griptape_nodes/machines/control_flow.py +195 -94
  26. griptape_nodes/machines/dag_builder.py +207 -0
  27. griptape_nodes/machines/fsm.py +10 -1
  28. griptape_nodes/machines/parallel_resolution.py +558 -0
  29. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +30 -57
  30. griptape_nodes/node_library/library_registry.py +34 -1
  31. griptape_nodes/retained_mode/events/app_events.py +5 -1
  32. griptape_nodes/retained_mode/events/base_events.py +9 -9
  33. griptape_nodes/retained_mode/events/config_events.py +30 -0
  34. griptape_nodes/retained_mode/events/execution_events.py +2 -2
  35. griptape_nodes/retained_mode/events/model_events.py +296 -0
  36. griptape_nodes/retained_mode/events/node_events.py +4 -3
  37. griptape_nodes/retained_mode/griptape_nodes.py +34 -12
  38. griptape_nodes/retained_mode/managers/agent_manager.py +23 -5
  39. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  40. griptape_nodes/retained_mode/managers/config_manager.py +44 -3
  41. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  42. griptape_nodes/retained_mode/managers/event_manager.py +8 -2
  43. griptape_nodes/retained_mode/managers/flow_manager.py +150 -206
  44. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  45. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  46. griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
  47. griptape_nodes/retained_mode/managers/node_manager.py +102 -220
  48. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  49. griptape_nodes/retained_mode/managers/os_manager.py +28 -13
  50. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  51. griptape_nodes/retained_mode/managers/settings.py +116 -7
  52. griptape_nodes/retained_mode/managers/static_files_manager.py +85 -12
  53. griptape_nodes/retained_mode/managers/sync_manager.py +17 -9
  54. griptape_nodes/retained_mode/managers/workflow_manager.py +186 -192
  55. griptape_nodes/retained_mode/retained_mode.py +19 -0
  56. griptape_nodes/servers/__init__.py +1 -0
  57. griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
  58. griptape_nodes/{app/api.py → servers/static.py} +43 -40
  59. griptape_nodes/traits/add_param_button.py +1 -1
  60. griptape_nodes/traits/button.py +334 -6
  61. griptape_nodes/traits/color_picker.py +66 -0
  62. griptape_nodes/traits/multi_options.py +188 -0
  63. griptape_nodes/traits/numbers_selector.py +77 -0
  64. griptape_nodes/traits/options.py +93 -2
  65. griptape_nodes/traits/traits.json +4 -0
  66. griptape_nodes/utils/async_utils.py +31 -0
  67. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +4 -1
  68. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +71 -48
  69. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
  70. /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
  71. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,504 @@
1
+ """Models command for managing AI models."""
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING
5
+
6
+ import typer
7
+ from rich.table import Table
8
+
9
+ from griptape_nodes.cli.shared import console
10
+ from griptape_nodes.retained_mode.events.model_events import (
11
+ DeleteModelDownloadRequest,
12
+ DeleteModelDownloadResultFailure,
13
+ DeleteModelDownloadResultSuccess,
14
+ DeleteModelRequest,
15
+ DeleteModelResultFailure,
16
+ DeleteModelResultSuccess,
17
+ ListModelDownloadsRequest,
18
+ ListModelDownloadsResultFailure,
19
+ ListModelDownloadsResultSuccess,
20
+ ListModelsRequest,
21
+ ListModelsResultFailure,
22
+ ListModelsResultSuccess,
23
+ ModelInfo,
24
+ QueryInfo,
25
+ SearchModelsRequest,
26
+ SearchModelsResultFailure,
27
+ SearchModelsResultSuccess,
28
+ )
29
+ from griptape_nodes.retained_mode.retained_mode import GriptapeNodes
30
+
31
+ if TYPE_CHECKING:
32
+ from griptape_nodes.retained_mode.events.model_events import ModelDownloadStatus
33
+
34
+ app = typer.Typer(help="Manage AI models.")
35
+ downloads_app = typer.Typer(help="Manage model download tracking records.")
36
+
37
+ # Add downloads subcommand
38
+ app.add_typer(downloads_app, name="downloads")
39
+
40
+
41
+ @app.command("download")
42
+ def download_command(
43
+ model_id: str = typer.Argument(..., help="Model ID or URL (e.g., 'microsoft/DialoGPT-medium')"),
44
+ local_dir: str | None = typer.Option(None, "--local-dir", help="Local directory to download the model to"),
45
+ revision: str = typer.Option("main", "--revision", help="Git revision to download"),
46
+ ) -> None:
47
+ """Download a model from Hugging Face Hub."""
48
+ asyncio.run(_download_model(model_id, local_dir, revision))
49
+
50
+
51
+ @app.command("list")
52
+ def list_command() -> None:
53
+ """List all downloaded model files in local cache."""
54
+ asyncio.run(_list_models())
55
+
56
+
57
+ @app.command("delete")
58
+ def delete_command(
59
+ model_id: str = typer.Argument(..., help="Model ID to delete (e.g., 'microsoft/DialoGPT-medium')"),
60
+ ) -> None:
61
+ """Delete model files from local cache."""
62
+ asyncio.run(_delete_model(model_id))
63
+
64
+
65
+ @downloads_app.command("status")
66
+ def downloads_status_command(
67
+ model_id: str = typer.Argument(None, help="Optional model ID to check download status for"),
68
+ ) -> None:
69
+ """Show download status for a specific model or all models."""
70
+ asyncio.run(_get_model_status(model_id))
71
+
72
+
73
+ @downloads_app.command("list")
74
+ def downloads_list_command() -> None:
75
+ """List all model download status records."""
76
+ asyncio.run(_get_model_status(None))
77
+
78
+
79
+ @downloads_app.command("delete")
80
+ def downloads_delete_command(
81
+ model_id: str = typer.Argument(
82
+ ..., help="Model ID to delete download status for (e.g., 'microsoft/DialoGPT-medium')"
83
+ ),
84
+ ) -> None:
85
+ """Delete download status tracking records for a model."""
86
+ asyncio.run(_delete_model_status(model_id))
87
+
88
+
89
+ @app.command("search")
90
+ def search_command(
91
+ query: str | None = typer.Argument(None, help="Search query to match against model names"),
92
+ task: str | None = typer.Option(None, "--task", help="Filter by task type"),
93
+ limit: int = typer.Option(20, "--limit", help="Maximum number of results (max: 100)"),
94
+ sort: str = typer.Option("downloads", "--sort", help="Sort results by"),
95
+ direction: str = typer.Option("desc", "--direction", help="Sort direction"),
96
+ ) -> None:
97
+ """Search for models on Hugging Face Hub."""
98
+ asyncio.run(_search_models(query, task, limit, sort, direction))
99
+
100
+
101
+ async def _download_model(
102
+ model_id: str,
103
+ local_dir: str | None,
104
+ revision: str,
105
+ ) -> None:
106
+ """Download a model from Hugging Face Hub.
107
+
108
+ Args:
109
+ model_id: Model ID or URL to download
110
+ local_dir: Local directory to download the model to
111
+ revision: Git revision to download
112
+ """
113
+ console.print(f"[bold green]Downloading model: {model_id}[/bold green]")
114
+
115
+ try:
116
+ # ModelManager DownloadModelRequest will use this command so it's important that we don't use the request ourselves
117
+ model_manager = GriptapeNodes.ModelManager()
118
+ local_path = model_manager.download_model(
119
+ model_id=model_id,
120
+ local_dir=local_dir,
121
+ revision=revision,
122
+ allow_patterns=None,
123
+ ignore_patterns=None,
124
+ )
125
+
126
+ # Success case
127
+ console.print("[bold green]Model downloaded successfully![/bold green]")
128
+ console.print(f"[green]Downloaded to: {local_path}[/green]")
129
+
130
+ except Exception as e:
131
+ console.print("[bold red]Model download failed:[/bold red]")
132
+ console.print(f"[red]{e}[/red]")
133
+
134
+
135
+ async def _list_models() -> None:
136
+ """List all downloaded models in the local cache."""
137
+ console.print("[bold green]Listing cached models...[/bold green]")
138
+
139
+ # Create the list request
140
+ request = ListModelsRequest()
141
+
142
+ try:
143
+ # Use the ModelManager to handle the listing
144
+ result = await GriptapeNodes.ahandle_request(request)
145
+ if isinstance(result, ListModelsResultSuccess):
146
+ # Success case
147
+ models = result.models
148
+ if models:
149
+ console.print(f"[bold green]Found {len(models)} cached models:[/bold green]")
150
+
151
+ table = Table()
152
+ table.add_column("Model ID", style="green")
153
+ table.add_column("Size (GB)", style="cyan", justify="right")
154
+
155
+ for model in models:
156
+ size_gb = round((model.size_bytes or 0) / (1024**3), 2) if model.size_bytes else 0.0
157
+ table.add_row(model.model_id, str(size_gb))
158
+ console.print(table)
159
+ else:
160
+ console.print("[yellow]No models found in local cache[/yellow]")
161
+
162
+ # Failure case
163
+
164
+ elif isinstance(result, ListModelsResultFailure):
165
+ console.print("[bold red]Model listing failed:[/bold red]")
166
+ if result.result_details:
167
+ console.print(f"[red]{result.result_details}[/red]")
168
+ if result.exception:
169
+ console.print(f"[dim]Error: {result.exception}[/dim]")
170
+ else:
171
+ console.print("[bold red]Model listing failed: Unknown error occurred[/bold red]")
172
+
173
+ except Exception as e:
174
+ console.print("[bold red]Unexpected error during model listing:[/bold red]")
175
+ console.print(f"[red]{e}[/red]")
176
+
177
+
178
+ async def _delete_model(model_id: str) -> None:
179
+ """Delete a model from the local cache.
180
+
181
+ Args:
182
+ model_id: Model ID to delete
183
+ """
184
+ console.print(f"[bold yellow]Deleting model: {model_id}[/bold yellow]")
185
+
186
+ # Create the delete request
187
+ request = DeleteModelRequest(model_id=model_id)
188
+
189
+ try:
190
+ # Use the ModelManager to handle the deletion
191
+ result = await GriptapeNodes.ahandle_request(request)
192
+
193
+ if isinstance(result, DeleteModelResultSuccess):
194
+ # Success case
195
+ console.print("[bold green]Model deleted successfully![/bold green]")
196
+ console.print(f"[green]Deleted: {result.deleted_path}[/green]")
197
+ # Failure case
198
+
199
+ elif isinstance(result, DeleteModelResultFailure):
200
+ console.print("[bold red]Model deletion failed:[/bold red]")
201
+ if result.result_details:
202
+ console.print(f"[red]{result.result_details}[/red]")
203
+ if result.exception:
204
+ console.print(f"[dim]Error: {result.exception}[/dim]")
205
+ else:
206
+ console.print("[bold red]Model deletion failed: Unknown error occurred[/bold red]")
207
+
208
+ except Exception as e:
209
+ console.print("[bold red]Unexpected error during model deletion:[/bold red]")
210
+ console.print(f"[red]{e}[/red]")
211
+
212
+
213
+ def _format_download_row(download: "ModelDownloadStatus") -> tuple[str, str, str, str, str, str]:
214
+ """Format a download status object into table row data.
215
+
216
+ Args:
217
+ download: ModelDownloadStatus object
218
+
219
+ Returns:
220
+ tuple: (model_id, status_colored, progress_str, size_str, eta_str, started_str)
221
+ """
222
+ progress_str = _format_progress(download)
223
+ size_str = _format_size(download)
224
+ eta_str = _format_eta(download)
225
+ started_str = _format_timestamp(download)
226
+ status_colored = _format_status(download)
227
+
228
+ return (
229
+ download.model_id,
230
+ status_colored,
231
+ progress_str,
232
+ size_str,
233
+ eta_str,
234
+ started_str,
235
+ )
236
+
237
+
238
+ def _format_progress(download: "ModelDownloadStatus") -> str:
239
+ """Format download progress information."""
240
+ if download.total_files is not None and download.completed_files is not None and download.total_files > 0:
241
+ progress_percent = (download.completed_files / download.total_files) * 100
242
+ return f"{progress_percent:.1f}%"
243
+ return "Unknown"
244
+
245
+
246
+ def _format_size(download: "ModelDownloadStatus") -> str:
247
+ """Format download size information."""
248
+ if download.total_files is not None and download.completed_files is not None:
249
+ return f"{download.completed_files}/{download.total_files} files"
250
+ return "Unknown"
251
+
252
+
253
+ def _format_eta(download: "ModelDownloadStatus") -> str:
254
+ """Format estimated time of arrival."""
255
+ # ETA is not available in the current ModelDownloadStatus structure
256
+ # For active downloads, we could potentially calculate based on progress
257
+ # but without timing data, we return a status-appropriate message
258
+ if download.status == "downloading":
259
+ return "In progress"
260
+ if download.status == "completed":
261
+ return "Completed"
262
+ if download.status == "failed":
263
+ return "Failed"
264
+ return "Unknown"
265
+
266
+
267
+ def _format_timestamp(download: "ModelDownloadStatus") -> str:
268
+ """Format started timestamp."""
269
+ started_at = download.started_at
270
+ if not started_at:
271
+ return "Unknown"
272
+
273
+ try:
274
+ from datetime import datetime
275
+
276
+ dt = datetime.fromisoformat(started_at)
277
+ return dt.strftime("%H:%M:%S")
278
+ except Exception:
279
+ return started_at[:10] # Fallback
280
+
281
+
282
+ def _format_status(download: "ModelDownloadStatus") -> str:
283
+ """Format status with color coding."""
284
+ status = download.status
285
+ status_colors = {
286
+ "completed": "green",
287
+ "failed": "red",
288
+ "downloading": "yellow",
289
+ }
290
+
291
+ if status in status_colors:
292
+ return f"[{status_colors[status]}]{status}[/{status_colors[status]}]"
293
+ return status
294
+
295
+
296
+ def _display_downloads_table(downloads: list["ModelDownloadStatus"]) -> None:
297
+ """Display downloads in a formatted table.
298
+
299
+ Args:
300
+ downloads: List of ModelDownloadStatus objects
301
+ """
302
+ console.print(f"[bold green]Found {len(downloads)} download(s):[/bold green]")
303
+
304
+ table = Table()
305
+ table.add_column("Model ID", style="green")
306
+ table.add_column("Status", style="cyan")
307
+ table.add_column("Progress", style="yellow", justify="right")
308
+ table.add_column("Size", style="blue", justify="right")
309
+ table.add_column("ETA", style="magenta", justify="right")
310
+ table.add_column("Started", style="dim")
311
+
312
+ for download in downloads:
313
+ row_data = _format_download_row(download)
314
+ table.add_row(*row_data)
315
+
316
+ console.print(table)
317
+
318
+
319
+ async def _get_model_status(model_id: str | None) -> None:
320
+ """Get download status for models.
321
+
322
+ Args:
323
+ model_id: Optional model ID to get status for
324
+ """
325
+ if model_id:
326
+ console.print(f"[bold green]Getting download status for: {model_id}[/bold green]")
327
+ else:
328
+ console.print("[bold green]Getting download status for all models...[/bold green]")
329
+
330
+ # Create the status request
331
+ request = ListModelDownloadsRequest(model_id=model_id)
332
+
333
+ try:
334
+ # Use the ModelManager to handle the status query
335
+ result = await GriptapeNodes.ahandle_request(request)
336
+
337
+ if isinstance(result, ListModelDownloadsResultSuccess):
338
+ # Success case
339
+ downloads = result.downloads
340
+ if downloads:
341
+ _display_downloads_table(downloads)
342
+ elif model_id:
343
+ console.print(f"[yellow]No download found for model: {model_id}[/yellow]")
344
+ else:
345
+ console.print("[yellow]No downloads found[/yellow]")
346
+
347
+ elif isinstance(result, ListModelDownloadsResultFailure):
348
+ console.print("[bold red]Failed to get download status:[/bold red]")
349
+ if result.result_details:
350
+ console.print(f"[red]{result.result_details}[/red]")
351
+ if result.exception:
352
+ console.print(f"[dim]Error: {result.exception}[/dim]")
353
+ else:
354
+ console.print("[bold red]Failed to get download status: Unknown error occurred[/bold red]")
355
+
356
+ except Exception as e:
357
+ console.print("[bold red]Unexpected error getting download status:[/bold red]")
358
+ console.print(f"[red]{e}[/red]")
359
+
360
+
361
+ async def _search_models(
362
+ query: str | None,
363
+ task: str | None,
364
+ limit: int,
365
+ sort: str,
366
+ direction: str,
367
+ ) -> None:
368
+ """Search for models on Hugging Face Hub.
369
+
370
+ Args:
371
+ query: Search query to match against model names
372
+ task: Filter by task type
373
+ limit: Maximum number of results
374
+ sort: Sort results by
375
+ direction: Sort direction
376
+ """
377
+ if query:
378
+ console.print(f"[bold green]Searching for models: {query}[/bold green]")
379
+ else:
380
+ console.print("[bold green]Searching for models...[/bold green]")
381
+
382
+ # Create the search request
383
+ request = SearchModelsRequest(
384
+ query=query,
385
+ task=task,
386
+ library=None,
387
+ author=None,
388
+ tags=None,
389
+ limit=limit,
390
+ sort=sort,
391
+ direction=direction,
392
+ )
393
+
394
+ try:
395
+ # Use the ModelManager to handle the search
396
+ result = await GriptapeNodes.ahandle_request(request)
397
+
398
+ if isinstance(result, SearchModelsResultSuccess):
399
+ # Success case
400
+ models = result.models
401
+ if models:
402
+ _display_search_results(models, result.query_info)
403
+ else:
404
+ console.print("[yellow]No models found matching the search criteria[/yellow]")
405
+
406
+ elif isinstance(result, SearchModelsResultFailure):
407
+ console.print("[bold red]Model search failed:[/bold red]")
408
+ if result.result_details:
409
+ console.print(f"[red]{result.result_details}[/red]")
410
+ if result.exception:
411
+ console.print(f"[dim]Error: {result.exception}[/dim]")
412
+ else:
413
+ console.print("[bold red]Model search failed: Unknown error occurred[/bold red]")
414
+
415
+ except Exception as e:
416
+ console.print("[bold red]Unexpected error during model search:[/bold red]")
417
+ console.print(f"[red]{e}[/red]")
418
+
419
+
420
+ def _display_search_results(models: list[ModelInfo], query_info: QueryInfo) -> None:
421
+ """Display model search results in a formatted table.
422
+
423
+ Args:
424
+ models: List of model information
425
+ query_info: Information about the search query
426
+ """
427
+ console.print(f"[bold green]Found {len(models)} models[/bold green]")
428
+
429
+ # Show search parameters if any were used
430
+ params = []
431
+ if query_info.query:
432
+ params.append(f"query: {query_info.query}")
433
+ if query_info.task:
434
+ params.append(f"task: {query_info.task}")
435
+ if query_info.library:
436
+ params.append(f"library: {query_info.library}")
437
+ if query_info.author:
438
+ params.append(f"author: {query_info.author}")
439
+ if query_info.tags:
440
+ params.append(f"tags: {', '.join(query_info.tags)}")
441
+
442
+ if params:
443
+ console.print(f"[dim]Search parameters: {', '.join(params)}[/dim]")
444
+
445
+ table = Table()
446
+ table.add_column("Model ID", style="green")
447
+ table.add_column("Author", style="blue")
448
+ table.add_column("Downloads", style="cyan", justify="right")
449
+ table.add_column("Likes", style="yellow", justify="right")
450
+ table.add_column("Task", style="magenta")
451
+ table.add_column("Library", style="dim")
452
+
453
+ for model in models:
454
+ downloads_str = f"{model.downloads:,}" if model.downloads else "0"
455
+ likes_str = str(model.likes or 0)
456
+ task_str = model.task or ""
457
+ library_str = model.library or ""
458
+ author_str = model.author or ""
459
+
460
+ table.add_row(
461
+ model.model_id,
462
+ author_str,
463
+ downloads_str,
464
+ likes_str,
465
+ task_str,
466
+ library_str,
467
+ )
468
+
469
+ console.print(table)
470
+
471
+
472
+ async def _delete_model_status(model_id: str) -> None:
473
+ """Delete download status records for a model.
474
+
475
+ Args:
476
+ model_id: Model ID to delete download status for
477
+ """
478
+ console.print(f"[bold yellow]Deleting download status for: {model_id}[/bold yellow]")
479
+
480
+ # Create the delete request
481
+ request = DeleteModelDownloadRequest(model_id=model_id)
482
+
483
+ try:
484
+ # Use the ModelManager to handle the deletion
485
+ result = await GriptapeNodes.ahandle_request(request)
486
+
487
+ if isinstance(result, DeleteModelDownloadResultSuccess):
488
+ # Success case
489
+ console.print("[bold green]Download status deleted successfully![/bold green]")
490
+ console.print(f"[green]Deleted status file: {result.deleted_path}[/green]")
491
+ # Failure case
492
+
493
+ elif isinstance(result, DeleteModelDownloadResultFailure):
494
+ console.print("[bold red]Download status deletion failed:[/bold red]")
495
+ if result.result_details:
496
+ console.print(f"[red]{result.result_details}[/red]")
497
+ if result.exception:
498
+ console.print(f"[dim]Error: {result.exception}[/dim]")
499
+ else:
500
+ console.print("[bold red]Download status deletion failed: Unknown error occurred[/bold red]")
501
+
502
+ except Exception as e:
503
+ console.print("[bold red]Unexpected error during download status deletion:[/bold red]")
504
+ console.print(f"[red]{e}[/red]")
@@ -0,0 +1,120 @@
1
+ """Self command for Griptape Nodes CLI."""
2
+
3
+ import shutil
4
+ import sys
5
+
6
+ import typer
7
+
8
+ from griptape_nodes.cli.shared import (
9
+ CONFIG_DIR,
10
+ DATA_DIR,
11
+ GITHUB_UPDATE_URL,
12
+ LATEST_TAG,
13
+ PYPI_UPDATE_URL,
14
+ console,
15
+ )
16
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
17
+ from griptape_nodes.utils.uv_utils import find_uv_bin
18
+ from griptape_nodes.utils.version_utils import (
19
+ get_complete_version_string,
20
+ get_current_version,
21
+ get_latest_version_git,
22
+ get_latest_version_pypi,
23
+ )
24
+
25
+ config_manager = GriptapeNodes.ConfigManager()
26
+ secrets_manager = GriptapeNodes.SecretsManager()
27
+ os_manager = GriptapeNodes.OSManager()
28
+
29
+ app = typer.Typer(help="Manage this CLI installation.")
30
+
31
+
32
+ @app.command()
33
+ def update() -> None:
34
+ """Update the CLI."""
35
+ _update_self()
36
+
37
+
38
+ @app.command()
39
+ def uninstall() -> None:
40
+ """Uninstall the CLI."""
41
+ _uninstall_self()
42
+
43
+
44
+ @app.command()
45
+ def version() -> None:
46
+ """Print the CLI version."""
47
+ _print_current_version()
48
+
49
+
50
+ def _get_latest_version(package: str, install_source: str) -> str:
51
+ """Fetches the latest release tag from PyPI.
52
+
53
+ Args:
54
+ package: The name of the package to fetch the latest version for.
55
+ install_source: The source from which the package is installed (e.g., "pypi", "git", "file").
56
+
57
+ Returns:
58
+ str: Latest release tag (e.g., "v0.31.4")
59
+ """
60
+ if install_source == "pypi":
61
+ return get_latest_version_pypi(package, PYPI_UPDATE_URL)
62
+ if install_source == "git":
63
+ return get_latest_version_git(package, GITHUB_UPDATE_URL, LATEST_TAG)
64
+ # If the package is installed from a file, just return the current version since the user is likely managing it manually
65
+ return get_current_version()
66
+
67
+
68
+ def _update_self() -> None:
69
+ """Installs the latest release of the CLI *and* refreshes bundled libraries."""
70
+ console.print("[bold green]Starting updater...[/bold green]")
71
+
72
+ os_manager.replace_process([sys.executable, "-m", "griptape_nodes.updater"])
73
+
74
+
75
+ def _print_current_version() -> None:
76
+ """Prints the current version of the script."""
77
+ version_string = get_complete_version_string()
78
+ console.print(f"[bold green]{version_string}[/bold green]")
79
+
80
+
81
+ def _uninstall_self() -> None:
82
+ """Uninstalls itself by removing config/data directories and the executable."""
83
+ console.print("[bold]Uninstalling Griptape Nodes...[/bold]")
84
+
85
+ # Remove config and data directories
86
+ console.print("[bold]Removing config and data directories...[/bold]")
87
+ dirs = [(CONFIG_DIR, "Config Dir"), (DATA_DIR, "Data Dir")]
88
+ caveats = []
89
+ for dir_path, dir_name in dirs:
90
+ if dir_path.exists():
91
+ console.print(f"[bold]Removing {dir_name} '{dir_path}'...[/bold]")
92
+ try:
93
+ shutil.rmtree(dir_path)
94
+ except OSError as exc:
95
+ console.print(f"[red]Error removing {dir_name} '{dir_path}': {exc}[/red]")
96
+ caveats.append(
97
+ f"- [red]Error removing {dir_name} '{dir_path}'. You may want remove this directory manually.[/red]"
98
+ )
99
+ else:
100
+ console.print(f"[yellow]{dir_name} '{dir_path}' does not exist; skipping.[/yellow]")
101
+
102
+ # Handle any remaining config files not removed by design
103
+ remaining_config_files = config_manager.config_files
104
+ if remaining_config_files:
105
+ caveats.append("- Some config files were intentionally not removed:")
106
+ caveats.extend(f"\t[yellow]- {file}[/yellow]" for file in remaining_config_files)
107
+
108
+ # If there were any caveats to the uninstallation process, print them
109
+ if caveats:
110
+ console.print("[bold]Caveats:[/bold]")
111
+ for line in caveats:
112
+ console.print(line)
113
+
114
+ # Remove the executable
115
+ console.print("[bold]Removing the executable...[/bold]")
116
+ console.print("[bold yellow]When done, press Enter to exit.[/bold yellow]")
117
+
118
+ # Remove the tool using UV
119
+ uv_path = find_uv_bin()
120
+ os_manager.replace_process([uv_path, "tool", "uninstall", "griptape-nodes"])
@@ -0,0 +1,56 @@
1
+ """Main CLI application using typer."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ # Add current directory to path for imports to work
10
+ sys.path.append(str(Path.cwd()))
11
+
12
+ from griptape_nodes.cli.commands import config, engine, init, libraries, models, self
13
+ from griptape_nodes.utils.version_utils import get_complete_version_string
14
+
15
+ console = Console()
16
+
17
+ app = typer.Typer(
18
+ name="griptape-nodes",
19
+ help="Griptape Nodes Engine CLI",
20
+ no_args_is_help=False,
21
+ rich_markup_mode="rich",
22
+ invoke_without_command=True,
23
+ )
24
+
25
+ # Add subcommands
26
+ app.command("init", help="Initialize engine configuration.")(init.init_command)
27
+ app.add_typer(config.app, name="config")
28
+ app.add_typer(self.app, name="self")
29
+ app.add_typer(libraries.app, name="libraries")
30
+ app.add_typer(models.app, name="models")
31
+ app.command("engine")(engine.engine_command)
32
+
33
+
34
+ @app.callback()
35
+ def main(
36
+ ctx: typer.Context,
37
+ version: bool = typer.Option( # noqa: FBT001
38
+ False, "--version", help="Show version and exit."
39
+ ),
40
+ no_update: bool = typer.Option( # noqa: FBT001
41
+ False, "--no-update", help="Skip the auto-update check."
42
+ ),
43
+ ) -> None:
44
+ """Griptape Nodes Engine CLI."""
45
+ if version:
46
+ version_string = get_complete_version_string()
47
+ console.print(f"[bold green]{version_string}[/bold green]")
48
+ raise typer.Exit
49
+
50
+ if ctx.invoked_subcommand is None:
51
+ # Default to engine command when no subcommand is specified
52
+ engine.engine_command(no_update=no_update)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ app()