SSkilltecabyclaudinhocode
Enviar skill
← Voltar para o catálogo

granola-export

Documentos

Extract personal data (notes, transcripts, AI summaries, attachments, custom recipes, chat history) from Granola.ai via their undocumented REST API using locally-stored auth tokens. Use when the user wants to back up, recover, snapshot, or export their Granola data — especially after a tier downgrade where the UI hides data the API still serves. Triggers on "export granola", "backup granola", "ext

1estrelas
Ver no GitHub ↗Autor: moona3kLicença: MIT

Granola Personal Data Export

Extract your own Granola.ai data — meetings, notes, transcripts, AI summaries — via their REST API using the auth tokens stored locally by the Granola Mac app. For personal data recovery only. Reads tokens from your already-logged-in session; never logs in or scrapes.

Why this exists

Granola already has two official ways to get your data out:

  • CSV export (Settings → Profile → "Generate CSV") — enabled by default on Basic (free) and Business plans, admin-toggle on Enterprise. Returns titles + short summaries only, emailed within hours, rate-limited to 1 per 24h. No transcripts, no AI panel content, no attachments.
  • Personal API (documented at docs.granola.ai/introduction) — GET /v1/notes, full notes with transcript + summary as JSON, auth via grn_ API keys. Business + Enterprise plans only.

If you're on Business+ and want JSON note access, use Granola's official API — it's the supported path with stable contracts.

This skill fills the gaps neither covers:

  • Free-tier full history. Basic plan's UI shows only the last 30 days, but the underlying API returns everything. This skill uses that path.
  • Markdown rendering per meeting. Granola's API returns JSON; this renders one Markdown file per meeting plus a sortable index.
  • Auxiliary data. Folders, recipes, panel templates, calendar links, attendee profiles — none in /v1/notes, all captured here.
  • AI panels with original + edited content. The full ProseMirror panel content, including the AI's first draft before user edits.
  • Claude Code skill packaging. Trigger phrases auto-load this skill; AI coding agents can run the export end-to-end.

About audio: Granola doesn't store audio recordings — they're transcribed in real time and then deleted, by design (privacy choice). So no tool, including this one, can recover the actual audio file. The audio_file_handle field on documents is a vestige of upload processing, not a permanent storage pointer.

Prior art worth knowing: theantichris/granola, magarcia/granola-cli, wassimk/granary, pedramamini/granola-mcp, and Joseph Thacker's Granola → Obsidian writeup. Different approaches (cache parsing vs. API calls vs. MCP). This skill's differentiators: full Claude Code skill packaging, defensive paid-tier endpoint coverage, idempotent backups.

Quick start

# Full export — meetings + transcripts + AI summaries + attachments
# + auxiliary endpoints (folders, recipes, templates, people, etc.)
python3 ~/.claude/skills/granola-export/scripts/extract.py

# Output:
#   ~/reference/granola/extracted/my-data/
#     ├── docs.json                       (all document metadata, 46 fields per doc)
#     ├── transcripts/<id>.json           (raw word-level transcripts)
#     ├── panels/<id>.json                (raw AI summary panels, ProseMirror JSON)
#     ├── attachments/                    (downloaded image attachments)
#     │   ├── _index.json
#     │   └── <attachment-id>.<ext>
#     ├── aux/                            (single-shot auxiliary endpoints)
#     │   ├── recipes.json, panel_templates.json, folders.json,
#     │   │   workspaces.json, people.json, subscription.json, etc. (~16 files)
#     │   └── rendered/*.md               (human-readable summaries)
#     └── meetings/                       (rendered Markdown — one file per meeting)
#         ├── INDEX.md                    (sortable master index)
#         └── YYYY-MM-DD_HHMM_title-slug_<id>.md × N

# To skip the extended fetch (only meetings, no aux/attachments):
python3 scripts/extract.py --no-extended

If the access token is stale, the script prints a clear refresh hint. To refresh:

~/.claude/skills/granola-export/scripts/refresh_token.sh

Auth model (one-page summary)

  • Granola uses WorkOS for auth. Cognito tokens also appear in supabase.json but are legacy and typically long-expired — vestigial from before the WorkOS migration. Use the WorkOS token, not Cognito.
  • Tokens live in ~/Library/Application Support/Granola/supabase.json as JSON-stringified inner JSON (jq -r '.workos_tokens' | jq -r '.access_token').
  • WorkOS access_token TTL: ~1 hour. Refresh token long-lived.
  • Auth header: Authorization: Bearer <access_token>.
  • All responses are gzipped — always send Accept-Encoding: gzip and use --compressed.

Full detail: references/auth.md.

Common operations

Full export (default)

python3 scripts/extract.py

Resume / top up after a partial run

python3 scripts/extract.py     # idempotent — skips files already on disk

Single document

python3 -c "from api import *; t = get_transcript('<doc-id>'); print(t)"

Refresh token without re-running export

./scripts/refresh_token.sh

Just the document index (no transcripts/panels)

python3 scripts/extract.py --index-only

Important endpoints (used by this skill)

MethodEndpointPurpose
POSThttps://api.granola.ai/v1/helloAuth probe; returns the user's UUID as plain text
POSThttps://api.granola.ai/v1/get-user-infoReturns full user record (email, workspace_ids, scopes)
POSThttps://api.granola.ai/v2/get-documentsPaginated document list. Body: {"limit": 100, "offset": N}
POSThttps://api.granola.ai/v1/get-document-transcriptBody: {"document_id": "<uuid>"} → array of transcript segments with start_timestamp, text, source (microphone/system)
POSThttps://api.granola.ai/v1/get-document-panelsBody: {"document_id": "<uuid>"} → array of AI summary panels in ProseMirror JSON
POSThttps://api.granola.ai/v1/get-feature-flagsAll feature flags for the user (also in local-state.json)
POSThttps://api.granola.ai/v1/get-attachmentsFile attachments uploaded into a doc
POSThttps://api.granola.ai/v1/get-recipesRecipes — returns dict with userRecipes, sharedRecipes, publicRecipes, unlistedRecipes, defaultRecipes, recipesUsage
POSThttps://api.granola.ai/v1/get-panel-templatesDefault + custom panel templates (returns array)
POSThttps://api.granola.ai/v2/get-document-listsFolder/list organization. v2/v1/get-document-lists returns "Not implemented"
POSThttps://api.granola.ai/v1/get-shared-documentsDocuments shared to you by others

Many more exist but are not needed for personal data export. Full curated list in references/endpoints.md.

Known pitfalls

Real failure modes encountered while building this — handled defensively in the bundled scripts, documented here so you understand what's happening if you hit them.

SymptomCauseFix (already in scripts)
HTTP 200 but binary garbage in responseResponse is gzippedAlways pass --compressed to curl, or Accept-Encoding: gzip header for libraries that auto-decompress
"error": "deprecated"Hitting a v1 endpoint that was supersededUse /v2/get-documents (not /v1/get-documents-delta)
"message": "Internal Server Error" after long timeoutBare {} body where backend expects pagination paramsPass {"limit": 100, "offset": 0}
'str' object has no attribute 'get' in ProseMirror conversionSome panels have content as a bare string instead of a dictThe pm_to_md() function in api.py handles strings, lists, dicts, and None defensively
jq Invalid string: control characters from U+0000 through U+001FGranola's API responses contain raw newlines/tabs in some string fields (notes content)Don't try t

Como adicionar

/plugin marketplace add moona3k/granola-export

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Comentários · Nenhum comentário

Entre para comentar. Entrar

  • Ainda não há comentários. Seja o primeiro.