← Public packages
@charlesmatte/todos
Shared todo, task and reminder store that any of your Kody-connected agents can use.
src/digest.ts
62 lines · 2.8 KB · TypeScriptimport { 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. 🎉`,
}
}