griptape-nodes 0.43.1__py3-none-any.whl → 0.45.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 (134) hide show
  1. griptape_nodes/__init__.py +46 -52
  2. griptape_nodes/app/.python-version +0 -0
  3. griptape_nodes/app/__init__.py +0 -0
  4. griptape_nodes/app/api.py +37 -41
  5. griptape_nodes/app/app.py +70 -3
  6. griptape_nodes/app/watch.py +5 -2
  7. griptape_nodes/bootstrap/__init__.py +0 -0
  8. griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
  9. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +7 -1
  10. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +90 -0
  11. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +7 -1
  12. griptape_nodes/drivers/__init__.py +0 -0
  13. griptape_nodes/drivers/storage/__init__.py +0 -0
  14. griptape_nodes/drivers/storage/base_storage_driver.py +90 -0
  15. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +48 -0
  16. griptape_nodes/drivers/storage/local_storage_driver.py +37 -0
  17. griptape_nodes/drivers/storage/storage_backend.py +0 -0
  18. griptape_nodes/exe_types/__init__.py +0 -0
  19. griptape_nodes/exe_types/connections.py +0 -0
  20. griptape_nodes/exe_types/core_types.py +222 -17
  21. griptape_nodes/exe_types/flow.py +0 -0
  22. griptape_nodes/exe_types/node_types.py +20 -5
  23. griptape_nodes/exe_types/type_validator.py +0 -0
  24. griptape_nodes/machines/__init__.py +0 -0
  25. griptape_nodes/machines/control_flow.py +5 -4
  26. griptape_nodes/machines/fsm.py +0 -0
  27. griptape_nodes/machines/node_resolution.py +110 -74
  28. griptape_nodes/mcp_server/__init__.py +0 -0
  29. griptape_nodes/mcp_server/server.py +16 -8
  30. griptape_nodes/mcp_server/ws_request_manager.py +0 -0
  31. griptape_nodes/node_library/__init__.py +0 -0
  32. griptape_nodes/node_library/advanced_node_library.py +0 -0
  33. griptape_nodes/node_library/library_registry.py +0 -0
  34. griptape_nodes/node_library/workflow_registry.py +29 -0
  35. griptape_nodes/py.typed +0 -0
  36. griptape_nodes/retained_mode/__init__.py +0 -0
  37. griptape_nodes/retained_mode/events/__init__.py +0 -0
  38. griptape_nodes/retained_mode/events/agent_events.py +0 -0
  39. griptape_nodes/retained_mode/events/app_events.py +3 -8
  40. griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
  41. griptape_nodes/retained_mode/events/base_events.py +15 -7
  42. griptape_nodes/retained_mode/events/config_events.py +0 -0
  43. griptape_nodes/retained_mode/events/connection_events.py +0 -0
  44. griptape_nodes/retained_mode/events/context_events.py +0 -0
  45. griptape_nodes/retained_mode/events/execution_events.py +0 -0
  46. griptape_nodes/retained_mode/events/flow_events.py +2 -1
  47. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  48. griptape_nodes/retained_mode/events/library_events.py +0 -0
  49. griptape_nodes/retained_mode/events/logger_events.py +0 -0
  50. griptape_nodes/retained_mode/events/node_events.py +36 -0
  51. griptape_nodes/retained_mode/events/object_events.py +0 -0
  52. griptape_nodes/retained_mode/events/os_events.py +98 -6
  53. griptape_nodes/retained_mode/events/parameter_events.py +0 -0
  54. griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  55. griptape_nodes/retained_mode/events/secrets_events.py +0 -0
  56. griptape_nodes/retained_mode/events/static_file_events.py +0 -0
  57. griptape_nodes/retained_mode/events/sync_events.py +60 -0
  58. griptape_nodes/retained_mode/events/validation_events.py +0 -0
  59. griptape_nodes/retained_mode/events/workflow_events.py +231 -0
  60. griptape_nodes/retained_mode/griptape_nodes.py +9 -4
  61. griptape_nodes/retained_mode/managers/__init__.py +0 -0
  62. griptape_nodes/retained_mode/managers/agent_manager.py +0 -0
  63. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
  64. griptape_nodes/retained_mode/managers/config_manager.py +1 -1
  65. griptape_nodes/retained_mode/managers/context_manager.py +0 -0
  66. griptape_nodes/retained_mode/managers/engine_identity_manager.py +0 -0
  67. griptape_nodes/retained_mode/managers/event_manager.py +0 -0
  68. griptape_nodes/retained_mode/managers/flow_manager.py +6 -0
  69. griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -0
  70. griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -0
  71. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -0
  72. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -0
  73. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -0
  74. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -0
  75. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -0
  76. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -0
  77. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -0
  78. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -0
  79. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -0
  80. griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -0
  81. griptape_nodes/retained_mode/managers/library_manager.py +8 -26
  82. griptape_nodes/retained_mode/managers/node_manager.py +78 -7
  83. griptape_nodes/retained_mode/managers/object_manager.py +0 -0
  84. griptape_nodes/retained_mode/managers/operation_manager.py +7 -0
  85. griptape_nodes/retained_mode/managers/os_manager.py +133 -8
  86. griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
  87. griptape_nodes/retained_mode/managers/session_manager.py +0 -0
  88. griptape_nodes/retained_mode/managers/settings.py +5 -0
  89. griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
  90. griptape_nodes/retained_mode/managers/sync_manager.py +498 -0
  91. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +0 -0
  92. griptape_nodes/retained_mode/managers/workflow_manager.py +736 -33
  93. griptape_nodes/retained_mode/retained_mode.py +23 -0
  94. griptape_nodes/retained_mode/utils/__init__.py +0 -0
  95. griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
  96. griptape_nodes/retained_mode/utils/name_generator.py +0 -0
  97. griptape_nodes/traits/__init__.py +0 -0
  98. griptape_nodes/traits/add_param_button.py +0 -0
  99. griptape_nodes/traits/button.py +0 -0
  100. griptape_nodes/traits/clamp.py +0 -0
  101. griptape_nodes/traits/compare.py +0 -0
  102. griptape_nodes/traits/compare_images.py +0 -0
  103. griptape_nodes/traits/file_system_picker.py +18 -0
  104. griptape_nodes/traits/minmax.py +0 -0
  105. griptape_nodes/traits/options.py +0 -0
  106. griptape_nodes/traits/slider.py +0 -0
  107. griptape_nodes/traits/trait_registry.py +0 -0
  108. griptape_nodes/traits/traits.json +0 -0
  109. griptape_nodes/updater/__init__.py +4 -2
  110. griptape_nodes/updater/__main__.py +0 -0
  111. griptape_nodes/utils/__init__.py +0 -0
  112. griptape_nodes/utils/dict_utils.py +0 -0
  113. griptape_nodes/utils/image_preview.py +0 -0
  114. griptape_nodes/utils/metaclasses.py +0 -0
  115. griptape_nodes/utils/uv_utils.py +18 -0
  116. griptape_nodes/utils/version_utils.py +51 -0
  117. griptape_nodes/version_compatibility/__init__.py +0 -0
  118. griptape_nodes/version_compatibility/versions/__init__.py +0 -0
  119. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
  120. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +0 -0
  121. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/METADATA +2 -1
  122. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/RECORD +42 -47
  123. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/WHEEL +1 -1
  124. griptape_nodes/bootstrap/bootstrap_script.py +0 -54
  125. griptape_nodes/bootstrap/post_build_install_script.sh +0 -3
  126. griptape_nodes/bootstrap/pre_build_install_script.sh +0 -4
  127. griptape_nodes/bootstrap/register_libraries_script.py +0 -32
  128. griptape_nodes/bootstrap/structure_config.yaml +0 -15
  129. griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -1
  130. griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -28
  131. griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -237
  132. griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +0 -62
  133. griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -11
  134. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -81,6 +81,9 @@ from griptape_nodes.retained_mode.events.node_events import (
81
81
  SerializeNodeToCommandsResultSuccess,
82
82
  SerializeSelectedNodesToCommandsRequest,
83
83
  SerializeSelectedNodesToCommandsResultSuccess,
84
+ SetLockNodeStateRequest,
85
+ SetLockNodeStateResultFailure,
86
+ SetLockNodeStateResultSuccess,
84
87
  SetNodeMetadataRequest,
85
88
  SetNodeMetadataResultFailure,
86
89
  SetNodeMetadataResultSuccess,
@@ -178,6 +181,7 @@ class NodeManager:
178
181
  DeserializeSelectedNodesFromCommandsRequest, self.on_deserialize_selected_nodes_from_commands
179
182
  )
