Files
metabuilder/workflow/plugins/ts/utility/set-variable/src/index.ts
johndoe6345789 5ac579a2ed 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>
2026-01-21 16:08:08 +00:00

221 lines
5.9 KiB
TypeScript

/**
* Set Variable Node Executor Plugin
* Handles setting workflow variables for subsequent nodes
*
* @packageDocumentation
*/
import {
INodeExecutor,
WorkflowNode,
WorkflowContext,
ExecutionState,
NodeResult,
ValidationResult
} from '@metabuilder/workflow';
import { interpolateTemplate } from '@metabuilder/workflow';
interface SetVariableOutput {
variablesSet: Record<string, string>;
count: number;
timestamp: number;
}
export class SetVariableExecutor implements INodeExecutor {
nodeType = 'set-variable';
async execute(
node: WorkflowNode,
context: WorkflowContext,
state: ExecutionState
): Promise<NodeResult> {
const startTime = Date.now();
try {
const { variables, mode } = node.parameters;
if (!variables || (typeof variables !== 'object' || Array.isArray(variables))) {
throw new Error('Set Variable node requires "variables" parameter as an object');
}
const variablesSet: Record<string, string> = {};
const interpolationContext = {
context,
state,
json: context.triggerData,
env: process.env
};
// Ensure context.variables exists
if (!state.variables) {
state.variables = {};
}
// Process variables based on mode
const processMode = mode || 'merge'; // 'merge', 'replace', or 'append'
if (processMode === 'replace') {
// Clear existing variables and start fresh
state.variables = {};
}
// Set each variable with template interpolation
for (const [key, value] of Object.entries(variables)) {
if (!this._isValidVariableName(key)) {
throw new Error(
`Invalid variable name "${key}" - must start with letter/underscore and contain only alphanumeric chars and underscores`
);
}
// Interpolate the value
let resolvedValue = value;
if (typeof value === 'string') {
resolvedValue = interpolateTemplate(value, interpolationContext);
} else if (typeof value === 'object' && value !== null) {
// For objects, recursively interpolate
resolvedValue = this._interpolateObject(value, interpolationContext);
}
// Store in execution state variables
state.variables[key] = resolvedValue;
variablesSet[key] = String(resolvedValue);
}
// Also update context variables if the context supports it
if (context.variables) {
Object.assign(context.variables, state.variables);
}
const duration = Date.now() - startTime;
const output: SetVariableOutput = {
variablesSet,
count: Object.keys(variablesSet).length,
timestamp: Date.now()
};
return {
status: 'success',
output,
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');
}
if (node.parameters.variables && typeof node.parameters.variables !== 'object') {
errors.push('Variables must be an object');
}
if (node.parameters.variables && Array.isArray(node.parameters.variables)) {
errors.push('Variables must be an object, not an array');
}
// Validate variable names
if (node.parameters.variables && typeof node.parameters.variables === 'object') {
for (const key of Object.keys(node.parameters.variables)) {
if (!this._isValidVariableName(key)) {
errors.push(
`Invalid variable name "${key}" - must start with letter/underscore and contain only alphanumeric chars and underscores`
);
}
// Warn about reserved names
if (this._isReservedName(key)) {
warnings.push(`"${key}" is a reserved name and may conflict with built-in variables`);
}
}
}
// Validate mode if provided
const validModes = ['merge', 'replace', 'append'];
if (node.parameters.mode && !validModes.includes(node.parameters.mode)) {
errors.push(`Mode must be one of: ${validModes.join(', ')}`);
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Check if variable name is valid
* Must start with letter or underscore, contain only alphanumeric and underscores
*/
private _isValidVariableName(name: string): boolean {
const validNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
return validNameRegex.test(name);
}
/**
* Check if name is reserved
*/
private _isReservedNames(name: string): boolean {
const reserved = [
'context',
'state',
'json',
'env',
'utils',
'$json',
'$context',
'$state',
'$env'
];
return reserved.includes(name.toLowerCase());
}
/**
* Alias for _isReservedNames to fix typo
*/
private _isReservedName(name: string): boolean {
return this._isReservedNames(name);
}
/**
* Recursively interpolate object values
*/
private _interpolateObject(obj: any, context: any): any {
if (typeof obj === 'string') {
return interpolateTemplate(obj, context);
}
if (Array.isArray(obj)) {
return obj.map((item) => this._interpolateObject(item, context));
}
if (obj !== null && typeof obj === 'object') {
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = this._interpolateObject(value, context);
}
return result;
}
return obj;
}
}
export const setVariableExecutor = new SetVariableExecutor();