Skip to content
← Public packages

@charlesmatte/granola

Search and read Granola meeting notes, summaries and transcripts from any agent; manage webhooks and legal holds safely.

src/notes/get.ts

69 lines · 3.2 KB · TypeScript
Raw
import { listEndpoint, parseWithAccount } from '../lib/api.ts'
import { granolaRequest } from '../lib/client.ts'
import { transcriptToText, type TranscriptItem } from '../lib/transcript.ts'
import { NOTE_ID_PATTERN } from '../lib/validate.ts'

/**
 * Get one meeting note (`GET /v1/notes/{note_id}`): title, owner, calendar event, attendees,
 * folder membership, AI summary (text + Markdown), web URL and — when the key belongs to the note's
 * owner — private notes. With `includeTranscript: true` the transcript is added; if Granola answers
 * `413 TRANSCRIPT_TOO_LARGE`, the note is re-fetched without it and the transcript is paged in
 * from `/transcript` automatically.
 *
 * @param input.account - Account name (secret `granola-<account>`). Required. @param input.secretName - Secret override.
 * @param input.noteId - `not_…` id (from ./notes/list or a webhook payload). Required.
 * @param input.includeTranscript - Add the transcript (default false).
 * @param input.transcriptFormat - `'text'` (default), `'items'` or `'both'` when the transcript is included.
 * @returns The Granola note object (`summary_markdown`, `attendees`, `calendar_event`, …) plus `transcript_text` / `transcript` as requested and `transcriptSource` (`inline` | `paged`).
 *
 * @example
 * import getNote from 'kody:@charlesmatte/granola/notes/get'
 *
 * const note = await getNote({ account: 'work', noteId: 'not_1d3tmYTlCICgjy', includeTranscript: true })
 */
export default async function getGranolaNote(input: unknown) {
	const p = parseWithAccount<{ noteId: string; includeTranscript?: boolean; transcriptFormat?: 'text' | 'items' | 'both' }>(
		input,
		{
			noteId: { type: 'string', required: true, pattern: NOTE_ID_PATTERN },
			includeTranscript: { type: 'boolean' },
			transcriptFormat: { type: 'enum', values: ['text', 'items', 'both'] },
		},
		'notes/get',
	)
	const path = `/v1/notes/${p.noteId}`
	let transcriptSource: 'inline' | 'paged' | undefined
	let items: Array<TranscriptItem> | undefined
	let note: Record<string, any>
	if (p.includeTranscript) {
		const res = await granolaRequest<Record<string, any>>(p.acct, { path, query: { include: 'transcript' }, throwOnError: false })
		if (res.status === 413) {
			note = (await granolaRequest<Record<string, any>>(p.acct, { path })).data ?? {}
			const t = await listEndpoint<TranscriptItem>(p.acct, { path: `${path}/transcript`, itemsKey: 'transcript', limit: 50000, maxPageSize: 100, paginate: true })
			items = t.items
			transcriptSource = 'paged'
		} else if (!res.ok) {
			note = (await granolaRequest<Record<string, any>>(p.acct, { path, query: { include: 'transcript' } })).data ?? {} // rethrows with a helpful message
		} else {
			note = res.data ?? {}
			items = Array.isArray(note.transcript) ? note.transcript : []
			transcriptSource = 'inline'
		}
	} else {
		note = (await granolaRequest<Record<string, any>>(p.acct, { path })).data ?? {}
	}
	const { transcript: _drop, ...rest } = note
	const format = p.transcriptFormat ?? 'text'
	return {
		account: p.acct.account,
		...rest,
		...(items
			? {
					transcriptSource,
					transcriptItemCount: items.length,
					...(format !== 'items' ? { transcript_text: transcriptToText(items) } : {}),
					...(format !== 'text' ? { transcript: items } : {}),
				}
			: {}),
	}
}