180
183
  event_manager.assign_manager_to_request_type(DuplicateSelectedNodesRequest, self.on_duplicate_selected_nodes)
184
+ event_manager.assign_manager_to_request_type(SetLockNodeStateRequest, self.on_toggle_lock_node_request)
181
185
 
182
186
  def handle_node_rename(self, old_name: str, new_name: str) -> None:
183
187
  # Get the node itself
@@ -781,6 +785,13 @@ class NodeManager:
781
785
  result = AddParameterToNodeResultFailure()
782
786
  return result
783
787
 
788
+ # Check if node is locked
789
+ if node.lock:
790
+ details = f"Attempted to add Parameter '{request.parameter_name}' to Node '{node_name}'. Failed because the Node was locked."
791
+ logger.error(details)
792
+ result = AddParameterToNodeResultFailure()
793
+ return result
794
+
784
795
  if request.parent_container_name and not request.initial_setup:
785
796
  parameter = node.get_parameter_by_name(request.parent_container_name)
786
797
  if parameter is None:
@@ -923,7 +934,13 @@ class NodeManager:
923
934
 
924
935
  result = RemoveParameterFromNodeResultFailure()
925
936
  return result
937
+ # Check if the node is locked
938
+ if node.lock:
939
+ details = f"Attempted to remove Element '{request.parameter_name}' from Node '{node_name}'. Failed because the Node was locked."
940
+ logger.error(details)
926
941
 
942
+ result = RemoveParameterFromNodeResultFailure()
943
+ return result
927
944
  # Does the Element actually exist on the Node?
928
945
  element = node.get_element_by_name_and_type(request.parameter_name)
929
946
  if element is None:
@@ -1246,7 +1263,7 @@ class NodeManager:
1246
1263
 
1247
1264
  return None
1248
1265
 
1249
- def on_alter_parameter_details_request(self, request: AlterParameterDetailsRequest) -> ResultPayload: # noqa: C901
1266
+ def on_alter_parameter_details_request(self, request: AlterParameterDetailsRequest) -> ResultPayload: # noqa: C901, PLR0911
1250
1267
  node_name = request.node_name
1251
1268
  node = None
1252
1269
 
@@ -1269,6 +1286,12 @@ class NodeManager:
1269
1286
 
1270
1287
  return AlterParameterDetailsResultFailure()
1271
1288
 
1289
+ # Is the node locked?
1290
+ if node.lock:
1291
+ details = f"Attempted to alter details for Parameter '{request.parameter_name}' from Node '{node_name}'. Failed because the Node was locked."
1292
+ logger.error(details)
1293
+ return AlterParameterDetailsResultFailure()
1294
+
1272
1295
  # Does the Element actually exist on the Node?
1273
1296
  element = node.get_element_by_name_and_type(request.parameter_name)
1274
1297
  if element is None:
@@ -1397,6 +1420,12 @@ class NodeManager:
1397
1420
  logger.error(details)
1398
1421
  return SetParameterValueResultFailure()
1399
1422
 
1423
+ # Is the node locked?
1424
+ if node.lock:
1425
+ details = f"Attempted to set parameter '{param_name}' value on node '{node_name}'. Failed because the Node was locked."
1426
+ logger.error(details)
1427
+ return SetParameterValueResultFailure()
1428
+
1400
1429
  # Does the Parameter actually exist on the Node?
1401
1430
  parameter = node.get_parameter_by_name(param_name)
1402
1431
  if parameter is None:
