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.

AGENTS.md

96 lines · 6.9 KB · Markdown
Raw

@charlesmatte/granola — agent guide

Granola meeting notes through the official public API (https://public-api.granola.ai/v1).

Rules

  1. Pass account on every call (except ., ./accounts, ./mcp/*). There is no default. If you don't know it, call ./accounts and use a ready: true account, or ask the user which one.
  2. Never ask the user to paste a Granola key into chat. Missing key → give them the setupUrl from ./accounts({ account }); host not approved → the hostApprovalUrl.
  3. Notes are read-only through the API. Reading is always fine for the user's own request.
  4. Writes (webhooks/create|update|delete, legal-holds/create|update|release, legal-holds/custodians/add|remove, mcp/connect) return a preview unless confirm: true. Show the preview; confirm only when the user asked for that exact change. legal-holds/release is permanent.
  5. webhooks/create with confirm: true needs saveSigningSecretAs or revealSigningSecret: true. Never echo a returned signing secret into logs; hand it to the user once.
  6. Unknown input keys throw; read the "Accepted shape" in the error and retry.

Imports

import overview from 'kody:@charlesmatte/granola'
import accounts from 'kody:@charlesmatte/granola/accounts'
import verify from 'kody:@charlesmatte/granola/verify'
import listNotes from 'kody:@charlesmatte/granola/notes/list'
import getNote from 'kody:@charlesmatte/granola/notes/get'
import transcript from 'kody:@charlesmatte/granola/notes/transcript'
import searchNotes from 'kody:@charlesmatte/granola/notes/search'
import listFolders from 'kody:@charlesmatte/granola/folders/list'
import listAudit from 'kody:@charlesmatte/granola/audit/list'
import listWebhooks from 'kody:@charlesmatte/granola/webhooks/list'
import createWebhook from 'kody:@charlesmatte/granola/webhooks/create'
import updateWebhook from 'kody:@charlesmatte/granola/webhooks/update'
import deleteWebhook from 'kody:@charlesmatte/granola/webhooks/delete'
import listHolds from 'kody:@charlesmatte/granola/legal-holds/list'
import getHold from 'kody:@charlesmatte/granola/legal-holds/get'
import createHold from 'kody:@charlesmatte/granola/legal-holds/create'
import updateHold from 'kody:@charlesmatte/granola/legal-holds/update'
import releaseHold from 'kody:@charlesmatte/granola/legal-holds/release'
import listCustodians from 'kody:@charlesmatte/granola/legal-holds/custodians/list'
import addCustodians from 'kody:@charlesmatte/granola/legal-holds/custodians/add'
import removeCustodian from 'kody:@charlesmatte/granola/legal-holds/custodians/remove'
import mcpTools from 'kody:@charlesmatte/granola/mcp/tools'
import mcpConnect from 'kody:@charlesmatte/granola/mcp/connect'
import askGranola from 'kody:@charlesmatte/granola/mcp/query'
import mcpCall from 'kody:@charlesmatte/granola/mcp/call'

Common flows

  • Pick the search kind. Exact names, codes, phrases or "which meetings mention X" → searchNotes({ account, query }) (keyword). Questions that need understanding or a summary across meetings → askGranola({ question }) (semantic, MCP). If one lane is not set up, use the other and say so.
  • Keyword search → searchNotes({ account, query: 'acme "next steps" -internal', createdAfter }). Narrow with dates/folderId before raising maxNotes. If capHit is true, say the search was partial (scanned notes) and offer to continue with cursor: nextCursor. Add includeTranscripts: true only for spoken wording. Link results with web_url.
  • Semantic search → askGranola({ question }). Keep the citations links in your answer. If it throws "not connected", give the user the one-time setup from the message (add server granola = https://mcp.granola.ai/mcp at https://kody.codes/account/mcp-servers, then sign in); only run mcpConnect({ confirm: true }) yourself if the user asked, and hand them the authUrl.
  • "What did we discuss with X?" → listNotes({ account, createdAfter, titleContains: 'X' }) (title filter is local; widen the date window or drop it if nothing matches) → getNote({ account, noteId }) → answer from summary_markdown; add includeTranscript: true only when quotes are needed.
  • Quotes / exact wording → transcript({ account, noteId }) returns text like [12:04] Them: …. Speaker names are often unknown: Me/Them or Speaker A.
  • By folder → listFolders({ account }) (each has path) → listNotes({ account, folderId }).
  • Since last run → listNotes({ account, updatedAfter: '<ISO>' , limit: 1000 }).
  • Other MCP tools → mcpTools({}) for status, then mcpCall({ tool, args }).

Return shapes

  • notes/list: { account, count, notes: [{ id, title, owner: { name, email }, created_at, updated_at }], fetched, filteredOut, hasMore, cursor, pages }
  • notes/get: Granola note (id, title, owner, created_at, updated_at, web_url, calendar_event, attendees, folder_membership, summary_text, summary_markdown, private_notes_text, private_notes_markdown) + transcriptSource, transcriptItemCount, transcript_text and/or transcript
  • notes/search: { account, query, terms, excluded, mode, searchedFields, scanned, listed, capHit, nextCursor, coverage, fetchErrors, matchedCount, results: [{ noteId, title, created_at, updated_at, web_url, owner, score, termsMatched, hits: { field: count }, snippets: [{ field, text }] }], resultsTruncated, elapsedMs }
  • mcp/query: { server, question, answer, citations: [{ ref, url }], isError, hint?, raw? }
  • notes/transcript: { account, noteId, count, text?, items?, hasMore, cursor, pages }
  • folders/list: { account, count, folders: [{ id, name, parent_folder_id, path, depth, childCount }], tree? }
  • verify: { account, secretName, keyWorks, keyKind ('personal or workspace API key' | 'audit key' | 'legal-hold key' | 'notes key with audit access' | 'invalid' | 'unknown'), keyKindNote, probes: { notes|folders|webhooks|audit|legalHolds: { status, ok, … } }, summary }
  • Write previews: { dryRun: true, executed: false, echo, reason, request, current?, next }; executed writes: { dryRun: false, executed: true, echo, … }

Errors

  • 401 → key invalid/revoked/expired; the message includes the Kody secret link.
  • 403 → plan, admin-controlled scope, or key type does not allow it.
  • 404 → not found or not visible (notes appear only after summary + transcript exist). On audit/list / legal-holds/* with a personal or workspace key, 404 means the wrong key type: the audit log needs an Audit API key, legal holds a Legal hold API key (each admin-created, Enterprise). Tell the user; don't retry. verify returns keyKind + keyKindNote.
  • "Kody has not approved host public-api.granola.ai" → send the user the approval link in the message.
  • 429 is retried automatically; persistent 429 → slow down.
  • notes/search aborts on 401 (bad key) instead of returning an empty result.
  • MCP "not connected" → the user must do the one-time sign-in (rule above); don't retry.