ralphx 0.3.4__py3-none-any.whl → 0.4.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.
- ralphx/__init__.py +1 -1
- ralphx/adapters/base.py +10 -2
- ralphx/adapters/claude_cli.py +222 -82
- ralphx/api/routes/auth.py +780 -98
- ralphx/api/routes/config.py +3 -56
- ralphx/api/routes/export_import.py +6 -9
- ralphx/api/routes/loops.py +4 -4
- ralphx/api/routes/planning.py +882 -19
- ralphx/api/routes/resources.py +528 -6
- ralphx/api/routes/stream.py +58 -56
- ralphx/api/routes/templates.py +2 -2
- ralphx/api/routes/workflows.py +258 -47
- ralphx/cli.py +4 -1
- ralphx/core/auth.py +372 -172
- ralphx/core/database.py +588 -164
- ralphx/core/executor.py +170 -19
- ralphx/core/loop.py +15 -2
- ralphx/core/loop_templates.py +29 -3
- ralphx/core/planning_iteration_executor.py +633 -0
- ralphx/core/planning_service.py +119 -24
- ralphx/core/preview.py +9 -25
- ralphx/core/project_db.py +864 -121
- ralphx/core/project_export.py +1 -5
- ralphx/core/project_import.py +14 -29
- ralphx/core/resources.py +28 -2
- ralphx/core/sample_project.py +1 -5
- ralphx/core/templates.py +9 -9
- ralphx/core/workflow_executor.py +32 -3
- ralphx/core/workflow_export.py +4 -7
- ralphx/core/workflow_import.py +3 -27
- ralphx/mcp/__init__.py +6 -2
- ralphx/mcp/registry.py +3 -3
- ralphx/mcp/tools/diagnostics.py +1 -1
- ralphx/mcp/tools/monitoring.py +10 -16
- ralphx/mcp/tools/workflows.py +115 -33
- ralphx/mcp_server.py +6 -2
- ralphx/static/assets/index-BuLI7ffn.css +1 -0
- ralphx/static/assets/index-DWvlqOTb.js +264 -0
- ralphx/static/assets/index-DWvlqOTb.js.map +1 -0
- ralphx/static/index.html +2 -2
- ralphx/templates/loop_templates/consumer.md +2 -2
- {ralphx-0.3.4.dist-info → ralphx-0.4.0.dist-info}/METADATA +33 -12
- {ralphx-0.3.4.dist-info → ralphx-0.4.0.dist-info}/RECORD +45 -44
- ralphx/static/assets/index-CcRDyY3b.css +0 -1
- ralphx/static/assets/index-CcxfTosc.js +0 -251
- ralphx/static/assets/index-CcxfTosc.js.map +0 -1
- {ralphx-0.3.4.dist-info → ralphx-0.4.0.dist-info}/WHEEL +0 -0
- {ralphx-0.3.4.dist-info → ralphx-0.4.0.dist-info}/entry_points.txt +0 -0
ralphx/core/project_export.py
CHANGED
|
@@ -34,7 +34,6 @@ class WorkflowSummary:
|
|
|
34
34
|
"""Summary of a workflow in the project."""
|
|
35
35
|
id: str
|
|
36
36
|
name: str
|
|
37
|
-
namespace: str
|
|
38
37
|
steps_count: int
|
|
39
38
|
items_count: int
|
|
40
39
|
resources_count: int
|
|
@@ -137,7 +136,6 @@ class ProjectExporter:
|
|
|
137
136
|
summaries.append(WorkflowSummary(
|
|
138
137
|
id=wf['id'],
|
|
139
138
|
name=wf['name'],
|
|
140
|
-
namespace=wf['namespace'],
|
|
141
139
|
steps_count=wf_preview.steps_count,
|
|
142
140
|
items_count=wf_preview.items_total,
|
|
143
141
|
resources_count=wf_preview.resources_count,
|
|
@@ -231,7 +229,7 @@ class ProjectExporter:
|
|
|
231
229
|
)
|
|
232
230
|
|
|
233
231
|
for wf in workflows:
|
|
234
|
-
wf_prefix = f"workflows/{wf['
|
|
232
|
+
wf_prefix = f"workflows/{wf['id']}/"
|
|
235
233
|
|
|
236
234
|
# Get workflow data
|
|
237
235
|
steps = self.db.list_workflow_steps(wf['id'])
|
|
@@ -261,7 +259,6 @@ class ProjectExporter:
|
|
|
261
259
|
'id': wf['id'],
|
|
262
260
|
'template_id': wf.get('template_id'),
|
|
263
261
|
'name': wf['name'],
|
|
264
|
-
'namespace': wf['namespace'],
|
|
265
262
|
'status': 'draft',
|
|
266
263
|
'current_step': 1,
|
|
267
264
|
'created_at': wf.get('created_at'),
|
|
@@ -432,7 +429,6 @@ class ProjectExporter:
|
|
|
432
429
|
{
|
|
433
430
|
'id': w['id'],
|
|
434
431
|
'name': w['name'],
|
|
435
|
-
'namespace': w['namespace'],
|
|
436
432
|
}
|
|
437
433
|
for w in workflows
|
|
438
434
|
],
|
ralphx/core/project_import.py
CHANGED
|
@@ -30,7 +30,6 @@ class WorkflowPreviewInfo:
|
|
|
30
30
|
"""Preview info for a workflow in the project export."""
|
|
31
31
|
id: str
|
|
32
32
|
name: str
|
|
33
|
-
namespace: str
|
|
34
33
|
steps_count: int
|
|
35
34
|
items_count: int
|
|
36
35
|
resources_count: int
|
|
@@ -263,8 +262,13 @@ class ProjectImporter:
|
|
|
263
262
|
|
|
264
263
|
# Get workflow info from manifest
|
|
265
264
|
for wf_info in manifest.get('contents', {}).get('workflows', []):
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
wf_id = wf_info['id']
|
|
266
|
+
# Support both new (workflow_id) and old (namespace) path formats
|
|
267
|
+
wf_prefix = f"workflows/{wf_id}/"
|
|
268
|
+
# Check for old namespace-based paths for backward compatibility
|
|
269
|
+
old_namespace = wf_info.get('namespace')
|
|
270
|
+
if old_namespace and f"workflows/{old_namespace}/workflow.json" in zf.namelist():
|
|
271
|
+
wf_prefix = f"workflows/{old_namespace}/"
|
|
268
272
|
|
|
269
273
|
# Count items
|
|
270
274
|
items_count = 0
|
|
@@ -304,7 +308,6 @@ class ProjectImporter:
|
|
|
304
308
|
workflows_info.append(WorkflowPreviewInfo(
|
|
305
309
|
id=wf_info['id'],
|
|
306
310
|
name=wf_info['name'],
|
|
307
|
-
namespace=wf_namespace,
|
|
308
311
|
steps_count=steps_count,
|
|
309
312
|
items_count=items_count,
|
|
310
313
|
resources_count=resources_count,
|
|
@@ -354,7 +357,6 @@ class ProjectImporter:
|
|
|
354
357
|
workflows=[WorkflowPreviewInfo(
|
|
355
358
|
id=wf_info.get('id', ''),
|
|
356
359
|
name=wf_info.get('name', ''),
|
|
357
|
-
namespace=wf_info.get('namespace', ''),
|
|
358
360
|
steps_count=contents.get('steps', 0),
|
|
359
361
|
items_count=contents.get('items_total', 0),
|
|
360
362
|
resources_count=contents.get('resources', 0),
|
|
@@ -484,8 +486,13 @@ class ProjectImporter:
|
|
|
484
486
|
import hashlib
|
|
485
487
|
import uuid
|
|
486
488
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
+
wf_id = wf_info['id']
|
|
490
|
+
# Support both new (workflow_id) and old (namespace) path formats
|
|
491
|
+
wf_prefix = f"workflows/{wf_id}/"
|
|
492
|
+
# Check for old namespace-based paths for backward compatibility
|
|
493
|
+
old_namespace = wf_info.get('namespace')
|
|
494
|
+
if old_namespace and f"workflows/{old_namespace}/workflow.json" in zf.namelist():
|
|
495
|
+
wf_prefix = f"workflows/{old_namespace}/"
|
|
489
496
|
|
|
490
497
|
# Read workflow data
|
|
491
498
|
workflow_data = json.loads(zf.read(f"{wf_prefix}workflow.json").decode('utf-8'))
|
|
@@ -522,15 +529,10 @@ class ProjectImporter:
|
|
|
522
529
|
hash_suffix = hashlib.md5(f"{old_id}-{uuid.uuid4().hex}".encode()).hexdigest()[:8]
|
|
523
530
|
id_mapping[old_id] = f"{old_id}-{hash_suffix}"
|
|
524
531
|
|
|
525
|
-
# Generate unique namespace
|
|
526
|
-
base_namespace = workflow_data['workflow']['namespace']
|
|
527
|
-
namespace = self._generate_unique_namespace(base_namespace)
|
|
528
|
-
|
|
529
532
|
# Create workflow
|
|
530
533
|
workflow = self.db.create_workflow(
|
|
531
534
|
id=new_wf_id,
|
|
532
535
|
name=workflow_data['workflow']['name'],
|
|
533
|
-
namespace=namespace,
|
|
534
536
|
template_id=workflow_data['workflow'].get('template_id'),
|
|
535
537
|
status='draft',
|
|
536
538
|
)
|
|
@@ -666,20 +668,3 @@ class ProjectImporter:
|
|
|
666
668
|
id_mapping=id_mapping,
|
|
667
669
|
warnings=warnings,
|
|
668
670
|
)
|
|
669
|
-
|
|
670
|
-
def _generate_unique_namespace(self, base_namespace: str) -> str:
|
|
671
|
-
"""Generate a unique namespace."""
|
|
672
|
-
import uuid
|
|
673
|
-
|
|
674
|
-
existing_workflows = self.db.list_workflows()
|
|
675
|
-
existing_namespaces = {w['namespace'] for w in existing_workflows}
|
|
676
|
-
|
|
677
|
-
if base_namespace not in existing_namespaces:
|
|
678
|
-
return base_namespace
|
|
679
|
-
|
|
680
|
-
for i in range(1, 1000):
|
|
681
|
-
candidate = f"{base_namespace[:56]}-{i}"
|
|
682
|
-
if candidate not in existing_namespaces:
|
|
683
|
-
return candidate
|
|
684
|
-
|
|
685
|
-
return f"{base_namespace[:50]}-{uuid.uuid4().hex[:8]}"
|
ralphx/core/resources.py
CHANGED
|
@@ -225,6 +225,12 @@ class ResourceManager:
|
|
|
225
225
|
"""
|
|
226
226
|
file_path = self._resources_path / resource_data["file_path"]
|
|
227
227
|
|
|
228
|
+
# Security: verify path stays within resources directory
|
|
229
|
+
resolved = file_path.resolve()
|
|
230
|
+
resources_root = self._resources_path.resolve()
|
|
231
|
+
if not str(resolved).startswith(str(resources_root) + "/") and resolved != resources_root:
|
|
232
|
+
return None
|
|
233
|
+
|
|
228
234
|
if not file_path.exists():
|
|
229
235
|
return None
|
|
230
236
|
|
|
@@ -426,8 +432,19 @@ class ResourceManager:
|
|
|
426
432
|
else:
|
|
427
433
|
file_name = name
|
|
428
434
|
|
|
435
|
+
# Security: prevent path traversal in resource names
|
|
436
|
+
# Only allow simple filenames (alphanumeric, hyphens, underscores, dots)
|
|
437
|
+
if ".." in file_name or "/" in file_name or "\\" in file_name or "\0" in file_name:
|
|
438
|
+
raise ValueError(f"Invalid resource name: {name} (path traversal characters not allowed)")
|
|
439
|
+
|
|
429
440
|
file_path = self.get_resources_path(resource_type) / f"{file_name}.md"
|
|
430
441
|
|
|
442
|
+
# Verify resolved path stays within resources directory
|
|
443
|
+
resolved = file_path.resolve()
|
|
444
|
+
resources_root = self.get_resources_path(resource_type).resolve()
|
|
445
|
+
if not str(resolved).startswith(str(resources_root) + "/") and resolved != resources_root:
|
|
446
|
+
raise ValueError(f"Invalid resource name: {name} (resolves outside resources directory)")
|
|
447
|
+
|
|
431
448
|
# Ensure directory exists
|
|
432
449
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
433
450
|
|
|
@@ -483,6 +500,11 @@ class ResourceManager:
|
|
|
483
500
|
# Update file content if provided
|
|
484
501
|
if content is not None:
|
|
485
502
|
file_path = self._resources_path / resource["file_path"]
|
|
503
|
+
# Security: verify path stays within resources directory
|
|
504
|
+
resolved = file_path.resolve()
|
|
505
|
+
resources_root = self._resources_path.resolve()
|
|
506
|
+
if not str(resolved).startswith(str(resources_root) + "/") and resolved != resources_root:
|
|
507
|
+
return False
|
|
486
508
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
487
509
|
file_path.write_text(content, encoding="utf-8")
|
|
488
510
|
|
|
@@ -519,8 +541,12 @@ class ResourceManager:
|
|
|
519
541
|
# Delete file if requested
|
|
520
542
|
if delete_file:
|
|
521
543
|
file_path = self._resources_path / resource["file_path"]
|
|
522
|
-
|
|
523
|
-
|
|
544
|
+
# Security: verify path stays within resources directory
|
|
545
|
+
resolved = file_path.resolve()
|
|
546
|
+
resources_root = self._resources_path.resolve()
|
|
547
|
+
if str(resolved).startswith(str(resources_root) + "/") or resolved == resources_root:
|
|
548
|
+
if file_path.exists():
|
|
549
|
+
file_path.unlink()
|
|
524
550
|
|
|
525
551
|
# Delete database entry
|
|
526
552
|
return self.db.delete_resource(resource_id)
|
ralphx/core/sample_project.py
CHANGED
|
@@ -124,14 +124,10 @@ def _create_workflow_with_stories(
|
|
|
124
124
|
logger.warning(f"Template '{template_id}' not found")
|
|
125
125
|
return None
|
|
126
126
|
|
|
127
|
-
#
|
|
128
|
-
namespace = f"excuse-gen-{uuid.uuid4().hex[:7]}"
|
|
129
|
-
|
|
130
|
-
# Create workflow
|
|
127
|
+
# Create workflow (namespace parameter removed in schema v16)
|
|
131
128
|
workflow = project_db.create_workflow(
|
|
132
129
|
id=workflow_id,
|
|
133
130
|
name="Build Excuse Generator",
|
|
134
|
-
namespace=namespace,
|
|
135
131
|
template_id=template_id,
|
|
136
132
|
status="draft",
|
|
137
133
|
)
|
ralphx/core/templates.py
CHANGED
|
@@ -9,15 +9,15 @@ from typing import Optional
|
|
|
9
9
|
|
|
10
10
|
# Base loop templates
|
|
11
11
|
TEMPLATES: dict[str, dict] = {
|
|
12
|
-
"
|
|
13
|
-
"name": "
|
|
14
|
-
"display_name": "
|
|
12
|
+
"extractgen_requirements": {
|
|
13
|
+
"name": "extractgen_requirements",
|
|
14
|
+
"display_name": "Extract Requirements Loop",
|
|
15
15
|
"description": "Discover and document user stories from design documents or web research",
|
|
16
16
|
"type": "generator",
|
|
17
17
|
"category": "discovery",
|
|
18
18
|
"config": {
|
|
19
|
-
"name": "
|
|
20
|
-
"display_name": "
|
|
19
|
+
"name": "extractgen_requirements",
|
|
20
|
+
"display_name": "Extract Requirements Loop",
|
|
21
21
|
"type": "generator",
|
|
22
22
|
"description": "Discover and document user stories from design documents",
|
|
23
23
|
"item_types": {
|
|
@@ -34,7 +34,7 @@ TEMPLATES: dict[str, dict] = {
|
|
|
34
34
|
"model": "claude-sonnet-4-20250514",
|
|
35
35
|
"timeout": 180,
|
|
36
36
|
"tools": [],
|
|
37
|
-
"prompt_template": "prompts/
|
|
37
|
+
"prompt_template": "prompts/extractgen_requirements_turbo.md",
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"name": "deep",
|
|
@@ -42,7 +42,7 @@ TEMPLATES: dict[str, dict] = {
|
|
|
42
42
|
"model": "claude-sonnet-4-20250514",
|
|
43
43
|
"timeout": 900,
|
|
44
44
|
"tools": ["web_search"],
|
|
45
|
-
"prompt_template": "prompts/
|
|
45
|
+
"prompt_template": "prompts/extractgen_requirements_deep.md",
|
|
46
46
|
},
|
|
47
47
|
],
|
|
48
48
|
"mode_selection": {
|
|
@@ -109,7 +109,7 @@ TEMPLATES: dict[str, dict] = {
|
|
|
109
109
|
"input": {
|
|
110
110
|
"singular": "story",
|
|
111
111
|
"plural": "stories",
|
|
112
|
-
"source": "
|
|
112
|
+
"source": "extractgen_requirements",
|
|
113
113
|
"description": "Stories to implement",
|
|
114
114
|
},
|
|
115
115
|
"output": {
|
|
@@ -219,7 +219,7 @@ def get_template(name: str) -> Optional[dict]:
|
|
|
219
219
|
"""Get a template by name.
|
|
220
220
|
|
|
221
221
|
Args:
|
|
222
|
-
name: Template name (e.g., '
|
|
222
|
+
name: Template name (e.g., 'extractgen_requirements', 'implementation')
|
|
223
223
|
|
|
224
224
|
Returns:
|
|
225
225
|
Template dict or None if not found
|
ralphx/core/workflow_executor.py
CHANGED
|
@@ -6,10 +6,13 @@ step transitions.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
+
import logging
|
|
9
10
|
import uuid
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any, Callable, Optional
|
|
12
13
|
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
13
16
|
from ralphx.core.executor import LoopExecutor
|
|
14
17
|
from ralphx.core.loop import LoopLoader
|
|
15
18
|
from ralphx.core.project import Project
|
|
@@ -50,6 +53,7 @@ class WorkflowExecutor:
|
|
|
50
53
|
self.workflow_id = workflow_id
|
|
51
54
|
self._on_step_change = on_step_change
|
|
52
55
|
self._running_executors: dict[int, LoopExecutor] = {}
|
|
56
|
+
self._running_tasks: dict[int, asyncio.Task] = {}
|
|
53
57
|
|
|
54
58
|
def get_workflow(self) -> Optional[dict]:
|
|
55
59
|
"""Get the current workflow."""
|
|
@@ -161,6 +165,9 @@ class WorkflowExecutor:
|
|
|
161
165
|
# Create new loop for this step
|
|
162
166
|
loop_config = self._create_step_loop(step, loop_name, loop_type)
|
|
163
167
|
|
|
168
|
+
# Save loop_name to the step record so the UI can find logs
|
|
169
|
+
self.db.update_workflow_step(step_id, loop_name=loop_name)
|
|
170
|
+
|
|
164
171
|
if not loop_config:
|
|
165
172
|
raise ValueError(f"Failed to create loop config for step {step_id}")
|
|
166
173
|
|
|
@@ -188,6 +195,9 @@ class WorkflowExecutor:
|
|
|
188
195
|
# Check if architecture-first mode is enabled
|
|
189
196
|
architecture_first = step_config.get("architecture_first", False)
|
|
190
197
|
|
|
198
|
+
# Get cross-step context links (for generator loops)
|
|
199
|
+
context_from_steps = step_config.get("context_from_steps") or []
|
|
200
|
+
|
|
191
201
|
# Create and start executor
|
|
192
202
|
executor = LoopExecutor(
|
|
193
203
|
project=self.project,
|
|
@@ -197,12 +207,14 @@ class WorkflowExecutor:
|
|
|
197
207
|
step_id=step_id,
|
|
198
208
|
consume_from_step_id=consume_from_step_id,
|
|
199
209
|
architecture_first=architecture_first,
|
|
210
|
+
context_from_steps=context_from_steps,
|
|
200
211
|
)
|
|
201
212
|
|
|
202
213
|
self._running_executors[step_id] = executor
|
|
203
214
|
|
|
204
|
-
# Run executor in background
|
|
205
|
-
asyncio.create_task(self._run_loop_and_advance(executor, step))
|
|
215
|
+
# Run executor in background (store task reference to prevent GC and enable cancellation)
|
|
216
|
+
task = asyncio.create_task(self._run_loop_and_advance(executor, step))
|
|
217
|
+
self._running_tasks[step_id] = task
|
|
206
218
|
|
|
207
219
|
async def _run_loop_and_advance(
|
|
208
220
|
self, executor: LoopExecutor, step: dict
|
|
@@ -241,8 +253,25 @@ class WorkflowExecutor:
|
|
|
241
253
|
if auto_advance:
|
|
242
254
|
await self.complete_step(step_id)
|
|
243
255
|
|
|
256
|
+
except Exception:
|
|
257
|
+
_logger.exception(
|
|
258
|
+
"Unhandled exception in background loop execution for step %s",
|
|
259
|
+
step_id,
|
|
260
|
+
)
|
|
261
|
+
# Mark step as failed so the error is visible in the UI
|
|
262
|
+
try:
|
|
263
|
+
self.db.update_workflow_step(
|
|
264
|
+
step_id,
|
|
265
|
+
status="error",
|
|
266
|
+
artifacts={"error": "Unexpected executor crash - check logs"},
|
|
267
|
+
)
|
|
268
|
+
self._emit_step_change(step["step_number"], "error")
|
|
269
|
+
except Exception:
|
|
270
|
+
_logger.exception("Failed to mark step %s as errored", step_id)
|
|
271
|
+
|
|
244
272
|
finally:
|
|
245
273
|
self._running_executors.pop(step_id, None)
|
|
274
|
+
self._running_tasks.pop(step_id, None)
|
|
246
275
|
|
|
247
276
|
def _create_step_loop(
|
|
248
277
|
self, step: dict, loop_name: str, loop_type: str
|
|
@@ -610,7 +639,7 @@ class WorkflowExecutor:
|
|
|
610
639
|
|
|
611
640
|
# Stop running executors
|
|
612
641
|
for executor in self._running_executors.values():
|
|
613
|
-
executor.stop()
|
|
642
|
+
await executor.stop()
|
|
614
643
|
|
|
615
644
|
self.db.update_workflow(self.workflow_id, status="paused")
|
|
616
645
|
return self.get_workflow()
|
ralphx/core/workflow_export.py
CHANGED
|
@@ -63,7 +63,7 @@ class SecretMatch:
|
|
|
63
63
|
class ExportPreview:
|
|
64
64
|
"""Preview of what will be exported."""
|
|
65
65
|
workflow_name: str
|
|
66
|
-
|
|
66
|
+
workflow_id: str
|
|
67
67
|
steps_count: int
|
|
68
68
|
items_total: int
|
|
69
69
|
items_by_step: dict[int, int] # step_id -> count
|
|
@@ -155,7 +155,7 @@ class WorkflowExporter:
|
|
|
155
155
|
|
|
156
156
|
return ExportPreview(
|
|
157
157
|
workflow_name=workflow['name'],
|
|
158
|
-
|
|
158
|
+
workflow_id=workflow['id'],
|
|
159
159
|
steps_count=len(steps),
|
|
160
160
|
items_total=total_items, # Show real count, not truncated count
|
|
161
161
|
items_by_step=items_by_step,
|
|
@@ -247,8 +247,7 @@ class WorkflowExporter:
|
|
|
247
247
|
|
|
248
248
|
# Generate filename
|
|
249
249
|
timestamp = datetime.utcnow().strftime('%Y%m%d-%H%M%S')
|
|
250
|
-
|
|
251
|
-
filename = f"workflow-{namespace}-{timestamp}.ralphx.zip"
|
|
250
|
+
filename = f"workflow-{workflow['id']}-{timestamp}.ralphx.zip"
|
|
252
251
|
|
|
253
252
|
zip_bytes = zip_buffer.getvalue()
|
|
254
253
|
|
|
@@ -342,7 +341,7 @@ class WorkflowExporter:
|
|
|
342
341
|
snippet=redacted[:50] + '...' if len(redacted) > 50 else redacted,
|
|
343
342
|
))
|
|
344
343
|
|
|
345
|
-
# Scan workflow name
|
|
344
|
+
# Scan workflow name (unlikely but check)
|
|
346
345
|
scan_text(workflow.get('name', ''), 'workflow.name')
|
|
347
346
|
|
|
348
347
|
# Scan resources
|
|
@@ -383,7 +382,6 @@ class WorkflowExporter:
|
|
|
383
382
|
'workflow': {
|
|
384
383
|
'id': workflow['id'],
|
|
385
384
|
'name': workflow['name'],
|
|
386
|
-
'namespace': workflow['namespace'],
|
|
387
385
|
'template_id': workflow.get('template_id'),
|
|
388
386
|
},
|
|
389
387
|
'contents': {
|
|
@@ -447,7 +445,6 @@ class WorkflowExporter:
|
|
|
447
445
|
'id': workflow['id'],
|
|
448
446
|
'template_id': workflow.get('template_id'),
|
|
449
447
|
'name': workflow['name'],
|
|
450
|
-
'namespace': workflow['namespace'],
|
|
451
448
|
'status': 'draft', # Reset status on export
|
|
452
449
|
'current_step': 1, # Reset to beginning
|
|
453
450
|
'created_at': workflow.get('created_at'),
|
ralphx/core/workflow_import.py
CHANGED
|
@@ -16,7 +16,7 @@ from enum import Enum
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Optional
|
|
18
18
|
|
|
19
|
-
from ralphx.core.project_db import PROJECT_SCHEMA_VERSION, ProjectDatabase
|
|
19
|
+
from ralphx.core.project_db import PROJECT_SCHEMA_VERSION, ProjectDatabase
|
|
20
20
|
from ralphx.core.workflow_export import EXPORT_FORMAT_NAME, EXPORT_FORMAT_VERSION
|
|
21
21
|
|
|
22
22
|
|
|
@@ -106,7 +106,7 @@ class ImportPreview:
|
|
|
106
106
|
"""Preview of what will be imported."""
|
|
107
107
|
# Basic info
|
|
108
108
|
workflow_name: str
|
|
109
|
-
|
|
109
|
+
workflow_id: str
|
|
110
110
|
exported_at: str
|
|
111
111
|
ralphx_version: str
|
|
112
112
|
schema_version: int
|
|
@@ -254,7 +254,7 @@ class WorkflowImporter:
|
|
|
254
254
|
|
|
255
255
|
return ImportPreview(
|
|
256
256
|
workflow_name=manifest['workflow']['name'],
|
|
257
|
-
|
|
257
|
+
workflow_id=manifest['workflow']['id'],
|
|
258
258
|
exported_at=manifest['exported_at'],
|
|
259
259
|
ralphx_version=manifest.get('ralphx_version', 'unknown'),
|
|
260
260
|
schema_version=manifest.get('schema_version', 0),
|
|
@@ -736,10 +736,6 @@ class WorkflowImporter:
|
|
|
736
736
|
old_wf_id = workflow_data['workflow']['id']
|
|
737
737
|
new_wf_id = id_mapping[old_wf_id]
|
|
738
738
|
|
|
739
|
-
# Generate new namespace (ensure unique)
|
|
740
|
-
base_namespace = workflow_data['workflow']['namespace']
|
|
741
|
-
namespace = self._generate_unique_namespace(base_namespace)
|
|
742
|
-
|
|
743
739
|
# Update items with new IDs
|
|
744
740
|
updated_items = self._update_references(items_data, id_mapping)
|
|
745
741
|
|
|
@@ -747,7 +743,6 @@ class WorkflowImporter:
|
|
|
747
743
|
workflow = self.db.create_workflow(
|
|
748
744
|
id=new_wf_id,
|
|
749
745
|
name=workflow_data['workflow']['name'],
|
|
750
|
-
namespace=namespace,
|
|
751
746
|
template_id=workflow_data['workflow'].get('template_id'),
|
|
752
747
|
status='draft',
|
|
753
748
|
)
|
|
@@ -905,7 +900,6 @@ class WorkflowImporter:
|
|
|
905
900
|
original_metadata = {
|
|
906
901
|
'imported_from': {
|
|
907
902
|
'original_workflow_id': old_wf_id,
|
|
908
|
-
'original_namespace': workflow_data['workflow']['namespace'],
|
|
909
903
|
'export_timestamp': manifest.get('exported_at'),
|
|
910
904
|
'export_version': manifest.get('ralphx_version'),
|
|
911
905
|
},
|
|
@@ -1153,21 +1147,3 @@ class WorkflowImporter:
|
|
|
1153
1147
|
id_mapping=step_id_mapping,
|
|
1154
1148
|
warnings=warnings,
|
|
1155
1149
|
)
|
|
1156
|
-
|
|
1157
|
-
def _generate_unique_namespace(self, base_namespace: str) -> str:
|
|
1158
|
-
"""Generate a unique namespace based on the base."""
|
|
1159
|
-
# Check if base namespace exists
|
|
1160
|
-
existing_workflows = self.db.list_workflows()
|
|
1161
|
-
existing_namespaces = {w['namespace'] for w in existing_workflows}
|
|
1162
|
-
|
|
1163
|
-
if base_namespace not in existing_namespaces:
|
|
1164
|
-
return base_namespace
|
|
1165
|
-
|
|
1166
|
-
# Add suffix until unique
|
|
1167
|
-
for i in range(1, 1000):
|
|
1168
|
-
candidate = f"{base_namespace[:56]}-{i}"
|
|
1169
|
-
if candidate not in existing_namespaces:
|
|
1170
|
-
return candidate
|
|
1171
|
-
|
|
1172
|
-
# Fallback with uuid
|
|
1173
|
-
return f"{base_namespace[:50]}-{uuid.uuid4().hex[:8]}"
|
ralphx/mcp/__init__.py
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
This module provides a modular MCP server implementation that exposes RalphX
|
|
4
4
|
functionality as tools that Claude Code can use.
|
|
5
5
|
|
|
6
|
-
Usage:
|
|
7
|
-
claude mcp add ralphx -- ralphx mcp
|
|
6
|
+
Usage (Linux/Mac):
|
|
7
|
+
claude mcp add ralphx -e PYTHONDONTWRITEBYTECODE=1 -- "$(which ralphx)" mcp
|
|
8
|
+
# Mac zsh: if "which" fails, run: conda init zsh && source ~/.zshrc
|
|
9
|
+
|
|
10
|
+
Usage (Windows - find path first with: where.exe ralphx):
|
|
11
|
+
claude mcp add ralphx -e PYTHONDONTWRITEBYTECODE=1 -- C:\\path\\to\\ralphx.exe mcp
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
from ralphx.mcp.server import MCPServer
|
ralphx/mcp/registry.py
CHANGED
|
@@ -39,11 +39,11 @@ class ToolRegistry:
|
|
|
39
39
|
"""Check if a tool is registered."""
|
|
40
40
|
return name in self._tools
|
|
41
41
|
|
|
42
|
-
def call(self,
|
|
42
|
+
def call(self, tool_name: str, **kwargs) -> Any:
|
|
43
43
|
"""Call a tool by name with arguments."""
|
|
44
|
-
tool = self._tools.get(
|
|
44
|
+
tool = self._tools.get(tool_name)
|
|
45
45
|
if not tool:
|
|
46
|
-
raise KeyError(f"Unknown tool: {
|
|
46
|
+
raise KeyError(f"Unknown tool: {tool_name}")
|
|
47
47
|
return tool.handler(**kwargs)
|
|
48
48
|
|
|
49
49
|
def get_definitions(self) -> list[dict]:
|
ralphx/mcp/tools/diagnostics.py
CHANGED
|
@@ -243,7 +243,7 @@ def get_stop_reason(
|
|
|
243
243
|
if sessions:
|
|
244
244
|
last_session = sessions[-1]
|
|
245
245
|
events = project_db.get_session_events(
|
|
246
|
-
session_id=last_session
|
|
246
|
+
session_id=last_session.get("session_id", last_session.get("id")),
|
|
247
247
|
event_type="error",
|
|
248
248
|
limit=5,
|
|
249
249
|
)
|
ralphx/mcp/tools/monitoring.py
CHANGED
|
@@ -73,8 +73,8 @@ def list_runs(
|
|
|
73
73
|
"workflow_id": r.get("workflow_id"),
|
|
74
74
|
"step_id": r.get("step_id"),
|
|
75
75
|
"status": r["status"],
|
|
76
|
-
"
|
|
77
|
-
"
|
|
76
|
+
"iterations_completed": r.get("iterations_completed", 0),
|
|
77
|
+
"items_generated": r.get("items_generated", 0),
|
|
78
78
|
"executor_pid": r.get("executor_pid"),
|
|
79
79
|
"started_at": r.get("started_at"),
|
|
80
80
|
"completed_at": r.get("completed_at"),
|
|
@@ -117,8 +117,8 @@ def get_run(
|
|
|
117
117
|
"workflow_id": run.get("workflow_id"),
|
|
118
118
|
"step_id": run.get("step_id"),
|
|
119
119
|
"status": run["status"],
|
|
120
|
-
"
|
|
121
|
-
"
|
|
120
|
+
"iterations_completed": run.get("iterations_completed", 0),
|
|
121
|
+
"items_generated": run.get("items_generated", 0),
|
|
122
122
|
"executor_pid": run.get("executor_pid"),
|
|
123
123
|
"started_at": run.get("started_at"),
|
|
124
124
|
"completed_at": run.get("completed_at"),
|
|
@@ -130,11 +130,11 @@ def get_run(
|
|
|
130
130
|
sessions = project_db.list_sessions(run_id=run_id)
|
|
131
131
|
result["sessions"] = [
|
|
132
132
|
{
|
|
133
|
-
"id": s
|
|
133
|
+
"id": s.get("session_id", s.get("id")),
|
|
134
|
+
"iteration": s.get("iteration"),
|
|
134
135
|
"status": s.get("status"),
|
|
135
136
|
"started_at": s.get("started_at"),
|
|
136
|
-
"
|
|
137
|
-
"event_count": s.get("event_count", 0),
|
|
137
|
+
"duration_seconds": s.get("duration_seconds"),
|
|
138
138
|
}
|
|
139
139
|
for s in sessions
|
|
140
140
|
]
|
|
@@ -146,7 +146,6 @@ def get_run(
|
|
|
146
146
|
def get_logs(
|
|
147
147
|
slug: str,
|
|
148
148
|
level: Optional[str] = None,
|
|
149
|
-
category: Optional[str] = None,
|
|
150
149
|
run_id: Optional[str] = None,
|
|
151
150
|
session_id: Optional[str] = None,
|
|
152
151
|
search: Optional[str] = None,
|
|
@@ -170,7 +169,6 @@ def get_logs(
|
|
|
170
169
|
try:
|
|
171
170
|
logs, total = project_db.get_logs(
|
|
172
171
|
level=level,
|
|
173
|
-
category=category,
|
|
174
172
|
run_id=run_id,
|
|
175
173
|
session_id=session_id,
|
|
176
174
|
search=search,
|
|
@@ -190,10 +188,8 @@ def get_logs(
|
|
|
190
188
|
"id": log.get("id"),
|
|
191
189
|
"timestamp": log.get("timestamp"),
|
|
192
190
|
"level": log.get("level"),
|
|
193
|
-
"category": log.get("category"),
|
|
194
191
|
"message": scrub_sensitive_data(log.get("message", "")),
|
|
195
192
|
"run_id": log.get("run_id"),
|
|
196
|
-
"session_id": log.get("session_id"),
|
|
197
193
|
}
|
|
198
194
|
for log in logs
|
|
199
195
|
],
|
|
@@ -311,13 +307,12 @@ def list_sessions(
|
|
|
311
307
|
return PaginatedResult(
|
|
312
308
|
items=[
|
|
313
309
|
{
|
|
314
|
-
"id": s
|
|
310
|
+
"id": s.get("session_id", s.get("id")),
|
|
315
311
|
"run_id": s.get("run_id"),
|
|
312
|
+
"iteration": s.get("iteration"),
|
|
316
313
|
"status": s.get("status"),
|
|
317
314
|
"started_at": s.get("started_at"),
|
|
318
|
-
"
|
|
319
|
-
"event_count": s.get("event_count", 0),
|
|
320
|
-
"item_id": s.get("item_id"),
|
|
315
|
+
"duration_seconds": s.get("duration_seconds"),
|
|
321
316
|
}
|
|
322
317
|
for s in paginated
|
|
323
318
|
],
|
|
@@ -432,7 +427,6 @@ def get_monitoring_tools() -> list[ToolDefinition]:
|
|
|
432
427
|
properties={
|
|
433
428
|
"slug": prop_string("Project slug"),
|
|
434
429
|
"level": prop_enum("Filter by log level", ["debug", "info", "warning", "error"]),
|
|
435
|
-
"category": prop_string("Filter by category"),
|
|
436
430
|
"run_id": prop_string("Filter by run ID"),
|
|
437
431
|
"session_id": prop_string("Filter by session ID"),
|
|
438
432
|
"search": prop_string("Search text in messages"),
|