griptape-nodes 0.44.0__py3-none-any.whl → 0.45.1__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.
- griptape_nodes/__init__.py +5 -1
- griptape_nodes/app/api.py +2 -35
- griptape_nodes/app/app.py +70 -3
- griptape_nodes/app/watch.py +5 -2
- griptape_nodes/drivers/storage/base_storage_driver.py +37 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +2 -1
- griptape_nodes/exe_types/core_types.py +109 -9
- griptape_nodes/exe_types/node_types.py +19 -5
- griptape_nodes/node_library/workflow_registry.py +29 -0
- griptape_nodes/retained_mode/events/app_events.py +3 -2
- griptape_nodes/retained_mode/events/base_events.py +9 -0
- griptape_nodes/retained_mode/events/sync_events.py +60 -0
- griptape_nodes/retained_mode/events/workflow_events.py +231 -0
- griptape_nodes/retained_mode/griptape_nodes.py +8 -0
- griptape_nodes/retained_mode/managers/library_manager.py +6 -18
- griptape_nodes/retained_mode/managers/node_manager.py +2 -2
- griptape_nodes/retained_mode/managers/operation_manager.py +7 -0
- griptape_nodes/retained_mode/managers/settings.py +5 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +498 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +682 -28
- griptape_nodes/retained_mode/retained_mode.py +23 -0
- griptape_nodes/updater/__init__.py +4 -2
- griptape_nodes/utils/uv_utils.py +18 -0
- {griptape_nodes-0.44.0.dist-info → griptape_nodes-0.45.1.dist-info}/METADATA +2 -1
- {griptape_nodes-0.44.0.dist-info → griptape_nodes-0.45.1.dist-info}/RECORD +27 -24
- {griptape_nodes-0.44.0.dist-info → griptape_nodes-0.45.1.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.44.0.dist-info → griptape_nodes-0.45.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal
|
|
2
3
|
|
|
3
4
|
from griptape_nodes.node_library.workflow_registry import WorkflowMetadata
|
|
4
5
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
@@ -364,3 +365,233 @@ class PublishWorkflowResultSuccess(ResultPayloadSuccess):
|
|
|
364
365
|
@PayloadRegistry.register
|
|
365
366
|
class PublishWorkflowResultFailure(ResultPayloadFailure):
|
|
366
367
|
"""Workflow publish failed. Common causes: workflow not found, publish error, file system error."""
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@dataclass
|
|
371
|
+
@PayloadRegistry.register
|
|
372
|
+
class BranchWorkflowRequest(RequestPayload):
|
|
373
|
+
"""Create a branch (copy) of an existing workflow with branch tracking.
|
|
374
|
+
|
|
375
|
+
Use when: Creating workflow variants, branching workflows for experimentation,
|
|
376
|
+
creating personal copies of shared workflows, preparing for workflow collaboration.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
workflow_name: Name of the workflow to branch
|
|
380
|
+
branched_workflow_name: Name for the branched workflow (None for auto-generated)
|
|
381
|
+
|
|
382
|
+
Results: BranchWorkflowResultSuccess (with branch name) | BranchWorkflowResultFailure (branch error)
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
workflow_name: str
|
|
386
|
+
branched_workflow_name: str | None = None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@dataclass
|
|
390
|
+
@PayloadRegistry.register
|
|
391
|
+
class BranchWorkflowResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
|
|
392
|
+
"""Workflow branched successfully.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
branched_workflow_name: Name of the created branch
|
|
396
|
+
original_workflow_name: Name of the original workflow
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
branched_workflow_name: str
|
|
400
|
+
original_workflow_name: str
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@dataclass
|
|
404
|
+
@PayloadRegistry.register
|
|
405
|
+
class BranchWorkflowResultFailure(ResultPayloadFailure):
|
|
406
|
+
"""Workflow branch failed. Common causes: workflow not found, name conflict, save error."""
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@dataclass
|
|
410
|
+
@PayloadRegistry.register
|
|
411
|
+
class MergeWorkflowBranchRequest(RequestPayload):
|
|
412
|
+
"""Merge a branch back into its source workflow, removing the branch when complete.
|
|
413
|
+
|
|
414
|
+
Use when: Integrating branch changes back into the original workflow, consolidating
|
|
415
|
+
successful branch experiments, applying approved branch modifications to source.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
workflow_name: Name of the branch workflow to merge back into its source
|
|
419
|
+
|
|
420
|
+
Results: MergeWorkflowBranchResultSuccess (with merge details) | MergeWorkflowBranchResultFailure (merge error)
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
workflow_name: str
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@dataclass
|
|
427
|
+
@PayloadRegistry.register
|
|
428
|
+
class MergeWorkflowBranchResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
|
|
429
|
+
"""Branch merge back to source completed successfully.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
merged_workflow_name: Name of the source workflow after merge
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
merged_workflow_name: str
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclass
|
|
439
|
+
@PayloadRegistry.register
|
|
440
|
+
class MergeWorkflowBranchResultFailure(ResultPayloadFailure):
|
|
441
|
+
"""Workflow branch merge failed."""
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass
|
|
445
|
+
@PayloadRegistry.register
|
|
446
|
+
class ResetWorkflowBranchRequest(RequestPayload):
|
|
447
|
+
"""Reset a branch to match its source workflow, discarding branch changes.
|
|
448
|
+
|
|
449
|
+
Use when: Discarding branch modifications, reverting branch to source state,
|
|
450
|
+
abandoning branch experiments, syncing branch with latest source changes.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
workflow_name: Name of the branch workflow to reset to its source
|
|
454
|
+
|
|
455
|
+
Results: ResetWorkflowBranchResultSuccess (with reset details) | ResetWorkflowBranchResultFailure (reset error)
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
workflow_name: str
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@dataclass
|
|
462
|
+
@PayloadRegistry.register
|
|
463
|
+
class ResetWorkflowBranchResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
|
|
464
|
+
"""Branch reset to source completed successfully.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
reset_workflow_name: Name of the branch workflow after reset
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
reset_workflow_name: str
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@dataclass
|
|
474
|
+
@PayloadRegistry.register
|
|
475
|
+
class ResetWorkflowBranchResultFailure(ResultPayloadFailure):
|
|
476
|
+
"""Workflow branch reset failed. Common causes: workflows not branch-related, reset conflict, save error."""
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@dataclass
|
|
480
|
+
@PayloadRegistry.register
|
|
481
|
+
class CompareWorkflowsRequest(RequestPayload):
|
|
482
|
+
"""Compare two workflows to determine if one is ahead, behind, or up-to-date relative to the other.
|
|
483
|
+
|
|
484
|
+
Use when: Checking if branched workflows need updates, determining if local changes exist,
|
|
485
|
+
managing workflow synchronization, preparing for merge operations.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
workflow_name: Name of the workflow to evaluate
|
|
489
|
+
compare_workflow_name: Name of the workflow to compare against
|
|
490
|
+
|
|
491
|
+
Results: CompareWorkflowsResultSuccess (with status details) | CompareWorkflowsResultFailure (evaluation error)
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
workflow_name: str
|
|
495
|
+
compare_workflow_name: str
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@dataclass
|
|
499
|
+
@PayloadRegistry.register
|
|
500
|
+
class CompareWorkflowsResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
501
|
+
"""Workflow comparison completed successfully.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
workflow_name: Name of the evaluated workflow
|
|
505
|
+
compare_workflow_name: Name of the workflow being compared against (if any)
|
|
506
|
+
status: Status relative to source - "up_to_date", "ahead", "behind", "diverged", or "no_source"
|
|
507
|
+
workflow_last_modified: Last modified timestamp of the workflow
|
|
508
|
+
source_last_modified: Last modified timestamp of the source (if exists)
|
|
509
|
+
details: Additional details about the comparison
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
workflow_name: str
|
|
513
|
+
compare_workflow_name: str | None
|
|
514
|
+
status: Literal["up_to_date", "ahead", "behind", "diverged", "no_source"]
|
|
515
|
+
workflow_last_modified: str | None
|
|
516
|
+
source_last_modified: str | None
|
|
517
|
+
details: str
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@dataclass
|
|
521
|
+
@PayloadRegistry.register
|
|
522
|
+
class CompareWorkflowsResultFailure(ResultPayloadFailure):
|
|
523
|
+
"""Workflow comparison failed. Common causes: workflow not found, source not accessible, comparison error."""
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@dataclass
|
|
527
|
+
@PayloadRegistry.register
|
|
528
|
+
class MoveWorkflowRequest(RequestPayload):
|
|
529
|
+
"""Move a workflow to a different directory in the workspace.
|
|
530
|
+
|
|
531
|
+
Use when: Organizing workflows into directories, restructuring workflow hierarchies,
|
|
532
|
+
moving workflows to categorized folders, cleaning up workspace organization.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
workflow_name: Name of the workflow to move
|
|
536
|
+
target_directory: Target directory path relative to workspace root
|
|
537
|
+
|
|
538
|
+
Results: MoveWorkflowResultSuccess (with new path) | MoveWorkflowResultFailure (move error)
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
workflow_name: str
|
|
542
|
+
target_directory: str
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@dataclass
|
|
546
|
+
@PayloadRegistry.register
|
|
547
|
+
class MoveWorkflowResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
|
|
548
|
+
"""Workflow moved successfully.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
moved_file_path: New file path after the move
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
moved_file_path: str
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@dataclass
|
|
558
|
+
@PayloadRegistry.register
|
|
559
|
+
class MoveWorkflowResultFailure(ResultPayloadFailure):
|
|
560
|
+
"""Workflow move failed. Common causes: workflow not found, invalid target directory, file system error."""
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@dataclass
|
|
564
|
+
@PayloadRegistry.register
|
|
565
|
+
class RegisterWorkflowsFromConfigRequest(RequestPayload):
|
|
566
|
+
"""Register workflows from configuration section.
|
|
567
|
+
|
|
568
|
+
Use when: Loading workflows from configuration after library initialization,
|
|
569
|
+
registering workflows from synced directories, batch workflow registration.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
config_section: Configuration section path containing workflow paths to register
|
|
573
|
+
|
|
574
|
+
Results: RegisterWorkflowsFromConfigResultSuccess (with count) | RegisterWorkflowsFromConfigResultFailure (registration error)
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
config_section: str
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@dataclass
|
|
581
|
+
@PayloadRegistry.register
|
|
582
|
+
class RegisterWorkflowsFromConfigResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
583
|
+
"""Workflows registered from configuration successfully.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
succeeded_workflows: List of workflow names that were successfully registered
|
|
587
|
+
failed_workflows: List of workflow names that failed to register
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
succeeded_workflows: list[str]
|
|
591
|
+
failed_workflows: list[str]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@dataclass
|
|
595
|
+
@PayloadRegistry.register
|
|
596
|
+
class RegisterWorkflowsFromConfigResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
597
|
+
"""Workflow registration from configuration failed. Common causes: configuration not found, invalid paths, registration errors."""
|
|
@@ -69,6 +69,7 @@ if TYPE_CHECKING:
|
|
|
69
69
|
from griptape_nodes.retained_mode.managers.static_files_manager import (
|
|
70
70
|
StaticFilesManager,
|
|
71
71
|
)
|
|
72
|
+
from griptape_nodes.retained_mode.managers.sync_manager import SyncManager
|
|
72
73
|
from griptape_nodes.retained_mode.managers.version_compatibility_manager import (
|
|
73
74
|
VersionCompatibilityManager,
|
|
74
75
|
)
|
|
@@ -138,6 +139,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
138
139
|
_version_compatibility_manager: VersionCompatibilityManager
|
|
139
140
|
_session_manager: SessionManager
|
|
140
141
|
_engine_identity_manager: EngineIdentityManager
|
|
142
|
+
_sync_manager: SyncManager
|
|
141
143
|
|
|
142
144
|
def __init__(self) -> None:
|
|
143
145
|
from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
|
|
@@ -161,6 +163,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
161
163
|
from griptape_nodes.retained_mode.managers.static_files_manager import (
|
|
162
164
|
StaticFilesManager,
|
|
163
165
|
)
|
|
166
|
+
from griptape_nodes.retained_mode.managers.sync_manager import SyncManager
|
|
164
167
|
from griptape_nodes.retained_mode.managers.version_compatibility_manager import (
|
|
165
168
|
VersionCompatibilityManager,
|
|
166
169
|
)
|
|
@@ -189,6 +192,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
189
192
|
self._version_compatibility_manager = VersionCompatibilityManager(self._event_manager)
|
|
190
193
|
self._session_manager = SessionManager(self._event_manager)
|
|
191
194
|
self._engine_identity_manager = EngineIdentityManager(self._event_manager)
|
|
195
|
+
self._sync_manager = SyncManager(self._event_manager, self._config_manager)
|
|
192
196
|
|
|
193
197
|
# Assign handlers now that these are created.
|
|
194
198
|
self._event_manager.assign_manager_to_request_type(
|
|
@@ -326,6 +330,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
326
330
|
def EngineIdentityManager(cls) -> EngineIdentityManager:
|
|
327
331
|
return GriptapeNodes.get_instance()._engine_identity_manager
|
|
328
332
|
|
|
333
|
+
@classmethod
|
|
334
|
+
def SyncManager(cls) -> SyncManager:
|
|
335
|
+
return GriptapeNodes.get_instance()._sync_manager
|
|
336
|
+
|
|
329
337
|
@classmethod
|
|
330
338
|
def clear_data(cls) -> None:
|
|
331
339
|
# Get canvas
|
|
@@ -13,7 +13,6 @@ from importlib.resources import files
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import TYPE_CHECKING, cast
|
|
15
15
|
|
|
16
|
-
import uv
|
|
17
16
|
from packaging.requirements import InvalidRequirement, Requirement
|
|
18
17
|
from pydantic import ValidationError
|
|
19
18
|
from rich.align import Align
|
|
@@ -96,6 +95,7 @@ from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.
|
|
|
96
95
|
)
|
|
97
96
|
from griptape_nodes.retained_mode.managers.library_lifecycle.library_status import LibraryStatus
|
|
98
97
|
from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
98
|
+
from griptape_nodes.utils.uv_utils import find_uv_bin
|
|
99
99
|
from griptape_nodes.utils.version_utils import get_complete_version_string
|
|
100
100
|
|
|
101
101
|
if TYPE_CHECKING:
|
|
@@ -110,21 +110,6 @@ logger = logging.getLogger("griptape_nodes")
|
|
|
110
110
|
console = Console()
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def _find_griptape_uv_bin() -> str:
|
|
114
|
-
"""Find the uv binary, checking dedicated Griptape installation first, then system uv.
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
Path to the uv binary to use
|
|
118
|
-
"""
|
|
119
|
-
# Check for dedicated Griptape uv installation first
|
|
120
|
-
dedicated_uv_path = xdg_data_home() / "griptape_nodes" / "bin" / "uv"
|
|
121
|
-
if dedicated_uv_path.exists():
|
|
122
|
-
return str(dedicated_uv_path)
|
|
123
|
-
|
|
124
|
-
# Fall back to system uv installation
|
|
125
|
-
return uv.find_uv_bin()
|
|
126
|
-
|
|
127
|
-
|
|
128
113
|
class LibraryManager:
|
|
129
114
|
SANDBOX_LIBRARY_NAME = "Sandbox Library"
|
|
130
115
|
|
|
@@ -960,10 +945,12 @@ class LibraryManager:
|
|
|
960
945
|
)
|
|
961
946
|
return RegisterLibraryFromRequirementSpecifierResultFailure()
|
|
962
947
|
|
|
948
|
+
uv_path = find_uv_bin()
|
|
949
|
+
|
|
963
950
|
logger.info("Installing dependency '%s' with pip in venv at %s", package_name, venv_path)
|
|
964
951
|
subprocess.run( # noqa: S603
|
|
965
952
|
[
|
|
966
|
-
|
|
953
|
+
uv_path,
|
|
967
954
|
"pip",
|
|
968
955
|
"install",
|
|
969
956
|
request.requirement_specifier,
|
|
@@ -1033,9 +1020,10 @@ class LibraryManager:
|
|
|
1033
1020
|
raise RuntimeError(error_message)
|
|
1034
1021
|
|
|
1035
1022
|
try:
|
|
1023
|
+
uv_path = find_uv_bin()
|
|
1036
1024
|
logger.info("Creating virtual environment at %s with Python %s", library_venv_path, python_version)
|
|
1037
1025
|
subprocess.run( # noqa: S603
|
|
1038
|
-
[
|
|
1026
|
+
[uv_path, "venv", str(library_venv_path), "--python", python_version],
|
|
1039
1027
|
check=True,
|
|
1040
1028
|
capture_output=True,
|
|
1041
1029
|
text=True,
|
|
@@ -1435,8 +1435,8 @@ class NodeManager:
|
|
|
1435
1435
|
result = SetParameterValueResultFailure()
|
|
1436
1436
|
return result
|
|
1437
1437
|
|
|
1438
|
-
# Validate that parameters can be set at all
|
|
1439
|
-
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:
|
|
1440
1440
|
details = f"Attempted to set parameter value for '{node_name}.{request.parameter_name}'. Failed because that Parameter was flagged as not settable."
|
|
1441
1441
|
logger.error(details)
|
|
1442
1442
|
result = SetParameterValueResultFailure()
|
|
@@ -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
|
|
|
@@ -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
|
+
)
|