Skip to main content

pp-granola

Every Granola feature — plus offline SQLite cross-meeting search, attendee timelines, and a MEMO pipeline runner... Trigger phrases: `memo run for today's meetings`, `what's in granola but not yet memo'd`, `every meeting we had with trevin`, `did i run the discovery recipe`, `talk time in last week's meetings`, `calendar overlay missed meetings`, `find duplicates in meeting transcripts`, `extract granola meeting`, `use granola`, `run granola`.

Jump to install

Source facts

Repository
mvanhorn/printing-press-library
Last source activity
August 5, 2026 at 05:51
Detected SKILL.md language
English
Stars
1,918
Forks
572

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
100 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
pp-granola
description
Every Granola feature — plus offline SQLite cross-meeting search, attendee timelines, and a MEMO pipeline runner... Trigger phrases: `memo run for today's meetings`, `what's in granola but not yet memo'd`, `every meeting we had with trevin`, `did i run the discovery recipe`, `talk time in last week's meetings`, `calendar overlay missed meetings`, `find duplicates in meeting transcripts`, `extract granola meeting`, `use granola`, `run granola`.
author
Damien Stevens
license
Apache-2.0
argument-hint
<command> [args] | install cli|mcp
allowed-tools
Read Bash
metadata
{"openclaw":{"requires":{"bins":"[Truncated]"}}}
<!-- // PATCH(dual-path-store-read): the Data Paths, Auth Setup, Auto-Refresh, and Troubleshooting sections describe the two-path reality after Granola desktop moved its data-encryption key into an entitlement-gated macOS keychain group. See library/productivity/granola/.printing-press-patches/ dual-path-store-read.json and dek-migration-classified-from-state-not-version.json. --> # Granola — Printing Press CLI ## Prerequisites: Install the CLI This skill drives the `granola-pp-cli` binary. **You must verify the CLI is installed before invoking any command from this skill.** If it is missing, install it first: 1. Install via the Printing Press installer. It defaults binaries to `$HOME/.local/bin` on macOS/Linux and `%LOCALAPPDATA%\Programs\PrintingPress\bin` on Windows: ```bash npx -y @mvanhorn/printing-press-library install granola --cli-only ``` 2. Verify: `granola-pp-cli --version` 3. Ensure the reported install directory is on `$PATH` for the agent/runtime that will invoke this skill. If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.5 or newer): ```bash go install github.com/mvanhorn/printing-press-library/library/productivity/granola/cmd/granola-pp-cli@latest ``` If `--version` reports "command not found" after install, the runtime cannot see the binary directory on `$PATH`. Do not proceed with skill commands until verification succeeds. ## Two Data Paths — Read This Before Running Anything Granola desktop keeps its data-encryption key in a macOS data-protection keychain group gated by an entitlement bound to Granola's own Apple Team ID. No third-party binary can read that key, so the encrypted desktop cache (`cache-v6.json.enc`), the SQLCipher store `granola.db`, and `supabase.json.enc` are all unreadable by this CLI on a current install. That does **not** leave the CLI without data. Run `granola-pp-cli auth login` once: the CLI signs in to Granola with its own session and syncs meetings over the API, no key and no paid workspace required. You approve one browser page; after that the session refreshes silently on every command. It never touches the Granola desktop app's session or your browser's. Three paths fill the local SQLite store: | Path | Hydrate with | Works on a current install? | |---|---|---| | CLI-owned session → local store | `auth login`, then `sync` | **Yes.** The default answer. Meetings, titles, timestamps, attendees. | | Granola public REST API → local store | `sync-api` | **Yes**, but only with a `GRANOLA_API_KEY`, which needs a Business or Enterprise workspace. | | Desktop encrypted cache → local store | `sync` | **Only on pre-migration builds**, or on a machine holding a pre-migration `storage.dek` supplied through `GRANOLA_SAFESTORAGE_KEY_OVERRIDE`. Otherwise `sync` runs degraded and reports so. | **Reading already-synced data needs no credential at all.** Every read command serves from the local store first and falls back to the desktop cache only when the store has no row and a cache is actually readable. Neither step makes a network call or consults a key. Credentials are only needed to fetch *new* data. ### Capability split Read this before telling a user their data is missing. An empty result on a migrated install usually means "not synced on this tier", not "you have none". **With a CLI-owned session (`auth login`), hydrated by `sync`:** - meetings, titles, timestamps - attendees - calendar events - **transcripts** (full segment list) Transcripts backfill incrementally. There is no bulk transcript endpoint, so `sync` fetches them one meeting at a time, newest first, up to `--transcript-budget` (default 250) per run. When work remains the command says so on stderr and records how many; re-run `sync` to continue, or pass `--transcript-budget -1` to fetch all remaining in one go. Meetings that genuinely have no recording are asked about once and then skipped forever. **This matters when answering questions.** Before telling a user a meeting has no transcript, check whether the backfill has reached it. `sync` reports `transcripts_remaining` in its summary; a non-zero value means "not fetched yet", not "no transcript exists". Commands that depend on transcripts (`talktime`, `memo run`, `attendee brief`, `collect`) will be thin until the backfill completes. - recipes and panel templates — `recipes list`, `recipes describe` - folders and folder membership — `folder list`, `folder stream` **Live on the same session, no sync needed:** - AI panels — `panel get`, and the `--panel` inlining in `attendee brief` and `folder stream` - workspaces — `workspaces list` Both read straight from the API on each call, so they need no store rows. The tradeoff is that `panel get` is the one read command with no local fallback at all: if the session lapses it fails hard where everything else degrades to stored data. **With a `GRANOLA_API_KEY`, hydrated by `sync-api`:** everything above plus note summaries (`summary_markdown`). **Frozen but still readable — AI chat threads (`chat list`, `chat get`):** Chats are the one surface that cannot advance. Granola's internal API exposes no chat endpoint (seven namings probed on 7.465.0, 2026-08-03, all 404), so the threads in the store are whatever the last desktop-cache sync captured and no re-sync will add more. `chat list` says so in both its human and JSON output. **Read the `staleness` block before answering from any of this.** Store-served reads carry one when the desktop cache is unreadable: `refreshable` tells you whether the surface can advance, `last_catalog_sync_at` dates refreshable surfaces like recipes, and `last_cache_sync_at` dates frozen ones like chats. A chat set can sit weeks behind the meetings it discusses — quote the date rather than presenting it as current. When something in the first group is asked for on a migrated install, say the data is not reachable. Do not synthesize a panel, a recipe result, or a chat thread from transcript text. ### Why `auth login` when Granola desktop is already signed in Because the desktop's session is not shareable. Its token lives behind the entitlement-gated key, and the refresh tokens the desktop and your browser hold are single-use — refreshing one signs *that* client out. So the CLI holds a chain of its own instead of borrowing. `auth login` stores it under the CLI's own data directory, readable only by you; `auth logout` removes it. Deleting it locally does not revoke it upstream. ## When to Use This CLI Reach for granola-pp-cli when you need to answer cross-meeting questions Granola.ai’s web app and the GUI cannot — attendee timelines, MEMO pipeline state, recipes coverage gaps, calendar overlay, talk-time aggregation. It is the right tool for an agent processing transcripts in a loop, a CSM doing pre-call prep, or a consultant running a weekly retro. Pair the --json default with --select dotted paths to keep agent context lean. ## When Not to Use This CLI Do not activate this CLI for requests that require creating, updating, deleting, publishing, commenting, upvoting, inviting, ordering, sending messages, booking, purchasing, or changing remote state. This printed CLI exposes read-only commands for inspection, export, sync, and analysis. ## Platform Notes `warm <id> <query>` drives the Granola desktop GUI via AppleScript and is **macOS-only**. It prints what it would do by default; pass `--launch` to actually activate the app. On non-macOS hosts the command exits 0 with a "not supported" message. All other commands are cross-platform. ## Unique Capabilities These capabilities aren't available in any other tool for this API. ### MEMO pipeline - **`memo run`** — Run the preflight → extract pipeline on one meeting or every new meeting since a timestamp, emitting the MEMO three-file artifact and an ndjson run-state ledger. _Replaces the per-meeting shell loop that drives the MEMO pipeline — one call, one ndjson stream, agent-readable._ ```bash granola-pp-cli memo run --since 24h --to ~/Documents/Dev/meeting-transcripts --json ``` - **`memo queue`** — List every meeting whose transcript is in the cache but whose MEMO triple is not yet on disk. _Answers the daily question “what’s still un-MEMO’d?” without the user opening Granola at all._ ```bash granola-pp-cli memo queue --since 7d --json ``` ### Attendee intelligence - **`attendee timeline`** — Every meeting with a given attendee, ordered oldest→newest, with title, date, folder, and recipe-applied flag per row. _Pre-call prep in one command; surfaces the conversation arc with a single person across months of meetings._ ```bash granola-pp-cli attendee timeline alice@example.com --since 60d --json --select id,title,started_at,folder,recipes ``` - **`attendee brief`** — Pulls the last N meetings with an attendee and stitches together their real cached notes plus real AI panel summaries — no synthesis. _Eliminates the click-each-meeting copy-paste that account leads do before every external call._ ```bash granola-pp-cli attendee brief alice@example.com --last 3 --panel action-items --json ``` ### Folders + recipes - **`folder stream`** — ndjson stream of every meeting in a Granola folder (resolved via documentLists + listRules) with notes and a named panel inlined. _Replaces the weekly retro workflow of opening a folder and copy-pasting each meeting’s summary into a spreadsheet._ ```bash granola-pp-cli folder stream client-foo --panel summary --json ``` - **`recipes coverage`** — Surface meetings that did NOT have a named panel template/recipe applied within a date range. _Friday retro question “did I run the Discovery recipe on every new-prospect call?” answered in one row per gap._ ```bash granola-pp-cli recipes coverage discovery --since 14d --json ``` ### Transcript analytics - **`talktime`** — Per-segment-source talk-time for one meeting — microphone (you) vs system (everyone else) in minutes. _Confidence column lets you grade transcript accuracy; mic vs system split is the input to “am I talking too much” retros._ ```bash granola-pp-cli talktime 196037d9 --json ``` - **`talktime`** — Lifts the per-source talk-time aggregation across N meetings since a date — who-talked-most over time. _Time-defrag retro input that no per-meeting tool can produce._ ```bash granola-pp-cli talktime --by participant --since 7d --json ``` ### Cache-native data Both of these originate in Granola desktop's own cache. `chat list` reads the threads a cache `sync` already hydrated into the local store, so it keeps answering on a migrated install — but nothing can advance that set, and the output says so along with the last-sync timestamp. `calendar overlay` reads calendar events, which `sync-api` hydrates, so it keeps working. - **`chat list`** — List and dump Granola’s AI chat threads anchored to a meeting (entities.chat_thread + entities.chat_message in the cache). _Recovers the AI Q&A history a user has accumulated against a meeting — useful when chasing what you asked about an account weeks ago._ ```bash granola-pp-cli chat list 196037d9 --json ``` - **`calendar overlay`** — Left-anti-join meetingsMetadata calendar events with documents.google_calendar_event to find calendared-but-not-recorded meetings. _Sarah’s Friday retro and Damien’s “what did I miss” sweep both reduce to this row-level diff._ ```bash granola-pp-cli calendar overlay --week 2026-05-11 --missed-only --json ``` ### Pipeline hygiene - **`duplicates scan`** — Hash (title, date-bucket, attendee-email-set) across the cache and a meeting-transcripts repo to surface duplicates at scale. _Repos accumulate near-duplicate files when meetings are re-extracted; this returns the dupe groups for cleanup._ ```bash granola-pp-cli duplicates scan --root ~/Documents/Dev/meeting-transcripts --json ``` - **`tiptap extract`** — Render documents[id].notes (TipTap JSON: headings, bullet_list, list_item, bold marks, paragraph_break) to canonical markdown instead of falling back to notes_plain. _The MEMO summary file’s quality is bounded by extractor fidelity; granola.py loses sub-list hierarchy and bold runs._ ```bash granola-pp-cli tiptap extract 196037d9 --as markdown ``` ## Command Reference This CLI exposes 35+ commands. The full tree is too long to inline; ask the CLI for the canonical list: ```bash granola-pp-cli --help # top-level commands granola-pp-cli <command> --help # subcommands + flags granola-pp-cli agent-context --json # machine-readable command tree for agents ``` Quick orientation by group: | Group | Commands | Purpose | |-------|----------|---------| | **MEMO pipeline** | `memo run`, `memo queue`, `preflight`, `extract` | Composed three-stream pipeline; reads cache + writes MEMO triple | | **Meetings** | `meetings list`, `meetings get`, `meetings fetch-batch`, `meetings delete`, `meetings restore`, `show` | List/inspect/mutate meetings (delete/restore mutate via internal API) | | **Streams** | `notes-show`, `panel get`, `transcript get`, `tiptap extract` | The three streams — human notes, AI panels, transcript — addressable separately | | **Export** | `export`, `export-all` | Combined three-stream markdown export, single or bulk | | **Cross-meeting analytics** | `attendee timeline`, `attendee brief`, `folder stream`, `recipes coverage`, `talktime`, `calendar overlay`, `stats frequency`, `stats duration`, `stats attendees`, `stats calendar`, `collect`, `duplicates scan`, `chat list`, `chat get` | Queries no per-meeting tool can answer | | **Folders / recipes / workspaces** | `folders` (public-API), `folder list`, `folder stream`, `recipes list`, `recipes describe`, `recipes coverage`, `workspaces list` | Granola organizational entities | | **Public-API mirrors** | `notes list`, `notes get`, `folders` | Typed Bearer-key endpoints | | **Sync / system** | `sync`, `sync-api`, `doctor`, `auth setup`, `auth status`, `auth set-token`, `auth logout`, `which`, `agent-context`, `version`, `import` | Local store hydration (`sync-api` is the working path on current installs), auth, capability discovery, batch import | | **GUI bridge** | `warm` (macOS only) | Drives Granola desktop app via AppleScript | ### Finding the right command When you know what you want to do but not which command does it, ask the CLI directly: ```bash granola-pp-cli which "<capability in your own words>" ``` `which` resolves a natural-language capability query to the best matching command from this CLI's curated feature index. Exit code `0` means at least one match; exit code `2` means no confident match — fall back to `--help` or use a narrower query. ## Recipes ### Daily MEMO loop ```bash granola-pp-cli memo run --since 24h --to ~/Documents/Dev/meeting-transcripts --json ``` Process every new meeting since yesterday into the MEMO triple format and yield only the new artifacts. ### Pre-call attendee brief ```bash granola-pp-cli attendee brief alice@example.com --last 3 --panel action-items --json --select meetings.title,meetings.started_at,panels.action_items ``` Pull the last three meetings with Trevin and only the title, date, and action-items panel content per meeting. ### Friday retro — missing recipes ```bash granola-pp-cli recipes coverage discovery --since 14d --json ``` Surface every new-prospect call in the last fortnight that did not have the Discovery panel applied. Omit the slug to list coverage gaps across every panel template. ### Repo-wide duplicate scrub ```bash granola-pp-cli duplicates scan --root ~/Documents/Dev/meeting-transcripts --json ``` Find duplicate-meeting clusters across the MEMO output repo for cleanup. ### Calendar-overlay missed-meeting sweep ```bash granola-pp-cli calendar overlay --week 2026-05-11 --missed-only --json ``` Calendared meetings with no Granola recording — weekly accountability check. ## Auth Setup ### 1. Get an API key — needed only to fetch new data API keys are created in **Granola desktop → Settings → Connectors → API keys**. Creating one **requires a Business or Enterprise Granola workspace**; personal and free workspaces cannot issue keys. Two scopes exist, `personal-notes` and `public-notes` — pick the narrowest one that covers the notes the user actually needs, which for a user reading their own meetings is `personal-notes`. Export it as an environment variable: ```bash export GRANOLA_API_KEY="grn_your_key_here" ``` Prefer the env var over persisting the key into `~/.config/granola-pp-cli/config.toml`. Backup and dotfile-sync tooling does not reliably preserve file modes, so a key written to a config file can end up world-readable inside a synced folder. The base URL is `https://public-api.granola.ai`. The list endpoints cap `page_size` at 30 and reject any temporal filter that is not a UTC `Z` timestamp; the CLI handles both, but keep it in mind if you script against the API directly. ### 2. Hydrate once Run `granola-pp-cli sync-api`. It pages the notes list, then fetches each note's detail with its transcript and writes meetings, attendees, calendar events, summaries, folder membership, and transcript segments into the local store — the tables every read command queries. On repeat runs narrow the window with `--since 7d`. The two sync paths do not clobber each other. Each clears only the rows it owns, so running `sync` and `sync-api` against the same store is safe in either order. Transcripts get one extra guard. Granola applies transcript retention upstream, so an older meeting can come back from the API pruned to a handful of segments while this store still holds the full recording from the cache path. A sync never replaces a transcript with a **smaller** copy from the other source — it keeps what is stored, skips that meeting, and reports it as `preserved_transcripts` in the sync summary plus a `warning:` line naming the meetings. A path rewriting its own earlier transcript is unaffected, whatever the size change. ### 3. Read with no key Once hydrated, every read command works offline with no credentials. `granola-pp-cli transcript get <id> --json` returns byte-identical output with and without `GRANOLA_API_KEY` set. ### Legacy and pre-migration installs On Granola desktop builds from before the key migration, the top-level `sync` command still reads the encrypted desktop cache. The first run triggers a macOS Keychain prompt for `Granola Safe Storage` — click "Always Allow" so later runs are silent. The CLI is **read-only against every desktop-owned token**. It never rotates a refresh token it found in Granola's own storage — `supabase.json`, `supabase.json.enc`, or the `stored-accounts.json` fallback — because those tokens are single-use and rotating one signs the user out of Granola desktop. If a request fails with "token expired", open Granola desktop briefly to refresh, then re-run.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub