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,188 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ from griptape_nodes.exe_types.core_types import Parameter, Trait
6
+
7
+
8
+ @dataclass(eq=False)
9
+ class MultiOptions(Trait):
10
+ # SERIALIZATION BUG FIX EXPLANATION:
11
+ #
12
+ # PROBLEM: Similar to Options trait, MultiOptions had a potential serialization bug
13
+ # where dynamically populated multi-options lists would work correctly during runtime
14
+ # but could revert after save/reload cycles. This happens because:
15
+ # 1. trait.choices was the "source of truth" during runtime
16
+ # 2. ui_options["multi_options"] was populated from trait.choices
17
+ # 3. Only ui_options gets serialized/deserialized (not trait fields)
18
+ # 4. After reload, trait.choices was stale but ui_options had correct data
19
+ # 5. Converters used stale trait.choices, causing validation issues
20
+ #
21
+ # SOLUTION: Make ui_options the primary source of truth, with _choices as fallback
22
+ # 1. choices property reads from ui_options["multi_options"] when available
23
+ # 2. choices setter writes to BOTH _choices and ui_options (dual sync)
24
+ # 3. This ensures serialized ui_options data is used after deserialization
25
+ # 4. _choices provides safety fallback if ui_options is missing/corrupted
26
+
27
+ _choices: list = field(default_factory=lambda: ["choice 1", "choice 2", "choice 3"])
28
+ element_id: str = field(default_factory=lambda: "MultiOptions")
29
+ placeholder: str = field(default="Select options...")
30
+ max_selected_display: int = field(default=3)
31
+ show_search: bool = field(default=True)
32
+ icon_size: str = field(default="small")
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ choices: list | None = None,
38
+ placeholder: str = "Select options...",
39
+ max_selected_display: int = 3,
40
+ show_search: bool = True,
41
+ icon_size: str = "small",
42
+ ) -> None:
43
+ super().__init__()
44
+ # Set choices through property to ensure dual sync from the start
45
+ if choices is not None:
46
+ self.choices = choices
47
+
48
+ self.placeholder = placeholder
49
+ self.max_selected_display = max_selected_display
50
+ self.show_search = show_search
51
+
52
+ # Validate icon_size
53
+ if icon_size not in ["small", "large"]:
54
+ self.icon_size = "small"
55
+ else:
56
+ self.icon_size = icon_size
57
+
58
+ @property
59
+ def choices(self) -> list:
60
+ """Get multi-options choices with ui_options as primary source of truth.
61
+
62
+ CRITICAL: This property prioritizes ui_options["multi_options"]["choices"] over _choices
63
+ because ui_options gets properly serialized/deserialized while trait fields don't.
64
+
65
+ Read priority:
66
+ 1. FIRST: ui_options["multi_options"]["choices"] (survives serialization cycles)
67
+ 2. FALLBACK: _choices field (safety net for edge cases)
68
+
69
+ This prevents bugs where available choices could become stale after reload.
70
+ """
71
+ # Check if we have a parent parameter with ui_options (normal case after trait attachment)
72
+ if self._parent and hasattr(self._parent, "ui_options"):
73
+ ui_options = getattr(self._parent, "ui_options", None)
74
+ if (
75
+ isinstance(ui_options, dict)
76
+ and "multi_options" in ui_options
77
+ and isinstance(ui_options["multi_options"], dict)
78
+ and "choices" in ui_options["multi_options"]
79
+ ):
80
+ # Use live ui_options data (this survives serialization)
81
+ return ui_options["multi_options"]["choices"]
82
+
83
+ # Fallback to internal field (used during initialization or if ui_options missing)
84
+ return self._choices
85
+
86
+ @choices.setter
87
+ def choices(self, value: list) -> None:
88
+ """Set multi-options choices with dual synchronization.
89
+
90
+ CRITICAL: This setter writes to BOTH locations to maintain consistency:
91
+ 1. _choices field (for fallback and ui_options_for_trait())
92
+ 2. ui_options["multi_options"]["choices"] (for serialization and runtime use)
93
+
94
+ This dual sync ensures:
95
+ - Immediate runtime consistency
96
+ - Proper serialization of choices data
97
+ - Fallback safety if either location fails
98
+ """
99
+ # Always update internal field first (provides fallback safety)
100
+ self._choices = value
101
+
102
+ # Sync to ui_options if we have a parent parameter (normal case after trait attachment)
103
+ if self._parent and hasattr(self._parent, "ui_options"):
104
+ ui_options = getattr(self._parent, "ui_options", None)
105
+ if not isinstance(ui_options, dict):
106
+ # Initialize ui_options if it doesn't exist or isn't a dict
107
+ self._parent.ui_options = {} # type: ignore[attr-defined]
108
+
109
+ # Ensure multi_options exists and is a dict
110
+ if "multi_options" not in self._parent.ui_options or not isinstance( # type: ignore[attr-defined]
111
+ self._parent.ui_options["multi_options"], # type: ignore[attr-defined]
112
+ dict,
113
+ ):
114
+ self._parent.ui_options["multi_options"] = {} # type: ignore[attr-defined]
115
+
116
+ # Write choices to ui_options (this gets serialized and survives reload)
117
+ self._parent.ui_options["multi_options"]["choices"] = value # type: ignore[attr-defined]
118
+
119
+ @classmethod
120
+ def get_trait_keys(cls) -> list[str]:
121
+ return ["multi_options"]
122
+
123
+ def converters_for_trait(self) -> list[Callable]:
124
+ def converter(value: Any) -> Any:
125
+ # CRITICAL: This converter uses self.choices property (not _choices field)
126
+ # The property reads from ui_options first, ensuring we use post-deserialization
127
+ # choices data instead of stale trait field data.
128
+
129
+ # Handle case where value is not a list (convert single values to list)
130
+ if not isinstance(value, list):
131
+ if value is None:
132
+ return []
133
+ value = [value]
134
+
135
+ # Filter out invalid choices and return valid ones
136
+ valid_choices = [v for v in value if v in self.choices]
137
+
138
+ # If no valid choices, return empty list (allow empty selection)
139
+ return valid_choices
140
+
141
+ return [converter]
142
+
143
+ def validators_for_trait(self) -> list[Callable[[Parameter, Any], Any]]:
144
+ def validator(param: Parameter, value: Any) -> None: # noqa: ARG001
145
+ # CRITICAL: This validator uses self.choices property (not _choices field)
146
+ # Same reasoning as converter - use live ui_options data after deserialization
147
+
148
+ # Allow None or empty list as valid (no selection)
149
+ if value is None or value == []:
150
+ return
151
+
152
+ # Ensure value is a list
153
+ if not isinstance(value, list):
154
+ msg = "MultiOptions value must be a list"
155
+ raise TypeError(msg)
156
+
157
+ # Check that all selected values are valid choices
158
+ invalid_choices = [v for v in value if v not in self.choices]
159
+ if invalid_choices:
160
+ msg = f"Invalid choices: {invalid_choices}"
161
+ raise ValueError(msg)
162
+
163
+ return [validator]
164
+
165
+ def ui_options_for_trait(self) -> dict:
166
+ """Provide UI options for trait initialization.
167
+
168
+ IMPORTANT: This method uses _choices (not self.choices property) to avoid
169
+ circular dependency during Parameter.ui_options property construction:
170
+
171
+ Circular dependency would be:
172
+ 1. Parameter.ui_options calls trait.ui_options_for_trait()
173
+ 2. ui_options_for_trait() calls self.choices property
174
+ 3. choices property tries to read parent.ui_options
175
+ 4. This triggers Parameter.ui_options again → infinite recursion
176
+
177
+ Using _choices directly breaks this cycle while still providing the correct
178
+ initial choices for UI rendering. The property-based sync handles runtime updates.
179
+ """
180
+ return {
181
+ "multi_options": {
182
+ "choices": self._choices,
183
+ "placeholder": self.placeholder,
184
+ "max_selected_display": self.max_selected_display,
185
+ "show_search": self.show_search,
186
+ "icon_size": self.icon_size,
187
+ }
188
+ }
@@ -0,0 +1,77 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ from griptape_nodes.exe_types.core_types import Parameter, ParameterMode, Trait
6
+
7
+
8
+ @dataclass(eq=False)
9
+ class NumbersSelector(Trait):
10
+ defaults: dict[str, float] = field(kw_only=True)
11
+ step: float = 1.0
12
+ overall_min: float | None = None
13
+ overall_max: float | None = None
14
+ element_id: str = field(default_factory=lambda: "NumbersSelector")
15
+
16
+ _allowed_modes: set = field(default_factory=lambda: {ParameterMode.PROPERTY})
17
+
18
+ def __init__(
19
+ self,
20
+ defaults: dict[str, float],
21
+ step: float = 1.0,
22
+ overall_min: float | None = None,
23
+ overall_max: float | None = None,
24
+ ) -> None:
25
+ super().__init__()
26
+ self.defaults = defaults
27
+ self.step = step
28
+ self.overall_min = overall_min
29
+ self.overall_max = overall_max
30
+
31
+ @classmethod
32
+ def get_trait_keys(cls) -> list[str]:
33
+ return ["numbers_selector"]
34
+
35
+ def ui_options_for_trait(self) -> dict:
36
+ return {
37
+ "numbers_selector": {
38
+ "step": self.step,
39
+ "overall_min": self.overall_min,
40
+ "overall_max": self.overall_max,
41
+ "defaults": self.defaults,
42
+ }
43
+ }
44
+
45
+ def display_options_for_trait(self) -> dict:
46
+ return {}
47
+
48
+ def converters_for_trait(self) -> list[Callable]:
49
+ return []
50
+
51
+ def validators_for_trait(self) -> list[Callable[..., Any]]:
52
+ def validate(_param: Parameter, value: Any) -> None:
53
+ if value is None:
54
+ return
55
+
56
+ if not isinstance(value, dict):
57
+ msg = "NumbersSelector value must be a dictionary"
58
+ raise TypeError(msg)
59
+
60
+ for key, val in value.items():
61
+ if not isinstance(key, str):
62
+ msg = f"NumbersSelector keys must be strings, got {type(key)}"
63
+ raise TypeError(msg)
64
+
65
+ if not isinstance(val, (int, float)):
66
+ msg = f"NumbersSelector values must be numbers, got {type(val)} for key '{key}'"
67
+ raise TypeError(msg)
68
+
69
+ if self.overall_min is not None and val < self.overall_min:
70
+ msg = f"Value {val} for key '{key}' is below minimum {self.overall_min}"
71
+ raise ValueError(msg)
72
+
73
+ if self.overall_max is not None and val > self.overall_max:
74
+ msg = f"Value {val} for key '{key}' is above maximum {self.overall_max}"
75
+ raise ValueError(msg)
76
+
77
+ return [validate]
@@ -7,15 +7,90 @@ from griptape_nodes.exe_types.core_types import Parameter, Trait
7
7
 
8
8
  @dataclass(eq=False)
9
9
  class Options(Trait):
10
- choices: list[str] = field(default_factory=lambda: ["choice 1", "choice 2", "choice 3"])
10
+ # SERIALIZATION BUG FIX EXPLANATION:
11
+ #
12
+ # PROBLEM: Options trait had a serialization bug where dynamically populated dropdown
13
+ # lists would work correctly during runtime but revert to the first choice after
14
+ # save/reload cycles. This happened because:
15
+ # 1. trait.choices was the "source of truth" during runtime
16
+ # 2. ui_options["simple_dropdown"] was populated from trait.choices
17
+ # 3. Only ui_options gets serialized/deserialized (not trait fields)
18
+ # 4. After reload, trait.choices was stale but ui_options had correct data
19
+ # 5. Converters used stale trait.choices, causing values to revert to first choice
20
+ #
21
+ # SOLUTION: Make ui_options the primary source of truth, with _choices as fallback
22
+ # 1. choices property reads from ui_options["simple_dropdown"] when available
23
+ # 2. choices setter writes to BOTH _choices and ui_options (dual sync)
24
+ # 3. This ensures serialized ui_options data is used after deserialization
25
+ # 4. _choices provides safety fallback if ui_options is missing/corrupted
26
+
27
+ _choices: list = field(default_factory=lambda: ["choice 1", "choice 2", "choice 3"])
11
28
  element_id: str = field(default_factory=lambda: "Options")
12
29
 
30
+ def __init__(self, *, choices: list | None = None) -> None:
31
+ super().__init__()
32
+ # Set choices through property to ensure dual sync from the start
33
+ if choices is not None:
34
+ self.choices = choices
35
+
36
+ @property
37
+ def choices(self) -> list:
38
+ """Get dropdown choices with ui_options as primary source of truth.
39
+
40
+ CRITICAL: This property prioritizes ui_options["simple_dropdown"] over _choices
41
+ because ui_options gets properly serialized/deserialized while trait fields don't.
42
+
43
+ Read priority:
44
+ 1. FIRST: ui_options["simple_dropdown"] (survives serialization cycles)
45
+ 2. FALLBACK: _choices field (safety net for edge cases)
46
+
47
+ This fixes the bug where selected values reverted to first choice after reload.
48
+ """
49
+ # Check if we have a parent parameter with ui_options (normal case after trait attachment)
50
+ if self._parent and hasattr(self._parent, "ui_options"):
51
+ ui_options = getattr(self._parent, "ui_options", None)
52
+ if isinstance(ui_options, dict) and "simple_dropdown" in ui_options:
53
+ # Use live ui_options data (this survives serialization)
54
+ return ui_options["simple_dropdown"]
55
+
56
+ # Fallback to internal field (used during initialization or if ui_options missing)
57
+ return self._choices
58
+
59
+ @choices.setter
60
+ def choices(self, value: list) -> None:
61
+ """Set dropdown choices with dual synchronization.
62
+
63
+ CRITICAL: This setter writes to BOTH locations to maintain consistency:
64
+ 1. _choices field (for fallback and ui_options_for_trait())
65
+ 2. ui_options["simple_dropdown"] (for serialization and runtime use)
66
+
67
+ This dual sync ensures:
68
+ - Immediate runtime consistency
69
+ - Proper serialization of choices data
70
+ - Fallback safety if either location fails
71
+ """
72
+ # Always update internal field first (provides fallback safety)
73
+ self._choices = value
74
+
75
+ # Sync to ui_options if we have a parent parameter (normal case after trait attachment)
76
+ if self._parent and hasattr(self._parent, "ui_options"):
77
+ ui_options = getattr(self._parent, "ui_options", None)
78
+ if not isinstance(ui_options, dict):
79
+ # Initialize ui_options if it doesn't exist or isn't a dict
80
+ self._parent.ui_options = {} # type: ignore[attr-defined]
81
+ # Write choices to ui_options (this gets serialized and survives reload)
82
+ self._parent.ui_options["simple_dropdown"] = value # type: ignore[attr-defined]
83
+
13
84
  @classmethod
