feat(config): add envelopeDateFormat for custom envelope timestamp formatting

This commit is contained in:
Raj bhoyar 2026-01-27 13:07:43 +05:30
parent 9a2be717b7
commit 17b78b406d
3 changed files with 88 additions and 6 deletions

View File

@ -28,9 +28,13 @@ export type EnvelopeFormatOptions = {
*/
includeElapsed?: boolean;
/**
* Optional user timezone used when timezone="user".
* Optional user timezone used when timezone = "user".
*/
userTimezone?: string;
/**
* Custom date format string (e.g., "ddd MMM D HH:mm"). Overrides default timestamp format.
*/
dateFormat?: string;
};
type NormalizedEnvelopeOptions = {
@ -38,6 +42,7 @@ type NormalizedEnvelopeOptions = {
includeTimestamp: boolean;
includeElapsed: boolean;
userTimezone?: string;
dateFormat?: string;
};
type ResolvedEnvelopeTimezone =
@ -52,6 +57,7 @@ export function resolveEnvelopeFormatOptions(cfg?: ClawdbotConfig): EnvelopeForm
includeTimestamp: defaults?.envelopeTimestamp !== "off",
includeElapsed: defaults?.envelopeElapsed !== "off",
userTimezone: defaults?.userTimezone,
dateFormat: defaults?.envelopeDateFormat,
};
}
@ -63,6 +69,7 @@ function normalizeEnvelopeOptions(options?: EnvelopeFormatOptions): NormalizedEn
includeTimestamp,
includeElapsed,
userTimezone: options?.userTimezone,
dateFormat: options?.dateFormat,
};
}
@ -132,11 +139,80 @@ function formatTimestamp(
const date = ts instanceof Date ? ts : new Date(ts);
if (Number.isNaN(date.getTime())) return undefined;
const zone = resolveEnvelopeTimezone(resolved);
if (resolved.dateFormat) {
let tz: string | undefined;
if (zone.mode === "iana") tz = zone.timeZone;
else if (zone.mode === "utc") tz = "UTC";
return formatDate(date, resolved.dateFormat, tz);
}
if (zone.mode === "utc") return formatUtcTimestamp(date);
if (zone.mode === "local") return formatZonedTimestamp(date);
return formatZonedTimestamp(date, zone.timeZone);
}
function formatDate(date: Date, format: string, checkTimeZone?: string): string {
// We need to extract parts in the correct timezone
const timeZone = checkTimeZone || undefined; // undefined = local
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false,
timeZoneName: "short",
}).formatToParts(date);
const getPart = (type: string) => parts.find((p) => p.type === type)?.value || "";
// Also get 12h hour and day period/ampm
const parts12 = new Intl.DateTimeFormat("en-US", {
timeZone,
hour: "numeric",
hour12: true,
}).formatToParts(date);
const hour12 = parts12.find((p) => p.type === "hour")?.value || "";
const dayPeriod = parts12.find((p) => p.type === "dayPeriod")?.value || "";
const Y = getPart("year");
const M = getPart("month"); // Long month
const D = getPart("day");
const d = getPart("weekday"); // Long weekday
const H = getPart("hour").padStart(2, "0"); // 0-23
const m = getPart("minute").padStart(2, "0");
const s = getPart("second").padStart(2, "0");
const z = parts.find((p) => p.type === "timeZoneName")?.value || "";
// Helper for short month/weekday
const MMM = M.slice(0, 3);
const MM = new Intl.DateTimeFormat("en-US", { timeZone, month: "2-digit" }).format(date);
const DD = D.padStart(2, "0");
const ddd = d.slice(0, 3);
const h = hour12.padStart(2, "0");
const map: Record<string, string> = {
YYYY: Y,
MMM: MMM,
MM: MM,
DD: DD,
D: D,
dddd: d,
ddd: ddd,
HH: H,
hh: h,
mm: m,
ss: s,
a: dayPeriod.toLowerCase(),
A: dayPeriod.toUpperCase(),
z: z,
};
return format.replace(/YYYY|MMM|MM|DD|D|dddd|ddd|HH|hh|mm|ss|a|A|z/g, (match) => map[match]);
}
function formatElapsedTime(currentMs: number, previousMs: number): string | undefined {
const elapsedMs = currentMs - previousMs;
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return undefined;
@ -161,11 +237,11 @@ export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
const elapsed =
resolved.includeElapsed && params.timestamp && params.previousTimestamp
? formatElapsedTime(
params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp,
params.previousTimestamp instanceof Date
? params.previousTimestamp.getTime()
: params.previousTimestamp,
)
params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp,
params.previousTimestamp instanceof Date
? params.previousTimestamp.getTime()
: params.previousTimestamp,
)
: undefined;
if (params.from?.trim()) {
const from = params.from.trim();

View File

@ -114,6 +114,11 @@ export type AgentDefaultsConfig = {
* Envelope timestamp timezone: "utc" (default), "local", "user", or an IANA timezone string.
*/
envelopeTimezone?: string;
/**
* Custom date format string (e.g., "ddd MMM D HH:mm"). Overrides default timestamp format.
* Supported tokens: YYYY, MMM, MM, DD, D, ddd, dddd, HH, hh, mm, ss, a, A, z
*/
envelopeDateFormat?: string;
/**
* Include absolute timestamps in message envelopes ("on" | "off", default: "on").
*/

View File

@ -48,6 +48,7 @@ export const AgentDefaultsSchema = z
userTimezone: z.string().optional(),
timeFormat: z.union([z.literal("auto"), z.literal("12"), z.literal("24")]).optional(),
envelopeTimezone: z.string().optional(),
envelopeDateFormat: z.string().optional(),
envelopeTimestamp: z.union([z.literal("on"), z.literal("off")]).optional(),
envelopeElapsed: z.union([z.literal("on"), z.literal("off")]).optional(),
contextTokens: z.number().int().positive().optional(),