a tool to help your Letta AI agents navigate bluesky
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};