14
85
  def get_trait_keys(cls) -> list[str]:
15
86
  return ["options", "models"]
16
87
 
17
88
  def converters_for_trait(self) -> list[Callable]:
18
89
  def converter(value: Any) -> Any:
90
+ # CRITICAL: This converter uses self.choices property (not _choices field)
91
+ # The property reads from ui_options first, ensuring we use post-deserialization
92
+ # choices data instead of stale trait field data. This prevents the bug where
93
+ # selected values revert to first choice after save/reload.
19
94
  if value not in self.choices:
20
95
  return self.choices[0]
21
96
  return value
@@ -24,6 +99,8 @@ class Options(Trait):
24
99
 
25
100
  def validators_for_trait(self) -> list[Callable[[Parameter, Any], Any]]:
26
101
  def validator(param: Parameter, value: Any) -> None: # noqa: ARG001
102
+ # CRITICAL: This validator uses self.choices property (not _choices field)
103
+ # Same reasoning as converter - use live ui_options data after deserialization
27
104
  if value not in self.choices:
28
105
  msg = "Choice not allowed"
29
106
  raise ValueError(msg)
@@ -31,4 +108,18 @@ class Options(Trait):
31
108
  return [validator]
32
109
 
33
110
  def ui_options_for_trait(self) -> dict:
34
- return {"simple_dropdown": self.choices}
111
+ """Provide UI options for trait initialization.
112
+
113
+ IMPORTANT: This method uses _choices (not self.choices property) to avoid
114
+ circular dependency during Parameter.ui_options property construction:
115
+
116
+ Circular dependency would be:
117
+ 1. Parameter.ui_options calls trait.ui_options_for_trait()
118
+ 2. ui_options_for_trait() calls self.choices property
119
+ 3. choices property tries to read parent.ui_options
120
+ 4. This triggers Parameter.ui_options again → infinite recursion
121
+
122
+ Using _choices directly breaks this cycle while still providing the correct
123
+ initial choices for UI rendering. The property-based sync handles runtime updates.
124
+ """
125
+ return {"simple_dropdown": self._choices}
@@ -1,4 +1,8 @@
1
1
  {
2
2
  "class_name": "MinMax",
3
3
  "file_name": "minmax.py"
4
+ },
5
+ {
6
+ "class_name": "ColorPicker",
7
+ "file_name": "color_picker.py"
4
8
  }
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import inspect
7
+ import logging
7
8
  import subprocess
8
9
  from typing import TYPE_CHECKING, Any
9
10
 
@@ -11,6 +12,9 @@ if TYPE_CHECKING:
11
12
  from collections.abc import Callable, Sequence
12
13
 
13
14
 
15
+ logger = logging.getLogger(__name__)
16
+
17
+
14
18
  async def call_function(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
15
19
  """Call a function, handling both sync and async cases.
16
20
 
@@ -87,3 +91,30 @@ async def subprocess_run(
87
91
  )
88
92
 
89
93
  return completed_process
94
+
95
+
96
+ async def cancel_subprocess(process: asyncio.subprocess.Process, name: str = "process") -> None:
97
+ """Cancel a subprocess with graceful termination then force kill.
98
+
99
+ Args:
100
+ process: The subprocess to cancel
101
+ name: Name/description for logging purposes
102
+ """
103
+ if process.returncode is not None: # Process already terminated
104
+ return
105
+
106
+ try:
107
+ process.terminate()
108
+ logger.info("Terminated %s", name)
109
+
110
+ # Give process a chance to terminate gracefully
111
+ try:
112
+ await asyncio.wait_for(process.wait(), timeout=5.0)
113
+ except TimeoutError:
114
+ # Force kill if it doesn't terminate
115
+ process.kill()
116
+ logger.info("Force killed %s", name)
117
+ await process.wait()
118
+ except ProcessLookupError:
119
+ # Process already terminated
120
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: griptape-nodes
3
- Version: 0.52.1
3
+ Version: 0.54.0
4
4
  Summary: Add your description here
5
5
  Requires-Dist: griptape>=1.8.2
6
6
  Requires-Dist: pydantic>=2.10.6
@@ -19,6 +19,9 @@ Requires-Dist: mcp[ws]>=1.10.1
19
19
  Requires-Dist: binaryornot>=0.4.4
20
20
  Requires-Dist: pillow>=11.3.0
21
21
  Requires-Dist: watchfiles>=1.1.0
22
+ Requires-Dist: typer>=0.15.0
23
+ Requires-Dist: huggingface-hub>=0.28.0
24
+ Requires-Dist: rich>=14.1.0
22
25
  Requires-Dist: austin-dist>=3.7.0 ; extra == 'profiling'
23
26
  Requires-Python: >=3.12.0, <3.13
24
27
  Provides-Extra: profiling