import { agentContext } from "./agentContext.ts"; import { Temporal } from "@js-temporal/polyfill"; /** * Parse a time string with unit suffix or raw milliseconds * @param value - Time string like "10s", "90m", "3h" or raw milliseconds * @returns Time in milliseconds * @example * parseTimeValue("10s") // → 10000 * parseTimeValue("90m") // → 5400000 * parseTimeValue("3h") // → 10800000 * parseTimeValue("5400000") // → 5400000 (backward compat) * parseTimeValue(10000) // → 10000 (already a number) */ function parseTimeValue(value: string | number | undefined): number { if (value === undefined || value === "") { throw new Error("Time value is required"); } if (typeof value === "number") { return value; } const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|ms)?$/i); if (!match) { throw new Error( `Invalid time format: "${value}". Expected: "10s", "90m", "3h", or raw milliseconds`, ); } const [, numStr, unit] = match; const num = parseFloat(numStr); if (isNaN(num) || num < 0) { throw new Error(`Time value must be a positive number: "${value}"`); } switch (unit?.toLowerCase()) { case "s": return msFrom.seconds(num); case "m": return msFrom.minutes(num); case "h": return msFrom.hours(num); case "ms": case undefined: return num; default: throw new Error(`Invalid unit: "${unit}". Use s/m/h/ms`); } } /** * Convert time units to milliseconds */ export const msFrom = { /** * Convert seconds to milliseconds * @param s - number of seconds */ seconds: (seconds: number): number => seconds * 1000, /** * Convert minutes to milliseconds * @param m - number of minutes */ minutes: (minutes: number): number => minutes * 60 * 1000, /** * Convert hours to milliseconds * @param h - number of hours */ hours: (hours: number): number => hours * 60 * 60 * 1000, /** * Parse a time string with unit suffix (e.g., "10s", "90m", "3h") or raw milliseconds * @param value - Time string or number * @returns Time in milliseconds */ parse: parseTimeValue, }; /** * Generate a random time interval in milliseconds within a defined range * * @param minimum - the minimum duration in milliseconds (default: 5 minutes) * @param maximum - the maximum duration in milliseconds (default: 15 minutes) * @returns A random time interval in milliseconds between the min and max range */ export const msRandomOffset = ( minimum: number = msFrom.minutes(5), maximum: number = msFrom.minutes(15), ): number => { if (maximum <= minimum) { throw new Error("Maximum time must be larger than minimum time"); } if (minimum < 0 || maximum < 0) { throw new Error("Time values must be non-negative"); } if (Math.max(minimum, maximum) > msFrom.hours(24)) { throw new Error( `time values must not exceed ${ msFrom.hours(24) } (24 hours). you entered: [min: ${minimum}ms, max: ${maximum}ms]`, ); } const min = Math.ceil(minimum); const max = Math.floor(maximum); return Math.floor(Math.random() * (max - min) + min); }; /** * finds the time in milliseconds until the next wake window * * @param minimumOffset - the minimum duration in milliseconds to offset from the window * @param maximumOffset - the maximum duration in milliseconds to offset from the window * @returns time until next wake window plus random offset, in milliseconds */ export const msUntilNextWakeWindow = ( minimumOffset: number, maximumOffset: number, ): number => { const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone); if (!agentContext.sleepEnabled) { return 0; } if ( current.hour >= agentContext.wakeTime && current.hour < agentContext.sleepTime ) { return 0; } else { let newTime; if (current.hour < agentContext.wakeTime) { newTime = current.with({ hour: agentContext.wakeTime }); } else { newTime = current.add({ days: 1 }).with({ hour: agentContext.wakeTime }); } return newTime.toInstant().epochMilliseconds + msRandomOffset(minimumOffset, maximumOffset) - current.toInstant().epochMilliseconds; } }; /** * Calculate the time until next configurable window, plus a random offset. * @param window - the hour of the day to wake up at * @param minimumOffset - the minimum duration in milliseconds to offset from the window * @param maximumOffset - the maximum duration in milliseconds to offset from the window * @returns time until next daily window plus random offset, in milliseconds */ export const msUntilDailyWindow = ( window: number, minimumOffset: number, maximumOffset: number, ): number => { const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone); if (window > 23) { throw Error("window hour cannot exceed 23 (11pm)"); } let msToWindow; if (current.hour < window) { msToWindow = current.with({ hour: window }).toInstant().epochMilliseconds; } else { msToWindow = current.add({ days: 1 }).with({ hour: window }).toInstant() .epochMilliseconds; } return msToWindow + msRandomOffset(minimumOffset, maximumOffset) - current.toInstant().epochMilliseconds; }; export const getNow = () => { return Temporal.Now.zonedDateTimeISO(agentContext.timeZone); }; /** * Format uptime from milliseconds into a human-readable string * @param ms - uptime in milliseconds * @returns Formatted string like "2 days, 3 hours, 15 minutes" or "3 hours, 15 minutes" */ export const formatUptime = (ms: number): string => { const days = Math.floor(ms / (1000 * 60 * 60 * 24)); const hours = Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)); const parts: string[] = []; if (days > 0) { parts.push(`${days} ${days === 1 ? "day" : "days"}`); } if (hours > 0) { parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`); } if (minutes > 0 || parts.length === 0) { parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); } return parts.join(", "); };