import { listEndpoint, pageSpec, parseWithAccount } from '../lib/api.ts' import { DATE_PATTERN, FOLDER_ID_PATTERN } from '../lib/validate.ts' type NoteSummary = { id: string; title: string | null; owner?: { name?: string | null; email?: string }; created_at: string; updated_at: string } /** * List Granola meeting notes the API key can see (newest first), following cursors until `limit`. * Wraps `GET /v1/notes`. Only notes with a generated AI summary and transcript are returned. * Use `updatedAfter` for incremental sync and `folderId` (from ./folders/list) to scope to a * folder and its subfolders. `titleContains` / `ownerEmail` filter the fetched page(s) locally * (the API has no text search), so combine them with a date window for large workspaces. * * @param input.account - Account name (Kody secret `granola-` holds the API key). Required. * @param input.secretName - Use this Kody secret instead of `granola-`. * @param input.createdAfter - `YYYY-MM-DD` or ISO date-time. @param input.createdBefore - same. * @param input.updatedAfter - same; notes edited after this time. * @param input.folderId - `fol_…` id; includes child folders. * @param input.titleContains - Case-insensitive local filter on the title. * @param input.ownerEmail - Case-insensitive local filter on the owner's email. * @param input.limit - Max notes to fetch (1-5000, default 30). @param input.paginate - Follow cursors (default true). * @param input.cursor - Continue from a previous result's `cursor`. * @returns `{ account, count, notes: [{ id, title, owner, created_at, updated_at }], hasMore, cursor, pages, filteredOut }` * * @example * import listNotes from 'kody:@charlesmatte/granola/notes/list' * * const recent = await listNotes({ account: 'work', createdAfter: '2026-09-01', limit: 50 }) */ export default async function listGranolaNotes(input: unknown) { const p = parseWithAccount<{ createdAfter?: string; createdBefore?: string; updatedAfter?: string; folderId?: string titleContains?: string; ownerEmail?: string; limit?: number; cursor?: string; paginate?: boolean }>( input, { createdAfter: { type: 'string', pattern: DATE_PATTERN }, createdBefore: { type: 'string', pattern: DATE_PATTERN }, updatedAfter: { type: 'string', pattern: DATE_PATTERN }, folderId: { type: 'string', pattern: FOLDER_ID_PATTERN }, titleContains: { type: 'string', maxLength: 200 }, ownerEmail: { type: 'string', maxLength: 320 }, ...pageSpec(5000), }, 'notes/list', ) const r = await listEndpoint(p.acct, { path: '/v1/notes', itemsKey: 'notes', query: { created_after: p.createdAfter, created_before: p.createdBefore, updated_after: p.updatedAfter, folder_id: p.folderId }, limit: p.limit ?? 30, maxPageSize: 30, cursor: p.cursor, paginate: p.paginate ?? true, }) const title = p.titleContains?.toLowerCase() const owner = p.ownerEmail?.toLowerCase() const notes = r.items.filter( (n) => (!title || (n.title ?? '').toLowerCase().includes(title)) && (!owner || (n.owner?.email ?? '').toLowerCase() === owner), ) return { account: p.acct.account, count: notes.length, notes, fetched: r.count, filteredOut: r.count - notes.length, hasMore: r.hasMore, cursor: r.cursor, pages: r.pages, ...(r.truncated ? { note: 'limit reached mid-page; continuing from cursor may skip the rest of that page' } : {}), } }