feat: Add Python plugins from AutoMetabuilder + restructure workflow folder

Restructure workflow/ for multi-language plugin support:
- Rename src/ to core/ (engine code: DAG executor, registry, types)
- Create executor/{cpp,python,ts}/ for language-specific runtimes
- Consolidate plugins to plugins/{ts,python}/ by language then category

Add 80+ Python plugins from AutoMetabuilder in 14 categories:
- control: bot control, switch logic, state management
- convert: type conversions (json, boolean, dict, list, number, string)
- core: AI requests, context management, tool calls
- dict: dictionary operations (get, set, keys, values, merge)
- list: list operations (concat, find, sort, slice, filter)
- logic: boolean logic (and, or, xor, equals, comparisons)
- math: arithmetic operations (add, subtract, multiply, power, etc.)
- string: string manipulation (concat, split, replace, format)
- notifications: Slack, Discord integrations
- test: assertion helpers and test suite runner
- tools: file operations, git, docker, testing utilities
- utils: filtering, mapping, reducing, condition branching
- var: variable store operations (get, set, delete, exists)
- web: Flask server, environment variables, JSON handling

Add language executor runtimes:
- TypeScript: direct import execution (default, fast startup)
- Python: child process with JSON stdin/stdout communication
- C++: placeholder for native FFI bindings (Phase 3)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 16:08:08 +00:00
parent c760bd7cd0
commit 5ac579a2ed
171 changed files with 2188 additions and 1565 deletions

View File

@@ -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
"""

View File

@@ -0,0 +1 @@
"""Control flow plugins: bot control and switch logic."""

View File

@@ -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()

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -0,0 +1 @@
"""Type conversion plugins."""

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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": {}}

View File

@@ -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)}

View File

@@ -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]}

View File

@@ -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"}

View File

@@ -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 ""}

View File

@@ -0,0 +1 @@
"""Core AI/workflow plugins: AI requests, message handling, context loading."""

View File

@@ -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)
}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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)
}

View File

@@ -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"])}

View File

@@ -0,0 +1 @@
"""Dictionary manipulation plugins."""

View File

@@ -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}

View File

@@ -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()]}

View File

@@ -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())}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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())}

View File

@@ -0,0 +1 @@
"""List manipulation plugins."""

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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"}

View File

@@ -0,0 +1 @@
"""Boolean logic plugins."""

View File

@@ -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)}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -0,0 +1 @@
"""Mathematical operations plugins."""

View File

@@ -0,0 +1,7 @@
"""Workflow plugin: absolute value."""
def run(_runtime, inputs):
"""Calculate absolute value."""
value = inputs.get("value", 0)
return {"result": abs(value)}

View File

@@ -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)}

View File

@@ -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}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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)}

View File

@@ -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}

View File

@@ -0,0 +1 @@
"""Notification plugins: Slack, Discord, and multi-channel notifications."""

View File

@@ -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
}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -0,0 +1 @@
"""String manipulation plugins."""

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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()}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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()}

View File

@@ -0,0 +1 @@
"""Unit testing assertion plugins."""

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1 @@
"""External tool integration plugins."""

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -0,0 +1 @@
"""Utility plugins: filtering, mapping, branching, MVP checking."""

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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)}

View File

@@ -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}

View File

@@ -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"}

View File

@@ -0,0 +1 @@
"""Variable management plugins for workflow store."""

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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
"""

View File

@@ -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}

Some files were not shown because too many files have changed in this diff Show More