@@ -1406,8 +1435,8 @@ class NodeManager:
1406
1435
  result = SetParameterValueResultFailure()
1407
1436
  return result
1408
1437
 
1409
- # Validate that parameters can be set at all
1410
- if not parameter.settable:
1438
+ # Validate that parameters can be set at all (note: we want the value to be set during initial setup, but not after)
1439
+ if not parameter.settable and not request.initial_setup:
1411
1440
  details = f"Attempted to set parameter value for '{node_name}.{request.parameter_name}'. Failed because that Parameter was flagged as not settable."
1412
1441
  logger.error(details)
1413
1442
  result = SetParameterValueResultFailure()
@@ -1596,6 +1625,7 @@ class NodeManager:
1596
1625
  result = GetAllNodeInfoResultSuccess(
1597
1626
  metadata=get_metadata_success.metadata,
1598
1627
  node_resolution_state=get_resolution_state_success.state,
1628
+ locked=node.lock,
1599
1629
  connections=list_connections_success,
1600
1630
  element_id_to_value=element_id_to_value,
1601
1631
  root_node_element=element_details,
@@ -1837,7 +1867,7 @@ class NodeManager:
1837
1867
  validation_succeeded=(len(all_exceptions) == 0), exceptions=all_exceptions
1838
1868
  )
1839
1869
 
1840
- def on_serialize_node_to_commands(self, request: SerializeNodeToCommandsRequest) -> ResultPayload: # noqa: C901, PLR0912
1870
+ def on_serialize_node_to_commands(self, request: SerializeNodeToCommandsRequest) -> ResultPayload: # noqa: C901, PLR0912, PLR0915
1841
1871
  node_name = request.node_name
1842
1872
  node = None
1843
1873
 
@@ -1923,12 +1953,19 @@ class NodeManager:
1923
1953
  )
1924
1954
  if set_param_value_requests is not None:
1925
1955
  set_value_commands.extend(set_param_value_requests)
1926
-
1956
+ else:
1957
+ create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
1958
+ # now check if locked
1959
+ if node.lock:
1960
+ lock_command = SetLockNodeStateRequest(node_name=None, lock=True)
1961
+ else:
1962
+ lock_command = None
1927
1963
  # Hooray
1928
1964
  serialized_node_commands = SerializedNodeCommands(
1929
1965
  create_node_command=create_node_request,
1930
1966
  element_modification_commands=element_modification_commands,
1931
1967
  node_library_details=library_details,
1968
+ lock_node_command=lock_command,
1932
1969
  )
1933
1970
  details = f"Successfully serialized node '{node_name}' into commands."
1934
1971
  logger.debug(details)
@@ -2073,7 +2110,6 @@ class NodeManager:
2073
2110
  details = f"Attempted to deserialize a serialized set of Node Creation commands. Failed to execute an element command for node '{node_name}'."
2074
2111
  logger.error(details)
2075
2112
  return DeserializeNodeFromCommandsResultFailure()
2076
-
2077
2113
  details = f"Successfully deserialized a serialized set of Node Creation commands for node '{node_name}'."
2078
2114
  logger.debug(details)
2079
2115
  return DeserializeNodeFromCommandsResultSuccess(node_name=node_name)
@@ -2091,6 +2127,8 @@ class NodeManager:
2091
2127
  connections_to_serialize = []
2092
2128
  # This is also node_uuid to the parameter serialization command.
2093
2129
  parameter_commands = {}
2130
+ # This is node_uuid to lock commands.
2131
+ lock_commands = {}
2094
2132
  # I need to store node names and parameter names to UUID
2095
2133
  unique_uuid_to_values = {}
2096
2134
  # And track how values map into that map.
@@ -2111,6 +2149,7 @@ class NodeManager:
2111
2149
  node_commands[node_name] = result.serialized_node_commands
2112
2150
  node_name_to_uuid[node_name] = result.serialized_node_commands.node_uuid
2113
2151
  parameter_commands[result.serialized_node_commands.node_uuid] = result.set_parameter_value_commands
