import { parseInput } from './lib/validate.ts' import { ctxField, kindField, priorityField, tzField } from './lib/fields.ts' import { allTodos, compareTodos, dueInstant, isOverdue, localDate, parseDue, summary, resolveTimeZone, type Kind, type Priority, } from './lib/store.ts' /** * List/search the shared todos and tasks, sorted by due date (undated last) then priority (high first). * Use for "what are my todos", "what's due this week", "tasks for project X", or to find an id. * * @param input - `{ status?: "open"|"done"|"all" (default "open"), overdue?: boolean, today?: boolean (due today in the store zone), dueBefore?: date|datetime (inclusive), dueAfter?: date|datetime (inclusive), undated?: boolean, tag?: string, project?: string, client?: string, kind?: "todo"|"task", priority?: "low"|"normal"|"high", q?: string (text search in title/notes/tags/project/client), includeDeleted?: boolean, limit?: 1-500 (default 100), timeZone?: string }` * @returns `{ count, total, todos: [{ id, title, due, priority, ..., overdue, line }], text }` — `text` is ready-to-show lines * * @example * import listTodos from 'kody:@charlesmatte/todos/list' * * const { text } = await listTodos({ status: 'open', dueBefore: '2026-10-20' }) */ export default async function listTodos(input: unknown = {}) { const p = parseInput<{ status?: 'open' | 'done' | 'all' overdue?: boolean today?: boolean dueBefore?: string dueAfter?: string undated?: boolean tag?: string project?: string client?: string kind?: Kind priority?: Priority q?: string includeDeleted?: boolean limit?: number timeZone?: string }>( input, { status: { type: 'enum', values: ['open', 'done', 'all'] }, overdue: { type: 'boolean' }, today: { type: 'boolean' }, dueBefore: { type: 'string', maxLength: 40 }, dueAfter: { type: 'string', maxLength: 40 }, undated: { type: 'boolean' }, tag: { type: 'string', maxLength: 60 }, project: ctxField, client: ctxField, kind: kindField, priority: priorityField, q: { type: 'string', maxLength: 200 }, includeDeleted: { type: 'boolean' }, limit: { type: 'integer', min: 1, max: 500 }, timeZone: tzField, }, 'todos/list', ) const now = Date.now() const tz = await resolveTimeZone(p.timeZone) const today = localDate(tz, now) const status = p.status ?? 'open' // Date-only bounds are inclusive of the whole local day. const before = p.dueBefore ? parseDue(p.dueBefore, tz, 'dueBefore') : null const after = p.dueAfter ? parseDue(p.dueAfter, tz, 'dueAfter') : null const tag = p.tag?.replace(/^#/, '').toLowerCase() const q = p.q?.toLowerCase() const eq = (a: string | null, b: string) => (a ?? '').toLowerCase() === b.toLowerCase() const filtered = (await allTodos()).filter((t) => { if (t.deleted && !p.includeDeleted) return false if (status !== 'all' && t.status !== status) return false if (p.overdue !== undefined && isOverdue(t, tz, now) !== p.overdue) return false if (p.today && t.due?.date !== today) return false if (p.undated !== undefined && !t.due !== p.undated) return false if (before) { if (!t.due) return false if (before.at ? dueInstant(t.due, tz) > Date.parse(before.at) : t.due.date > before.date) return false } if (after) { if (!t.due) return false if (after.at ? dueInstant(t.due, tz) < Date.parse(after.at) : t.due.date < after.date) return false } if (tag && !t.tags.includes(tag)) return false if (p.project && !eq(t.project, p.project)) return false if (p.client && !eq(t.client, p.client)) return false if (p.kind && t.kind !== p.kind) return false if (p.priority && t.priority !== p.priority) return false if (q) { const hay = [t.title, t.notes, t.project, t.client, t.id, ...t.tags].join(' ').toLowerCase() if (!q.split(/\s+/).every((w) => hay.includes(w))) return false } return true }) filtered.sort((a, b) => compareTodos(a, b, tz)) const limited = filtered.slice(0, p.limit ?? 100) const todos = limited.map((t) => summary(t, tz, now)) return { today, timeZone: tz, total: filtered.length, count: todos.length, todos, text: todos.length ? todos.map((t) => `- ${t.line}`).join('\n') : 'No matching todos.', } }