import { parseInput } from './lib/validate.ts' import { addDays, allTodos, compareTodos, isOverdue, line, localDate, resolveTimeZone } from './lib/store.ts' /** * Daily digest / reminder summary of the open todos in the shared list: overdue, due today, due in the next N days, and undated. * Use for the morning list, "what's on my plate", or reminder jobs. Returns concise ready-to-send text. * * @param input - `{ days?: 1-60 (default 7), includeUndated?: boolean (default true), tag?: string, project?: string, client?: string, timeZone?: string }` * @returns `{ today, counts: { overdue, today, upcoming, undated }, sections, text }` * * @example * import digest from 'kody:@charlesmatte/todos/digest' * * const { text } = await digest({}) */ export default async function todoDigest(input: unknown = {}) { const p = parseInput<{ days?: number; includeUndated?: boolean; tag?: string; project?: string; client?: string; timeZone?: string }>( input, { days: { type: 'integer', min: 1, max: 60 }, includeUndated: { type: 'boolean' }, tag: { type: 'string', maxLength: 60 }, project: { type: 'string', maxLength: 120 }, client: { type: 'string', maxLength: 120 }, timeZone: { type: 'string', maxLength: 64 }, }, 'todos/digest', ) const now = Date.now() const tz = await resolveTimeZone(p.timeZone) const today = localDate(tz, now) const horizon = addDays(today, p.days ?? 7) const tag = p.tag?.replace(/^#/, '').toLowerCase() const eq = (a: string | null, b: string) => (a ?? '').toLowerCase() === b.toLowerCase() const open = (await allTodos()) .filter((t) => t.status === 'open' && !t.deleted) .filter((t) => (!tag || t.tags.includes(tag)) && (!p.project || eq(t.project, p.project)) && (!p.client || eq(t.client, p.client))) .sort((a, b) => compareTodos(a, b, tz)) const overdue = open.filter((t) => isOverdue(t, tz, now)) const dueToday = open.filter((t) => !isOverdue(t, tz, now) && t.due?.date === today) const upcoming = open.filter((t) => t.due && t.due.date > today && t.due.date <= horizon) const undated = p.includeUndated === false ? [] : open.filter((t) => !t.due) const sec = (title: string, items: typeof open) => (items.length ? `${title} (${items.length})\n${items.map((t) => `- ${line(t, tz, now)}`).join('\n')}` : '') const parts = [ sec('Overdue', overdue), sec('Due today', dueToday), sec(`Next ${p.days ?? 7} days`, upcoming), sec('No due date', undated), ].filter(Boolean) return { today, timeZone: tz, counts: { overdue: overdue.length, today: dueToday.length, upcoming: upcoming.length, undated: undated.length }, sections: { overdue: overdue.map((t) => t.id), today: dueToday.map((t) => t.id), upcoming: upcoming.map((t) => t.id), undated: undated.map((t) => t.id), }, text: parts.length ? `Todos — ${today}\n\n${parts.join('\n\n')}` : `Todos — ${today}: nothing open. 🎉`, } }