2152
+ lock_commands[result.serialized_node_commands.node_uuid] = result.serialized_node_commands.lock_node_command
2114
2153
  try:
2115
2154
  flow_name = self.get_node_parent_flow_by_name(node_name)
2116
2155
  GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
@@ -2146,6 +2185,7 @@ class NodeManager:
2146
2185
  serialized_node_commands=list(node_commands.values()),
2147
2186
  serialized_connection_commands=serialized_connections,
2148
2187
  set_parameter_value_commands=parameter_commands,
2188
+ set_lock_commands_per_node=lock_commands,
2149
2189
  )
2150
2190
  # Set everything in the clipboard!
2151
2191
  GriptapeNodes.ContextManager()._clipboard.node_commands = final_result
@@ -2209,6 +2249,12 @@ class NodeManager:
2209
2249
  if not set_parameter_result.succeeded():
2210
2250
  details = f"Failed to set parameter value for {param_request.parameter_name} on node {param_request.node_name}"
2211
2251
  logger.warning(details)
2252
+ lock_command = commands.set_lock_commands_per_node[node_command.node_uuid]
2253
+ if lock_command is not None:
2254
+ lock_node_result = GriptapeNodes.handle_request(lock_command)
2255
+ if not lock_node_result.succeeded():
2256
+ details = f"Failed to lock node {lock_command.node_name}"
2257
+ logger.warning(details)
2212
2258
  # create Connections
2213
2259
  for connection_command in connections:
