From 17b78b406d50cfc41c535c63356d17daefe8839c Mon Sep 17 00:00:00 2001 From: Raj bhoyar <85496077+rajbhoyar729@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:07:43 +0530 Subject: [PATCH] feat(config): add envelopeDateFormat for custom envelope timestamp formatting --- src/auto-reply/envelope.ts | 88 +++++++++++++++++++++++-- src/config/types.agent-defaults.ts | 5 ++ src/config/zod-schema.agent-defaults.ts | 1 + 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/auto-reply/envelope.ts b/src/auto-reply/envelope.ts index 53622d5e5..7a1593445 100644 --- a/src/auto-reply/envelope.ts +++ b/src/auto-reply/envelope.ts @@ -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 = { + 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(); diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 9c6ce0211..5988f5127 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -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"). */ diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index a849078ed..479001aae 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -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(),