a tool to help your Letta AI agents navigate bluesky
at main 6.2 kB view raw
1import { agentContext } from "./agentContext.ts"; 2import { Temporal } from "@js-temporal/polyfill"; 3 4/** 5 * Parse a time string with unit suffix or raw milliseconds 6 * @param value - Time string like "10s", "90m", "3h" or raw milliseconds 7 * @returns Time in milliseconds 8 * @example 9 * parseTimeValue("10s") // → 10000 10 * parseTimeValue("90m") // → 5400000 11 * parseTimeValue("3h") // → 10800000 12 * parseTimeValue("5400000") // → 5400000 (backward compat) 13 * parseTimeValue(10000) // → 10000 (already a number) 14 */ 15function parseTimeValue(value: string | number | undefined): number { 16 if (value === undefined || value === "") { 17 throw new Error("Time value is required"); 18 } 19 20 if (typeof value === "number") { 21 return value; 22 } 23 24 const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|ms)?$/i); 25 26 if (!match) { 27 throw new Error( 28 `Invalid time format: "${value}". Expected: "10s", "90m", "3h", or raw milliseconds`, 29 ); 30 } 31 32 const [, numStr, unit] = match; 33 const num = parseFloat(numStr); 34 35 if (isNaN(num) || num < 0) { 36 throw new Error(`Time value must be a positive number: "${value}"`); 37 } 38 39 switch (unit?.toLowerCase()) { 40 case "s": 41 return msFrom.seconds(num); 42 case "m": 43 return msFrom.minutes(num); 44 case "h": 45 return msFrom.hours(num); 46 case "ms": 47 case undefined: 48 return num; 49 default: 50 throw new Error(`Invalid unit: "${unit}". Use s/m/h/ms`); 51 } 52} 53 54/** 55 * Convert time units to milliseconds 56 */ 57export const msFrom = { 58 /** 59 * Convert seconds to milliseconds 60 * @param s - number of seconds 61 */ 62 seconds: (seconds: number): number => seconds * 1000, 63 /** 64 * Convert minutes to milliseconds 65 * @param m - number of minutes 66 */ 67 minutes: (minutes: number): number => minutes * 60 * 1000, 68 /** 69 * Convert hours to milliseconds 70 * @param h - number of hours 71 */ 72 hours: (hours: number): number => hours * 60 * 60 * 1000, 73 /** 74 * Parse a time string with unit suffix (e.g., "10s", "90m", "3h") or raw milliseconds 75 * @param value - Time string or number 76 * @returns Time in milliseconds 77 */ 78 parse: parseTimeValue, 79}; 80 81/** 82 * Generate a random time interval in milliseconds within a defined range 83 * 84 * @param minimum - the minimum duration in milliseconds (default: 5 minutes) 85 * @param maximum - the maximum duration in milliseconds (default: 15 minutes) 86 * @returns A random time interval in milliseconds between the min and max range 87 */ 88 89export const msRandomOffset = ( 90 minimum: number = msFrom.minutes(5), 91 maximum: number = msFrom.minutes(15), 92): number => { 93 if (maximum <= minimum) { 94 throw new Error("Maximum time must be larger than minimum time"); 95 } 96 97 if (minimum < 0 || maximum < 0) { 98 throw new Error("Time values must be non-negative"); 99 } 100 101 if (Math.max(minimum, maximum) > msFrom.hours(24)) { 102 throw new Error( 103 `time values must not exceed ${ 104 msFrom.hours(24) 105 } (24 hours). you entered: [min: ${minimum}ms, max: ${maximum}ms]`, 106 ); 107 } 108 109 const min = Math.ceil(minimum); 110 const max = Math.floor(maximum); 111 112 return Math.floor(Math.random() * (max - min) + min); 113}; 114 115/** 116 * finds the time in milliseconds until the next wake window 117 * 118 * @param minimumOffset - the minimum duration in milliseconds to offset from the window 119 * @param maximumOffset - the maximum duration in milliseconds to offset from the window 120 * @returns time until next wake window plus random offset, in milliseconds 121 */ 122export const msUntilNextWakeWindow = ( 123 minimumOffset: number, 124 maximumOffset: number, 125): number => { 126 const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone); 127 128 if (!agentContext.sleepEnabled) { 129 return 0; 130 } 131 132 if ( 133 current.hour >= agentContext.wakeTime && 134 current.hour < agentContext.sleepTime 135 ) { 136 return 0; 137 } else { 138 let newTime; 139 140 if (current.hour < agentContext.wakeTime) { 141 newTime = current.with({ hour: agentContext.wakeTime }); 142 } else { 143 newTime = current.add({ days: 1 }).with({ hour: agentContext.wakeTime }); 144 } 145 146 return newTime.toInstant().epochMilliseconds + 147 msRandomOffset(minimumOffset, maximumOffset) - 148 current.toInstant().epochMilliseconds; 149 } 150}; 151 152/** 153 * Calculate the time until next configurable window, plus a random offset. 154 * @param window - the hour of the day to wake up at 155 * @param minimumOffset - the minimum duration in milliseconds to offset from the window 156 * @param maximumOffset - the maximum duration in milliseconds to offset from the window 157 * @returns time until next daily window plus random offset, in milliseconds 158 */ 159export const msUntilDailyWindow = ( 160 window: number, 161 minimumOffset: number, 162 maximumOffset: number, 163): number => { 164 const current = Temporal.Now.zonedDateTimeISO(agentContext.timeZone); 165 166 if (window > 23) { 167 throw Error("window hour cannot exceed 23 (11pm)"); 168 } 169 170 let msToWindow; 171 if (current.hour < window) { 172 msToWindow = current.with({ hour: window }).toInstant().epochMilliseconds; 173 } else { 174 msToWindow = current.add({ days: 1 }).with({ hour: window }).toInstant() 175 .epochMilliseconds; 176 } 177 178 return msToWindow + 179 msRandomOffset(minimumOffset, maximumOffset) - 180 current.toInstant().epochMilliseconds; 181}; 182 183export const getNow = () => { 184 return Temporal.Now.zonedDateTimeISO(agentContext.timeZone); 185}; 186 187/** 188 * Format uptime from milliseconds into a human-readable string 189 * @param ms - uptime in milliseconds 190 * @returns Formatted string like "2 days, 3 hours, 15 minutes" or "3 hours, 15 minutes" 191 */ 192export const formatUptime = (ms: number): string => { 193 const days = Math.floor(ms / (1000 * 60 * 60 * 24)); 194 const hours = Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 195 const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)); 196 197 const parts: string[] = []; 198 199 if (days > 0) { 200 parts.push(`${days} ${days === 1 ? "day" : "days"}`); 201 } 202 if (hours > 0) { 203 parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`); 204 } 205 if (minutes > 0 || parts.length === 0) { 206 parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); 207 } 208 209 return parts.join(", "); 210};