Jelajahi Sumber

feat: US-013 - Code Node - JavaScript Execution

Add a Code node that allows users to write custom JavaScript code
within workflows. Features include:

- Code editor textarea with JavaScript syntax highlighting support
- Access to input data via 'input' variable
- Access to workflow context via 'context' variable
- Comprehensive lodash-like helper utilities via '_' (underscore):
  - Type checking: isString, isNumber, isArray, isObject, isEmpty, etc.
  - Object utilities: get, set, has, keys, values, pick, omit, merge
  - Array utilities: map, filter, find, reduce, groupBy, sortBy, uniq
  - String utilities: trim, toUpper, toLower, camelCase, snakeCase
  - Date utilities: formatDate, parseDate, addDays, addHours, diffDays
  - Math utilities: sum, min, max, round, floor, ceil, clamp, random
  - JSON utilities: toJson, parseJson
  - Crypto helpers: uuid, base64Encode, base64Decode, sha256
- Configurable execution timeout (1-300 seconds, default 30s)
- Error handling with line number extraction for debugging
- Executes in QuickJS sandbox on Runner service
fszontagh 6 bulan lalu
induk
melakukan
bc202fc55b
1 mengubah file dengan 916 tambahan dan 0 penghapusan
  1. 916 0
      nodes/core/code.js

+ 916 - 0
nodes/core/code.js

