spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/daemon/cron.ts4 * Description: Dependency-free cron parser (5 fields: min hour dom month dow) with next-run computation (v2 design §7).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910export interface CronSpec {11 minutes: Set<number>;12 hours: Set<number>;13 daysOfMonth: Set<number>;14 months: Set<number>;15 daysOfWeek: Set<number>;16}1718function parseField(field: string, min: number, max: number): Set<number> | null {19 const out = new Set<number>();20 for (const part of field.split(",")) {21 const step = /^(\*|\d+(?:-\d+)?)\/(\d+)$/.exec(part);22 if (step !== null) {23 const stepBy = Number.parseInt(step[2] as string, 10);24 if (Number.isNaN(stepBy) || stepBy <= 0) return null;25 let lo = min;26 let hi = max;27 const range = step[1] as string;28 if (range !== "*") {29 const dash = range.split("-");30 lo = Number.parseInt(dash[0] as string, 10);31 hi = dash.length > 1 ? Number.parseInt(dash[1] as string, 10) : max;32 }33 for (let v = lo; v <= hi; v += stepBy) out.add(v);34 continue;35 }36 if (part === "*") {37 for (let v = min; v <= max; v += 1) out.add(v);38 continue;39 }40 const dash = /^(\d+)-(\d+)$/.exec(part);41 if (dash !== null) {42 const lo = Number.parseInt(dash[1] as string, 10);43 const hi = Number.parseInt(dash[2] as string, 10);44 if (lo < min || hi > max || lo > hi) return null;45 for (let v = lo; v <= hi; v += 1) out.add(v);46 continue;47 }48 const value = Number.parseInt(part, 10);49 if (Number.isNaN(value) || value < min || value > max) return null;50 out.add(value);51 }52 return out.size > 0 ? out : null;53}5455/** Parse a 5-field cron expression. Returns null when invalid. */56export function parseCron(expr: string): CronSpec | null {57 const fields = expr.trim().split(/\s+/);58 if (fields.length !== 5) return null;59 const minutes = parseField(fields[0] as string, 0, 59);60 const hours = parseField(fields[1] as string, 0, 23);61 const daysOfMonth = parseField(fields[2] as string, 1, 31);62 const months = parseField(fields[3] as string, 1, 12);63 const daysOfWeek = parseField(fields[4] as string, 0, 7);64 if (minutes === null || hours === null || daysOfMonth === null || months === null || daysOfWeek === null) {65 return null;66 }67 // Cron convention: 7 == 0 == Sunday.68 if (daysOfWeek.has(7)) daysOfWeek.add(0);69 return { minutes, hours, daysOfMonth, months, daysOfWeek };70}7172function matches(spec: CronSpec, date: Date): boolean {73 return (74 spec.minutes.has(date.getMinutes()) &&75 spec.hours.has(date.getHours()) &&76 spec.daysOfMonth.has(date.getDate()) &&77 spec.months.has(date.getMonth() + 1) &&78 spec.daysOfWeek.has(date.getDay())79 );80}8182const MAX_SCAN_MINUTES = 366 * 24 * 60;8384/** Next fire time strictly after `from`. Null when the spec can never fire within a year. */85export function nextCronRun(spec: CronSpec, from: Date): Date | null {86 const cursor = new Date(from.getTime());87 cursor.setSeconds(0, 0);88 for (let i = 0; i < MAX_SCAN_MINUTES; i += 1) {89 cursor.setTime(cursor.getTime() + 60_000);90 if (matches(spec, cursor)) return new Date(cursor.getTime());91 }92 return null;93}9495/** Parse "07:00-23:00"-style active hours. Null = always active. */96export function parseActiveHours(value: string | undefined): { start: number; end: number } | null {97 if (value === undefined) return null;98 const match = /^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/.exec(value.trim());99 if (match === null) return null;100 const start = Number.parseInt(match[1] as string, 10) * 60 + Number.parseInt(match[2] as string, 10);101 const end = Number.parseInt(match[3] as string, 10) * 60 + Number.parseInt(match[4] as string, 10);102 return { start, end };103}104105/** True when `date` falls inside the active-hours window (wrapping windows supported). */106export function withinActiveHours(107 hours: { start: number; end: number } | null,108 date: Date,109): boolean {110 if (hours === null) return true;111 const minute = date.getHours() * 60 + date.getMinutes();112 if (hours.start <= hours.end) return minute >= hours.start && minute < hours.end;113 return minute >= hours.start || minute < hours.end;114}115