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