This file is read by Claude Code when a user opens this repository. It tells you (Claude) how to onboard the user, what state to check, and what actions to offer.
Wispr Thoughts is a personal-fingerprint pipeline. It pulls the user's voice dictation (from Wispr Flow's local SQLite) and meeting transcripts (from Fathom's API and/or Granola's local cache), groups everything into Sunday-Saturday weeks, runs an LLM over each week to extract themes and problems, runs a cross-week auditor that surfaces stuck threads, drift, avoidance, and novelty, and produces a weekly digest the user reads via a local HTML viewer at http://127.0.0.1:8080. An Apple Notes exporter also ships and writes a parallel corpus to data/notes/, but notes are deliberately not mixed into the digest. Everything runs locally on the user's Mac. All LLM calls go through the user's local claude -p (their Claude Code subscription), never the Anthropic API. No API key needed. The repo is the engine; the user's data lives under data/ (gitignored).
The viewer has a clickable pill in the top-right corner that opens a Settings drawer. This is where the user manages:
- Auto-sync: a toggle that installs a launchd job (
~/Library/LaunchAgents/io.wisprthoughts.weekly.plist) running every Sunday 8:00 AM. The schedule is fixed; only the on/off toggle is user-controlled. - Sources: per-source toggles for Wispr Flow / Fathom / Granola. Each row shows DETECTED / not found and writes back to
config.local.toml. - Fathom API key: when Fathom is enabled but no key is set, an inline input appears in the Sources card. Pasting writes
export FATHOM_API_KEY='...'to~/.zshrc(which the cron sources viazsh -lc) and updates the live server's env. - Backfill: a row showing the count of unthemed past weeks with a button to theme them all (
build_themes.pyper week, thenbuild_trends.py, thenbuild_viewer.py).
Always defer to the panel for these settings rather than editing config.local.toml manually. The TOML editor path still works, but the panel is the canonical UX for new users.
When a user opens this repository for the first time in Claude Code:
-
Detect setup state by checking these markers in order:
- Does
config.local.tomlexist? If no → user has never run setup. - Does
data/entries/contain markdown files? If no → initial Wispr export hasn't run. - Does
data/viewer/index.htmlexist? If no → viewer hasn't been built. - Is
serve.pyrunning on 127.0.0.1:8080? Check withlsof -nP -iTCP:8080 -sTCP:LISTENor by curlinghttp://127.0.0.1:8080/api/status.
- Does
-
Greet briefly with what state they're in and what comes next:
- First time: "Welcome to Wispr Thoughts. Two minutes to a working local viewer; everything else is configured by clicking the pill once it's open. If you want a sample first (no setup), run
./demo.sh." - Partially configured: "You've got config but no Wispr export yet. Run the initial export now?"
- Fully configured: "Setup complete. Type
viewerto start the local server, then click the pill in the top-right to manage auto-sync, Fathom key, sources, or backfill."
- First time: "Welcome to Wispr Thoughts. Two minutes to a working local viewer; everything else is configured by clicking the pill once it's open. If you want a sample first (no setup), run
-
Walk through setup directly. Don't shell out to
bootstrap.sh; do each step in chat. Steps:Step A: config.local.toml
cp config.example.toml config.local.toml
Step B: verify
claudeCLI is logged in- Run
claude --versionandclaude -p "respond with OK"to confirm the user's Claude Code subscription is reachable. All LLM calls go throughclaude -pexclusively. Ifclaude -psays "Not logged in", tell the user to runclaudeinteractively once, sign in, then come back. Never propose the Anthropic API path as a setup step.
Step C: detect available sources
- Wispr Flow installed? Check
~/Library/Application Support/Wispr Flow/flow.sqlite. - Granola installed? Check
~/Library/Application Support/Granola/cache-v6.json. - Tell the user which sources are auto-detected. Don't ask about Fathom here — they'll paste the key in the panel later. Apple Notes is a parallel corpus and not surfaced in the panel; mention it only if the user asks.
Step D: initial Wispr/Granola export
python3 src/export_wispr.py --refresh-snapshot # if Wispr installed python3 src/export_granola.py # if Granola installed
Skip Fathom — it's configured in the panel via key paste, then the next sync pulls it.
Step E: install the
digestandviewercommands in~/binmkdir -p ~/binMirror what
bootstrap.shwrites:~/bin/viewer:cd <repo> && python3 src/build_viewer.py >/dev/null && exec python3 src/serve.py~/bin/digest:cd <repo> && exec python3 src/weekly_email.py "$@"Both should source~/.zshrcfirst so they inheritFATHOM_API_KEY. Appendexport PATH="$HOME/bin:$PATH"to~/.zshrcif not already there.
Step F: build viewer + start the server
python3 src/build_viewer.py python3 src/serve.py
The server binds to 127.0.0.1:8080 and auto-opens the browser. The pill in the corner is the entry point to all remaining setup.
- Run
-
Hand off to the panel when the server is running:
- "You're set up. The viewer is open at http://127.0.0.1:8080. Click the pill in the top-right corner to:
- Toggle 'Run weekly automatically' on (Sunday 8 AM auto-sync)
- Paste your Fathom API key if you use Fathom
- Toggle Wispr / Granola sources on or off
- Backfill themes for any past weeks captured but not yet themed"*
- "You're set up. The viewer is open at http://127.0.0.1:8080. Click the pill in the top-right corner to:
- "How do I add my Fathom key?" → Click the pill, scroll to Sources, toggle Fathom on, paste the key in the inline input that appears, click Save. It writes to
~/.zshrcand the cron will pick it up. - "How do I turn on auto-sync?" → Click the pill, flip the "Run weekly automatically" toggle. Schedule is fixed at Sunday 8 AM.
- "How do I theme my old weeks?" → Click the pill. If there are unthemed past weeks, the Auto-sync card shows a "Backfill N" button. Click it; modal streams the log.
- "How do I send myself email?" → Email delivery is intentionally not in the panel. If the user really wants it, they edit
config.local.toml[email]section directly and runsecurity add-generic-password -s <service> -a <email> -w '<gmail-app-password>'. The local viewer is the primary output.
Today's digest adapters: Wispr Flow, Fathom, Granola. Apple Notes ships an exporter that writes a parallel corpus to data/notes/, but those notes are not mixed into the weekly digest. These are on the roadmap:
- Mobile Wispr Flow (phone dictations not synced to desktop SQLite)
- Slack sent messages (DMs and their own channel posts only)
- Gmail sent mail (long-form written voice)
- iMessage (closest-relationship private speech)
- Apple Calendar (titles and durations only)
- Apple Voice Memos (speech recorded outside Wispr)
- GitHub commits (what they actually built each week)
- Notion (structured second-brain pages)
- Cursor / VS Code activity (when and what they worked on)
The pipeline is source-agnostic. Adding a new source is one Python script in src/export_<name>.py that writes per-day or per-meeting markdown into data/. Frame additions as opening a GitHub issue at https://github.com/pvilk/wispr-thoughts/issues with the source they want.
- "Open the viewer" — start the server:
python3 src/serve.py(orviewerif installed). The viewer rebuilds first then auto-opens the browser. - "Sync now" — auto-sync runs Sunday 8 AM via launchd; for an immediate run, the user can run
digestfrom a terminal or wait for the cron. There's no manual "sync now" button in the panel anymore — auto-sync is the canonical path. - "Re-pull Wispr data" —
python3 src/export_wispr.py --refresh-snapshot - "Re-pull Fathom" —
python3 src/export_fathom.py(env must haveFATHOM_API_KEY; loaded from~/.zshrcautomatically when the server starts) - "Theme last week" —
python3 src/build_themes.py --week <YYYY-Www>(andbuild_themes_meetings.pyif any meeting source is enabled) - "Regenerate trends" —
python3 src/build_trends.py - "What's stuck right now" — read
data/master/20_trends/unresolved_threads.md - "Search across my corpus" — grep
data/entries/,data/weeks/, anddata/master/50_weeks/ - "Block a personal term from accidental commits" — edit
.git/hooks/personal-denylist.txt
- The user's
data/directory contains their personal voice corpus. Never offer to commit it. The.gitignorealready excludes it; trust that boundary. - Critical caveat about gitignore.
.gitignoreonly blocks UNTRACKED files. If a path is already tracked (committed at any point in history), gitignore does nothing. If you ever seegit statusshowingdata/somethingas modified, that means it's been tracked at some point and needsgit rm --cachedbefore gitignore takes effect. This actually happened on this repo: 84 personal files leaked into the initial commit despite gitignore havingdata/from day one. Defense layers exist now (pre-commit hook blocks any staged path under data/, GitHub Actions workflow refuses pushes with data/ files, scripts/check-public.sh audits before publication) but the lesson stands: never trust gitignore retroactively. - The pre-commit hook at
.git/hooks/pre-commitwill refuse commits containing terms in.git/hooks/personal-denylist.txt, paths underdata/, or credential-shaped strings (sk-, xox-, ghp_, AKIA). If a commit fails, the user has a personal term in their staged diff. Guide them to remove it, not bypass with--no-verify(unless they confirm it's a false positive). - Before flipping the repo from private to public (or pushing to any new public remote), run
./scripts/check-public.sh. It scans current tree + full history + credential patterns + denylist matches and refuses if anything is flagged. - When the user pastes a Fathom API key in chat (instead of in the panel), write it to
~/.zshrcimmediately. Don't echo it. Don't write it to a file inside the repo. If the server is running, prefer directing them to the panel instead — it does the same write and updates the live process env. - Don't suggest adding
data/files to git. - Don't ask for the user's name or email. The tool doesn't need them.
wispr-thoughts/
├── src/ All Python (stdlib only)
│ ├── _config.py / _llm.py / _parsers.py
│ ├── _settings.py TOML write helper for the panel
│ ├── _schedule.py launchd plist render + install/remove
│ ├── export_wispr.py / export_fathom.py / export_granola.py / export_notes.py
│ ├── build_themes.py / build_themes_meetings.py
│ ├── build_trends.py
│ ├── build_auditor.py / build_auditor_meetings.py
│ ├── build_viewer.py
│ ├── serve.py local HTTP server: 127.0.0.1:8080, settings + sync APIs
│ ├── weekly_email.py pipeline orchestrator (run by cron + manual)
│ ├── prompts/ themes / auditor / carryover prompts
│ └── experimental/ stub source parsers (gmail, slack), not yet wired in
├── tests/ synthetic Alice fixture + parser tests
├── scripts/ pre-commit hook, denylist template, launchd plist template
├── assets/ README screenshot
├── bootstrap.sh minimal setup: install + first export + start server
├── demo.sh 30-second sample digest with synthetic Alice data
├── config.example.toml template the user copies into config.local.toml
├── data/ user's personal corpus (gitignored)
└── CLAUDE.md / README.md / LICENSE