import { GRANOLA_HOST, GRANOLA_ORIGIN, SECRET_PREFIX } from './lib/account.ts' import { discoverAccounts, userSecrets } from './lib/secrets.ts' import { DOCS } from './lib/urls.ts' import { parseInput } from './lib/validate.ts' const EXPORTS = { './accounts': 'Configured accounts (secrets granola-) and their Kody host approval', './verify': 'Prove a key works and see which endpoints it can reach (read-only probes)', './notes/list': 'List notes (date/folder filters, local title/owner filter, auto-pagination)', './notes/get': 'One note: summary (Markdown), attendees, calendar event, folders, private notes, optional transcript', './notes/search': 'Keyword search (words, "phrases", -exclude) over titles, summaries, attendees, private notes and optionally transcripts; ranked with snippets', './notes/transcript': 'Full transcript in pages, as readable text and/or raw items', './folders/list': 'Folders with computed paths and optional tree', './audit/list': 'Workspace audit events (needs an Audit API key, Enterprise)', './webhooks/list': 'Webhook endpoints this key manages', './webhooks/create': 'WRITE (confirm): register an HTTPS endpoint for note events', './webhooks/update': 'WRITE (confirm): change URL/scopes/events/folders/enabled', './webhooks/delete': 'WRITE (confirm): delete an endpoint', './legal-holds/list': 'Legal holds (Legal hold API key, Enterprise)', './legal-holds/get': 'One legal hold', './legal-holds/create': 'WRITE (confirm): place a hold', './legal-holds/update': 'WRITE (confirm): rename / change workspace coverage', './legal-holds/release': 'WRITE (confirm): release a hold (terminal)', './legal-holds/custodians/list': 'People named on a hold', './legal-holds/custodians/add': 'WRITE (confirm): name people on a hold', './legal-holds/custodians/remove': 'WRITE (confirm): remove a custodian', './mcp/tools': 'Optional MCP lane: connection status and tools of Granola’s official MCP server', './mcp/connect': 'WRITE (confirm): add Granola’s MCP server to Kody (browser OAuth sign-in)', './mcp/query': 'Semantic search: ask Granola a question about your meetings (MCP query_granola_meetings), answer with citations', './mcp/call': 'Call any read-only Granola MCP tool', } /** * Overview of the Granola package: how accounts and keys work, every export, and the accounts * already configured in this Kody account (secret metadata only; no Granola request). * Start here, then ./verify({ account }) and ./notes/list({ account }). * * @returns `{ api, auth, accounts, exports, docs, safety }` * * @example * import granola from 'kody:@charlesmatte/granola' * * const overview = await granola({}) */ export default async function granolaOverview(input: unknown) { parseInput(input, {}, 'granola') const accounts = discoverAccounts(await userSecrets()) return { api: { baseUrl: GRANOLA_ORIGIN, plans: 'Personal/workspace API keys: Business or Enterprise (notes, search, folders, webhooks). Audit log: Enterprise + separate Audit API key. Legal holds: Enterprise + separate Legal hold API key. MCP lane: every plan.', rateLimit: 'burst 25 requests, then 5 requests/second (429 is retried with backoff)' }, auth: { convention: `Kody user secret ${SECRET_PREFIX} = a Granola API key (grn_…), allowed host ${GRANOLA_HOST}. Pass { account } on every call; there is no default.`, createKey: `Granola desktop app → Settings → Connectors → API keys → Create new key (${DOCS.apiKeys})`, }, accounts: accounts?.map((a) => ({ account: a.account, ready: a.ready, hostApprovalUrl: a.hostApprovalUrl })) ?? 'not listable here; see ./accounts', exports: EXPORTS, search: { keyword: './notes/search({ account, query }) — API key; scans notes (default 100 newest, throttled to 5 req/s), exact words/phrases, snippets.', semantic: './mcp/query({ question }) — Granola MCP; natural-language answer with citations. One-time setup at https://kody.codes/account/mcp-servers (server "granola", URL https://mcp.granola.ai/mcp, then sign in to Granola).', }, docs: DOCS, safety: 'Reads are free. Every write previews by default and runs only with confirm: true (dryRun: true always wins). Keys never enter the isolate: Kody injects them for the approved host only.', } }