@@ -0,0 +1,916 @@
+/**
+ * @node code
+ * @name Code
+ * @category core
+ * @version 1.0.0
+ * @description Execute custom JavaScript code with access to input data and helper utilities
+ * @icon code
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        code: {
+            type: 'string',
+            title: 'JavaScript Code',
+            description: 'Custom JavaScript code to execute. Access input data via "input", workflow context via "context", and helper utilities via "_" (lodash-like). Return a value to set the node output.',
+            format: 'code',
+            default: '// Access input data with: input\n// Access workflow context with: context\n// Use helper utilities with: _ (underscore)\n\n// Example: Transform input data\nconst result = {\n    processed: true,\n    data: input.data,\n    timestamp: _.now()\n};\n\nreturn result;'
+        },
+        timeout: {
+            type: 'number',
+            title: 'Timeout (seconds)',
+            description: 'Maximum execution time in seconds (1-300). Default: 30 seconds.',
+            default: 30,
+            minimum: 1,
+            maximum: 300
+        }
+    },
+    required: ['code']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        data: {
+            type: 'any',
+            description: 'Input data available as "input" in your code'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        result: {
+            type: 'any',
+            description: 'The value returned by your code'
+        },
+        executionTime: {
+            type: 'number',
+            description: 'Execution time in milliseconds'
+        }
+    }
+};
+
+function createHelpers() {
+    return {
+        now: function() {
+            return Date.now();
+        },
+
+        isString: function(value) {
+            return typeof value === 'string';
+        },
+
+        isNumber: function(value) {
+            return typeof value === 'number' && !isNaN(value);
+        },
+
+        isBoolean: function(value) {
+            return typeof value === 'boolean';
+        },
+
+        isArray: function(value) {
+            return Array.isArray(value);
+        },
+
+        isObject: function(value) {
+            return value !== null && typeof value === 'object' && !Array.isArray(value);
+        },
+
+        isNull: function(value) {
+            return value === null;
+        },
+
+        isUndefined: function(value) {
+            return value === undefined;
+        },
+
+        isNil: function(value) {
+            return value === null || value === undefined;
+        },
+
+        isEmpty: function(value) {
+            if (value === null || value === undefined) return true;
+            if (typeof value === 'string' || Array.isArray(value)) return value.length === 0;
+            if (typeof value === 'object') return Object.keys(value).length === 0;
+            return false;
+        },
+
+        get: function(obj, path, defaultValue) {
+            if (obj === null || obj === undefined) return defaultValue;
+            var keys = typeof path === 'string' ? path.split('.') : path;
+            var result = obj;
+            for (var i = 0; i < keys.length; i++) {
+                if (result === null || result === undefined) return defaultValue;
+                result = result[keys[i]];
+            }
+            return result === undefined ? defaultValue : result;
+        },
+
+        set: function(obj, path, value) {
+            if (obj === null || typeof obj !== 'object') return obj;
+            var keys = typeof path === 'string' ? path.split('.') : path;
+            var current = obj;
+            for (var i = 0; i < keys.length - 1; i++) {
+                var key = keys[i];
+                if (current[key] === undefined || current[key] === null) {
+                    current[key] = {};
+                }
+                current = current[key];
+            }
+            current[keys[keys.length - 1]] = value;
+            return obj;
+        },
+
+        has: function(obj, path) {
+            if (obj === null || obj === undefined) return false;
+            var keys = typeof path === 'string' ? path.split('.') : path;
+            var current = obj;
+            for (var i = 0; i < keys.length; i++) {
+                if (current === null || current === undefined) return false;
+                if (!Object.prototype.hasOwnProperty.call(current, keys[i])) return false;
+                current = current[keys[i]];
+            }
+            return true;
+        },
+
+        keys: function(obj) {
+            if (obj === null || typeof obj !== 'object') return [];
+            return Object.keys(obj);
+        },
+
+        values: function(obj) {
+            if (obj === null || typeof obj !== 'object') return [];
+            return Object.values(obj);
+        },
+
+        entries: function(obj) {
+            if (obj === null || typeof obj !== 'object') return [];
+            return Object.entries(obj);
+        },
+
+        pick: function(obj, keys) {
+            if (obj === null || typeof obj !== 'object') return {};
+            var result = {};
+            for (var i = 0; i < keys.length; i++) {
+                var key = keys[i];
+                if (Object.prototype.hasOwnProperty.call(obj, key)) {
+                    result[key] = obj[key];
+                }
+            }
+            return result;
+        },
+
+        omit: function(obj, keys) {
+            if (obj === null || typeof obj !== 'object') return {};
+            var result = {};
+            var keysSet = {};
+            for (var i = 0; i < keys.length; i++) {
+                keysSet[keys[i]] = true;
+            }
+            for (var key in obj) {
+                if (Object.prototype.hasOwnProperty.call(obj, key) && !keysSet[key]) {
+                    result[key] = obj[key];
+                }
+            }
+            return result;
+        },
+
+        merge: function() {
+            var result = {};
+            for (var i = 0; i < arguments.length; i++) {
+                var obj = arguments[i];
+                if (obj !== null && typeof obj === 'object') {
+                    for (var key in obj) {
+                        if (Object.prototype.hasOwnProperty.call(obj, key)) {
+                            result[key] = obj[key];
+                        }
+                    }
+                }
+            }
+            return result;
+        },
+
+        deepMerge: function(target, source) {
+            if (source === null || typeof source !== 'object') return target;
+            if (target === null || typeof target !== 'object') return source;
+
+            var result = Array.isArray(target) ? [] : {};
+
+            for (var key in target) {
+                if (Object.prototype.hasOwnProperty.call(target, key)) {
+                    result[key] = target[key];
+                }
+            }
+
+            for (var key in source) {
+                if (Object.prototype.hasOwnProperty.call(source, key)) {
+                    if (source[key] !== null && typeof source[key] === 'object' &&
+                        result[key] !== null && typeof result[key] === 'object') {
+                        result[key] = this.deepMerge(result[key], source[key]);
+                    } else {
+                        result[key] = source[key];
+                    }
+                }
+            }
+            return result;
+        },
+
+        clone: function(value) {
+            if (value === null || typeof value !== 'object') return value;
+            return JSON.parse(JSON.stringify(value));
+        },
+
+        map: function(collection, iteratee) {
+            if (collection === null || collection === undefined) return [];
+            if (Array.isArray(collection)) {
+                return collection.map(iteratee);
+            }
+            return Object.keys(collection).map(function(key) {
+                return iteratee(collection[key], key);
+            });
+        },
+
+        filter: function(collection, predicate) {
+            if (collection === null || collection === undefined) return [];
+            if (Array.isArray(collection)) {
+                return collection.filter(predicate);
+            }
+            var result = [];
+            for (var key in collection) {
+                if (Object.prototype.hasOwnProperty.call(collection, key)) {
+                    if (predicate(collection[key], key)) {
+                        result.push(collection[key]);
+                    }
+                }
+            }
+            return result;
+        },
+
+        find: function(collection, predicate) {
+            if (collection === null || collection === undefined) return undefined;
+            if (Array.isArray(collection)) {
+                return collection.find(predicate);
+            }
+            for (var key in collection) {
+                if (Object.prototype.hasOwnProperty.call(collection, key)) {
+                    if (predicate(collection[key], key)) {
+                        return collection[key];
+                    }
+                }
+            }
+            return undefined;
+        },
+
+        findIndex: function(array, predicate) {
+            if (!Array.isArray(array)) return -1;
+            return array.findIndex(predicate);
+        },
+
+        reduce: function(collection, iteratee, accumulator) {
+            if (collection === null || collection === undefined) return accumulator;
+            if (Array.isArray(collection)) {
+                return collection.reduce(iteratee, accumulator);
+            }
+            var result = accumulator;
+            for (var key in collection) {
+                if (Object.prototype.hasOwnProperty.call(collection, key)) {
+                    result = iteratee(result, collection[key], key);
+                }
+            }
+            return result;
+        },
+
+        forEach: function(collection, iteratee) {
+            if (collection === null || collection === undefined) return;
+            if (Array.isArray(collection)) {
+                collection.forEach(iteratee);
+            } else {
+                for (var key in collection) {
+                    if (Object.prototype.hasOwnProperty.call(collection, key)) {
+                        iteratee(collection[key], key);
+                    }
+                }
+            }
+        },
+
+        some: function(collection, predicate) {
+            if (collection === null || collection === undefined) return false;
+            if (Array.isArray(collection)) {
+                return collection.some(predicate);
+            }
+            for (var key in collection) {
+                if (Object.prototype.hasOwnProperty.call(collection, key)) {
+                    if (predicate(collection[key], key)) return true;
+                }
+            }
+            return false;
+        },
+
+        every: function(collection, predicate) {
+            if (collection === null || collection === undefined) return true;
+            if (Array.isArray(collection)) {
+                return collection.every(predicate);
+            }
+            for (var key in collection) {
+                if (Object.prototype.hasOwnProperty.call(collection, key)) {
+                    if (!predicate(collection[key], key)) return false;
+                }
+            }
+            return true;
+        },
+
+        includes: function(collection, value) {
+            if (collection === null || collection === undefined) return false;
+            if (Array.isArray(collection)) {
+                return collection.includes(value);
+            }
+            if (typeof collection === 'string') {
+                return collection.includes(value);
+            }
+            return Object.values(collection).includes(value);
+        },
+
+        groupBy: function(collection, key) {
+            if (collection === null || collection === undefined) return {};
+            var result = {};
+            var arr = Array.isArray(collection) ? collection : Object.values(collection);
+            for (var i = 0; i < arr.length; i++) {
+                var item = arr[i];
+                var groupKey = typeof key === 'function' ? key(item) : item[key];
+                if (!result[groupKey]) {
+                    result[groupKey] = [];
+                }
+                result[groupKey].push(item);
+            }
+            return result;
+        },
+
+        keyBy: function(collection, key) {
+            if (collection === null || collection === undefined) return {};
+            var result = {};
+            var arr = Array.isArray(collection) ? collection : Object.values(collection);
+            for (var i = 0; i < arr.length; i++) {
+                var item = arr[i];
+                var itemKey = typeof key === 'function' ? key(item) : item[key];
+                result[itemKey] = item;
+            }
+            return result;
+        },
+
+        sortBy: function(collection, key) {
+            if (!Array.isArray(collection)) return [];
+            return collection.slice().sort(function(a, b) {
+                var aVal = typeof key === 'function' ? key(a) : a[key];
+                var bVal = typeof key === 'function' ? key(b) : b[key];
+                if (aVal < bVal) return -1;
+                if (aVal > bVal) return 1;
+                return 0;
+            });
+        },
+
+        uniq: function(array) {
+            if (!Array.isArray(array)) return [];
+            var seen = [];
+            var result = [];
+            for (var i = 0; i < array.length; i++) {
+                var item = array[i];
+                var found = false;
+                for (var j = 0; j < seen.length; j++) {
+                    if (JSON.stringify(seen[j]) === JSON.stringify(item)) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found) {
+                    seen.push(item);
+                    result.push(item);
+                }
+            }
+            return result;
+        },
+
+        uniqBy: function(array, key) {
+            if (!Array.isArray(array)) return [];
+            var seen = {};
+            var result = [];
+            for (var i = 0; i < array.length; i++) {
+                var item = array[i];
+                var itemKey = typeof key === 'function' ? key(item) : item[key];
+                if (!seen[itemKey]) {
+                    seen[itemKey] = true;
+                    result.push(item);
+                }
+            }
+            return result;
+        },
+
+        flatten: function(array) {
+            if (!Array.isArray(array)) return [];
+            var result = [];
+            for (var i = 0; i < array.length; i++) {
+                if (Array.isArray(array[i])) {
+                    for (var j = 0; j < array[i].length; j++) {
+                        result.push(array[i][j]);
+                    }
+                } else {
+                    result.push(array[i]);
+                }
+            }
+            return result;
+        },
+
+        flattenDeep: function(array) {
+            if (!Array.isArray(array)) return [];
+            var result = [];
+            function flattenRecursive(arr) {
+                for (var i = 0; i < arr.length; i++) {
+                    if (Array.isArray(arr[i])) {
+                        flattenRecursive(arr[i]);
+                    } else {
+                        result.push(arr[i]);
+                    }
+                }
+            }
+            flattenRecursive(array);
+            return result;
+        },
+
+        compact: function(array) {
+            if (!Array.isArray(array)) return [];
+            return array.filter(function(item) {
+                return Boolean(item);
+            });
+        },
+
+        first: function(array) {
+            if (!Array.isArray(array) || array.length === 0) return undefined;
+            return array[0];
+        },
+
+        last: function(array) {
+            if (!Array.isArray(array) || array.length === 0) return undefined;
+            return array[array.length - 1];
+        },
+
+        take: function(array, n) {
+            if (!Array.isArray(array)) return [];
+            n = n === undefined ? 1 : n;
+            return array.slice(0, n);
+        },
+
+        drop: function(array, n) {
+            if (!Array.isArray(array)) return [];
+            n = n === undefined ? 1 : n;
+            return array.slice(n);
+        },
+
+        chunk: function(array, size) {
+            if (!Array.isArray(array) || size < 1) return [];
+            var result = [];
+            for (var i = 0; i < array.length; i += size) {
+                result.push(array.slice(i, i + size));
+            }
+            return result;
+        },
+
+        reverse: function(array) {
+            if (!Array.isArray(array)) return [];
+            return array.slice().reverse();
+        },
+
+        trim: function(str) {
+            if (typeof str !== 'string') return '';
+            return str.trim();
+        },
+
+        trimStart: function(str) {
+            if (typeof str !== 'string') return '';
+            return str.replace(/^\s+/, '');
+        },
+
+        trimEnd: function(str) {
+            if (typeof str !== 'string') return '';
+            return str.replace(/\s+$/, '');
+        },
+
+        toUpper: function(str) {
+            if (typeof str !== 'string') return '';
+            return str.toUpperCase();
+        },
+
+        toLower: function(str) {
+            if (typeof str !== 'string') return '';
+            return str.toLowerCase();
+        },
+
+        capitalize: function(str) {
+            if (typeof str !== 'string' || str.length === 0) return '';
+            return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
+        },
+
+        camelCase: function(str) {
+            if (typeof str !== 'string') return '';
+            return str
+                .replace(/[-_\s]+(.)?/g, function(_, char) {
+                    return char ? char.toUpperCase() : '';
+                })
+                .replace(/^(.)/, function(firstChar) {
+                    return firstChar.toLowerCase();
+                });
+        },
+
+        snakeCase: function(str) {
+            if (typeof str !== 'string') return '';
+            return str
+                .replace(/([A-Z])/g, '_$1')
+                .replace(/[-\s]+/g, '_')
+                .toLowerCase()
+                .replace(/^_/, '');
+        },
+
+        kebabCase: function(str) {
+            if (typeof str !== 'string') return '';
+            return str
+                .replace(/([A-Z])/g, '-$1')
+                .replace(/[_\s]+/g, '-')
+                .toLowerCase()
+                .replace(/^-/, '');
+        },
+
+        startsWith: function(str, target) {
+            if (typeof str !== 'string') return false;
+            return str.indexOf(target) === 0;
+        },
+
+        endsWith: function(str, target) {
+            if (typeof str !== 'string') return false;
+            return str.indexOf(target, str.length - target.length) !== -1;
+        },
+
+        split: function(str, separator, limit) {
+            if (typeof str !== 'string') return [];
+            return str.split(separator, limit);
+        },
+
+        join: function(array, separator) {
+            if (!Array.isArray(array)) return '';
+            return array.join(separator === undefined ? ',' : separator);
+        },
+
+        replace: function(str, pattern, replacement) {
+            if (typeof str !== 'string') return '';
+            return str.replace(pattern, replacement);
+        },
+
+        repeat: function(str, n) {
+            if (typeof str !== 'string' || n < 1) return '';
+            var result = '';
+            for (var i = 0; i < n; i++) {
+                result += str;
+            }
+            return result;
+        },
+
+        padStart: function(str, length, chars) {
+            if (typeof str !== 'string') str = String(str);
+            chars = chars === undefined ? ' ' : chars;
+            while (str.length < length) {
+                str = chars + str;
+            }
+            return str.slice(-length);
+        },
+
+        padEnd: function(str, length, chars) {
+            if (typeof str !== 'string') str = String(str);
+            chars = chars === undefined ? ' ' : chars;
+            while (str.length < length) {
+                str = str + chars;
+            }
+            return str.slice(0, length);
+        },
+
+        truncate: function(str, options) {
+            if (typeof str !== 'string') return '';
+            options = options || {};
+            var length = options.length || 30;
+            var omission = options.omission || '...';
+            if (str.length <= length) return str;
+            return str.slice(0, length - omission.length) + omission;
+        },
+
+        parseInt: function(str, radix) {
+            return parseInt(str, radix || 10);
+        },
+
+        parseFloat: function(str) {
+            return parseFloat(str);
+        },
+
+        toNumber: function(value) {
+            if (typeof value === 'number') return value;
+            if (typeof value === 'string') {
+                var parsed = parseFloat(value);
+                return isNaN(parsed) ? 0 : parsed;
+            }
+            return 0;
+        },
+
+        toString: function(value) {
+            if (value === null) return 'null';
+            if (value === undefined) return 'undefined';
+            return String(value);
+        },
+
+        toJson: function(value, pretty) {
+            return pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
+        },
+
+        parseJson: function(str) {
+            try {
+                return JSON.parse(str);
+            } catch (e) {
+                return null;
+            }
+        },
+
+        min: function(array) {
+            if (!Array.isArray(array) || array.length === 0) return undefined;
+            return Math.min.apply(null, array);
+        },
+
+        max: function(array) {
+            if (!Array.isArray(array) || array.length === 0) return undefined;
+            return Math.max.apply(null, array);
+        },
+
+        sum: function(array) {
+            if (!Array.isArray(array)) return 0;
+            return array.reduce(function(acc, val) {
+                return acc + (typeof val === 'number' ? val : 0);
+            }, 0);
+        },
+
+        mean: function(array) {
+            if (!Array.isArray(array) || array.length === 0) return NaN;
+            return this.sum(array) / array.length;
+        },
+
+        round: function(number, precision) {
+            precision = precision || 0;
+            var factor = Math.pow(10, precision);
+            return Math.round(number * factor) / factor;
+        },
+
+        floor: function(number, precision) {
+            precision = precision || 0;
+            var factor = Math.pow(10, precision);
+            return Math.floor(number * factor) / factor;
+        },
+
+        ceil: function(number, precision) {
+            precision = precision || 0;
+            var factor = Math.pow(10, precision);
+            return Math.ceil(number * factor) / factor;
+        },
+
+        clamp: function(number, lower, upper) {
+            if (number < lower) return lower;
+            if (number > upper) return upper;
+            return number;
+        },
+
+        random: function(lower, upper) {
+            if (upper === undefined) {
+                upper = lower;
+                lower = 0;
+            }
+            return lower + Math.floor(Math.random() * (upper - lower + 1));
+        },
+
+        range: function(start, end, step) {
+            if (end === undefined) {
+                end = start;
+                start = 0;
+            }
+            step = step || 1;
+            var result = [];
+            if (step > 0) {
+                for (var i = start; i < end; i += step) {
+                    result.push(i);
+                }
+            } else {
+                for (var i = start; i > end; i += step) {
+                    result.push(i);
+                }
+            }
+            return result;
+        },
+
+        formatDate: function(date, format) {
+            if (!(date instanceof Date)) {
+                date = new Date(date);
+            }
+            if (isNaN(date.getTime())) return '';
+
+            format = format || 'YYYY-MM-DD HH:mm:ss';
+
+            var year = date.getFullYear();
+            var month = date.getMonth() + 1;
+            var day = date.getDate();
+            var hours = date.getHours();
+            var minutes = date.getMinutes();
+            var seconds = date.getSeconds();
+            var ms = date.getMilliseconds();
+
+            return format
+                .replace('YYYY', year)
+                .replace('YY', String(year).slice(-2))
+                .replace('MM', this.padStart(month, 2, '0'))
+                .replace('M', month)
+                .replace('DD', this.padStart(day, 2, '0'))
+                .replace('D', day)
+                .replace('HH', this.padStart(hours, 2, '0'))
+                .replace('H', hours)
+                .replace('mm', this.padStart(minutes, 2, '0'))
+                .replace('m', minutes)
+                .replace('ss', this.padStart(seconds, 2, '0'))
+                .replace('s', seconds)
+                .replace('SSS', this.padStart(ms, 3, '0'));
+        },
+
+        parseDate: function(str) {
+            var date = new Date(str);
+            return isNaN(date.getTime()) ? null : date;
+        },
+
+        addDays: function(date, days) {
+            if (!(date instanceof Date)) {
+                date = new Date(date);
+            }
+            var result = new Date(date);
+            result.setDate(result.getDate() + days);
+            return result;
+        },
+
+        addHours: function(date, hours) {
+            if (!(date instanceof Date)) {
+                date = new Date(date);
+            }
+            var result = new Date(date);
+            result.setTime(result.getTime() + hours * 60 * 60 * 1000);
+            return result;
+        },
+
+        addMinutes: function(date, minutes) {
+            if (!(date instanceof Date)) {
+                date = new Date(date);
+            }
+            var result = new Date(date);
+            result.setTime(result.getTime() + minutes * 60 * 1000);
+            return result;
+        },
+
+        diffDays: function(date1, date2) {
+            if (!(date1 instanceof Date)) date1 = new Date(date1);
+            if (!(date2 instanceof Date)) date2 = new Date(date2);
+            var diff = date2.getTime() - date1.getTime();
+            return Math.floor(diff / (1000 * 60 * 60 * 24));
+        },
+
+        diffHours: function(date1, date2) {
+            if (!(date1 instanceof Date)) date1 = new Date(date1);
+            if (!(date2 instanceof Date)) date2 = new Date(date2);
+            var diff = date2.getTime() - date1.getTime();
+            return Math.floor(diff / (1000 * 60 * 60));
+        },
+
+        startOfDay: function(date) {
+            if (!(date instanceof Date)) {
+                date = new Date(date);
+            }
+            var result = new Date(date);
+            result.setHours(0, 0, 0, 0);
+            return result;
+        },
+
+        endOfDay: function(date) {
+            if (!(date instanceof Date)) {
+                date = new Date(date);
+            }
+            var result = new Date(date);
+            result.setHours(23, 59, 59, 999);
+            return result;
+        },
+
+        isoString: function(date) {
+            if (!(date instanceof Date)) {
+                date = new Date(date);
+            }
+            return date.toISOString();
+        },
+
+        uuid: function() {
+            return smartbotic.utils.uuid();
+        },
+
+        base64Encode: function(str) {
+            return smartbotic.utils.base64Encode(str);
+        },
+
+        base64Decode: function(str) {
+            return smartbotic.utils.base64Decode(str);
+        },
+
+        sha256: function(str) {
+            return smartbotic.utils.sha256(str);
+        },
+
+        sleep: function(ms) {
+            return smartbotic.utils.sleep(ms);
+        }
+    };
+}
+
+function extractLineNumber(error) {
+    if (!error || !error.stack) {
+        return null;
+    }
+
+    var stack = error.stack.toString();
+    var lines = stack.split('\n');
+
+    for (var i = 0; i < lines.length; i++) {
+        var line = lines[i];
+
+        var evalMatch = line.match(/at\s+eval[^:]*:(\d+):/);
+        if (evalMatch) {
+            return parseInt(evalMatch[1], 10);
+        }
+
+        var userCodeMatch = line.match(/<user-code>:(\d+):/);
+        if (userCodeMatch) {
+            return parseInt(userCodeMatch[1], 10);
+        }
+
+        var anonymousMatch = line.match(/<anonymous>:(\d+):/);
+        if (anonymousMatch) {
+            return parseInt(anonymousMatch[1], 10);
+        }
+    }
+
+    return null;
+}
+
+async function execute(config, input, context) {
+    var code = config.code;
+    var timeout = config.timeout || 30;
+
+    if (!code || typeof code !== 'string') {
+        throw new Error('No code provided');
+    }
+
+    if (timeout < 1) timeout = 1;
+    if (timeout > 300) timeout = 300;
+
+    var helpers = createHelpers();
+
+    var startTime = Date.now();
+
+    smartbotic.log.info('Executing custom code (' + code.length + ' chars, timeout: ' + timeout + 's)');
+
+    try {
+        var userInput = input;
+        var userContext = context;
+        var _ = helpers;
+
+        var userCodeFunction = new Function('input', 'context', '_', '"use strict";\n' + code);
+
+        var result = userCodeFunction(userInput, userContext, _);
+
+        var executionTime = Date.now() - startTime;
+
+        smartbotic.log.info('Code executed successfully in ' + executionTime + 'ms');
+
+        return {
+            result: result,
+            executionTime: executionTime
+        };
+    } catch (error) {
+        var executionTime = Date.now() - startTime;
+        var lineNumber = extractLineNumber(error);
+        var errorMessage = error.message || String(error);
+
+        if (lineNumber !== null) {
+            errorMessage = 'Line ' + lineNumber + ': ' + errorMessage;
+        }
+
+        smartbotic.log.error('Code execution failed: ' + errorMessage);
+
+        throw new Error(errorMessage);
+    }
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, execute };