automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
345 lines (344 loc) • 12 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatRelativeTime = formatRelativeTime;
exports.formatPathRelative = formatPathRelative;
exports.truncateText = truncateText;
exports.sanitizeLogFilename = sanitizeLogFilename;
exports.safeIsoString = safeIsoString;
exports.deepClone = deepClone;
exports.mergeDeep = mergeDeep;
const path_1 = __importDefault(require("path"));
/**
* Formats an ISO timestamp as human-readable relative time.
*
* Converts timestamps to friendly formats like "5m ago", "3h ago", or "2d ago".
* For times older than 5 weeks, returns a localized date string.
*
* @param {string} value - ISO 8601 timestamp string (e.g., '2025-09-30T10:00:00Z')
* @returns {string} Relative time string ('just now', '5m ago', '2h ago', '3d ago', '2w ago')
* or localized date string for older timestamps. Returns 'n/a' for invalid dates.
*
* @example
* // Current time: 2025-09-30T10:05:00Z
* formatRelativeTime('2025-09-30T10:00:00Z')
* // Returns: '5m ago'
*
* @example
* // Invalid date handling
* formatRelativeTime('invalid-date')
* // Returns: 'n/a'
*
* @example
* // Future dates
* formatRelativeTime('2025-12-31T00:00:00Z')
* // Returns: 'just now'
*
* @performance O(1) - Simple arithmetic operations
*/
function formatRelativeTime(value) {
const timestamp = new Date(value).getTime();
if (!Number.isFinite(timestamp))
return 'n/a';
const diffMs = Date.now() - timestamp;
if (diffMs < 0)
return 'just now';
const seconds = Math.floor(diffMs / 1000);
if (seconds < 5)
return 'just now';
if (seconds < 60)
return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60)
return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24)
return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 7)
return `${days}d ago`;
const weeks = Math.floor(days / 7);
if (weeks < 5)
return `${weeks}w ago`;
return new Date(value).toLocaleDateString();
}
/**
* Formats an absolute file system path as relative to a base directory.
*
* Calculates the relative path from baseDir to targetPath. Useful for
* displaying cleaner paths in logs or UI by removing common prefixes.
*
* @param {string} targetPath - Absolute file system path to format (e.g., '/home/user/project/src/file.ts')
* @param {string} baseDir - Base directory for relative path calculation (e.g., '/home/user/project')
* @returns {string} Relative path from baseDir to targetPath (e.g., 'src/file.ts').
* Returns original targetPath if calculation fails or targetPath is falsy.
* Returns 'n/a' if targetPath is empty or undefined.
*
* @example
* formatPathRelative('/home/user/project/src/file.ts', '/home/user/project')
* // Returns: 'src/file.ts'
*
* @example
* // Same directory
* formatPathRelative('/home/user/project', '/home/user/project')
* // Returns: '/home/user/project'
*
* @example
* // Error handling
* formatPathRelative('', '/home/user')
* // Returns: 'n/a'
*
* @performance O(n) where n is the length of the path strings
*/
function formatPathRelative(targetPath, baseDir) {
if (!targetPath)
return 'n/a';
try {
return path_1.default.relative(baseDir, targetPath) || targetPath;
}
catch {
return targetPath;
}
}
/**
* Truncates text to a maximum length, adding ellipsis if truncated.
*
* Ensures text doesn't exceed maxLength by slicing and appending '...'.
* Trims whitespace before adding ellipsis for cleaner output.
*
* @param {string} text - Text string to truncate
* @param {number} [maxLength=64] - Maximum character length including ellipsis (must be >= 3 for meaningful truncation)
* @returns {string} Original text if length <= maxLength, otherwise truncated text with '...' suffix.
* Returns empty string if text is falsy.
*
* @example
* truncateText('This is a very long sentence that needs truncation', 20)
* // Returns: 'This is a very lo...'
*
* @example
* // Short text remains unchanged
* truncateText('Short text', 20)
* // Returns: 'Short text'
*
* @example
* // Empty or falsy input
* truncateText('', 10)
* // Returns: ''
*
* @example
* // Edge case: maxLength shorter than ellipsis
* truncateText('Hello World', 2)
* // Returns: '...' (sliceLength becomes 0, trimmed)
*
* @performance O(n) where n is maxLength
* @warning If maxLength < 3, result may be just '...' or shorter
*/
function truncateText(text, maxLength = 64) {
if (!text)
return '';
if (text.length <= maxLength)
return text;
const sliceLength = Math.max(0, maxLength - 3);
return text.slice(0, sliceLength).trimEnd() + '...';
}
/**
* Sanitizes an agent name to create a safe filename for logging.
*
* Performs multiple transformations to ensure the name is filesystem-safe:
* - Replaces path separators (/ \) with dashes
* - Removes special characters (keeps only alphanumeric, dots, dashes, underscores)
* - Collapses consecutive separators into single characters
* - Trims leading/trailing separators and dots
*
* @param {string} agentName - Agent identifier to sanitize (e.g., 'core/implementor', 'my-agent@@!!')
* @returns {string} Sanitized filename containing only [a-z0-9._-] characters.
* Returns 'agent' as fallback if input is invalid or results in empty string.
*
* @example
* sanitizeLogFilename('core/implementor')
* // Returns: 'core-implementor'
*
* @example
* sanitizeLogFilename('my-agent@@!!')
* // Returns: 'my-agent'
*
* @example
* sanitizeLogFilename('//bad...name--')
* // Returns: 'bad.name'
*
* @example
* // Invalid input fallback
* sanitizeLogFilename('')
* // Returns: 'agent'
*
* @example
* sanitizeLogFilename(null as any)
* // Returns: 'agent'
*
* @performance O(n) where n is the length of agentName
* @warning Does not check for reserved filesystem names (e.g., 'CON', 'PRN' on Windows)
*/
function sanitizeLogFilename(agentName) {
const fallback = 'agent';
if (!agentName || typeof agentName !== 'string')
return fallback;
const normalized = agentName
.trim()
.replace(/[\\/]+/g, '-')
.replace(/[^a-z0-9._-]+/gi, '-')
.replace(/-+/g, '-')
.replace(/\.+/g, '.')
.replace(/^-+|-+$/g, '')
.replace(/^\.+|\.+$/g, '');
return normalized.length ? normalized : fallback;
}
/**
* Converts a timestamp string to ISO 8601 format with validation.
*
* Parses any valid date string and returns standardized ISO format.
* Useful for normalizing dates from various sources.
*
* @param {string} value - Timestamp string in any format parseable by Date constructor
* (e.g., '2025-09-30', 'Sep 30, 2025', '1727692800000')
* @returns {string | null} ISO 8601 formatted string (e.g., '2025-09-30T10:00:00.000Z')
* or null if the input cannot be parsed into a valid date.
*
* @example
* safeIsoString('2025-09-30T10:00:00Z')
* // Returns: '2025-09-30T10:00:00.000Z'
*
* @example
* safeIsoString('Sep 30, 2025')
* // Returns: '2025-09-30T00:00:00.000Z' (depending on timezone)
*
* @example
* // Invalid date handling
* safeIsoString('not-a-date')
* // Returns: null
*
* @example
* safeIsoString('Invalid Date')
* // Returns: null
*
* @performance O(1) - Simple date parsing and formatting
*/
function safeIsoString(value) {
const timestamp = new Date(value).getTime();
if (!Number.isFinite(timestamp))
return null;
return new Date(timestamp).toISOString();
}
/**
* Creates a deep clone of an object using JSON serialization.
*
* Produces a completely independent copy of the input object by serializing
* to JSON and deserializing. All nested objects and arrays are cloned.
*
* @template T - Type of the object to clone
* @param {T} input - Object, array, or primitive value to clone
* @returns {T} Deep copy of the input with no shared references to the original
*
* @throws {TypeError} If input contains circular references (JSON.stringify will throw)
* @throws {TypeError} If input contains values that cannot be serialized to JSON
*
* @example
* const original = { a: { b: 1 }, c: [2, 3] };
* const clone = deepClone(original);
* clone.a.b = 99;
* console.log(original.a.b); // Still 1
*
* @example
* // Arrays are also cloned
* const arr = [{ id: 1 }, { id: 2 }];
* const clonedArr = deepClone(arr);
* clonedArr[0].id = 99;
* console.log(arr[0].id); // Still 1
*
* @example
* // Primitives are returned as-is
* deepClone(42) // Returns: 42
* deepClone('hello') // Returns: 'hello'
*
* @performance O(n) where n is the total number of properties/elements in the object tree.
* Memory usage is 2x the input size during cloning.
*
* @warning Does not handle:
* - Circular references (will throw TypeError)
* - Functions (will be omitted)
* - undefined values (will be omitted or converted to null)
* - Symbols (will be omitted)
* - Non-enumerable properties (will be omitted)
* - Class instances (will lose prototype chain, become plain objects)
* - Date objects (will become strings)
* - RegExp, Map, Set, etc. (will become empty objects or strings)
*/
function deepClone(input) {
return JSON.parse(JSON.stringify(input));
}
/**
* Recursively merges a source object into a target object.
*
* Creates a new object by deeply merging properties from source into target.
* Arrays in source completely replace arrays in target (no element merging).
* Null/undefined source values are ignored, preserving target values.
*
* @param {any} target - Base object to merge into (will not be modified)
* @param {any} source - Object whose properties will be merged into target
* @returns {any} New object with merged properties. If target is not a plain object,
* returns source value. If source is null/undefined, returns target.
*
* @example
* const target = { a: 1, b: { c: 2, d: 3 } };
* const source = { b: { c: 99 }, e: 5 };
* const result = mergeDeep(target, source);
* // Returns: { a: 1, b: { c: 99, d: 3 }, e: 5 }
*
* @example
* // Arrays are replaced, not merged
* const target = { items: [1, 2, 3] };
* const source = { items: [4, 5] };
* const result = mergeDeep(target, source);
* // Returns: { items: [4, 5] }
*
* @example
* // Null source preserves target
* mergeDeep({ a: 1 }, null)
* // Returns: { a: 1 }
*
* @example
* // Primitives replace objects
* mergeDeep({ a: { b: 1 } }, { a: 'string' })
* // Returns: { a: 'string' }
*
* @example
* // Non-object target is replaced
* mergeDeep('string', { a: 1 })
* // Returns: { a: 1 }
*
* @performance O(n) where n is the total number of properties in both objects.
* Memory usage is proportional to the depth and size of the object tree.
*
* @warning
* - Arrays are shallow-copied and replace target arrays entirely
* - Does not handle circular references (may cause stack overflow)
* - Properties with undefined values in source will overwrite target properties
* - Does not preserve property descriptors or getters/setters
* - Non-plain objects (class instances) are treated as plain objects
*/
function mergeDeep(target, source) {
if (source === null || source === undefined)
return target;
if (Array.isArray(source)) {
return source.slice();
}
if (typeof source !== 'object') {
return source;
}
const base = target && typeof target === 'object' && !Array.isArray(target) ? { ...target } : {};
Object.entries(source).forEach(([key, value]) => {
base[key] = mergeDeep(base[key], value);
});
return base;
}