2214
2260
  connection_request = CreateConnectionRequest(
@@ -2389,7 +2435,7 @@ class NodeManager:
2389
2435
  commands.append(output_command)
2390
2436
  return commands if commands else None
2391
2437
 
2392
- def on_rename_parameter_request(self, request: RenameParameterRequest) -> ResultPayload: # noqa: C901, PLR0912
2438
+ def on_rename_parameter_request(self, request: RenameParameterRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
2393
2439
  """Handle renaming a parameter on a node.
2394
2440
 
2395
2441
  Args:
@@ -2415,6 +2461,12 @@ class NodeManager:
2415
2461
  logger.error(details)
2416
2462
  return RenameParameterResultFailure()
2417
2463
 
2464
+ # Is the node locked?
2465
+ if node.lock:
2466
+ details = f"Attempted to rename Parameter '{request.parameter_name}' on Node '{node_name}'. Failed because the Node is locked."
2467
+ logger.error(details)
2468
+ return RenameParameterResultFailure()
2469
+
2418
2470
  # Get the parameter
2419
2471
  parameter = node.get_parameter_by_name(request.parameter_name)
2420
2472
  if parameter is None:
@@ -2469,3 +2521,22 @@ class NodeManager:
2469
2521
  return RenameParameterResultSuccess(
2470
2522
  old_parameter_name=old_name, new_parameter_name=request.new_parameter_name, node_name=node_name
2471
2523
  )
2524
+
2525
+ def on_toggle_lock_node_request(self, request: SetLockNodeStateRequest) -> ResultPayload:
2526
+ node_name = request.node_name
2527
+ if node_name is None:
2528
+ if not GriptapeNodes.ContextManager().has_current_node():
2529
+ details = "Attempted to lock node in the Current Context. Failed because the Current Context was empty."
2530
+ logger.error(details)
2531
+ return SetLockNodeStateResultFailure()
2532
+ node = GriptapeNodes.ContextManager().get_current_node()
2533
+ node_name = node.name
2534
+ else:
2535
+ try:
2536
+ node = self.get_node_by_name(node_name)
2537
+ except ValueError as err:
2538
+ details = f"Attempted to lock node '{request.node_name}'. Failed because the Node could not be found. Error: {err}"
2539
+ logger.error(details)
2540
+ return SetLockNodeStateResultFailure()
2541
+ node.lock = request.lock
2542
+ return SetLockNodeStateResultSuccess(node_name=node_name, locked=node.lock)
File without changes
@@ -45,6 +45,7 @@ if TYPE_CHECKING:
45
45
  GetNodeMetadataRequest,
46
46
  GetNodeResolutionStateRequest,
47
47
  ListParametersOnNodeRequest,
48
+ SetLockNodeStateRequest,
48
49
  SetNodeMetadataRequest,
49
50
  )
50
51
  from griptape_nodes.retained_mode.events.parameter_events import (
@@ -417,6 +418,12 @@ class PayloadConverter:
417
418
  """Handle RenameParameterRequest payloads."""
418
419
  return f"""cmd.rename_param(node_name="{payload.node_name}",parameter_name="{payload.parameter_name}",new_parameter_name="{payload.new_parameter_name}")"""
419
420
 
421
+ @staticmethod
422
+ def _handle_SetLockNodeStateRequest(payload: SetLockNodeStateRequest) -> str:
423
+ """Handle SetLockNodeStateRequest payloads."""
424
+ node_name_param = f'node_name="{payload.node_name}"' if payload.node_name is not None else "node_name=None"
425
+ return f"""cmd.set_lock_node_state({node_name_param}, lock={payload.lock})"""
426
+
420
427
  # GENERIC HANDLERS FOR PAYLOADS WITHOUT SPECIFIC HANDLERS
421
428
 
422
429
 
@@ -13,6 +13,9 @@ from rich.console import Console
13
13
 
14
14
  from griptape_nodes.retained_mode.events.base_events import ResultPayload
15
15
  from griptape_nodes.retained_mode.events.os_events import (
16
+ CreateFileRequest,
17
+ CreateFileResultFailure,
18
+ CreateFileResultSuccess,
16
19
  FileSystemEntry,
17
20
  ListDirectoryRequest,
18
21
  ListDirectoryResultFailure,
@@ -23,6 +26,9 @@ from griptape_nodes.retained_mode.events.os_events import (
23
26
  ReadFileRequest,
24
27
  ReadFileResultFailure,
25
28
  ReadFileResultSuccess,
29
+ RenameFileRequest,
30
+ RenameFileResultFailure,
31
+ RenameFileResultSuccess,
26
32
  )
27
33
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes, logger
28
34
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
@@ -59,6 +65,14 @@ class OSManager:
59
65
  request_type=ReadFileRequest, callback=self.on_read_file_request
60
66
  )
61
67
 
68
+ event_manager.assign_manager_to_request_type(
69
+ request_type=CreateFileRequest, callback=self.on_create_file_request
70
+ )
71
+
72
+ event_manager.assign_manager_to_request_type(
73
+ request_type=RenameFileRequest, callback=self.on_rename_file_request
74
+ )
75
+
62
76
  def _get_workspace_path(self) -> Path:
63
77
  """Get the workspace path from config."""
64
78
  return GriptapeNodes.ConfigManager().workspace_path
@@ -235,7 +249,7 @@ class OSManager:
235
249
  logger.error(msg)
236
250
  return OpenAssociatedFileResultFailure()
237
251
 
238
- # Sanitize and validate the file path
252
+ # Sanitize and validate the path (file or directory)
239
253
  try:
240
254
  # Resolve the path (no workspace fallback for open requests)
241
255
  path = self._resolve_file_path(file_path_str, workspace_only=False)
@@ -244,12 +258,12 @@ class OSManager:
244
258
  logger.info(details)
245
259
  return OpenAssociatedFileResultFailure()
246
260
 
247
- if not path.exists() or not path.is_file():
248
- details = f"File does not exist: '{path}'"
261
+ if not path.exists():
262
+ details = f"Path does not exist: '{path}'"
249
263
  logger.info(details)
250
264
  return OpenAssociatedFileResultFailure()
251
265
 
252
- logger.info("Attempting to open: %s on platform: %s", path, sys.platform)
266
+ logger.info("Attempting to open path: %s on platform: %s", path, sys.platform)
253
267
 
254
268
  try:
255
269
  platform_name = sys.platform
@@ -257,7 +271,7 @@ class OSManager:
257
271
  # Linter complains but this is the recommended way on Windows
258
272
  # We can ignore this warning as we've validated the path
259
273
  os.startfile(str(path)) # noqa: S606 # pyright: ignore[reportAttributeAccessIssue]
260
- logger.info("Started file on Windows: %s", path)
274
+ logger.info("Opened path on Windows: %s", path)
261
275
  elif self.is_mac():
262
276
  # On macOS, open should be in a standard location
263
277
  subprocess.run( # noqa: S603
@@ -266,7 +280,7 @@ class OSManager:
266
280
  capture_output=True,
267
281
  text=True,
268
282
  )
269
- logger.info("Started file on macOS: %s", path)
283
+ logger.info("Opened path on macOS: %s", path)
270
284
  elif self.is_linux():
271
285
  # Use full path to xdg-open to satisfy linter
272
286
  # Common locations for xdg-open:
@@ -283,7 +297,7 @@ class OSManager:
283
297
  capture_output=True,
284
298
  text=True,
285
299
  )
286
- logger.info("Started file on Linux: %s", path)
300
+ logger.info("Opened path on Linux: %s", path)
287
301
  else:
288
302
  details = f"Unsupported platform: '{platform_name}'"
289
303
  logger.info(details)
@@ -299,9 +313,24 @@ class OSManager:
299
313
  )
300
314
  return OpenAssociatedFileResultFailure()
301
315
  except Exception as e:
302
- logger.error("Exception occurred when trying to open file: %s", type(e).__name__)
316
+ logger.error("Exception occurred when trying to open path: %s", type(e).__name__)
303
317
  return OpenAssociatedFileResultFailure()
304
318
 
319
+ def _detect_mime_type(self, file_path: Path) -> str | None:
320
+ """Detect MIME type for a file. Returns None for directories or if detection fails."""
321
+ if file_path.is_dir():
322
+ return None
323
+
324
+ try:
325
+ mime_type, _ = mimetypes.guess_type(str(file_path), strict=True)
326
+ if mime_type is None:
327
+ mime_type = "text/plain"
328
+ return mime_type # noqa: TRY300
329
+ except Exception as e:
330
+ msg = f"MIME type detection failed for {file_path}: {e}"
331
+ logger.warning(msg)
332
+ return "text/plain"
333
+
305
334
  def on_list_directory_request(self, request: ListDirectoryRequest) -> ResultPayload: # noqa: C901, PLR0911
306
335
  """Handle a request to list directory contents."""
307
336
  try:
@@ -345,6 +374,7 @@ class OSManager:
345
374
  stat = entry.stat()
346
375
  # Get path relative to workspace if within workspace
347
376
  is_entry_in_workspace, entry_path = self._validate_workspace_path(entry)
377
+ mime_type = self._detect_mime_type(entry)
348
378
  entries.append(
349
379
  FileSystemEntry(
350
380
  name=entry.name,
@@ -352,6 +382,7 @@ class OSManager:
352
382
  is_dir=entry.is_dir(),
353
383
  size=stat.st_size,
354
384
  modified_time=stat.st_mtime,
385
+ mime_type=mime_type,
355
386
  )
356
387
  )
357
388
  except (OSError, PermissionError) as e:
@@ -703,3 +734,97 @@ class OSManager:
703
734
  logger.error("Attempted to clean up old files from %s, but no files could be deleted.")
704
735
 
705
736
  return removed_count > 0
737
+
738
+ def on_create_file_request(self, request: CreateFileRequest) -> ResultPayload:
739
+ """Handle a request to create a file or directory."""
740
+ try:
741
+ # Get the full path using the new method
742
+ full_path_str = request.get_full_path()
743
+
744
+ # Determine if path is absolute (not constrained to workspace)
745
+ is_absolute = Path(full_path_str).is_absolute()
746
+
747
+ # If workspace_only is True and path is absolute, it's outside workspace
748
+ if request.workspace_only and is_absolute:
749
+ msg = f"Absolute path is outside workspace: {full_path_str}"
750
+ logger.error(msg)
751
+ return CreateFileResultFailure()
752
+
753
+ # Resolve path - if absolute, use as-is; if relative, align to workspace
754
+ if is_absolute:
755
+ file_path = Path(full_path_str).resolve()
756
+ else:
757
+ file_path = (self._get_workspace_path() / full_path_str).resolve()
758
+
759
+ # Check if it already exists - warn but treat as success
760
+ if file_path.exists():
761
+ msg = f"Path already exists: {file_path}"
762
+ logger.warning(msg)
763
+ return CreateFileResultSuccess(created_path=str(file_path))
764
+
765
+ # Create parent directories if needed
766
+ file_path.parent.mkdir(parents=True, exist_ok=True)
767
+
768
+ if request.is_directory:
769
+ file_path.mkdir()
770
+ logger.info("Created directory: %s", file_path)
771
+ # Create file with optional content
772
+ elif request.content is not None:
773
+ with file_path.open("w", encoding=request.encoding) as f:
774
+ f.write(request.content)
775
+ logger.info("Created file with content: %s", file_path)
776
+ else:
777
+ file_path.touch()
778
+ logger.info("Created empty file: %s", file_path)
779
+
780
+ return CreateFileResultSuccess(created_path=str(file_path))
781
+
782
+ except Exception as e:
783
+ path_info = request.get_full_path() if hasattr(request, "get_full_path") else str(request.path)
784
+ msg = f"Failed to create {'directory' if request.is_directory else 'file'} at {path_info}: {e}"
785
+ logger.error(msg)
786
+ return CreateFileResultFailure()
787
+
788
+ def on_rename_file_request(self, request: RenameFileRequest) -> ResultPayload:
789
+ """Handle a request to rename a file or directory."""
790
+ try:
791
+ # Resolve and validate old path
792
+ old_path = self._resolve_file_path(request.old_path, workspace_only=request.workspace_only is True)
793
+
794
+ # Resolve and validate new path
795
+ new_path = self._resolve_file_path(request.new_path, workspace_only=request.workspace_only is True)
796
+
797
+ # Check if old path exists
798
+ if not old_path.exists():
799
+ msg = f"Source path does not exist: {old_path}"
800
+ logger.error(msg)
801
+ return RenameFileResultFailure()
802
+
803
+ # Check if new path already exists
804
+ if new_path.exists():
805
+ msg = f"Destination path already exists: {new_path}"
806
+ logger.error(msg)
807
+ return RenameFileResultFailure()
808
+
809
+ # Check workspace constraints for both paths
810
+ is_old_in_workspace, _ = self._validate_workspace_path(old_path)
811
+ is_new_in_workspace, _ = self._validate_workspace_path(new_path)
812
+
813
+ if request.workspace_only and (not is_old_in_workspace or not is_new_in_workspace):
814
+ msg = f"One or both paths are outside workspace: {old_path} -> {new_path}"
815
+ logger.error(msg)
816
+ return RenameFileResultFailure()
817
+
818
+ # Create parent directories for new path if needed
819
+ new_path.parent.mkdir(parents=True, exist_ok=True)
820
+
821
+ # Perform the rename operation
822
+ old_path.rename(new_path)
823
+ logger.info("Renamed: %s -> %s", old_path, new_path)
824
+
825
+ return RenameFileResultSuccess(old_path=str(old_path), new_path=str(new_path))
826
+
827
+ except Exception as e:
828
+ msg = f"Failed to rename {request.old_path} to {request.new_path}: {e}"
829
+ logger.error(msg)
830
+ return RenameFileResultFailure()
File without changes
File without changes
@@ -33,6 +33,7 @@ class AppEvents(BaseModel):
33
33
  "SingleExecutionStepRequest",
34
34
  "SingleNodeStepRequest",
35
35
  "ContinueExecutionStepRequest",
36
+ "SetLockNodeStateRequest",
36
37
  ]
37
38
  )
38
39
 
@@ -95,3 +96,7 @@ class Settings(BaseModel):
95
96
  minimum_disk_space_gb_workflows: float = Field(
96
97
  default=1.0, description="Minimum disk space in GB required for saving workflows"
97
98
  )
99
+ synced_workflows_directory: str = Field(
100
+ default="synced_workflows",
101
+ description="Path to the synced workflows directory, relative to the workspace directory.",
102
+ )