diff --git a/workflow/src/executor/dag-executor.ts b/workflow/core/executor/dag-executor.ts similarity index 100% rename from workflow/src/executor/dag-executor.ts rename to workflow/core/executor/dag-executor.ts diff --git a/workflow/src/index.ts b/workflow/core/index.ts similarity index 100% rename from workflow/src/index.ts rename to workflow/core/index.ts diff --git a/workflow/src/registry/node-executor-registry.ts b/workflow/core/registry/node-executor-registry.ts similarity index 100% rename from workflow/src/registry/node-executor-registry.ts rename to workflow/core/registry/node-executor-registry.ts diff --git a/workflow/src/types.ts b/workflow/core/types.ts similarity index 100% rename from workflow/src/types.ts rename to workflow/core/types.ts diff --git a/workflow/src/utils/priority-queue.ts b/workflow/core/utils/priority-queue.ts similarity index 100% rename from workflow/src/utils/priority-queue.ts rename to workflow/core/utils/priority-queue.ts diff --git a/workflow/src/utils/template-engine.ts b/workflow/core/utils/template-engine.ts similarity index 100% rename from workflow/src/utils/template-engine.ts rename to workflow/core/utils/template-engine.ts diff --git a/workflow/executor/README.md b/workflow/executor/README.md new file mode 100644 index 000000000..42ed68f26 --- /dev/null +++ b/workflow/executor/README.md @@ -0,0 +1,66 @@ +# Workflow Executor Runtimes + +This folder contains language-specific runtime executors for the workflow engine. + +## Structure + +``` +executor/ +├── cpp/ # C++ runtime (high-performance) +├── python/ # Python runtime (AI/ML capabilities) +└── ts/ # TypeScript runtime (default, orchestration) +``` + +## Purpose + +Each runtime provides the execution environment for plugins written in that language: + +### TypeScript Runtime (`ts/`) +- Default runtime for orchestration +- Direct JavaScript/TypeScript execution +- Full type safety +- Fastest startup time + +### Python Runtime (`python/`) +- Child process execution +- AI/ML library access (TensorFlow, PyTorch, transformers) +- Data science capabilities (pandas, numpy) +- NLP processing (spaCy, NLTK) + +### C++ Runtime (`cpp/`) +- Native FFI bindings +- 100-1000x faster than TypeScript +- Low memory footprint +- Ideal for bulk data processing + +## How It Works + +``` +┌─────────────────────────────────────────┐ +│ DAGExecutor (TypeScript Core) │ +│ - Orchestrates workflow execution │ +│ - Resolves dependencies │ +│ - Manages execution state │ +└─────────────────┬───────────────────────┘ + │ + ┌─────────┼─────────┐ + │ │ │ + ↓ ↓ ↓ + ┌────────┬────────┬────────┐ + │ TS │ C++ │ Python │ + │Runtime │Runtime │Runtime │ + └────────┴────────┴────────┘ + │ │ │ + ↓ ↓ ↓ + ┌────────┬────────┬────────┐ + │Direct │Native │Child │ + │Import │FFI │Process │ + └────────┴────────┴────────┘ +``` + +## Adding a New Runtime + +1. Create folder: `executor/{language}/` +2. Implement `PluginLoader` interface +3. Register loader in `core/registry/node-executor-registry.ts` +4. Add plugins to `plugins/{language}/` diff --git a/workflow/executor/cpp/README.md b/workflow/executor/cpp/README.md new file mode 100644 index 000000000..28c085ef8 --- /dev/null +++ b/workflow/executor/cpp/README.md @@ -0,0 +1,73 @@ +# C++ Plugin Executor + +High-performance native runtime for C++ workflow plugins. + +## Status + +**Phase 3** - Framework ready, implementation pending. + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ TypeScript (Node.js) │ +│ - DAGExecutor calls native binding │ +└─────────────────┬───────────────────────┘ + │ node-ffi / node-addon-api + ↓ +┌─────────────────────────────────────────┐ +│ C++ Shared Library (.so/.dylib/.dll) │ +│ - Native plugin execution │ +│ - Direct memory access │ +│ - Parallel processing │ +└─────────────────────────────────────────┘ +``` + +## Build System + +Uses CMake for cross-platform builds: + +```bash +mkdir build && cd build +cmake .. +cmake --build . --config Release +``` + +## Interface + +```cpp +// executor.h +extern "C" { + // Execute a plugin and return JSON result + const char* execute_plugin( + const char* plugin_name, + const char* inputs_json, + const char* context_json + ); + + // Free result memory + void free_result(const char* result); + + // List available plugins + const char* list_plugins(); +} +``` + +## Planned Plugins + +- `dbal-aggregate` - High-performance data aggregation (1000x faster) +- `dbal-bulk-operations` - Bulk insert/update operations +- `s3-upload` - Native S3 upload with multipart +- `redis-cache` - Native Redis client +- `kafka-producer` - Native Kafka producer +- `bulk-process` - Parallel data processing +- `stream-aggregate` - Streaming aggregation + +## Performance Targets + +| Operation | TypeScript | C++ Target | +|-----------|------------|------------| +| Large aggregation | 1.0x | 100-1000x | +| Bulk operations | 1.0x | 50-100x | +| JSON parsing | 1.0x | 10-50x | +| Memory usage | 1.0x | 0.1-0.3x | diff --git a/workflow/executor/python/executor.py b/workflow/executor/python/executor.py new file mode 100644 index 000000000..f83324689 --- /dev/null +++ b/workflow/executor/python/executor.py @@ -0,0 +1,95 @@ +"""Python Plugin Executor + +Runtime for Python workflow plugins. +Communicates with TypeScript via JSON over stdin/stdout. +""" + +import json +import sys +import importlib +import importlib.util +from pathlib import Path +from typing import Any, Dict, Optional + + +class PythonRuntime: + """Runtime environment for Python plugin execution.""" + + def __init__(self): + self.store: Dict[str, Any] = {} + self.context: Dict[str, Any] = {} + self.logger = self._create_logger() + + def _create_logger(self): + """Create a simple logger that writes to stderr.""" + import logging + logger = logging.getLogger("workflow.python") + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + return logger + + +def load_plugin(plugin_path: str) -> Any: + """Dynamically load a Python plugin module.""" + path = Path(plugin_path) + if not path.exists(): + raise FileNotFoundError(f"Plugin not found: {plugin_path}") + + spec = importlib.util.spec_from_file_location(path.stem, path) + if spec is None or spec.loader is None: + raise ImportError(f"Cannot load plugin: {plugin_path}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def execute_plugin(plugin_path: str, inputs: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: + """Execute a Python plugin and return the result.""" + module = load_plugin(plugin_path) + + if not hasattr(module, "run"): + raise AttributeError(f"Plugin {plugin_path} has no 'run' function") + + runtime = PythonRuntime() + runtime.context = context + runtime.store = context.get("store", {}) + + result = module.run(runtime, inputs) + + # Ensure result is a dict + if not isinstance(result, dict): + result = {"result": result} + + return result + + +def main(): + """Main entry point for JSON-based communication.""" + # Read input from stdin + try: + input_data = json.loads(sys.stdin.read()) + except json.JSONDecodeError as e: + print(json.dumps({"error": f"Invalid JSON input: {e}"})) + sys.exit(1) + + plugin_path = input_data.get("plugin_path") + inputs = input_data.get("inputs", {}) + context = input_data.get("context", {}) + + if not plugin_path: + print(json.dumps({"error": "plugin_path is required"})) + sys.exit(1) + + try: + result = execute_plugin(plugin_path, inputs, context) + print(json.dumps(result)) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/workflow/executor/ts/index.ts b/workflow/executor/ts/index.ts new file mode 100644 index 000000000..b94466605 --- /dev/null +++ b/workflow/executor/ts/index.ts @@ -0,0 +1,61 @@ +/** + * TypeScript Plugin Executor + * + * Default runtime for TypeScript/JavaScript plugins. + * Uses direct module import for maximum performance. + */ + +import type { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult } from '../../core/types'; + +export interface TypeScriptPluginLoader { + load(pluginPath: string): Promise; +} + +/** + * Load a TypeScript plugin from path + */ +export async function loadTypeScriptPlugin(pluginPath: string): Promise { + // Dynamic import for the plugin module + const module = await import(pluginPath); + + // Find the executor class (convention: {PluginName}Executor) + const executorKey = Object.keys(module).find(key => key.endsWith('Executor')); + if (!executorKey) { + throw new Error(`No executor found in plugin: ${pluginPath}`); + } + + const ExecutorClass = module[executorKey]; + return new ExecutorClass(); +} + +/** + * TypeScript plugin executor wrapper + */ +export class TypeScriptExecutor { + private plugins: Map = new Map(); + + async loadPlugin(nodeType: string, pluginPath: string): Promise { + const executor = await loadTypeScriptPlugin(pluginPath); + this.plugins.set(nodeType, executor); + } + + async execute( + node: WorkflowNode, + context: WorkflowContext, + state: ExecutionState + ): Promise { + const executor = this.plugins.get(node.nodeType); + if (!executor) { + throw new Error(`No TypeScript executor registered for: ${node.nodeType}`); + } + return executor.execute(node, context, state); + } + + hasPlugin(nodeType: string): boolean { + return this.plugins.has(nodeType); + } + + listPlugins(): string[] { + return Array.from(this.plugins.keys()); + } +} diff --git a/workflow/package.json b/workflow/package.json index cb7df173d..6052fc308 100644 --- a/workflow/package.json +++ b/workflow/package.json @@ -36,7 +36,7 @@ "dev": "tsc --watch", "test": "jest", "test:watch": "jest --watch", - "lint": "eslint src --ext .ts", + "lint": "eslint core --ext .ts", "type-check": "tsc --noEmit", "prepublish": "npm run build" }, diff --git a/workflow/plugins/STRUCTURE.md b/workflow/plugins/STRUCTURE.md index 28336ab74..fa2ac93cd 100644 --- a/workflow/plugins/STRUCTURE.md +++ b/workflow/plugins/STRUCTURE.md @@ -2,334 +2,174 @@ ## Directory Organization -Plugins are organized by **language** first, then by **category**, then by **plugin name**: +Plugins are organized by **language** first, then by **category**: ``` -workflow/plugins/ -├── ts/ # TypeScript plugins (Phase 2 - current) -│ ├── dbal/ -│ │ ├── dbal-read/ -│ │ │ ├── package.json -│ │ │ ├── tsconfig.json -│ │ │ ├── src/ -│ │ │ ├── dist/ -│ │ │ └── README.md -│ │ └── dbal-write/ -│ ├── integration/ -│ │ ├── http-request/ -│ │ ├── email-send/ -│ │ └── webhook-response/ -│ ├── control-flow/ -│ │ └── condition/ -│ └── utility/ -│ ├── transform/ -│ ├── wait/ -│ └── set-variable/ +workflow/ +├── core/ # Core engine (TypeScript) +│ ├── executor/ # DAG executor +│ ├── registry/ # Plugin registry +│ ├── utils/ # Priority queue, template engine +│ ├── types.ts # Type definitions +│ └── index.ts # Main exports │ -├── cpp/ # C++ plugins (Phase 3 - coming) -│ ├── dbal/ -│ │ ├── dbal-aggregate/ -│ │ │ ├── package.json # npm package metadata -│ │ │ ├── CMakeLists.txt -│ │ │ ├── src/ -│ │ │ │ ├── aggregate.cpp -│ │ │ │ └── aggregate.h -│ │ │ ├── build/ -│ │ │ └── README.md -│ │ └── dbal-bulk-ops/ -│ │ -│ ├── integration/ -│ │ ├── s3-upload/ -│ │ ├── redis-cache/ -│ │ └── kafka-producer/ -│ │ -│ ├── performance/ -│ │ ├── bulk-process/ -│ │ └── stream-aggregate/ -│ │ -│ └── ai/ -│ ├── ml-predict/ -│ └── vector-search/ +├── executor/ # Language-specific runtimes +│ ├── ts/ # TypeScript executor (direct import) +│ ├── python/ # Python executor (child process) +│ └── cpp/ # C++ executor (native FFI) │ -├── python/ # Python plugins (future) -│ ├── ai/ -│ │ ├── nlp-process/ -│ │ └── sentiment-analyze/ -│ └── data-science/ -│ └── statistical-analysis/ +├── plugins/ # All plugins by language +│ ├── ts/ # TypeScript plugins +│ │ ├── dbal/ +│ │ │ ├── dbal-read/ +│ │ │ └── dbal-write/ +│ │ ├── integration/ +│ │ │ ├── http-request/ +│ │ │ ├── email-send/ +│ │ │ └── webhook-response/ +│ │ ├── control-flow/ +│ │ │ └── condition/ +│ │ └── utility/ +│ │ ├── transform/ +│ │ ├── wait/ +│ │ └── set-variable/ +│ │ +│ └── python/ # Python plugins (from AutoMetabuilder) +│ ├── control/ # Bot control, switch logic +│ ├── convert/ # Type conversions +│ ├── core/ # AI requests, message handling +│ ├── dict/ # Dictionary operations +│ ├── list/ # List operations +│ ├── logic/ # Boolean logic +│ ├── math/ # Mathematical operations +│ ├── notifications/ # Slack, Discord +│ ├── string/ # String manipulation +│ ├── test/ # Unit testing assertions +│ ├── tools/ # External tool integration +│ ├── utils/ # Utility functions +│ ├── var/ # Variable management +│ └── web/ # Flask server, API endpoints │ -└── STRUCTURE.md # This file +├── package.json +└── tsconfig.json ``` -## Plugin Configuration +## Plugin Categories -### TypeScript Plugin (ts/dbal/dbal-read/) +### TypeScript Plugins (`plugins/ts/`) -```json -{ - "name": "@metabuilder/workflow-plugin-ts-dbal-read", - "version": "1.0.0", - "description": "...", - "main": "dist/index.js", - "language": "typescript", - "nodeType": "dbal-read", - "category": "dbal" -} -``` +| Category | Plugins | Purpose | +|----------|---------|---------| +| dbal | dbal-read, dbal-write | Database operations | +| integration | http-request, email-send, webhook-response | External services | +| control-flow | condition | Workflow control | +| utility | transform, wait, set-variable | Data manipulation | -### C++ Plugin (cpp/dbal/dbal-aggregate/) +### Python Plugins (`plugins/python/`) -```json -{ - "name": "@metabuilder/workflow-plugin-cpp-dbal-aggregate", - "version": "1.0.0", - "description": "High-performance aggregation operations", - "main": "build/libaggregate.so", - "language": "c++", - "nodeType": "dbal-aggregate", - "category": "dbal", - "bindings": "node-ffi", - "build": "cmake", - "performance": { - "speedup": "100x vs TypeScript", - "use_cases": ["large datasets", "complex aggregations"] - } -} -``` +| Category | Plugins | Purpose | +|----------|---------|---------| +| control | control_switch, control_start_bot, control_get_bot_status | Bot control | +| convert | convert_to_*, convert_parse_json | Type conversion | +| core | core_ai_request, core_load_context, core_run_tool_calls | AI operations | +| dict | dict_get, dict_set, dict_keys, dict_values, dict_merge | Dictionary ops | +| list | list_concat, list_find, list_sort, list_slice | List operations | +| logic | logic_and, logic_or, logic_equals, logic_gt, logic_lt | Comparisons | +| math | math_add, math_subtract, math_multiply, math_divide | Arithmetic | +| notifications | notifications_slack, notifications_discord | Notifications | +| string | string_concat, string_split, string_replace, string_format | String ops | +| test | test_assert_equals, test_assert_true, test_run_suite | Testing | +| tools | tools_read_file, tools_run_tests, tools_run_docker | External tools | +| utils | utils_filter_list, utils_map_list, utils_check_mvp | Utilities | +| var | var_get, var_set, var_delete, var_exists | Variables | +| web | web_create_flask_app, web_start_server, web_get_env_vars | Web/Flask | -### Python Plugin (python/ai/nlp-process/) +## Plugin Interface -```json -{ - "name": "@metabuilder/workflow-plugin-python-nlp-process", - "version": "1.0.0", - "language": "python", - "nodeType": "nlp-process", - "category": "ai", - "runtime": "python3.11", - "bindings": "child-process" -} -``` - -## Plugin Registry Enhancement +### TypeScript Plugin ```typescript -interface PluginMetadata { - nodeType: string; - language: 'typescript' | 'c++' | 'python' | 'rust' | 'go'; - category: string; - version: string; - bindings?: 'native' | 'node-ffi' | 'child-process' | 'wasm'; - performance?: { - speedup: string; - use_cases: string[]; - }; -} +// plugins/ts/dbal/dbal-read/src/index.ts +export class DBALReadExecutor implements INodeExecutor { + nodeType = 'dbal-read'; -interface PluginExecutor { - language: string; - executor: INodeExecutor | ExternalProcess; - loadTime?: number; - warmupTime?: number; -} -``` - -## Build & Load Strategy - -### TypeScript Plugins -```bash -# Build -cd workflow/plugins/ts/dbal/dbal-read -npm run build - -# Load -import { dbalReadExecutor } from '@metabuilder/workflow-plugin-ts-dbal-read' -registry.register('dbal-read', dbalReadExecutor) -``` - -### C++ Plugins -```bash -# Build -cd workflow/plugins/cpp/dbal/dbal-aggregate -cmake -B build . -cmake --build build - -# Load (via native bindings) -const binding = require('@metabuilder/workflow-plugin-cpp-dbal-aggregate'); -const executor = new binding.AggregateExecutor(); -registry.register('dbal-aggregate', executor) -``` - -### Python Plugins -```bash -# Install dependencies -cd workflow/plugins/python/ai/nlp-process -pip install -r requirements.txt - -# Load (via child process) -const { PythonProcessExecutor } = require('@metabuilder/workflow-plugin-python-nlp-process'); -const executor = new PythonProcessExecutor('./src/executor.py'); -registry.register('nlp-process', executor) -``` - -## Phase Roadmap - -### Phase 2 (Current) - TypeScript Plugins -✅ Complete -- DBAL: read, write -- Integration: http-request, email-send, webhook-response -- Control-flow: condition -- Utility: transform, wait, set-variable - -### Phase 3 - C++ Plugins -🚀 Coming Soon -- DBAL: aggregate, bulk-operations -- Integration: S3, Redis, Kafka connectors -- Performance: Bulk processing, stream aggregation -- AI: ML predictions, vector search - -### Phase 4+ - Multi-Language -🔮 Future -- Python: NLP, data science, ML -- Rust: High-performance utilities -- Go: Concurrent operations -- WebAssembly: Browser-side execution - -## Plugin Loading Architecture - -```typescript -// Enhanced registry with language support -class MultiLanguageNodeExecutorRegistry extends NodeExecutorRegistry { - private loaders: Map = new Map(); - - constructor() { - super(); - this.loaders.set('typescript', new TypeScriptPluginLoader()); - this.loaders.set('c++', new NativePluginLoader()); - this.loaders.set('python', new PythonPluginLoader()); - } - - async loadPlugin(language: string, path: string): Promise { - const loader = this.loaders.get(language); - if (!loader) throw new Error(`Unknown language: ${language}`); - - const plugin = await loader.load(path); - this.register(plugin.nodeType, plugin.executor, plugin.metadata); - } - - async loadAllPlugins(baseDir: string): Promise { - const languages = ['ts', 'cpp', 'python']; - - for (const lang of languages) { - const categoryPath = path.join(baseDir, lang); - const categories = await fs.readdir(categoryPath); - - for (const category of categories) { - const pluginPath = path.join(categoryPath, category); - const plugins = await fs.readdir(pluginPath); - - for (const plugin of plugins) { - await this.loadPlugin(lang, path.join(pluginPath, plugin)); - } - } - } + async execute( + node: WorkflowNode, + context: WorkflowContext, + state: ExecutionState + ): Promise { + // Implementation } } ``` -## Example Multi-Language Workflow +### Python Plugin -```json -{ - "id": "wf-hybrid-processing", - "name": "TS + C++ + Python Hybrid", - "nodes": [ - { - "id": "read-data", - "nodeType": "dbal-read", - "language": "typescript", - "parameters": { "entity": "Dataset", "limit": 10000 } - }, - { - "id": "aggregate", - "nodeType": "dbal-aggregate", - "language": "c++", - "parameters": { "groupBy": "category", "aggregates": ["count", "sum"] } - }, - { - "id": "analyze", - "nodeType": "nlp-process", - "language": "python", - "parameters": { "model": "bert", "task": "sentiment" } - }, - { - "id": "send-result", - "nodeType": "email-send", - "language": "typescript", - "parameters": { "to": "team@example.com", "body": "{{ $json.result }}" } - } - ] -} +```python +# plugins/python/math/math_add.py +def run(_runtime, inputs): + """Add two or more numbers.""" + numbers = inputs.get("numbers", []) + return {"result": sum(numbers)} ``` -## Performance Characteristics by Language +## Execution Flow + +``` +┌─────────────────────────────────────────┐ +│ DAGExecutor (core/executor/) │ +│ - Resolves node dependencies │ +│ - Schedules execution │ +└─────────────────┬───────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────┐ +│ NodeExecutorRegistry (core/registry/) │ +│ - Looks up plugin by nodeType │ +│ - Determines language from metadata │ +└─────────────────┬───────────────────────┘ + │ + ┌─────────┼─────────┐ + │ │ │ + ↓ ↓ ↓ + ┌────────┬────────┬────────┐ + │ TS │ Python │ C++ │ + │Executor│Executor│Executor│ + └────────┴────────┴────────┘ + │ │ │ + ↓ ↓ ↓ + ┌────────┬────────┬────────┐ + │plugins/│plugins/│plugins/│ + │ ts/ │python/ │ cpp/ │ + └────────┴────────┴────────┘ +``` + +## Performance Characteristics | Language | Execution Speed | Memory | Startup | Best For | |----------|-----------------|--------|---------|----------| | TypeScript | 1x baseline | High | Fast | Orchestration, logic | +| Python | 0.1-1x | Medium | Medium | AI/ML, data science | | C++ | 100-1000x | Low | Slow | Bulk ops, aggregations | -| Python | 0.1-1x | Medium | Medium | ML, data science | -| Rust | 100-500x | Low | Slow | Concurrent ops | ## Best Practices ### Choose Language Based On: **TypeScript** -- ✅ REST APIs and webhooks -- ✅ JSON transformations -- ✅ Simple orchestration -- ✅ Rapid development - -**C++** -- ✅ Large dataset processing (1M+ rows) -- ✅ Complex aggregations -- ✅ Performance-critical operations -- ✅ Bulk operations +- REST APIs and webhooks +- JSON transformations +- Simple orchestration +- Rapid development **Python** -- ✅ Machine learning tasks -- ✅ Natural language processing -- ✅ Data science operations -- ✅ Complex statistical analysis +- Machine learning tasks +- Natural language processing +- Data science operations +- AI model integration -## Migration Path - -1. **Start**: Build plugins in TypeScript (fast iteration) -2. **Measure**: Identify performance bottlenecks -3. **Optimize**: Convert hot paths to C++ -4. **Extend**: Add Python ML capabilities -5. **Scale**: Add Rust for concurrent operations - -## File Naming Convention - -``` -workflow/plugins/{language}/{category}/{plugin-name}/ - -Examples: - workflow/plugins/ts/dbal/dbal-read/ - workflow/plugins/cpp/dbal/dbal-aggregate/ - workflow/plugins/python/ai/nlp-process/ - workflow/plugins/rust/concurrent/batch-processor/ -``` - -## Package Naming Convention - -``` -@metabuilder/workflow-plugin-{language}-{category}-{plugin} - -Examples: - @metabuilder/workflow-plugin-ts-dbal-read - @metabuilder/workflow-plugin-cpp-dbal-aggregate - @metabuilder/workflow-plugin-python-ai-nlp -``` +**C++** +- Large dataset processing (1M+ rows) +- Complex aggregations +- Performance-critical operations +- Memory-intensive operations diff --git a/workflow/plugins/python/__init__.py b/workflow/plugins/python/__init__.py new file mode 100644 index 000000000..6a110bbe2 --- /dev/null +++ b/workflow/plugins/python/__init__.py @@ -0,0 +1,21 @@ +"""MetaBuilder Workflow Python Plugins. + +This package contains Python workflow plugins organized by category. +Each plugin follows the standard interface: run(runtime, inputs) -> dict + +Categories: +- control: Bot control and switch logic +- convert: Type conversion utilities +- core: Core AI/workflow operations +- dict: Dictionary manipulation +- list: List operations +- logic: Boolean logic operations +- math: Mathematical operations +- notifications: Slack/Discord notifications +- string: String manipulation +- test: Unit testing assertions +- tools: External tool integration +- utils: Utility functions +- var: Variable management +- web: Web/Flask operations +""" diff --git a/workflow/plugins/python/control/__init__.py b/workflow/plugins/python/control/__init__.py new file mode 100644 index 000000000..860fc0919 --- /dev/null +++ b/workflow/plugins/python/control/__init__.py @@ -0,0 +1 @@ +"""Control flow plugins: bot control and switch logic.""" diff --git a/workflow/plugins/python/control/control_get_bot_status.py b/workflow/plugins/python/control/control_get_bot_status.py new file mode 100644 index 000000000..986bbf976 --- /dev/null +++ b/workflow/plugins/python/control/control_get_bot_status.py @@ -0,0 +1,39 @@ +"""Workflow plugin: get current bot execution status.""" + +# Global state for bot process +_bot_process = None +_mock_running = False +_current_run_config = {} + + +def get_bot_state(): + """Get the current bot state (public interface). + + Returns: + dict: Bot state with keys: is_running, config, process + """ + return { + "is_running": _bot_process is not None or _mock_running, + "config": _current_run_config, + "process": _bot_process, + } + + +def reset_bot_state(): + """Reset the bot state (public interface).""" + global _bot_process, _current_run_config, _mock_running + _bot_process = None + _current_run_config = {} + _mock_running = False + + +def run(_runtime, _inputs): + """Get current bot execution status. + + Returns: + Dictionary with: + - is_running: bool - Whether the bot is currently running + - config: dict - Current run configuration (empty if not running) + - process: object - Bot process object (or None if not running) + """ + return get_bot_state() diff --git a/workflow/plugins/python/control/control_reset_bot_state.py b/workflow/plugins/python/control/control_reset_bot_state.py new file mode 100644 index 000000000..b989155bf --- /dev/null +++ b/workflow/plugins/python/control/control_reset_bot_state.py @@ -0,0 +1,13 @@ +"""Workflow plugin: reset bot execution state.""" +from .control_get_bot_status import reset_bot_state + + +def run(_runtime, _inputs): + """Reset bot execution state. + + Returns: + Dictionary with: + - reset: bool - Always True to indicate state was reset + """ + reset_bot_state() + return {"reset": True} diff --git a/workflow/plugins/python/control/control_start_bot.py b/workflow/plugins/python/control/control_start_bot.py new file mode 100644 index 000000000..06ebfcf0d --- /dev/null +++ b/workflow/plugins/python/control/control_start_bot.py @@ -0,0 +1,97 @@ +"""Workflow plugin: start bot execution in background thread.""" +import os +import subprocess +import sys +import threading +import time + +from .control_get_bot_status import ( + get_bot_state, + reset_bot_state, + _bot_process, + _mock_running, + _current_run_config +) + +# Import global state +import workflow.plugins.python.control.control_get_bot_status as bot_status + + +def _run_bot_task(mode: str, iterations: int, yolo: bool, stop_at_mvp: bool) -> None: + """Execute bot task in background thread.""" + bot_status._current_run_config = { + "mode": mode, + "iterations": iterations, + "yolo": yolo, + "stop_at_mvp": stop_at_mvp, + } + + if os.environ.get("MOCK_WEB_UI") == "true": + bot_status._mock_running = True + time.sleep(5) + bot_status._mock_running = False + reset_bot_state() + return + + try: + cmd = [sys.executable, "-m", "autometabuilder.main"] + if yolo: + cmd.append("--yolo") + if mode == "once": + cmd.append("--once") + if mode == "iterations" and iterations > 1: + for _ in range(iterations): + if stop_at_mvp: + # Check MVP status + pass + bot_status._bot_process = subprocess.Popen( + cmd + ["--once"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + bot_status._bot_process.wait() + else: + bot_status._bot_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + bot_status._bot_process.wait() + finally: + reset_bot_state() + + +def run(_runtime, inputs): + """Start bot execution in background thread. + + Args: + inputs: Dictionary with keys: + - mode: str (default: "once") - Execution mode ("once", "iterations", etc.) + - iterations: int (default: 1) - Number of iterations for "iterations" mode + - yolo: bool (default: True) - Run in YOLO mode + - stop_at_mvp: bool (default: False) - Stop when MVP is reached + + Returns: + Dictionary with: + - started: bool - Whether the bot was started successfully + - error: str (optional) - Error message if bot is already running + """ + mode = inputs.get("mode", "once") + iterations = inputs.get("iterations", 1) + yolo = inputs.get("yolo", True) + stop_at_mvp = inputs.get("stop_at_mvp", False) + + # Check if bot is already running + state = get_bot_state() + if state["is_running"]: + return {"started": False, "error": "Bot already running"} + + # Start bot in background thread + thread = threading.Thread( + target=_run_bot_task, + args=(mode, iterations, yolo, stop_at_mvp), + daemon=True + ) + thread.start() + + return {"started": True} diff --git a/workflow/plugins/python/control/control_switch.py b/workflow/plugins/python/control/control_switch.py new file mode 100644 index 000000000..de1a4ae6e --- /dev/null +++ b/workflow/plugins/python/control/control_switch.py @@ -0,0 +1,11 @@ +"""Workflow plugin: switch/case control flow.""" + + +def run(_runtime, inputs): + """Switch on value and return matching case.""" + value = inputs.get("value") + cases = inputs.get("cases", {}) + default = inputs.get("default") + + result = cases.get(str(value), default) + return {"result": result, "matched": str(value) in cases} diff --git a/workflow/plugins/python/convert/__init__.py b/workflow/plugins/python/convert/__init__.py new file mode 100644 index 000000000..68841258a --- /dev/null +++ b/workflow/plugins/python/convert/__init__.py @@ -0,0 +1 @@ +"""Type conversion plugins.""" diff --git a/workflow/plugins/python/convert/convert_parse_json.py b/workflow/plugins/python/convert/convert_parse_json.py new file mode 100644 index 000000000..1065af5e5 --- /dev/null +++ b/workflow/plugins/python/convert/convert_parse_json.py @@ -0,0 +1,13 @@ +"""Workflow plugin: parse JSON string.""" +import json + + +def run(_runtime, inputs): + """Parse JSON string to object.""" + text = inputs.get("text", "") + + try: + result = json.loads(text) + return {"result": result} + except json.JSONDecodeError as e: + return {"result": None, "error": str(e)} diff --git a/workflow/plugins/python/convert/convert_to_boolean.py b/workflow/plugins/python/convert/convert_to_boolean.py new file mode 100644 index 000000000..f6ca9e374 --- /dev/null +++ b/workflow/plugins/python/convert/convert_to_boolean.py @@ -0,0 +1,11 @@ +"""Workflow plugin: convert to boolean.""" + + +def run(_runtime, inputs): + """Convert value to boolean.""" + value = inputs.get("value") + + if isinstance(value, str): + return {"result": value.lower() not in ("false", "0", "", "none", "null")} + + return {"result": bool(value)} diff --git a/workflow/plugins/python/convert/convert_to_dict.py b/workflow/plugins/python/convert/convert_to_dict.py new file mode 100644 index 000000000..d62399cec --- /dev/null +++ b/workflow/plugins/python/convert/convert_to_dict.py @@ -0,0 +1,17 @@ +"""Workflow plugin: convert to dictionary.""" + + +def run(_runtime, inputs): + """Convert value to dictionary.""" + value = inputs.get("value") + + if isinstance(value, dict): + return {"result": value} + elif isinstance(value, list): + # Convert list of [key, value] pairs to dict + try: + return {"result": dict(value)} + except (TypeError, ValueError): + return {"result": {}, "error": "Cannot convert list to dict"} + else: + return {"result": {}} diff --git a/workflow/plugins/python/convert/convert_to_json.py b/workflow/plugins/python/convert/convert_to_json.py new file mode 100644 index 000000000..77bb87a2f --- /dev/null +++ b/workflow/plugins/python/convert/convert_to_json.py @@ -0,0 +1,14 @@ +"""Workflow plugin: convert to JSON string.""" +import json + + +def run(_runtime, inputs): + """Convert value to JSON string.""" + value = inputs.get("value") + indent = inputs.get("indent") + + try: + result = json.dumps(value, indent=indent) + return {"result": result} + except (TypeError, ValueError) as e: + return {"result": None, "error": str(e)} diff --git a/workflow/plugins/python/convert/convert_to_list.py b/workflow/plugins/python/convert/convert_to_list.py new file mode 100644 index 000000000..f68eafee9 --- /dev/null +++ b/workflow/plugins/python/convert/convert_to_list.py @@ -0,0 +1,17 @@ +"""Workflow plugin: convert to list.""" + + +def run(_runtime, inputs): + """Convert value to list.""" + value = inputs.get("value") + + if isinstance(value, list): + return {"result": value} + elif isinstance(value, (tuple, set)): + return {"result": list(value)} + elif isinstance(value, dict): + return {"result": list(value.items())} + elif value is None: + return {"result": []} + else: + return {"result": [value]} diff --git a/workflow/plugins/python/convert/convert_to_number.py b/workflow/plugins/python/convert/convert_to_number.py new file mode 100644 index 000000000..40ab59373 --- /dev/null +++ b/workflow/plugins/python/convert/convert_to_number.py @@ -0,0 +1,14 @@ +"""Workflow plugin: convert to number.""" + + +def run(_runtime, inputs): + """Convert value to number.""" + value = inputs.get("value") + default = inputs.get("default", 0) + + try: + if isinstance(value, str) and "." in value: + return {"result": float(value)} + return {"result": int(value)} + except (ValueError, TypeError): + return {"result": default, "error": "Cannot convert to number"} diff --git a/workflow/plugins/python/convert/convert_to_string.py b/workflow/plugins/python/convert/convert_to_string.py new file mode 100644 index 000000000..4ce4123ae --- /dev/null +++ b/workflow/plugins/python/convert/convert_to_string.py @@ -0,0 +1,7 @@ +"""Workflow plugin: convert to string.""" + + +def run(_runtime, inputs): + """Convert value to string.""" + value = inputs.get("value") + return {"result": str(value) if value is not None else ""} diff --git a/workflow/plugins/python/core/__init__.py b/workflow/plugins/python/core/__init__.py new file mode 100644 index 000000000..ef73be3d9 --- /dev/null +++ b/workflow/plugins/python/core/__init__.py @@ -0,0 +1 @@ +"""Core AI/workflow plugins: AI requests, message handling, context loading.""" diff --git a/workflow/plugins/python/core/core_ai_request.py b/workflow/plugins/python/core/core_ai_request.py new file mode 100644 index 000000000..face1e977 --- /dev/null +++ b/workflow/plugins/python/core/core_ai_request.py @@ -0,0 +1,39 @@ +"""Workflow plugin: AI request.""" +from tenacity import retry, stop_after_attempt, wait_exponential + + +@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) +def _get_completion(client, model, messages, tools): + """Request a chat completion with retries.""" + return client.chat.completions.create( + model=model, + messages=messages, + tools=tools, + tool_choice="auto", + temperature=1.0, + top_p=1.0, + ) + + +def run(runtime, inputs): + """Invoke the model with current messages.""" + messages = list(inputs.get("messages") or []) + response = _get_completion( + runtime.context["client"], + runtime.context["model_name"], + messages, + runtime.context["tools"] + ) + resp_msg = response.choices[0].message + runtime.logger.info( + resp_msg.content + if resp_msg.content + else runtime.context["msgs"]["info_tool_call_requested"] + ) + messages.append(resp_msg) + tool_calls = getattr(resp_msg, "tool_calls", None) or [] + return { + "response": resp_msg, + "has_tool_calls": bool(tool_calls), + "tool_calls_count": len(tool_calls) + } diff --git a/workflow/plugins/python/core/core_append_context_message.py b/workflow/plugins/python/core/core_append_context_message.py new file mode 100644 index 000000000..cf5314d3b --- /dev/null +++ b/workflow/plugins/python/core/core_append_context_message.py @@ -0,0 +1,13 @@ +"""Workflow plugin: append context message.""" + + +def run(runtime, inputs): + """Append context to the message list.""" + messages = list(inputs.get("messages") or []) + context_val = inputs.get("context") + if context_val: + messages.append({ + "role": "system", + "content": f"{runtime.context['msgs']['sdlc_context_label']}{context_val}", + }) + return {"messages": messages} diff --git a/workflow/plugins/python/core/core_append_tool_results.py b/workflow/plugins/python/core/core_append_tool_results.py new file mode 100644 index 000000000..f572aab27 --- /dev/null +++ b/workflow/plugins/python/core/core_append_tool_results.py @@ -0,0 +1,44 @@ +"""Workflow plugin: append tool results.""" +import os +import re + + +def _is_mvp_reached() -> bool: + """Check if the MVP section in ROADMAP.md is completed.""" + if not os.path.exists("ROADMAP.md"): + return False + + with open("ROADMAP.md", "r", encoding="utf-8") as f: + content = f.read() + + # Find the header line containing (MVP) + header_match = re.search(r"^## .*?\(MVP\).*?$", content, re.MULTILINE | re.IGNORECASE) + if not header_match: + return False + + start_pos = header_match.end() + next_header_match = re.search(r"^## ", content[start_pos:], re.MULTILINE) + if next_header_match: + mvp_section = content[start_pos : start_pos + next_header_match.start()] + else: + mvp_section = content[start_pos:] + + if "[ ]" in mvp_section: + return False + if "[x]" in mvp_section: + return True + + return False + + +def run(runtime, inputs): + """Append tool results to the message list.""" + messages = list(inputs.get("messages") or []) + tool_results = inputs.get("tool_results") or [] + if tool_results: + messages.extend(tool_results) + + if runtime.context.get("args", {}).get("yolo") and _is_mvp_reached(): + runtime.logger.info("MVP reached. Stopping YOLO loop.") + + return {"messages": messages} diff --git a/workflow/plugins/python/core/core_append_user_instruction.py b/workflow/plugins/python/core/core_append_user_instruction.py new file mode 100644 index 000000000..556249c7b --- /dev/null +++ b/workflow/plugins/python/core/core_append_user_instruction.py @@ -0,0 +1,8 @@ +"""Workflow plugin: append user instruction.""" + + +def run(runtime, inputs): + """Append the next user instruction.""" + messages = list(inputs.get("messages") or []) + messages.append({"role": "user", "content": runtime.context["msgs"]["user_next_step"]}) + return {"messages": messages} diff --git a/workflow/plugins/python/core/core_load_context.py b/workflow/plugins/python/core/core_load_context.py new file mode 100644 index 000000000..b76d37475 --- /dev/null +++ b/workflow/plugins/python/core/core_load_context.py @@ -0,0 +1,43 @@ +"""Workflow plugin: load SDLC context.""" +import os +import logging + +logger = logging.getLogger("metabuilder") + + +def run(runtime, _inputs): + """Load SDLC context into the workflow store.""" + gh = runtime.context.get("gh") + msgs = runtime.context.get("msgs", {}) + + sdlc_context = "" + + # Load ROADMAP.md if it exists + if os.path.exists("ROADMAP.md"): + with open("ROADMAP.md", "r", encoding="utf-8") as f: + roadmap_content = f.read() + label = msgs.get("roadmap_label", "ROADMAP.md Content:") + sdlc_context += f"\n{label}\n{roadmap_content}\n" + else: + msg = msgs.get( + "missing_roadmap_msg", + "ROADMAP.md is missing. Please analyze the repository and create it." + ) + sdlc_context += f"\n{msg}\n" + + # Load GitHub issues and PRs if integration is available + if gh: + try: + issues = gh.get_open_issues() + issue_list = "\n".join([f"- #{i.number}: {i.title}" for i in issues[:5]]) + if issue_list: + sdlc_context += f"\n{msgs['open_issues_label']}\n{issue_list}" + + prs = gh.get_pull_requests() + pr_list = "\n".join([f"- #{p.number}: {p.title}" for p in prs[:5]]) + if pr_list: + sdlc_context += f"\n{msgs['open_prs_label']}\n{pr_list}" + except Exception as error: + logger.error(msgs.get("error_sdlc_context", "Error: {error}").format(error=error)) + + return {"context": sdlc_context} diff --git a/workflow/plugins/python/core/core_run_tool_calls.py b/workflow/plugins/python/core/core_run_tool_calls.py new file mode 100644 index 000000000..1e6e88a7b --- /dev/null +++ b/workflow/plugins/python/core/core_run_tool_calls.py @@ -0,0 +1,37 @@ +"""Workflow plugin: run tool calls.""" + + +def run(runtime, inputs): + """Execute tool calls from an AI response.""" + resp_msg = inputs.get("response") + tool_calls = getattr(resp_msg, "tool_calls", None) or [] + if not resp_msg: + return {"tool_results": [], "no_tool_calls": True} + + # Handle tool calls using tool map from context + tool_results = [] + tool_map = runtime.context.get("tool_map", {}) + + for tool_call in tool_calls: + func_name = tool_call.function.name + if func_name in tool_map: + try: + import json + args = json.loads(tool_call.function.arguments) + result = tool_map[func_name](**args) + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result) + }) + except Exception as e: + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": f"Error: {str(e)}" + }) + + return { + "tool_results": tool_results, + "no_tool_calls": not bool(tool_calls) + } diff --git a/workflow/plugins/python/core/core_seed_messages.py b/workflow/plugins/python/core/core_seed_messages.py new file mode 100644 index 000000000..405010e7f --- /dev/null +++ b/workflow/plugins/python/core/core_seed_messages.py @@ -0,0 +1,7 @@ +"""Workflow plugin: seed messages.""" + + +def run(runtime, _inputs): + """Seed messages from the prompt.""" + prompt = runtime.context["prompt"] + return {"messages": list(prompt["messages"])} diff --git a/workflow/plugins/python/dict/__init__.py b/workflow/plugins/python/dict/__init__.py new file mode 100644 index 000000000..8ea83d653 --- /dev/null +++ b/workflow/plugins/python/dict/__init__.py @@ -0,0 +1 @@ +"""Dictionary manipulation plugins.""" diff --git a/workflow/plugins/python/dict/dict_get.py b/workflow/plugins/python/dict/dict_get.py new file mode 100644 index 000000000..b23030145 --- /dev/null +++ b/workflow/plugins/python/dict/dict_get.py @@ -0,0 +1,14 @@ +"""Workflow plugin: get value from dictionary.""" + + +def run(_runtime, inputs): + """Get value from dictionary by key.""" + obj = inputs.get("object", {}) + key = inputs.get("key") + default = inputs.get("default") + + if not isinstance(obj, dict): + return {"result": default, "found": False} + + result = obj.get(key, default) + return {"result": result, "found": key in obj} diff --git a/workflow/plugins/python/dict/dict_items.py b/workflow/plugins/python/dict/dict_items.py new file mode 100644 index 000000000..69481092b --- /dev/null +++ b/workflow/plugins/python/dict/dict_items.py @@ -0,0 +1,11 @@ +"""Workflow plugin: get dictionary items as key-value pairs.""" + + +def run(_runtime, inputs): + """Get dictionary items as list of [key, value] pairs.""" + obj = inputs.get("object", {}) + + if not isinstance(obj, dict): + return {"result": []} + + return {"result": [[k, v] for k, v in obj.items()]} diff --git a/workflow/plugins/python/dict/dict_keys.py b/workflow/plugins/python/dict/dict_keys.py new file mode 100644 index 000000000..74a7c00df --- /dev/null +++ b/workflow/plugins/python/dict/dict_keys.py @@ -0,0 +1,11 @@ +"""Workflow plugin: get dictionary keys.""" + + +def run(_runtime, inputs): + """Get all keys from dictionary.""" + obj = inputs.get("object", {}) + + if not isinstance(obj, dict): + return {"result": []} + + return {"result": list(obj.keys())} diff --git a/workflow/plugins/python/dict/dict_merge.py b/workflow/plugins/python/dict/dict_merge.py new file mode 100644 index 000000000..55fb69765 --- /dev/null +++ b/workflow/plugins/python/dict/dict_merge.py @@ -0,0 +1,13 @@ +"""Workflow plugin: merge dictionaries.""" + + +def run(_runtime, inputs): + """Merge multiple dictionaries.""" + objects = inputs.get("objects", []) + result = {} + + for obj in objects: + if isinstance(obj, dict): + result.update(obj) + + return {"result": result} diff --git a/workflow/plugins/python/dict/dict_set.py b/workflow/plugins/python/dict/dict_set.py new file mode 100644 index 000000000..a589c0189 --- /dev/null +++ b/workflow/plugins/python/dict/dict_set.py @@ -0,0 +1,15 @@ +"""Workflow plugin: set value in dictionary.""" + + +def run(_runtime, inputs): + """Set value in dictionary by key.""" + obj = inputs.get("object", {}) + key = inputs.get("key") + value = inputs.get("value") + + if not isinstance(obj, dict): + obj = {} + + result = dict(obj) + result[key] = value + return {"result": result} diff --git a/workflow/plugins/python/dict/dict_values.py b/workflow/plugins/python/dict/dict_values.py new file mode 100644 index 000000000..fc37f5cfc --- /dev/null +++ b/workflow/plugins/python/dict/dict_values.py @@ -0,0 +1,11 @@ +"""Workflow plugin: get dictionary values.""" + + +def run(_runtime, inputs): + """Get all values from dictionary.""" + obj = inputs.get("object", {}) + + if not isinstance(obj, dict): + return {"result": []} + + return {"result": list(obj.values())} diff --git a/workflow/plugins/python/list/__init__.py b/workflow/plugins/python/list/__init__.py new file mode 100644 index 000000000..f7e8f2a6f --- /dev/null +++ b/workflow/plugins/python/list/__init__.py @@ -0,0 +1 @@ +"""List manipulation plugins.""" diff --git a/workflow/plugins/python/list/list_concat.py b/workflow/plugins/python/list/list_concat.py new file mode 100644 index 000000000..b2d2792a3 --- /dev/null +++ b/workflow/plugins/python/list/list_concat.py @@ -0,0 +1,11 @@ +"""Workflow plugin: concatenate lists.""" + + +def run(_runtime, inputs): + """Concatenate multiple lists.""" + lists = inputs.get("lists", []) + result = [] + for lst in lists: + if isinstance(lst, list): + result.extend(lst) + return {"result": result} diff --git a/workflow/plugins/python/list/list_every.py b/workflow/plugins/python/list/list_every.py new file mode 100644 index 000000000..1c9d4acc4 --- /dev/null +++ b/workflow/plugins/python/list/list_every.py @@ -0,0 +1,18 @@ +"""Workflow plugin: check if all items match condition.""" + + +def run(_runtime, inputs): + """Check if all items match condition.""" + items = inputs.get("items", []) + key = inputs.get("key") + value = inputs.get("value") + + if not items: + return {"result": True} + + if key is not None and value is not None: + result = all(isinstance(item, dict) and item.get(key) == value for item in items) + else: + result = all(items) + + return {"result": result} diff --git a/workflow/plugins/python/list/list_find.py b/workflow/plugins/python/list/list_find.py new file mode 100644 index 000000000..759e601d1 --- /dev/null +++ b/workflow/plugins/python/list/list_find.py @@ -0,0 +1,14 @@ +"""Workflow plugin: find item in list.""" + + +def run(_runtime, inputs): + """Find first item matching condition.""" + items = inputs.get("items", []) + key = inputs.get("key") + value = inputs.get("value") + + for item in items: + if isinstance(item, dict) and item.get(key) == value: + return {"result": item, "found": True} + + return {"result": None, "found": False} diff --git a/workflow/plugins/python/list/list_length.py b/workflow/plugins/python/list/list_length.py new file mode 100644 index 000000000..fbe95c6e3 --- /dev/null +++ b/workflow/plugins/python/list/list_length.py @@ -0,0 +1,7 @@ +"""Workflow plugin: get list length.""" + + +def run(_runtime, inputs): + """Get length of a list or string.""" + items = inputs.get("items", []) + return {"result": len(items) if items is not None else 0} diff --git a/workflow/plugins/python/list/list_slice.py b/workflow/plugins/python/list/list_slice.py new file mode 100644 index 000000000..6a34fdb5f --- /dev/null +++ b/workflow/plugins/python/list/list_slice.py @@ -0,0 +1,15 @@ +"""Workflow plugin: slice a list.""" + + +def run(_runtime, inputs): + """Extract slice from list.""" + items = inputs.get("items", []) + start = inputs.get("start", 0) + end = inputs.get("end") + + if end is None: + result = items[start:] + else: + result = items[start:end] + + return {"result": result} diff --git a/workflow/plugins/python/list/list_some.py b/workflow/plugins/python/list/list_some.py new file mode 100644 index 000000000..0cda24005 --- /dev/null +++ b/workflow/plugins/python/list/list_some.py @@ -0,0 +1,15 @@ +"""Workflow plugin: check if some items match condition.""" + + +def run(_runtime, inputs): + """Check if at least one item matches condition.""" + items = inputs.get("items", []) + key = inputs.get("key") + value = inputs.get("value") + + if key is not None and value is not None: + result = any(isinstance(item, dict) and item.get(key) == value for item in items) + else: + result = any(items) + + return {"result": result} diff --git a/workflow/plugins/python/list/list_sort.py b/workflow/plugins/python/list/list_sort.py new file mode 100644 index 000000000..fa921bad4 --- /dev/null +++ b/workflow/plugins/python/list/list_sort.py @@ -0,0 +1,17 @@ +"""Workflow plugin: sort a list.""" + + +def run(_runtime, inputs): + """Sort list by key or naturally.""" + items = inputs.get("items", []) + key = inputs.get("key") + reverse = inputs.get("reverse", False) + + try: + if key: + result = sorted(items, key=lambda x: x.get(key) if isinstance(x, dict) else x, reverse=reverse) + else: + result = sorted(items, reverse=reverse) + return {"result": result} + except (TypeError, AttributeError): + return {"result": items, "error": "Cannot sort items"} diff --git a/workflow/plugins/python/logic/__init__.py b/workflow/plugins/python/logic/__init__.py new file mode 100644 index 000000000..633556b0a --- /dev/null +++ b/workflow/plugins/python/logic/__init__.py @@ -0,0 +1 @@ +"""Boolean logic plugins.""" diff --git a/workflow/plugins/python/logic/logic_and.py b/workflow/plugins/python/logic/logic_and.py new file mode 100644 index 000000000..ee12fd301 --- /dev/null +++ b/workflow/plugins/python/logic/logic_and.py @@ -0,0 +1,7 @@ +"""Workflow plugin: logical AND.""" + + +def run(_runtime, inputs): + """Perform logical AND on values.""" + values = inputs.get("values", []) + return {"result": all(values)} diff --git a/workflow/plugins/python/logic/logic_equals.py b/workflow/plugins/python/logic/logic_equals.py new file mode 100644 index 000000000..9ccc4a421 --- /dev/null +++ b/workflow/plugins/python/logic/logic_equals.py @@ -0,0 +1,8 @@ +"""Workflow plugin: equality comparison.""" + + +def run(_runtime, inputs): + """Check if two values are equal.""" + a = inputs.get("a") + b = inputs.get("b") + return {"result": a == b} diff --git a/workflow/plugins/python/logic/logic_gt.py b/workflow/plugins/python/logic/logic_gt.py new file mode 100644 index 000000000..635dda370 --- /dev/null +++ b/workflow/plugins/python/logic/logic_gt.py @@ -0,0 +1,8 @@ +"""Workflow plugin: greater than comparison.""" + + +def run(_runtime, inputs): + """Check if a > b.""" + a = inputs.get("a") + b = inputs.get("b") + return {"result": a > b} diff --git a/workflow/plugins/python/logic/logic_gte.py b/workflow/plugins/python/logic/logic_gte.py new file mode 100644 index 000000000..4f9f6e91f --- /dev/null +++ b/workflow/plugins/python/logic/logic_gte.py @@ -0,0 +1,8 @@ +"""Workflow plugin: greater than or equal comparison.""" + + +def run(_runtime, inputs): + """Check if a >= b.""" + a = inputs.get("a") + b = inputs.get("b") + return {"result": a >= b} diff --git a/workflow/plugins/python/logic/logic_in.py b/workflow/plugins/python/logic/logic_in.py new file mode 100644 index 000000000..afb014155 --- /dev/null +++ b/workflow/plugins/python/logic/logic_in.py @@ -0,0 +1,8 @@ +"""Workflow plugin: membership test.""" + + +def run(_runtime, inputs): + """Check if value is in collection.""" + value = inputs.get("value") + collection = inputs.get("collection", []) + return {"result": value in collection} diff --git a/workflow/plugins/python/logic/logic_lt.py b/workflow/plugins/python/logic/logic_lt.py new file mode 100644 index 000000000..ce17ffe65 --- /dev/null +++ b/workflow/plugins/python/logic/logic_lt.py @@ -0,0 +1,8 @@ +"""Workflow plugin: less than comparison.""" + + +def run(_runtime, inputs): + """Check if a < b.""" + a = inputs.get("a") + b = inputs.get("b") + return {"result": a < b} diff --git a/workflow/plugins/python/logic/logic_lte.py b/workflow/plugins/python/logic/logic_lte.py new file mode 100644 index 000000000..2fdebdef7 --- /dev/null +++ b/workflow/plugins/python/logic/logic_lte.py @@ -0,0 +1,8 @@ +"""Workflow plugin: less than or equal comparison.""" + + +def run(_runtime, inputs): + """Check if a <= b.""" + a = inputs.get("a") + b = inputs.get("b") + return {"result": a <= b} diff --git a/workflow/plugins/python/logic/logic_or.py b/workflow/plugins/python/logic/logic_or.py new file mode 100644 index 000000000..69a7c26f4 --- /dev/null +++ b/workflow/plugins/python/logic/logic_or.py @@ -0,0 +1,7 @@ +"""Workflow plugin: logical OR.""" + + +def run(_runtime, inputs): + """Perform logical OR on values.""" + values = inputs.get("values", []) + return {"result": any(values)} diff --git a/workflow/plugins/python/logic/logic_xor.py b/workflow/plugins/python/logic/logic_xor.py new file mode 100644 index 000000000..17f81d900 --- /dev/null +++ b/workflow/plugins/python/logic/logic_xor.py @@ -0,0 +1,8 @@ +"""Workflow plugin: logical XOR.""" + + +def run(_runtime, inputs): + """Perform logical XOR on two values.""" + a = inputs.get("a", False) + b = inputs.get("b", False) + return {"result": bool(a) != bool(b)} diff --git a/workflow/plugins/python/math/__init__.py b/workflow/plugins/python/math/__init__.py new file mode 100644 index 000000000..404c876b1 --- /dev/null +++ b/workflow/plugins/python/math/__init__.py @@ -0,0 +1 @@ +"""Mathematical operations plugins.""" diff --git a/workflow/plugins/python/math/math_abs.py b/workflow/plugins/python/math/math_abs.py new file mode 100644 index 000000000..344054ad8 --- /dev/null +++ b/workflow/plugins/python/math/math_abs.py @@ -0,0 +1,7 @@ +"""Workflow plugin: absolute value.""" + + +def run(_runtime, inputs): + """Calculate absolute value.""" + value = inputs.get("value", 0) + return {"result": abs(value)} diff --git a/workflow/plugins/python/math/math_add.py b/workflow/plugins/python/math/math_add.py new file mode 100644 index 000000000..726dea252 --- /dev/null +++ b/workflow/plugins/python/math/math_add.py @@ -0,0 +1,7 @@ +"""Workflow plugin: add numbers.""" + + +def run(_runtime, inputs): + """Add two or more numbers.""" + numbers = inputs.get("numbers", []) + return {"result": sum(numbers)} diff --git a/workflow/plugins/python/math/math_divide.py b/workflow/plugins/python/math/math_divide.py new file mode 100644 index 000000000..aff10336b --- /dev/null +++ b/workflow/plugins/python/math/math_divide.py @@ -0,0 +1,12 @@ +"""Workflow plugin: divide numbers.""" + + +def run(_runtime, inputs): + """Divide a by b.""" + a = inputs.get("a", 0) + b = inputs.get("b", 1) + + if b == 0: + return {"result": None, "error": "Division by zero"} + + return {"result": a / b} diff --git a/workflow/plugins/python/math/math_max.py b/workflow/plugins/python/math/math_max.py new file mode 100644 index 000000000..42e2728aa --- /dev/null +++ b/workflow/plugins/python/math/math_max.py @@ -0,0 +1,11 @@ +"""Workflow plugin: maximum value.""" + + +def run(_runtime, inputs): + """Find maximum value in numbers.""" + numbers = inputs.get("numbers", []) + + if not numbers: + return {"result": None} + + return {"result": max(numbers)} diff --git a/workflow/plugins/python/math/math_min.py b/workflow/plugins/python/math/math_min.py new file mode 100644 index 000000000..71b883109 --- /dev/null +++ b/workflow/plugins/python/math/math_min.py @@ -0,0 +1,11 @@ +"""Workflow plugin: minimum value.""" + + +def run(_runtime, inputs): + """Find minimum value in numbers.""" + numbers = inputs.get("numbers", []) + + if not numbers: + return {"result": None} + + return {"result": min(numbers)} diff --git a/workflow/plugins/python/math/math_modulo.py b/workflow/plugins/python/math/math_modulo.py new file mode 100644 index 000000000..f64578890 --- /dev/null +++ b/workflow/plugins/python/math/math_modulo.py @@ -0,0 +1,12 @@ +"""Workflow plugin: modulo operation.""" + + +def run(_runtime, inputs): + """Calculate a modulo b.""" + a = inputs.get("a", 0) + b = inputs.get("b", 1) + + if b == 0: + return {"result": None, "error": "Modulo by zero"} + + return {"result": a % b} diff --git a/workflow/plugins/python/math/math_multiply.py b/workflow/plugins/python/math/math_multiply.py new file mode 100644 index 000000000..ed4cfc529 --- /dev/null +++ b/workflow/plugins/python/math/math_multiply.py @@ -0,0 +1,10 @@ +"""Workflow plugin: multiply numbers.""" + + +def run(_runtime, inputs): + """Multiply two or more numbers.""" + numbers = inputs.get("numbers", []) + result = 1 + for num in numbers: + result *= num + return {"result": result} diff --git a/workflow/plugins/python/math/math_power.py b/workflow/plugins/python/math/math_power.py new file mode 100644 index 000000000..931126898 --- /dev/null +++ b/workflow/plugins/python/math/math_power.py @@ -0,0 +1,8 @@ +"""Workflow plugin: power operation.""" + + +def run(_runtime, inputs): + """Calculate a to the power of b.""" + a = inputs.get("a", 0) + b = inputs.get("b", 1) + return {"result": a ** b} diff --git a/workflow/plugins/python/math/math_round.py b/workflow/plugins/python/math/math_round.py new file mode 100644 index 000000000..4fd81f5e8 --- /dev/null +++ b/workflow/plugins/python/math/math_round.py @@ -0,0 +1,8 @@ +"""Workflow plugin: round number.""" + + +def run(_runtime, inputs): + """Round number to specified precision.""" + value = inputs.get("value", 0) + precision = inputs.get("precision", 0) + return {"result": round(value, precision)} diff --git a/workflow/plugins/python/math/math_subtract.py b/workflow/plugins/python/math/math_subtract.py new file mode 100644 index 000000000..1898efd7d --- /dev/null +++ b/workflow/plugins/python/math/math_subtract.py @@ -0,0 +1,8 @@ +"""Workflow plugin: subtract numbers.""" + + +def run(_runtime, inputs): + """Subtract b from a.""" + a = inputs.get("a", 0) + b = inputs.get("b", 0) + return {"result": a - b} diff --git a/workflow/plugins/python/notifications/__init__.py b/workflow/plugins/python/notifications/__init__.py new file mode 100644 index 000000000..5adf81e29 --- /dev/null +++ b/workflow/plugins/python/notifications/__init__.py @@ -0,0 +1 @@ +"""Notification plugins: Slack, Discord, and multi-channel notifications.""" diff --git a/workflow/plugins/python/notifications/notifications_all.py b/workflow/plugins/python/notifications/notifications_all.py new file mode 100644 index 000000000..345013dec --- /dev/null +++ b/workflow/plugins/python/notifications/notifications_all.py @@ -0,0 +1,33 @@ +"""Workflow plugin: send notification to all channels.""" +import os +import logging + +logger = logging.getLogger("metabuilder.notifications") + + +def run(runtime, inputs): + """Send a notification to all configured channels (Slack and Discord). + + Inputs: + message: The message to send to all channels + + Returns: + dict: Contains success status for all channels + """ + message = inputs.get("message", "") + + # Import sibling plugins + from . import notifications_slack, notifications_discord + + # Send to Slack + slack_result = notifications_slack.run(runtime, {"message": message}) + + # Send to Discord + discord_result = notifications_discord.run(runtime, {"message": message}) + + return { + "success": True, + "message": "Notifications sent to all channels", + "slack": slack_result, + "discord": discord_result + } diff --git a/workflow/plugins/python/notifications/notifications_discord.py b/workflow/plugins/python/notifications/notifications_discord.py new file mode 100644 index 000000000..412ff7fc9 --- /dev/null +++ b/workflow/plugins/python/notifications/notifications_discord.py @@ -0,0 +1,67 @@ +"""Workflow plugin: send Discord notification.""" +import os +import logging +import asyncio + +logger = logging.getLogger("metabuilder.notifications") + + +async def _send_discord_notification_async(message: str, token: str, intents, channel_id: str): + """Send Discord notification asynchronously.""" + import discord + + client = discord.Client(intents=intents) + + @client.event + async def on_ready(): + channel = client.get_channel(int(channel_id)) + if channel: + await channel.send(message) + logger.info("Discord notification sent successfully.") + await client.close() + + try: + await client.start(token) + except Exception as e: + logger.error(f"Error sending Discord notification: {e}") + raise + + +def run(runtime, inputs): + """Send a notification to Discord. + + Inputs: + message: The message to send + channel_id: Optional channel ID (defaults to DISCORD_CHANNEL_ID env var) + + Returns: + dict: Contains success status and any error message + """ + message = inputs.get("message", "") + channel_id = inputs.get("channel_id") or os.environ.get("DISCORD_CHANNEL_ID") + + token = runtime.context.get("discord_token") + intents = runtime.context.get("discord_intents") + + if not token: + logger.warning("Discord notification skipped: Discord client not initialized.") + return { + "success": False, + "skipped": True, + "error": "Discord client not initialized" + } + + if not channel_id: + logger.warning("Discord notification skipped: DISCORD_CHANNEL_ID missing.") + return { + "success": False, + "skipped": True, + "error": "DISCORD_CHANNEL_ID missing" + } + + try: + asyncio.run(_send_discord_notification_async(message, token, intents, channel_id)) + return {"success": True, "message": "Discord notification sent"} + except Exception as e: + logger.error(f"Error running Discord notification: {e}") + return {"success": False, "error": str(e)} diff --git a/workflow/plugins/python/notifications/notifications_slack.py b/workflow/plugins/python/notifications/notifications_slack.py new file mode 100644 index 000000000..711406f43 --- /dev/null +++ b/workflow/plugins/python/notifications/notifications_slack.py @@ -0,0 +1,46 @@ +"""Workflow plugin: send Slack notification.""" +import os +import logging + +logger = logging.getLogger("metabuilder.notifications") + + +def run(runtime, inputs): + """Send a notification to Slack. + + Inputs: + message: The message to send + channel: Optional channel (defaults to SLACK_CHANNEL env var) + + Returns: + dict: Contains success status and any error message + """ + message = inputs.get("message", "") + channel = inputs.get("channel") or os.environ.get("SLACK_CHANNEL") + + client = runtime.context.get("slack_client") + + if not client: + logger.warning("Slack notification skipped: Slack client not initialized.") + return { + "success": False, + "skipped": True, + "error": "Slack client not initialized" + } + + if not channel: + logger.warning("Slack notification skipped: SLACK_CHANNEL missing.") + return { + "success": False, + "skipped": True, + "error": "SLACK_CHANNEL missing" + } + + try: + from slack_sdk.errors import SlackApiError + client.chat_postMessage(channel=channel, text=message) + logger.info("Slack notification sent successfully.") + return {"success": True, "message": "Slack notification sent"} + except SlackApiError as e: + logger.error(f"Error sending Slack notification: {e}") + return {"success": False, "error": str(e)} diff --git a/workflow/plugins/python/string/__init__.py b/workflow/plugins/python/string/__init__.py new file mode 100644 index 000000000..ee5bfcdf4 --- /dev/null +++ b/workflow/plugins/python/string/__init__.py @@ -0,0 +1 @@ +"""String manipulation plugins.""" diff --git a/workflow/plugins/python/string/string_concat.py b/workflow/plugins/python/string/string_concat.py new file mode 100644 index 000000000..2935b2013 --- /dev/null +++ b/workflow/plugins/python/string/string_concat.py @@ -0,0 +1,10 @@ +"""Workflow plugin: concatenate strings.""" + + +def run(_runtime, inputs): + """Concatenate multiple strings.""" + strings = inputs.get("strings", []) + separator = inputs.get("separator", "") + + str_list = [str(s) for s in strings] + return {"result": separator.join(str_list)} diff --git a/workflow/plugins/python/string/string_format.py b/workflow/plugins/python/string/string_format.py new file mode 100644 index 000000000..e913d2f5c --- /dev/null +++ b/workflow/plugins/python/string/string_format.py @@ -0,0 +1,13 @@ +"""Workflow plugin: format string with variables.""" + + +def run(_runtime, inputs): + """Format string with variables.""" + template = inputs.get("template", "") + variables = inputs.get("variables", {}) + + try: + result = template.format(**variables) + return {"result": result} + except (KeyError, ValueError) as e: + return {"result": template, "error": str(e)} diff --git a/workflow/plugins/python/string/string_length.py b/workflow/plugins/python/string/string_length.py new file mode 100644 index 000000000..e459e5d18 --- /dev/null +++ b/workflow/plugins/python/string/string_length.py @@ -0,0 +1,7 @@ +"""Workflow plugin: get string length.""" + + +def run(_runtime, inputs): + """Get length of a string.""" + text = inputs.get("text", "") + return {"result": len(text)} diff --git a/workflow/plugins/python/string/string_lower.py b/workflow/plugins/python/string/string_lower.py new file mode 100644 index 000000000..73e8f7357 --- /dev/null +++ b/workflow/plugins/python/string/string_lower.py @@ -0,0 +1,7 @@ +"""Workflow plugin: convert string to lowercase.""" + + +def run(_runtime, inputs): + """Convert string to lowercase.""" + text = inputs.get("text", "") + return {"result": text.lower()} diff --git a/workflow/plugins/python/string/string_replace.py b/workflow/plugins/python/string/string_replace.py new file mode 100644 index 000000000..2d23b7e3e --- /dev/null +++ b/workflow/plugins/python/string/string_replace.py @@ -0,0 +1,12 @@ +"""Workflow plugin: replace in string.""" + + +def run(_runtime, inputs): + """Replace occurrences in string.""" + text = inputs.get("text", "") + old = inputs.get("old", "") + new = inputs.get("new", "") + count = inputs.get("count", -1) + + result = text.replace(old, new, count) + return {"result": result} diff --git a/workflow/plugins/python/string/string_split.py b/workflow/plugins/python/string/string_split.py new file mode 100644 index 000000000..6b38fae50 --- /dev/null +++ b/workflow/plugins/python/string/string_split.py @@ -0,0 +1,15 @@ +"""Workflow plugin: split string.""" + + +def run(_runtime, inputs): + """Split string by separator.""" + text = inputs.get("text", "") + separator = inputs.get("separator", " ") + max_splits = inputs.get("max_splits") + + if max_splits is not None: + result = text.split(separator, max_splits) + else: + result = text.split(separator) + + return {"result": result} diff --git a/workflow/plugins/python/string/string_trim.py b/workflow/plugins/python/string/string_trim.py new file mode 100644 index 000000000..e1126d3b9 --- /dev/null +++ b/workflow/plugins/python/string/string_trim.py @@ -0,0 +1,16 @@ +"""Workflow plugin: trim whitespace from string.""" + + +def run(_runtime, inputs): + """Trim whitespace from string.""" + text = inputs.get("text", "") + mode = inputs.get("mode", "both") + + if mode == "start": + result = text.lstrip() + elif mode == "end": + result = text.rstrip() + else: + result = text.strip() + + return {"result": result} diff --git a/workflow/plugins/python/string/string_upper.py b/workflow/plugins/python/string/string_upper.py new file mode 100644 index 000000000..2490a34f3 --- /dev/null +++ b/workflow/plugins/python/string/string_upper.py @@ -0,0 +1,7 @@ +"""Workflow plugin: convert string to uppercase.""" + + +def run(_runtime, inputs): + """Convert string to uppercase.""" + text = inputs.get("text", "") + return {"result": text.upper()} diff --git a/workflow/plugins/python/test/__init__.py b/workflow/plugins/python/test/__init__.py new file mode 100644 index 000000000..47948dd67 --- /dev/null +++ b/workflow/plugins/python/test/__init__.py @@ -0,0 +1 @@ +"""Unit testing assertion plugins.""" diff --git a/workflow/plugins/python/test/test_assert_equals.py b/workflow/plugins/python/test/test_assert_equals.py new file mode 100644 index 000000000..ecea4e3ff --- /dev/null +++ b/workflow/plugins/python/test/test_assert_equals.py @@ -0,0 +1,26 @@ +"""Workflow plugin: assert two values are equal.""" + + +def run(_runtime, inputs): + """Assert that two values are equal.""" + actual = inputs.get("actual") + expected = inputs.get("expected") + message = inputs.get("message", "") + + passed = actual == expected + + if not passed: + error_msg = f"Assertion failed: {message}" if message else "Assertion failed" + error_msg += f"\n Expected: {expected}\n Actual: {actual}" + return { + "passed": False, + "error": error_msg, + "expected": expected, + "actual": actual + } + + return { + "passed": True, + "expected": expected, + "actual": actual + } diff --git a/workflow/plugins/python/test/test_assert_exists.py b/workflow/plugins/python/test/test_assert_exists.py new file mode 100644 index 000000000..13a139d37 --- /dev/null +++ b/workflow/plugins/python/test/test_assert_exists.py @@ -0,0 +1,23 @@ +"""Workflow plugin: assert value exists (is not None/null).""" + + +def run(_runtime, inputs): + """Assert that a value exists (is not None).""" + value = inputs.get("value") + message = inputs.get("message", "") + + passed = value is not None + + if not passed: + error_msg = f"Assertion failed: {message}" if message else "Assertion failed" + error_msg += f"\n Expected: non-null value\n Actual: None" + return { + "passed": False, + "error": error_msg, + "value": value + } + + return { + "passed": True, + "value": value + } diff --git a/workflow/plugins/python/test/test_assert_false.py b/workflow/plugins/python/test/test_assert_false.py new file mode 100644 index 000000000..11073e539 --- /dev/null +++ b/workflow/plugins/python/test/test_assert_false.py @@ -0,0 +1,23 @@ +"""Workflow plugin: assert value is false.""" + + +def run(_runtime, inputs): + """Assert that a value is false.""" + value = inputs.get("value") + message = inputs.get("message", "") + + passed = value is False + + if not passed: + error_msg = f"Assertion failed: {message}" if message else "Assertion failed" + error_msg += f"\n Expected: False\n Actual: {value}" + return { + "passed": False, + "error": error_msg, + "value": value + } + + return { + "passed": True, + "value": value + } diff --git a/workflow/plugins/python/test/test_assert_true.py b/workflow/plugins/python/test/test_assert_true.py new file mode 100644 index 000000000..8bc25d37c --- /dev/null +++ b/workflow/plugins/python/test/test_assert_true.py @@ -0,0 +1,23 @@ +"""Workflow plugin: assert value is true.""" + + +def run(_runtime, inputs): + """Assert that a value is true.""" + value = inputs.get("value") + message = inputs.get("message", "") + + passed = value is True + + if not passed: + error_msg = f"Assertion failed: {message}" if message else "Assertion failed" + error_msg += f"\n Expected: True\n Actual: {value}" + return { + "passed": False, + "error": error_msg, + "value": value + } + + return { + "passed": True, + "value": value + } diff --git a/workflow/plugins/python/test/test_run_suite.py b/workflow/plugins/python/test/test_run_suite.py new file mode 100644 index 000000000..900f39ece --- /dev/null +++ b/workflow/plugins/python/test/test_run_suite.py @@ -0,0 +1,63 @@ +"""Workflow plugin: run a suite of test assertions and report results.""" + + +def run(_runtime, inputs): + """Run a suite of test assertions and aggregate results. + + Inputs: + - results: Array of test result objects (each with 'passed' field) + - suite_name: Optional name for the test suite + + Outputs: + - passed: Boolean indicating if all tests passed + - total: Total number of tests + - passed_count: Number of tests that passed + - failed_count: Number of tests that failed + - failures: Array of failed test details + """ + results = inputs.get("results", []) + suite_name = inputs.get("suite_name", "Test Suite") + + if not isinstance(results, list): + return { + "passed": False, + "error": "results must be an array", + "total": 0, + "passed_count": 0, + "failed_count": 0, + "failures": [] + } + + total = len(results) + passed_count = 0 + failed_count = 0 + failures = [] + + for i, result in enumerate(results): + if isinstance(result, dict) and result.get("passed") is True: + passed_count += 1 + else: + failed_count += 1 + failure_info = { + "test_index": i, + "error": result.get("error", "Unknown error") if isinstance(result, dict) else str(result) + } + if isinstance(result, dict): + failure_info.update({ + "expected": result.get("expected"), + "actual": result.get("actual") + }) + failures.append(failure_info) + + all_passed = failed_count == 0 and total > 0 + + summary = f"{suite_name}: {passed_count}/{total} tests passed" + + return { + "passed": all_passed, + "total": total, + "passed_count": passed_count, + "failed_count": failed_count, + "failures": failures, + "summary": summary + } diff --git a/workflow/plugins/python/tools/__init__.py b/workflow/plugins/python/tools/__init__.py new file mode 100644 index 000000000..eee3c5846 --- /dev/null +++ b/workflow/plugins/python/tools/__init__.py @@ -0,0 +1 @@ +"""External tool integration plugins.""" diff --git a/workflow/plugins/python/tools/tools_create_branch.py b/workflow/plugins/python/tools/tools_create_branch.py new file mode 100644 index 000000000..e11142d10 --- /dev/null +++ b/workflow/plugins/python/tools/tools_create_branch.py @@ -0,0 +1,11 @@ +"""Workflow plugin: create branch.""" + + +def run(runtime, inputs): + """Create a branch via tool runner.""" + result = runtime.tool_runner.call( + "create_branch", + branch_name=inputs.get("branch_name"), + base_branch=inputs.get("base_branch", "main") + ) + return {"result": result} diff --git a/workflow/plugins/python/tools/tools_create_pull_request.py b/workflow/plugins/python/tools/tools_create_pull_request.py new file mode 100644 index 000000000..1128f6f77 --- /dev/null +++ b/workflow/plugins/python/tools/tools_create_pull_request.py @@ -0,0 +1,13 @@ +"""Workflow plugin: create pull request.""" + + +def run(runtime, inputs): + """Create a pull request via tool runner.""" + result = runtime.tool_runner.call( + "create_pull_request", + title=inputs.get("title"), + body=inputs.get("body"), + head_branch=inputs.get("head_branch"), + base_branch=inputs.get("base_branch", "main") + ) + return {"result": result} diff --git a/workflow/plugins/python/tools/tools_list_files.py b/workflow/plugins/python/tools/tools_list_files.py new file mode 100644 index 000000000..90a8facfd --- /dev/null +++ b/workflow/plugins/python/tools/tools_list_files.py @@ -0,0 +1,7 @@ +"""Workflow plugin: list files.""" + + +def run(runtime, inputs): + """List files via tool runner.""" + result = runtime.tool_runner.call("list_files", directory=inputs.get("path", ".")) + return {"files": result} diff --git a/workflow/plugins/python/tools/tools_read_file.py b/workflow/plugins/python/tools/tools_read_file.py new file mode 100644 index 000000000..b41962315 --- /dev/null +++ b/workflow/plugins/python/tools/tools_read_file.py @@ -0,0 +1,7 @@ +"""Workflow plugin: read file.""" + + +def run(runtime, inputs): + """Read a file via tool runner.""" + result = runtime.tool_runner.call("read_file", path=inputs.get("path")) + return {"content": result} diff --git a/workflow/plugins/python/tools/tools_run_docker.py b/workflow/plugins/python/tools/tools_run_docker.py new file mode 100644 index 000000000..513a9764f --- /dev/null +++ b/workflow/plugins/python/tools/tools_run_docker.py @@ -0,0 +1,59 @@ +"""Workflow plugin: run command in Docker container.""" +import subprocess +import os +import logging + +logger = logging.getLogger("metabuilder.docker") + + +def _run_command_in_docker(image: str, command: str, volumes: dict = None, workdir: str = None): + """Run a command inside a Docker container. + + :param image: Docker image to use. + :param command: Command to execute. + :param volumes: Dictionary of volume mappings {host_path: container_path}. + :param workdir: Working directory inside the container. + :return: Standard output of the command. + """ + docker_command = ["docker", "run", "--rm"] + + if volumes: + for host_path, container_path in volumes.items(): + docker_command.extend(["-v", f"{os.path.abspath(host_path)}:{container_path}"]) + + if workdir: + docker_command.extend(["-w", workdir]) + + docker_command.append(image) + docker_command.extend(["sh", "-c", command]) + + logger.info(f"Executing in Docker ({image}): {command}") + result = subprocess.run(docker_command, capture_output=True, text=True, check=False) + + output = result.stdout + if result.stderr: + output += "\n" + result.stderr + + logger.info(output) + return output + + +def run(_runtime, inputs): + """Run a command inside a Docker container. + + Inputs: + - image: Docker image to use + - command: Command to execute + - volumes: Optional dict of volume mappings {host_path: container_path} + - workdir: Optional working directory inside the container + """ + image = inputs.get("image") + command = inputs.get("command") + volumes = inputs.get("volumes") + workdir = inputs.get("workdir") + + if not image or not command: + return {"error": "Both 'image' and 'command' are required"} + + output = _run_command_in_docker(image, command, volumes, workdir) + return {"output": output} diff --git a/workflow/plugins/python/tools/tools_run_lint.py b/workflow/plugins/python/tools/tools_run_lint.py new file mode 100644 index 000000000..085c363a1 --- /dev/null +++ b/workflow/plugins/python/tools/tools_run_lint.py @@ -0,0 +1,7 @@ +"""Workflow plugin: run lint.""" + + +def run(runtime, inputs): + """Run lint via tool runner.""" + result = runtime.tool_runner.call("run_lint", path=inputs.get("path", "src")) + return {"results": result} diff --git a/workflow/plugins/python/tools/tools_run_tests.py b/workflow/plugins/python/tools/tools_run_tests.py new file mode 100644 index 000000000..2061f3a1f --- /dev/null +++ b/workflow/plugins/python/tools/tools_run_tests.py @@ -0,0 +1,7 @@ +"""Workflow plugin: run tests.""" + + +def run(runtime, inputs): + """Run tests via tool runner.""" + result = runtime.tool_runner.call("run_tests", path=inputs.get("path", "tests")) + return {"results": result} diff --git a/workflow/plugins/python/utils/__init__.py b/workflow/plugins/python/utils/__init__.py new file mode 100644 index 000000000..304cae565 --- /dev/null +++ b/workflow/plugins/python/utils/__init__.py @@ -0,0 +1 @@ +"""Utility plugins: filtering, mapping, branching, MVP checking.""" diff --git a/workflow/plugins/python/utils/utils_branch_condition.py b/workflow/plugins/python/utils/utils_branch_condition.py new file mode 100644 index 000000000..117b9f1bb --- /dev/null +++ b/workflow/plugins/python/utils/utils_branch_condition.py @@ -0,0 +1,25 @@ +"""Workflow plugin: branch condition.""" +import re + + +def run(_runtime, inputs): + """Evaluate a branch condition.""" + value = inputs.get("value") + mode = inputs.get("mode", "is_truthy") + compare = inputs.get("compare", "") + decision = False + + if mode == "is_empty": + decision = not value if isinstance(value, (list, dict, str)) else not bool(value) + elif mode == "is_truthy": + decision = bool(value) + elif mode == "equals": + decision = str(value) == compare + elif mode == "not_equals": + decision = str(value) != compare + elif mode == "contains": + decision = compare in str(value) + elif mode == "regex": + decision = bool(re.search(compare, str(value))) + + return {"result": decision} diff --git a/workflow/plugins/python/utils/utils_check_mvp.py b/workflow/plugins/python/utils/utils_check_mvp.py new file mode 100644 index 000000000..215d29315 --- /dev/null +++ b/workflow/plugins/python/utils/utils_check_mvp.py @@ -0,0 +1,37 @@ +"""Workflow plugin: check if MVP is reached.""" +import os +import re + + +def _is_mvp_reached() -> bool: + """Check if the MVP section in ROADMAP.md is completed.""" + if not os.path.exists("ROADMAP.md"): + return False + + with open("ROADMAP.md", "r", encoding="utf-8") as f: + content = f.read() + + # Find the header line containing (MVP) + header_match = re.search(r"^## .*?\(MVP\).*?$", content, re.MULTILINE | re.IGNORECASE) + if not header_match: + return False + + start_pos = header_match.end() + next_header_match = re.search(r"^## ", content[start_pos:], re.MULTILINE) + if next_header_match: + mvp_section = content[start_pos : start_pos + next_header_match.start()] + else: + mvp_section = content[start_pos:] + + if "[ ]" in mvp_section: + return False + if "[x]" in mvp_section: + return True + + return False + + +def run(_runtime, _inputs): + """Check if the MVP section in ROADMAP.md is completed.""" + mvp_reached = _is_mvp_reached() + return {"mvp_reached": mvp_reached} diff --git a/workflow/plugins/python/utils/utils_filter_list.py b/workflow/plugins/python/utils/utils_filter_list.py new file mode 100644 index 000000000..9e62d4fe6 --- /dev/null +++ b/workflow/plugins/python/utils/utils_filter_list.py @@ -0,0 +1,33 @@ +"""Workflow plugin: filter list.""" +import re + + +def run(_runtime, inputs): + """Filter items using a match mode.""" + items = inputs.get("items", []) + if not isinstance(items, list): + items = [items] if items else [] + + mode = inputs.get("mode", "contains") + pattern = inputs.get("pattern", "") + filtered = [] + + for item in items: + candidate = str(item) + matched = False + if mode == "contains": + matched = pattern in candidate + elif mode == "regex": + matched = bool(re.search(pattern, candidate)) + elif mode == "equals": + matched = candidate == pattern + elif mode == "not_equals": + matched = candidate != pattern + elif mode == "starts_with": + matched = candidate.startswith(pattern) + elif mode == "ends_with": + matched = candidate.endswith(pattern) + if matched: + filtered.append(item) + + return {"items": filtered} diff --git a/workflow/plugins/python/utils/utils_map_list.py b/workflow/plugins/python/utils/utils_map_list.py new file mode 100644 index 000000000..bccc39b1e --- /dev/null +++ b/workflow/plugins/python/utils/utils_map_list.py @@ -0,0 +1,19 @@ +"""Workflow plugin: map list.""" + + +def run(_runtime, inputs): + """Map items to formatted strings.""" + items = inputs.get("items", []) + if not isinstance(items, list): + items = [items] if items else [] + + template = inputs.get("template", "{item}") + mapped = [] + + for item in items: + try: + mapped.append(template.format(item=item)) + except Exception: + mapped.append(str(item)) + + return {"items": mapped} diff --git a/workflow/plugins/python/utils/utils_not.py b/workflow/plugins/python/utils/utils_not.py new file mode 100644 index 000000000..880e00f25 --- /dev/null +++ b/workflow/plugins/python/utils/utils_not.py @@ -0,0 +1,7 @@ +"""Workflow plugin: boolean not.""" + + +def run(_runtime, inputs): + """Negate a boolean value.""" + value = inputs.get("value") + return {"result": not bool(value)} diff --git a/workflow/plugins/python/utils/utils_reduce_list.py b/workflow/plugins/python/utils/utils_reduce_list.py new file mode 100644 index 000000000..69bb12820 --- /dev/null +++ b/workflow/plugins/python/utils/utils_reduce_list.py @@ -0,0 +1,18 @@ +"""Workflow plugin: reduce list.""" + + +def run(_runtime, inputs): + """Reduce a list into a string.""" + items = inputs.get("items", []) + if not isinstance(items, list): + items = [items] if items else [] + + separator = inputs.get("separator", "") + # Handle escape sequences + if separator == "\\n": + separator = "\n" + elif separator == "\\t": + separator = "\t" + + reduced = separator.join([str(item) for item in items]) + return {"result": reduced} diff --git a/workflow/plugins/python/utils/utils_update_roadmap.py b/workflow/plugins/python/utils/utils_update_roadmap.py new file mode 100644 index 000000000..7750eb17a --- /dev/null +++ b/workflow/plugins/python/utils/utils_update_roadmap.py @@ -0,0 +1,21 @@ +"""Workflow plugin: update roadmap file.""" +import logging + +logger = logging.getLogger("metabuilder") + + +def _update_roadmap(content: str): + """Update ROADMAP.md with new content.""" + with open("ROADMAP.md", "w", encoding="utf-8") as f: + f.write(content) + logger.info("ROADMAP.md updated successfully.") + + +def run(_runtime, inputs): + """Update ROADMAP.md with new content.""" + content = inputs.get("content") + if not content: + return {"error": "Content is required"} + + _update_roadmap(content) + return {"result": "ROADMAP.md updated successfully"} diff --git a/workflow/plugins/python/var/__init__.py b/workflow/plugins/python/var/__init__.py new file mode 100644 index 000000000..b351e5fd1 --- /dev/null +++ b/workflow/plugins/python/var/__init__.py @@ -0,0 +1 @@ +"""Variable management plugins for workflow store.""" diff --git a/workflow/plugins/python/var/var_delete.py b/workflow/plugins/python/var/var_delete.py new file mode 100644 index 000000000..b78a7466a --- /dev/null +++ b/workflow/plugins/python/var/var_delete.py @@ -0,0 +1,15 @@ +"""Workflow plugin: delete variable from workflow store.""" + + +def run(runtime, inputs): + """Delete variable from workflow store.""" + key = inputs.get("key") + + if key is None: + return {"result": False, "deleted": False, "error": "key is required"} + + if key in runtime.store: + del runtime.store[key] + return {"result": True, "deleted": True} + + return {"result": False, "deleted": False} diff --git a/workflow/plugins/python/var/var_exists.py b/workflow/plugins/python/var/var_exists.py new file mode 100644 index 000000000..7e7ed44de --- /dev/null +++ b/workflow/plugins/python/var/var_exists.py @@ -0,0 +1,13 @@ +"""Workflow plugin: check if variable exists in workflow store.""" + + +def run(runtime, inputs): + """Check if variable exists in workflow store.""" + key = inputs.get("key") + + if key is None: + return {"result": False, "error": "key is required"} + + exists = key in runtime.store + + return {"result": exists} diff --git a/workflow/plugins/python/var/var_get.py b/workflow/plugins/python/var/var_get.py new file mode 100644 index 000000000..f34b7dac0 --- /dev/null +++ b/workflow/plugins/python/var/var_get.py @@ -0,0 +1,15 @@ +"""Workflow plugin: get variable from workflow store.""" + + +def run(runtime, inputs): + """Get variable value from workflow store.""" + key = inputs.get("key") + default = inputs.get("default") + + if key is None: + return {"result": default, "exists": False, "error": "key is required"} + + exists = key in runtime.store + value = runtime.store.get(key, default) + + return {"result": value, "exists": exists} diff --git a/workflow/plugins/python/var/var_set.py b/workflow/plugins/python/var/var_set.py new file mode 100644 index 000000000..baea19953 --- /dev/null +++ b/workflow/plugins/python/var/var_set.py @@ -0,0 +1,14 @@ +"""Workflow plugin: set variable in workflow store.""" + + +def run(runtime, inputs): + """Set variable value in workflow store.""" + key = inputs.get("key") + value = inputs.get("value") + + if key is None: + return {"result": None, "key": None, "error": "key is required"} + + runtime.store[key] = value + + return {"result": value, "key": key} diff --git a/workflow/plugins/python/web/__init__.py b/workflow/plugins/python/web/__init__.py new file mode 100644 index 000000000..388865998 --- /dev/null +++ b/workflow/plugins/python/web/__init__.py @@ -0,0 +1,44 @@ +"""Web workflow plugins: Flask server, API endpoints, file I/O, translations. + +These plugins provide workflow-based access to web data operations, enabling +declarative workflows to interact with web-related functionality. + +Available Plugins: + +Environment Management: +- web_get_env_vars - Load environment variables +- web_persist_env_vars - Save environment variables + +File I/O: +- web_read_json - Read JSON files +- web_get_recent_logs - Get recent log entries +- web_load_messages - Load translation messages + +Translation Management: +- web_list_translations - List available translations +- web_load_translation - Load a translation +- web_create_translation - Create new translation +- web_update_translation - Update translation +- web_delete_translation - Delete translation +- web_get_ui_messages - Get UI messages with fallback + +Navigation & Metadata: +- web_get_navigation_items - Get navigation menu items + +Prompt Management: +- web_get_prompt_content - Read prompt content +- web_write_prompt - Write prompt content +- web_build_prompt_yaml - Build YAML prompt + +Workflow Operations: +- web_get_workflow_content - Read workflow JSON +- web_write_workflow - Write workflow JSON +- web_load_workflow_packages - Load workflow packages +- web_summarize_workflow_packages - Summarize packages + +Flask Server Setup: +- web_create_flask_app - Create Flask application +- web_register_blueprint - Register Flask blueprints +- web_start_server - Start Flask server +- web_build_context - Build API context +""" diff --git a/workflow/plugins/python/web/web_build_prompt_yaml.py b/workflow/plugins/python/web/web_build_prompt_yaml.py new file mode 100644 index 000000000..4c891935e --- /dev/null +++ b/workflow/plugins/python/web/web_build_prompt_yaml.py @@ -0,0 +1,29 @@ +"""Workflow plugin: build prompt YAML.""" + + +def run(_runtime, inputs): + """Build prompt YAML from system and user content.""" + system_content = inputs.get("system_content") + user_content = inputs.get("user_content") + model = inputs.get("model") + + def indent_block(text): + if not text: + return "" + return "\n ".join(line.rstrip() for line in text.splitlines()) + + model_value = model or "openai/gpt-4o" + system_block = indent_block(system_content) + user_block = indent_block(user_content) + + yaml_content = f"""messages: + - role: system + content: >- + {system_block} + - role: user + content: >- + {user_block} +model: {model_value} +""" + + return {"result": yaml_content} diff --git a/workflow/plugins/python/web/web_create_flask_app.py b/workflow/plugins/python/web/web_create_flask_app.py new file mode 100644 index 000000000..889656413 --- /dev/null +++ b/workflow/plugins/python/web/web_create_flask_app.py @@ -0,0 +1,27 @@ +"""Workflow plugin: create Flask app.""" +from flask import Flask + + +def run(runtime, inputs): + """Create a Flask application instance. + + Inputs: + name: Application name (default: __name__) + config: Dictionary of Flask configuration options + + Returns: + dict: Contains the Flask app in result + """ + name = inputs.get("name", "__main__") + config = inputs.get("config", {}) + + app = Flask(name) + + # Apply configuration + for key, value in config.items(): + app.config[key] = value + + # Store app in runtime context for other plugins to use + runtime.context["flask_app"] = app + + return {"result": app, "message": "Flask app created"} diff --git a/workflow/plugins/python/web/web_get_env_vars.py b/workflow/plugins/python/web/web_get_env_vars.py new file mode 100644 index 000000000..cbd491e0a --- /dev/null +++ b/workflow/plugins/python/web/web_get_env_vars.py @@ -0,0 +1,22 @@ +"""Workflow plugin: get environment variables.""" +from pathlib import Path + + +def run(_runtime, _inputs): + """Get environment variables from .env file.""" + env_path = Path(".env") + if not env_path.exists(): + return {"result": {}} + + result = {} + for raw in env_path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + continue + key, value = line.split("=", 1) + value = value.strip().strip("'\"") + result[key.strip()] = value + + return {"result": result} diff --git a/workflow/plugins/python/web/web_get_prompt_content.py b/workflow/plugins/python/web/web_get_prompt_content.py new file mode 100644 index 000000000..fd8eb67df --- /dev/null +++ b/workflow/plugins/python/web/web_get_prompt_content.py @@ -0,0 +1,12 @@ +"""Workflow plugin: get prompt content.""" +import os +from pathlib import Path + + +def run(_runtime, _inputs): + """Get prompt content from prompt file.""" + path = Path(os.environ.get("PROMPT_PATH", "prompt.yml")) + if path.is_file(): + content = path.read_text(encoding="utf-8") + return {"result": content} + return {"result": ""} diff --git a/workflow/plugins/python/web/web_get_recent_logs.py b/workflow/plugins/python/web/web_get_recent_logs.py new file mode 100644 index 000000000..46dcce418 --- /dev/null +++ b/workflow/plugins/python/web/web_get_recent_logs.py @@ -0,0 +1,16 @@ +"""Workflow plugin: get recent logs.""" +from pathlib import Path + + +def run(_runtime, inputs): + """Get recent log entries.""" + lines = inputs.get("lines", 50) + log_file = Path("metabuilder.log") + + if not log_file.exists(): + return {"result": ""} + + with log_file.open("r", encoding="utf-8") as handle: + content = handle.readlines() + + return {"result": "".join(content[-lines:])} diff --git a/workflow/plugins/python/web/web_persist_env_vars.py b/workflow/plugins/python/web/web_persist_env_vars.py new file mode 100644 index 000000000..0cdcbbd32 --- /dev/null +++ b/workflow/plugins/python/web/web_persist_env_vars.py @@ -0,0 +1,15 @@ +"""Workflow plugin: persist environment variables.""" +from pathlib import Path + + +def run(_runtime, inputs): + """Persist environment variables to .env file.""" + from dotenv import set_key + + updates = inputs.get("updates", {}) + env_path = Path(".env") + env_path.touch(exist_ok=True) + for key, value in updates.items(): + set_key(env_path, key, value) + + return {"result": "Environment variables persisted"} diff --git a/workflow/plugins/python/web/web_read_json.py b/workflow/plugins/python/web/web_read_json.py new file mode 100644 index 000000000..201e30e7d --- /dev/null +++ b/workflow/plugins/python/web/web_read_json.py @@ -0,0 +1,21 @@ +"""Workflow plugin: read JSON file.""" +import json +from pathlib import Path + + +def run(_runtime, inputs): + """Read JSON file.""" + path = inputs.get("path") + if not path: + return {"error": "path is required"} + + path_obj = Path(path) + if not path_obj.exists(): + return {"result": {}} + + try: + json_data = json.loads(path_obj.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {"result": {}} + + return {"result": json_data} diff --git a/workflow/plugins/python/web/web_start_server.py b/workflow/plugins/python/web/web_start_server.py new file mode 100644 index 000000000..2bfb9f618 --- /dev/null +++ b/workflow/plugins/python/web/web_start_server.py @@ -0,0 +1,26 @@ +"""Workflow plugin: start Flask server.""" + + +def run(runtime, inputs): + """Start the Flask web server. + + Inputs: + host: Host address (default: 0.0.0.0) + port: Port number (default: 8000) + debug: Enable debug mode (default: False) + + Returns: + dict: Success indicator (note: this blocks until server stops) + """ + app = runtime.context.get("flask_app") + if not app: + return {"error": "Flask app not found in context. Run web.create_flask_app first."} + + host = inputs.get("host", "0.0.0.0") + port = inputs.get("port", 8000) + debug = inputs.get("debug", False) + + # This will block until the server is stopped + app.run(host=host, port=port, debug=debug) + + return {"result": "Server stopped"} diff --git a/workflow/plugins/python/web/web_write_prompt.py b/workflow/plugins/python/web/web_write_prompt.py new file mode 100644 index 000000000..7a3ffc4b5 --- /dev/null +++ b/workflow/plugins/python/web/web_write_prompt.py @@ -0,0 +1,11 @@ +"""Workflow plugin: write prompt.""" +import os +from pathlib import Path + + +def run(_runtime, inputs): + """Write prompt content to file.""" + content = inputs.get("content", "") + path = Path(os.environ.get("PROMPT_PATH", "prompt.yml")) + path.write_text(content or "", encoding="utf-8") + return {"result": "Prompt written successfully"} diff --git a/workflow/plugins/control-flow/condition/package.json b/workflow/plugins/ts/control-flow/condition/package.json similarity index 100% rename from workflow/plugins/control-flow/condition/package.json rename to workflow/plugins/ts/control-flow/condition/package.json diff --git a/workflow/plugins/control-flow/condition/src/index.ts b/workflow/plugins/ts/control-flow/condition/src/index.ts similarity index 100% rename from workflow/plugins/control-flow/condition/src/index.ts rename to workflow/plugins/ts/control-flow/condition/src/index.ts diff --git a/workflow/plugins/control-flow/condition/tsconfig.json b/workflow/plugins/ts/control-flow/condition/tsconfig.json similarity index 100% rename from workflow/plugins/control-flow/condition/tsconfig.json rename to workflow/plugins/ts/control-flow/condition/tsconfig.json diff --git a/workflow/plugins/dbal-read/README.md b/workflow/plugins/ts/dbal-read/README.md similarity index 100% rename from workflow/plugins/dbal-read/README.md rename to workflow/plugins/ts/dbal-read/README.md diff --git a/workflow/plugins/dbal-read/package.json b/workflow/plugins/ts/dbal-read/package.json similarity index 100% rename from workflow/plugins/dbal-read/package.json rename to workflow/plugins/ts/dbal-read/package.json diff --git a/workflow/plugins/dbal-read/src/index.ts b/workflow/plugins/ts/dbal-read/src/index.ts similarity index 100% rename from workflow/plugins/dbal-read/src/index.ts rename to workflow/plugins/ts/dbal-read/src/index.ts diff --git a/workflow/plugins/dbal-read/tsconfig.json b/workflow/plugins/ts/dbal-read/tsconfig.json similarity index 100% rename from workflow/plugins/dbal-read/tsconfig.json rename to workflow/plugins/ts/dbal-read/tsconfig.json diff --git a/workflow/plugins/dbal-write/package.json b/workflow/plugins/ts/dbal-write/package.json similarity index 100% rename from workflow/plugins/dbal-write/package.json rename to workflow/plugins/ts/dbal-write/package.json diff --git a/workflow/plugins/dbal-write/src/index.ts b/workflow/plugins/ts/dbal-write/src/index.ts similarity index 100% rename from workflow/plugins/dbal-write/src/index.ts rename to workflow/plugins/ts/dbal-write/src/index.ts diff --git a/workflow/plugins/dbal-write/tsconfig.json b/workflow/plugins/ts/dbal-write/tsconfig.json similarity index 100% rename from workflow/plugins/dbal-write/tsconfig.json rename to workflow/plugins/ts/dbal-write/tsconfig.json diff --git a/workflow/plugins/dbal/dbal-read/README.md b/workflow/plugins/ts/dbal/dbal-read/README.md similarity index 100% rename from workflow/plugins/dbal/dbal-read/README.md rename to workflow/plugins/ts/dbal/dbal-read/README.md diff --git a/workflow/plugins/dbal/dbal-read/package.json b/workflow/plugins/ts/dbal/dbal-read/package.json similarity index 100% rename from workflow/plugins/dbal/dbal-read/package.json rename to workflow/plugins/ts/dbal/dbal-read/package.json diff --git a/workflow/plugins/dbal/dbal-read/src/index.ts b/workflow/plugins/ts/dbal/dbal-read/src/index.ts similarity index 100% rename from workflow/plugins/dbal/dbal-read/src/index.ts rename to workflow/plugins/ts/dbal/dbal-read/src/index.ts diff --git a/workflow/plugins/dbal/dbal-read/tsconfig.json b/workflow/plugins/ts/dbal/dbal-read/tsconfig.json similarity index 100% rename from workflow/plugins/dbal/dbal-read/tsconfig.json rename to workflow/plugins/ts/dbal/dbal-read/tsconfig.json diff --git a/workflow/plugins/dbal/dbal-write/package.json b/workflow/plugins/ts/dbal/dbal-write/package.json similarity index 100% rename from workflow/plugins/dbal/dbal-write/package.json rename to workflow/plugins/ts/dbal/dbal-write/package.json diff --git a/workflow/plugins/dbal/dbal-write/src/index.ts b/workflow/plugins/ts/dbal/dbal-write/src/index.ts similarity index 100% rename from workflow/plugins/dbal/dbal-write/src/index.ts rename to workflow/plugins/ts/dbal/dbal-write/src/index.ts diff --git a/workflow/plugins/dbal/dbal-write/tsconfig.json b/workflow/plugins/ts/dbal/dbal-write/tsconfig.json similarity index 100% rename from workflow/plugins/dbal/dbal-write/tsconfig.json rename to workflow/plugins/ts/dbal/dbal-write/tsconfig.json diff --git a/workflow/plugins/integration/email-send/package.json b/workflow/plugins/ts/integration/email-send/package.json similarity index 100% rename from workflow/plugins/integration/email-send/package.json rename to workflow/plugins/ts/integration/email-send/package.json diff --git a/workflow/plugins/integration/email-send/src/index.ts b/workflow/plugins/ts/integration/email-send/src/index.ts similarity index 100% rename from workflow/plugins/integration/email-send/src/index.ts rename to workflow/plugins/ts/integration/email-send/src/index.ts diff --git a/workflow/plugins/integration/email-send/tsconfig.json b/workflow/plugins/ts/integration/email-send/tsconfig.json similarity index 100% rename from workflow/plugins/integration/email-send/tsconfig.json rename to workflow/plugins/ts/integration/email-send/tsconfig.json diff --git a/workflow/plugins/integration/http-request/package.json b/workflow/plugins/ts/integration/http-request/package.json similarity index 100% rename from workflow/plugins/integration/http-request/package.json rename to workflow/plugins/ts/integration/http-request/package.json diff --git a/workflow/plugins/integration/http-request/src/index.ts b/workflow/plugins/ts/integration/http-request/src/index.ts similarity index 100% rename from workflow/plugins/integration/http-request/src/index.ts rename to workflow/plugins/ts/integration/http-request/src/index.ts diff --git a/workflow/plugins/integration/http-request/tsconfig.json b/workflow/plugins/ts/integration/http-request/tsconfig.json similarity index 100% rename from workflow/plugins/integration/http-request/tsconfig.json rename to workflow/plugins/ts/integration/http-request/tsconfig.json diff --git a/workflow/plugins/integration/webhook-response/README.md b/workflow/plugins/ts/integration/webhook-response/README.md similarity index 100% rename from workflow/plugins/integration/webhook-response/README.md rename to workflow/plugins/ts/integration/webhook-response/README.md diff --git a/workflow/plugins/integration/webhook-response/package.json b/workflow/plugins/ts/integration/webhook-response/package.json similarity index 100% rename from workflow/plugins/integration/webhook-response/package.json rename to workflow/plugins/ts/integration/webhook-response/package.json diff --git a/workflow/plugins/integration/webhook-response/src/index.ts b/workflow/plugins/ts/integration/webhook-response/src/index.ts similarity index 100% rename from workflow/plugins/integration/webhook-response/src/index.ts rename to workflow/plugins/ts/integration/webhook-response/src/index.ts diff --git a/workflow/plugins/integration/webhook-response/tsconfig.json b/workflow/plugins/ts/integration/webhook-response/tsconfig.json similarity index 100% rename from workflow/plugins/integration/webhook-response/tsconfig.json rename to workflow/plugins/ts/integration/webhook-response/tsconfig.json diff --git a/workflow/plugins/utility/set-variable/README.md b/workflow/plugins/ts/utility/set-variable/README.md similarity index 100% rename from workflow/plugins/utility/set-variable/README.md rename to workflow/plugins/ts/utility/set-variable/README.md diff --git a/workflow/plugins/utility/set-variable/package.json b/workflow/plugins/ts/utility/set-variable/package.json similarity index 100% rename from workflow/plugins/utility/set-variable/package.json rename to workflow/plugins/ts/utility/set-variable/package.json diff --git a/workflow/plugins/utility/set-variable/src/index.ts b/workflow/plugins/ts/utility/set-variable/src/index.ts similarity index 100% rename from workflow/plugins/utility/set-variable/src/index.ts rename to workflow/plugins/ts/utility/set-variable/src/index.ts diff --git a/workflow/plugins/utility/set-variable/tsconfig.json b/workflow/plugins/ts/utility/set-variable/tsconfig.json similarity index 100% rename from workflow/plugins/utility/set-variable/tsconfig.json rename to workflow/plugins/ts/utility/set-variable/tsconfig.json diff --git a/workflow/plugins/utility/transform/README.md b/workflow/plugins/ts/utility/transform/README.md similarity index 100% rename from workflow/plugins/utility/transform/README.md rename to workflow/plugins/ts/utility/transform/README.md diff --git a/workflow/plugins/utility/transform/package.json b/workflow/plugins/ts/utility/transform/package.json similarity index 100% rename from workflow/plugins/utility/transform/package.json rename to workflow/plugins/ts/utility/transform/package.json diff --git a/workflow/plugins/utility/transform/src/index.ts b/workflow/plugins/ts/utility/transform/src/index.ts similarity index 100% rename from workflow/plugins/utility/transform/src/index.ts rename to workflow/plugins/ts/utility/transform/src/index.ts diff --git a/workflow/plugins/utility/transform/tsconfig.json b/workflow/plugins/ts/utility/transform/tsconfig.json similarity index 100% rename from workflow/plugins/utility/transform/tsconfig.json rename to workflow/plugins/ts/utility/transform/tsconfig.json diff --git a/workflow/plugins/utility/wait/package.json b/workflow/plugins/ts/utility/wait/package.json similarity index 100% rename from workflow/plugins/utility/wait/package.json rename to workflow/plugins/ts/utility/wait/package.json diff --git a/workflow/plugins/utility/wait/src/index.ts b/workflow/plugins/ts/utility/wait/src/index.ts similarity index 100% rename from workflow/plugins/utility/wait/src/index.ts rename to workflow/plugins/ts/utility/wait/src/index.ts diff --git a/workflow/plugins/utility/wait/tsconfig.json b/workflow/plugins/ts/utility/wait/tsconfig.json similarity index 100% rename from workflow/plugins/utility/wait/tsconfig.json rename to workflow/plugins/ts/utility/wait/tsconfig.json diff --git a/workflow/src/plugins/condition.plugin.ts b/workflow/src/plugins/condition.plugin.ts deleted file mode 100644 index 892814711..000000000 --- a/workflow/src/plugins/condition.plugin.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Condition Node Executor Plugin - * Evaluates conditions and routes execution to different paths - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { evaluateTemplate } from '../utils/template-engine'; - -export class ConditionExecutor implements INodeExecutor { - nodeType = 'condition'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { condition } = node.parameters; - - if (!condition) { - throw new Error('Condition node requires "condition" parameter'); - } - - // Evaluate condition with template engine - const result = evaluateTemplate(condition, { context, state, json: context.triggerData }); - - const duration = Date.now() - startTime; - - return { - status: 'success', - output: { - result: Boolean(result), - condition, - evaluated: true - }, - timestamp: Date.now(), - duration - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'CONDITION_EVAL_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.condition) { - errors.push('Condition is required'); - } - - const condition = node.parameters.condition || ''; - if (condition.includes('==') && !condition.includes('===')) { - warnings.push('Consider using === instead of == for strict equality'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } -} - -export const conditionExecutor = new ConditionExecutor(); diff --git a/workflow/src/plugins/dbal-read.plugin.ts b/workflow/src/plugins/dbal-read.plugin.ts deleted file mode 100644 index 6d9b510ed..000000000 --- a/workflow/src/plugins/dbal-read.plugin.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * DBAL Read Node Executor Plugin - * Handles database query operations with filtering, sorting, pagination - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { interpolateTemplate } from '../utils/template-engine'; - -export class DBALReadExecutor implements INodeExecutor { - nodeType = 'dbal-read'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { entity, operation, filter, sort, limit, offset } = node.parameters; - - if (!entity) { - throw new Error('DBAL read node requires "entity" parameter'); - } - - // Interpolate template variables in filter - const resolvedFilter = filter ? interpolateTemplate(filter, { context, state, json: context.triggerData }) : {}; - - // Enforce multi-tenant filtering - if (context.tenantId && !resolvedFilter.tenantId) { - resolvedFilter.tenantId = context.tenantId; - } - - // Execute appropriate operation - let result: any; - - switch (operation || 'read') { - case 'read': - // Mock implementation - in real code, call DBAL client - result = { - entity, - filter: resolvedFilter, - sort: sort || undefined, - limit: limit || 100, - offset: offset || 0, - items: [], - total: 0 - }; - console.log(`[DBAL] Reading ${entity}:`, result); - break; - - case 'validate': - // Validation operation - check data against rules - result = this._validateData(context.triggerData, node.parameters.rules || {}); - break; - - case 'aggregate': - // Aggregation operation - result = { - entity, - groupBy: node.parameters.groupBy, - aggregates: node.parameters.aggregates, - results: [] - }; - break; - - default: - throw new Error(`Unknown operation: ${operation}`); - } - - const duration = Date.now() - startTime; - - return { - status: 'success', - output: result, - timestamp: Date.now(), - duration - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'DBAL_READ_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.entity) { - errors.push('Entity is required'); - } - - if (node.parameters.limit && node.parameters.limit > 10000) { - warnings.push('Limit exceeds 10000 - may cause performance issues'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } - - /** - * Validate data against rules - */ - private _validateData(data: any, rules: Record): { valid: boolean; errors: Record } { - const errors: Record = {}; - - for (const [field, ruleStr] of Object.entries(rules)) { - const value = data[field]; - const ruleSet = ruleStr.split('|'); - - for (const rule of ruleSet) { - const [ruleName, ...ruleParams] = rule.split(':'); - - switch (ruleName) { - case 'required': - if (value === undefined || value === null || value === '') { - errors[field] = `${field} is required`; - } - break; - - case 'email': - if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { - errors[field] = `${field} must be a valid email`; - } - break; - - case 'uuid': - if ( - value && - !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) - ) { - errors[field] = `${field} must be a valid UUID`; - } - break; - - case 'number': - if (value !== undefined && typeof value !== 'number') { - errors[field] = `${field} must be a number`; - } - break; - - case 'min': - if (value !== undefined && value < parseFloat(ruleParams[0])) { - errors[field] = `${field} must be at least ${ruleParams[0]}`; - } - break; - - case 'max': - if (value !== undefined && value > parseFloat(ruleParams[0])) { - errors[field] = `${field} must be at most ${ruleParams[0]}`; - } - break; - } - } - } - - return { - valid: Object.keys(errors).length === 0, - errors - }; - } -} - -export const dbalReadExecutor = new DBALReadExecutor(); diff --git a/workflow/src/plugins/dbal-read/index.ts b/workflow/src/plugins/dbal-read/index.ts deleted file mode 100644 index 07d4af1c9..000000000 --- a/workflow/src/plugins/dbal-read/index.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * DBAL Read Node Executor Plugin - * Handles database query operations with filtering, sorting, pagination - * - * @packageDocumentation - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../../types'; -import { interpolateTemplate } from '../../utils/template-engine'; - -export class DBALReadExecutor implements INodeExecutor { - nodeType = 'dbal-read'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { entity, operation, filter, sort, limit, offset } = node.parameters; - - if (!entity) { - throw new Error('DBAL read node requires "entity" parameter'); - } - - // Interpolate template variables in filter - const resolvedFilter = filter ? interpolateTemplate(filter, { context, state, json: context.triggerData }) : {}; - - // Enforce multi-tenant filtering - if (context.tenantId && !resolvedFilter.tenantId) { - resolvedFilter.tenantId = context.tenantId; - } - - let result: any; - - switch (operation || 'read') { - case 'read': - result = { - entity, - filter: resolvedFilter, - sort: sort || undefined, - limit: limit || 100, - offset: offset || 0, - items: [], - total: 0 - }; - break; - - case 'validate': - result = this._validateData(context.triggerData, node.parameters.rules || {}); - break; - - case 'aggregate': - result = { - entity, - groupBy: node.parameters.groupBy, - aggregates: node.parameters.aggregates, - results: [] - }; - break; - - default: - throw new Error(`Unknown operation: ${operation}`); - } - - const duration = Date.now() - startTime; - - return { - status: 'success', - output: result, - timestamp: Date.now(), - duration - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'DBAL_READ_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.entity) { - errors.push('Entity is required'); - } - - if (node.parameters.limit && node.parameters.limit > 10000) { - warnings.push('Limit exceeds 10000 - may cause performance issues'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } - - private _validateData(data: any, rules: Record): { valid: boolean; errors: Record } { - const errors: Record = {}; - - for (const [field, ruleStr] of Object.entries(rules)) { - const value = data[field]; - const ruleSet = ruleStr.split('|'); - - for (const rule of ruleSet) { - const [ruleName, ...ruleParams] = rule.split(':'); - - switch (ruleName) { - case 'required': - if (value === undefined || value === null || value === '') { - errors[field] = `${field} is required`; - } - break; - - case 'email': - if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { - errors[field] = `${field} must be a valid email`; - } - break; - - case 'uuid': - if ( - value && - !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) - ) { - errors[field] = `${field} must be a valid UUID`; - } - break; - - case 'number': - if (value !== undefined && typeof value !== 'number') { - errors[field] = `${field} must be a number`; - } - break; - - case 'min': - if (value !== undefined && value < parseFloat(ruleParams[0])) { - errors[field] = `${field} must be at least ${ruleParams[0]}`; - } - break; - - case 'max': - if (value !== undefined && value > parseFloat(ruleParams[0])) { - errors[field] = `${field} must be at most ${ruleParams[0]}`; - } - break; - } - } - } - - return { - valid: Object.keys(errors).length === 0, - errors - }; - } -} - -export const dbalReadExecutor = new DBALReadExecutor(); diff --git a/workflow/src/plugins/dbal-read/package.json b/workflow/src/plugins/dbal-read/package.json deleted file mode 100644 index 9abbf660f..000000000 --- a/workflow/src/plugins/dbal-read/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@metabuilder/workflow-plugin-dbal-read", - "version": "1.0.0", - "description": "DBAL Read node executor - Query database with filtering and pagination", - "main": "../../dist/plugins/dbal-read/index.js", - "types": "../../dist/plugins/dbal-read/index.d.ts", - "exports": { - ".": { - "import": "../../dist/plugins/dbal-read/index.js", - "require": "../../dist/plugins/dbal-read/index.js", - "types": "../../dist/plugins/dbal-read/index.d.ts" - } - }, - "keywords": [ - "workflow", - "plugin", - "dbal", - "database", - "query", - "read" - ], - "author": "MetaBuilder Team", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/metabuilder/metabuilder.git", - "directory": "workflow/src/plugins/dbal-read" - } -} diff --git a/workflow/src/plugins/dbal-write.plugin.ts b/workflow/src/plugins/dbal-write.plugin.ts deleted file mode 100644 index 1e777fc5c..000000000 --- a/workflow/src/plugins/dbal-write.plugin.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * DBAL Write Node Executor Plugin - * Handles create/update operations with conflict resolution - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { interpolateTemplate } from '../utils/template-engine'; - -export class DBALWriteExecutor implements INodeExecutor { - nodeType = 'dbal-write'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { entity, operation, data, id, filter } = node.parameters; - - if (!entity) { - throw new Error('DBAL write node requires "entity" parameter'); - } - - if (!operation || !['create', 'update', 'upsert'].includes(operation)) { - throw new Error('DBAL write requires operation: create, update, or upsert'); - } - - // Interpolate template variables in data - const resolvedData = interpolateTemplate(data, { context, state, json: context.triggerData }); - - // Enforce multi-tenant data - if (context.tenantId) { - resolvedData.tenantId = context.tenantId; - } - - // Add audit fields - resolvedData.updatedAt = new Date(); - if (operation === 'create') { - resolvedData.createdAt = new Date(); - resolvedData.createdBy = context.userId; - } - resolvedData.updatedBy = context.userId; - - let result: any; - - switch (operation) { - case 'create': - result = { operation, entity, data: resolvedData, created: true }; - break; - - case 'update': - if (!id && !filter) { - throw new Error('Update requires either id or filter parameter'); - } - result = { operation, entity, data: resolvedData, updated: true }; - break; - - case 'upsert': - if (!filter) { - throw new Error('Upsert requires filter parameter'); - } - result = { operation, entity, data: resolvedData, upserted: true }; - break; - } - - const duration = Date.now() - startTime; - - return { - status: 'success', - output: result, - timestamp: Date.now(), - duration - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'DBAL_WRITE_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.entity) { - errors.push('Entity is required'); - } - - if (!node.parameters.operation) { - errors.push('Operation is required (create, update, or upsert)'); - } - - if (!node.parameters.data) { - errors.push('Data is required'); - } - - if (node.parameters.operation === 'update' && !node.parameters.id && !node.parameters.filter) { - errors.push('Update operation requires either id or filter'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } -} - -export const dbalWriteExecutor = new DBALWriteExecutor(); diff --git a/workflow/src/plugins/email-send.plugin.ts b/workflow/src/plugins/email-send.plugin.ts deleted file mode 100644 index 731d02486..000000000 --- a/workflow/src/plugins/email-send.plugin.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Email Send Node Executor Plugin - * Sends emails with template support - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { interpolateTemplate } from '../utils/template-engine'; - -export interface EmailService { - send(options: any): Promise<{ messageId: string }>; -} - -let emailService: EmailService | null = null; - -export function setEmailService(service: EmailService): void { - emailService = service; -} - -export class EmailSendExecutor implements INodeExecutor { - nodeType = 'email-send'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { to, cc, bcc, subject, body, template, data } = node.parameters; - - if (!to) { - throw new Error('Email send requires "to" parameter'); - } - - if (!subject) { - throw new Error('Email send requires "subject" parameter'); - } - - if (!body && !template) { - throw new Error('Email send requires either "body" or "template" parameter'); - } - - // Interpolate template variables - const resolvedTo = interpolateTemplate(to, { context, state, json: context.triggerData }); - const resolvedSubject = interpolateTemplate(subject, { context, state, json: context.triggerData }); - const resolvedCc = cc ? interpolateTemplate(cc, { context, state, json: context.triggerData }) : undefined; - const resolvedBcc = bcc ? interpolateTemplate(bcc, { context, state, json: context.triggerData }) : undefined; - - let emailBody: string; - if (template) { - emailBody = this._renderTemplate(template, data || {}); - } else { - emailBody = interpolateTemplate(body, { context, state, json: context.triggerData }); - } - - // Mock implementation - in real code, call email service - console.log(`[Email] To: ${resolvedTo}, Subject: ${resolvedSubject}`); - - const mockMessageId = `msg-${Date.now()}`; - - return { - status: 'success', - output: { - messageId: mockMessageId, - to: resolvedTo, - subject: resolvedSubject, - timestamp: new Date().toISOString() - }, - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'EMAIL_SEND_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.to) { - errors.push('Recipient email (to) is required'); - } - - if (!node.parameters.subject) { - errors.push('Email subject is required'); - } - - if (!node.parameters.body && !node.parameters.template) { - errors.push('Either body or template must be provided'); - } - - const to = node.parameters.to || ''; - if (to && !to.includes('{{') && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(to)) { - errors.push('Invalid email format in "to" parameter'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } - - private _renderTemplate(template: string, data: Record): string { - let html = template; - Object.entries(data).forEach(([key, value]) => { - const placeholder = `{{${key}}}`; - html = html.replace(new RegExp(placeholder, 'g'), String(value)); - }); - return html; - } -} - -export const emailSendExecutor = new EmailSendExecutor(); diff --git a/workflow/src/plugins/http-request.plugin.ts b/workflow/src/plugins/http-request.plugin.ts deleted file mode 100644 index 30b1a204f..000000000 --- a/workflow/src/plugins/http-request.plugin.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * HTTP Request Node Executor Plugin - * Handles outbound HTTP calls with retry and response parsing - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { interpolateTemplate } from '../utils/template-engine'; - -export class HTTPRequestExecutor implements INodeExecutor { - nodeType = 'http-request'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { url, method, body, headers, timeout } = node.parameters; - - if (!url) { - throw new Error('HTTP request requires "url" parameter'); - } - - // Interpolate template variables - const resolvedUrl = interpolateTemplate(url, { - context, - state, - json: context.triggerData, - env: process.env - }); - const resolvedHeaders = interpolateTemplate(headers || {}, { - context, - state, - json: context.triggerData, - env: process.env - }); - const resolvedBody = body - ? interpolateTemplate(body, { context, state, json: context.triggerData }) - : undefined; - - // Set default headers - const requestHeaders: Record = { - 'Content-Type': 'application/json', - 'User-Agent': 'MetaBuilder-Workflow/3.0.0', - ...resolvedHeaders - }; - - // Mock implementation - in real code, use fetch/axios - const mockResponse = { - statusCode: 200, - statusText: 'OK', - body: { success: true }, - headers: requestHeaders, - url: resolvedUrl, - method: method || 'GET' - }; - - const duration = Date.now() - startTime; - - return { - status: 'success', - output: mockResponse, - timestamp: Date.now(), - duration - }; - } catch (error) { - const duration = Date.now() - startTime; - const errorMsg = error instanceof Error ? error.message : String(error); - - let errorCode = 'HTTP_ERROR'; - if (errorMsg.includes('timeout')) { - errorCode = 'TIMEOUT'; - } - - return { - status: 'error', - error: errorMsg, - errorCode, - timestamp: Date.now(), - duration - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.url) { - errors.push('URL is required'); - } - - if (node.parameters.timeout && node.parameters.timeout > 120000) { - warnings.push('Timeout exceeds 2 minutes - may cause workflow delays'); - } - - if ( - !['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes((node.parameters.method || 'GET').toUpperCase()) - ) { - errors.push('Invalid HTTP method'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } -} - -export const httpRequestExecutor = new HTTPRequestExecutor(); diff --git a/workflow/src/plugins/index.ts b/workflow/src/plugins/index.ts deleted file mode 100644 index cc392e8a6..000000000 --- a/workflow/src/plugins/index.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Built-in Node Executors for MetaBuilder Workflow Engine - * Includes data operations, HTTP, email, control flow, and utilities - */ - -export { dbalReadExecutor } from './dbal-read.plugin'; -export { dbalWriteExecutor } from './dbal-write.plugin'; -export { httpRequestExecutor } from './http-request.plugin'; -export { conditionExecutor } from './condition.plugin'; -export { emailSendExecutor, setEmailService } from './email-send.plugin'; -export { webhookResponseExecutor } from './webhook-response.plugin'; -export { transformExecutor } from './transform.plugin'; -export { waitExecutor } from './wait.plugin'; -export { setVariableExecutor } from './set-variable.plugin'; - -/** - * Import all executors for batch registration - */ -import { dbalReadExecutor } from './dbal-read.plugin'; -import { dbalWriteExecutor } from './dbal-write.plugin'; -import { httpRequestExecutor } from './http-request.plugin'; -import { conditionExecutor } from './condition.plugin'; -import { emailSendExecutor } from './email-send.plugin'; -import { webhookResponseExecutor } from './webhook-response.plugin'; -import { transformExecutor } from './transform.plugin'; -import { waitExecutor } from './wait.plugin'; -import { setVariableExecutor } from './set-variable.plugin'; -import { getNodeExecutorRegistry } from '../registry/node-executor-registry'; - -/** - * Register all built-in executors - */ -export function registerBuiltInExecutors() { - const registry = getNodeExecutorRegistry(); - - registry.registerBatch([ - { - nodeType: 'dbal-read', - executor: dbalReadExecutor, - plugin: { - nodeType: 'dbal-read', - version: '1.0.0', - executor: dbalReadExecutor, - metadata: { - description: 'Read data from database with filtering, sorting, and pagination', - category: 'data', - icon: 'database' - } - } - }, - { - nodeType: 'dbal-write', - executor: dbalWriteExecutor, - plugin: { - nodeType: 'dbal-write', - version: '1.0.0', - executor: dbalWriteExecutor, - metadata: { - description: 'Write, update, or upsert database records', - category: 'data', - icon: 'database' - } - } - }, - { - nodeType: 'http-request', - executor: httpRequestExecutor, - plugin: { - nodeType: 'http-request', - version: '1.0.0', - executor: httpRequestExecutor, - metadata: { - description: 'Make HTTP requests with support for headers, authentication, and retries', - category: 'integration', - icon: 'network' - } - } - }, - { - nodeType: 'condition', - executor: conditionExecutor, - plugin: { - nodeType: 'condition', - version: '1.0.0', - executor: conditionExecutor, - metadata: { - description: 'Evaluate conditions and route to different execution paths', - category: 'control-flow', - icon: 'branch' - } - } - }, - { - nodeType: 'email-send', - executor: emailSendExecutor, - plugin: { - nodeType: 'email-send', - version: '1.0.0', - executor: emailSendExecutor, - metadata: { - description: 'Send emails with template support and attachments', - category: 'action', - icon: 'mail' - } - } - }, - { - nodeType: 'webhook-response', - executor: webhookResponseExecutor, - plugin: { - nodeType: 'webhook-response', - version: '1.0.0', - executor: webhookResponseExecutor, - metadata: { - description: 'Return HTTP response to webhook sender', - category: 'integration', - icon: 'webhook' - } - } - }, - { - nodeType: 'transform', - executor: transformExecutor, - plugin: { - nodeType: 'transform', - version: '1.0.0', - executor: transformExecutor, - metadata: { - description: 'Transform and map data using template expressions', - category: 'data', - icon: 'transform' - } - } - }, - { - nodeType: 'wait', - executor: waitExecutor, - plugin: { - nodeType: 'wait', - version: '1.0.0', - executor: waitExecutor, - metadata: { - description: 'Pause execution for a specified duration', - category: 'control-flow', - icon: 'pause' - } - } - }, - { - nodeType: 'set-variable', - executor: setVariableExecutor, - plugin: { - nodeType: 'set-variable', - version: '1.0.0', - executor: setVariableExecutor, - metadata: { - description: 'Set workflow variables for use in subsequent nodes', - category: 'utility', - icon: 'variable' - } - } - } - ]); - - console.log('✓ Built-in node executors registered'); -} diff --git a/workflow/src/plugins/set-variable.plugin.ts b/workflow/src/plugins/set-variable.plugin.ts deleted file mode 100644 index dfe7a154e..000000000 --- a/workflow/src/plugins/set-variable.plugin.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Set Variable Node Executor Plugin - * Sets workflow variables for use in subsequent nodes - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { interpolateTemplate } from '../utils/template-engine'; - -export class SetVariableExecutor implements INodeExecutor { - nodeType = 'set-variable'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { variables } = node.parameters; - - if (!variables) { - throw new Error('Set variable node requires "variables" parameter'); - } - - // Interpolate template for all variables - const resolvedVariables: Record = {}; - - for (const [name, value] of Object.entries(variables)) { - resolvedVariables[name] = interpolateTemplate(value, { context, state, json: context.triggerData }); - } - - // Store variables in context for subsequent nodes - context.variables = { ...context.variables, ...resolvedVariables }; - - const duration = Date.now() - startTime; - - return { - status: 'success', - output: { - variables: resolvedVariables, - set: true - }, - timestamp: Date.now(), - duration - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'SET_VARIABLE_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.variables) { - errors.push('Variables object is required'); - } - - const variables = node.parameters.variables || {}; - if (Object.keys(variables).length === 0) { - warnings.push('No variables defined - this node will have no effect'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } -} - -export const setVariableExecutor = new SetVariableExecutor(); diff --git a/workflow/src/plugins/transform.plugin.ts b/workflow/src/plugins/transform.plugin.ts deleted file mode 100644 index a3f797be7..000000000 --- a/workflow/src/plugins/transform.plugin.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Transform Node Executor Plugin - * Transforms and maps data using template expressions - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { interpolateTemplate, buildDefaultUtilities } from '../utils/template-engine'; - -export class TransformExecutor implements INodeExecutor { - nodeType = 'transform'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { mapping } = node.parameters; - - if (!mapping) { - throw new Error('Transform node requires "mapping" parameter'); - } - - // Build utilities context - const utils = buildDefaultUtilities(); - - // Interpolate template with utilities - const result = interpolateTemplate(mapping, { - context, - state, - json: context.triggerData, - utils - }); - - const duration = Date.now() - startTime; - - return { - status: 'success', - output: result, - timestamp: Date.now(), - duration - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'TRANSFORM_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.mapping) { - errors.push('Mapping is required'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } -} - -export const transformExecutor = new TransformExecutor(); diff --git a/workflow/src/plugins/wait.plugin.ts b/workflow/src/plugins/wait.plugin.ts deleted file mode 100644 index b3c2081c1..000000000 --- a/workflow/src/plugins/wait.plugin.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Wait Node Executor Plugin - * Pauses execution for a specified duration - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; - -export class WaitExecutor implements INodeExecutor { - nodeType = 'wait'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { duration, unit } = node.parameters; - - if (duration === undefined) { - throw new Error('Wait node requires "duration" parameter'); - } - - // Convert duration to milliseconds - const unitMap: Record = { - ms: 1, - second: 1000, - seconds: 1000, - minute: 60000, - minutes: 60000, - hour: 3600000, - hours: 3600000 - }; - - const multiplier = unitMap[unit || 'seconds'] || 1000; - const waitMs = duration * multiplier; - - // Wait - await new Promise((resolve) => setTimeout(resolve, waitMs)); - - const actualDuration = Date.now() - startTime; - - return { - status: 'success', - output: { - waited: true, - duration: actualDuration, - unit: unit || 'ms' - }, - timestamp: Date.now(), - duration: actualDuration - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'WAIT_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (node.parameters.duration === undefined) { - errors.push('Duration is required'); - } - - if (node.parameters.duration && node.parameters.duration > 3600) { - warnings.push('Wait duration exceeds 1 hour - consider using schedule trigger instead'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } -} - -export const waitExecutor = new WaitExecutor(); diff --git a/workflow/src/plugins/webhook-response.plugin.ts b/workflow/src/plugins/webhook-response.plugin.ts deleted file mode 100644 index 49a353d71..000000000 --- a/workflow/src/plugins/webhook-response.plugin.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Webhook Response Node Executor Plugin - * Returns HTTP response to webhook sender - */ - -import { INodeExecutor, WorkflowNode, WorkflowContext, ExecutionState, NodeResult, ValidationResult } from '../types'; -import { interpolateTemplate } from '../utils/template-engine'; - -export class WebhookResponseExecutor implements INodeExecutor { - nodeType = 'webhook-response'; - - async execute( - node: WorkflowNode, - context: WorkflowContext, - state: ExecutionState - ): Promise { - const startTime = Date.now(); - - try { - const { statusCode, body, headers } = node.parameters; - - if (!statusCode) { - throw new Error('Webhook response requires "statusCode" parameter'); - } - - const resolvedBody = body ? interpolateTemplate(body, { context, state, json: context.triggerData }) : undefined; - const resolvedHeaders = headers ? interpolateTemplate(headers, { context, state, json: context.triggerData }) : {}; - - return { - status: 'success', - output: { - statusCode, - body: resolvedBody, - headers: resolvedHeaders, - timestamp: new Date().toISOString() - }, - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } catch (error) { - return { - status: 'error', - error: error instanceof Error ? error.message : String(error), - errorCode: 'WEBHOOK_RESPONSE_ERROR', - timestamp: Date.now(), - duration: Date.now() - startTime - }; - } - } - - validate(node: WorkflowNode): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - if (!node.parameters.statusCode) { - errors.push('Status code is required'); - } - - const statusCode = node.parameters.statusCode; - if (statusCode && (statusCode < 100 || statusCode > 599)) { - errors.push('Status code must be between 100 and 599'); - } - - if (statusCode && statusCode >= 300 && statusCode < 400 && !node.parameters.headers?.Location) { - warnings.push('Redirect status code used but no Location header provided'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } -} - -export const webhookResponseExecutor = new WebhookResponseExecutor(); diff --git a/workflow/tsconfig.json b/workflow/tsconfig.json index 68081a19b..1ccb12df6 100644 --- a/workflow/tsconfig.json +++ b/workflow/tsconfig.json @@ -4,7 +4,7 @@ "module": "commonjs", "lib": ["ES2020"], "outDir": "./dist", - "rootDir": "./src", + "rootDir": "./core", "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -23,6 +23,6 @@ "allowSyntheticDefaultImports": true, "moduleResolution": "node" }, - "include": ["src/**/*"], + "include": ["core/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] }