|
| 1 | +import { readFileSync } from "node:fs"; |
| 2 | +import { join } from "node:path"; |
| 3 | +import * as v from "valibot"; |
| 4 | + |
| 5 | +// --- Schema --- |
| 6 | + |
| 7 | +const TimeString = v.pipe( |
| 8 | + v.string(), |
| 9 | + v.regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/, 'Must be a valid "HH:MM" time (00:00–23:59)'), |
| 10 | +); |
| 11 | + |
| 12 | +export const AfterHoursConfig = v.object({ |
| 13 | + enabled: v.optional(v.boolean(), true), |
| 14 | + quietHoursStart: v.optional(TimeString, "23:00"), |
| 15 | + quietHoursEnd: v.optional(TimeString, "07:00"), |
| 16 | + messageLimit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)), 3), |
| 17 | + warningTime: v.optional(TimeString, "23:30"), |
| 18 | + blockMessage: v.optional(v.string(), "The agent is working. You can rest now and check results in the morning."), |
| 19 | +}); |
| 20 | +export type AfterHoursConfig = v.InferOutput<typeof AfterHoursConfig>; |
| 21 | + |
| 22 | +// --- Config loading --- |
| 23 | + |
| 24 | +export function configPath(): string { |
| 25 | + return join(process.env.HOME ?? "~", ".pi", "agent", "extensions", "pi-after-hours.json"); |
| 26 | +} |
| 27 | + |
| 28 | +export function loadConfig(path = configPath()): AfterHoursConfig { |
| 29 | + let raw: unknown = {}; |
| 30 | + try { |
| 31 | + raw = JSON.parse(readFileSync(path, "utf-8")); |
| 32 | + } catch (err) { |
| 33 | + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { |
| 34 | + throw new Error(`after-hours: failed to read config at ${path}: ${err instanceof Error ? err.message : err}`); |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + try { |
| 39 | + return v.parse(AfterHoursConfig, raw); |
| 40 | + } catch (err) { |
| 41 | + throw new Error(`after-hours: invalid config at ${path}: ${err instanceof Error ? err.message : err}`); |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +// --- Time helpers --- |
| 46 | + |
| 47 | +export function toMinutes(timeStr: string): number { |
| 48 | + const [h, m] = timeStr.split(":").map(Number); |
| 49 | + return h! * 60 + m!; |
| 50 | +} |
| 51 | + |
| 52 | +/** Check if a time (in minutes since midnight) falls within quiet hours. */ |
| 53 | +export function isInQuietHours(now: number, start: number, end: number): boolean { |
| 54 | + return start > end ? now >= start || now < end : now >= start && now < end; |
| 55 | +} |
| 56 | + |
| 57 | +/** Check if a time is past the warning threshold within quiet hours. */ |
| 58 | +export function isPastWarningTime(now: number, warn: number, end: number, inQuiet: boolean): boolean { |
| 59 | + if (!inQuiet) return false; |
| 60 | + if (now < end && warn > end) return true; |
| 61 | + return now >= warn; |
| 62 | +} |
0 commit comments