1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
#+TITLE: Google Keep <-> Emacs integration — Spec
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-24
#+TODO: TODO | DONE SUPERSEDED CANCELLED
* Metadata
| Status | ready for review (decisions resolved) |
|----------+------------------------------------------------------------|
| Owner | Craig |
|----------+------------------------------------------------------------|
| Reviewer | Codex (spec-review) |
|----------+------------------------------------------------------------|
| Related | [[file:../../todo.org][todo.org: google-keep in-editor integration]] |
* Problem / Context
Craig keeps quick notes in Google Keep but works almost entirely in Emacs. Today, reading or acting on a Keep note means leaving Emacs for the phone or the web app — a context switch for content that wants to live next to his org files. He wants Keep notes native to Emacs: browsable, searchable, greppable, and editable, with a path to publish the result as a standalone package.
Two hard constraints shape every choice:
1. *No official API.* Google Keep has no public API. Every live client (gkeepapi and the tools built on it) reverse-engineers the private mobile endpoint. That layer is fragile: it breaks when Google changes auth, and it needs a Google master token, not a password.
2. *The existing MCP is agent-only.* Craig already has a google-keep MCP with full read/write (create/update/find/labels/archive/list-items). But MCP tools are invoked by the *agent* (Claude), not from elisp — there is no elisp MCP client. So the in-editor integration cannot reuse the MCP as its data path; it needs its own.
Researched 2026-06-24: there is no in-editor Emacs Google Keep package on MELPA or GitHub — only KeepToOrg, a one-shot Takeout-HTML-to-org importer (unmaintained). So this is a build, not an adopt.
* Goals and Non-Goals
** Goals
- Keep notes visible and usable inside Emacs without leaving the editor.
- An org-native representation, so notes are searchable/greppable and reuse org machinery.
- A structure that starts as glue in =.emacs.d= and can be extracted to a publishable package (the VAMP / pearl module-to-package pattern).
- Read-write (create/edit notes from Emacs) as the immediate v2 increment — v1 ships read-only first, but the read path is built to carry write, so v2 is additive.
** Non-Goals
- Full bidirectional offline sync, conflict resolution, or real-time updates.
- Faithful round-tripping of every Keep feature (list checkboxes, collaborators, drawings, images).
- Reusing the MCP from elisp (infeasible — agent-only).
** Scope tiers
- v1: read-only. Fetch Keep notes through the gkeepapi bridge and render them as an org page (each note an org header). A manual refresh command. Auth via auth-source. Graceful degradation when the bridge or credentials are missing. The read path establishes a round-trip-ready data model and a stable per-note identity, because v2 builds on them.
- v2 (immediate follow-on, not deferred): read-write — create a note from a region or capture, and edit a note back to Keep. Reuses v1's bridge, auth, data model, and note-identity; adds the inverse direction and a staleness check.
- Out of scope: list/checkbox fidelity, collaborators, drawings/images, and any background or real-time sync.
- Later: list/checkbox rendering, and extracting the core to a standalone package.
* Design
** For the user
A command (e.g. =cj/keep-refresh=) pulls the current Keep notes and writes them into one org file (e.g. =~/org/keep.org=). Each note becomes a top-level org heading: the title (or a derived title) as the heading text, the note body as the entry, and Keep metadata as properties — labels as org tags, plus =:KEEP_ID:=, =:PINNED:=, =:COLOR:=, =:ARCHIVED:=, =:UPDATED:= in a drawer. Pinned notes sort first. The file is plain org, so it is searchable with the agenda, greppable, and linkable. Opening it is just visiting the file; a keybinding and a dashboard entry make it one keystroke. v1 is read-only: editing the org file does not push back to Keep (a header note says so), so there is no accidental-mutation risk while the integration is young. The =:KEEP_ID:= and =:UPDATED:= on each header are what v2 later uses to target an update and detect a stale local copy.
** For the implementer
Three layers, cleanly separable so the core can later be a package:
1. *Data bridge (Python).* A small script using gkeepapi: authenticate with a stored master token, fetch notes, emit JSON (id, title, text, labels, pinned, color, archived, timestamps) on stdout. The =id= and =updated= fields are the stable note-identity and freshness anchors v2 needs. This is the one place the unofficial API lives, isolated so a break is contained and swappable; the same bridge gains a write subcommand in v2.
2. *Org renderer (elisp).* Runs the bridge as a subprocess, parses its JSON, and writes the org page (heading + body + property drawer per note, with =:KEEP_ID:=/=:UPDATED:= recorded), with =cj/keep-refresh= as the entry point. Reads the master token via =auth-source=. The JSON-to-org transform is the round-trip contract v2 inverts.
3. *Access UX (elisp).* Keybindings, a dashboard entry, and (later) a dedicated buffer/mode.
* Alternatives Considered
** A — Takeout import (one-shot HTML -> org)
- Good, because no auth, fully offline, dead simple, and zero ongoing breakage risk.
- Bad, because it is not live — Craig must manually export a Takeout archive, so notes are stale the moment they are imported, and it cannot write, so it is useless for the v2 write path.
- Neutral, because it is the right tool for an archival snapshot, the wrong one for daily use. Deferred — not built in v1; a possible later no-auth archive importer if ever wanted.
** B (chosen for the data path) — gkeepapi via a Python subprocess bridge
- Good, because it is the only path that gives live notes and (in v2) write-back from inside Emacs, with the full note model.
- Bad, because gkeepapi reverse-engineers a private API: it breaks on Google auth changes, needs Python plus a stored master token, and the bridge is glue Craig owns and maintains.
- Neutral, because the fragility is isolated to one script; when it breaks, the renderer degrades to a warning.
** C — Reuse the google-keep MCP from elisp
- Good, because the MCP already has full read/write and is maintained outside the config.
- Bad, because MCP tools are invoked by the agent, not elisp — there is no elisp MCP client, so this is infeasible for an in-editor feature.
- Neutral, because the MCP stays the right tool for agent-driven Keep access; it just can't power an in-editor integration.
** D — A local HTTP server wrapping gkeepapi
- Good, because elisp would talk clean HTTP instead of spawning a subprocess each refresh.
- Bad, because a long-running personal server is more infrastructure than a single-user note view warrants.
- Neutral, because it is a heavier variant of B; revisit only if subprocess latency ever bites.
* Decisions [5/5]
** DONE Presentation shape: org page of headers
- Decision: We will render notes as one *org page*, each note a top-level header (title + body + metadata properties, labels as tags). Confirmed by Craig 2026-06-25.
- Consequences: easier — reuses org search/agenda/grep/links, nothing bespoke to render, and read-only is safe. Harder — a large Keep collection makes a long file (mitigated by pinned-first sort and org folding).
** DONE Direction: read-only v1, read-write v2 (immediate)
- Decision: We will ship v1 read-only (fetch + render + refresh), then move directly into v2 read-write (create/edit back to Keep). v2 is the immediate next increment, not a deferred someday. The read path must establish a round-trip-ready data model and a stable per-note identity up front, so v2 is additive rather than a rewrite. Confirmed by Craig 2026-06-25.
- Consequences: easier — v1 ships the visible value fast on a path write reuses wholesale, and a parse bug can't corrupt real Keep data while the model is being proven. Harder — the read path carries design weight it wouldn't if write were truly far off (note-identity and the freshness field have to be right in v1), and the write work — note targeting, staleness/overwrite handling — still has to be built right after.
** DONE Data path: gkeepapi subprocess bridge (Takeout deferred)
- Decision: We will use a small Python gkeepapi bridge that emits JSON as the sole data path; it powers read in v1 and gains a write subcommand in v2. A failure degrades to a clear warning. The Takeout-import path is deferred (read-only and stale, so it can't serve v2). The MCP is not in the data path. Confirmed by Craig 2026-06-25.
- Consequences: easier — one live path that serves both read and write, fragility isolated to one swappable script. Harder — a Python + gkeepapi dependency and a maintained bridge; the unofficial API can break and needs a master token, with no second read path as a safety net (the warning is the degradation).
** DONE Auth: master token in authinfo.gpg via auth-source
- Decision: We will store the master token in =authinfo.gpg= and read it via =auth-source= (the pattern the rest of the config uses), and document the one-time master-token retrieval. Confirmed by Craig 2026-06-25.
- Consequences: easier — consistent with existing credential handling, no plaintext secret, the daemon's auth-source cache applies. Harder — the one-time token retrieval is a manual setup step, and a revoked/expired token surfaces as an auth failure the renderer must report cleanly.
** DONE Structure: google-keep-config.el glue + extractable core
- Decision: We will build the integration as =modules/google-keep-config.el= (the =.emacs.d= glue: paths, keys, dashboard, auth wiring) plus a self-contained core (the bridge runner + org renderer) written so the core can later move to a standalone =keep.el=-style package, mirroring the VAMP / pearl migration. Confirmed by Craig 2026-06-25.
- Consequences: easier — usable immediately in =.emacs.d=, with a clean seam for later extraction. Harder — the discipline of keeping the core free of =.emacs.d=-specific assumptions from the start.
* Implementation phases
** Phase 1 — Data bridge
A Python script (gkeepapi) that authenticates with the stored master token and prints notes as JSON (id, title, text, labels, pinned, color, archived, updated) on stdout. The =id= and =updated= fields are deliberate — they are the note-identity and freshness anchors v2 reads. Standalone and testable from the shell with a fixture; no Emacs yet. Tree stays working (new files only).
** Phase 2 — Org renderer + refresh
=modules/google-keep-config.el= (and its extractable core): run the bridge as a subprocess, parse the JSON, and write the org page (heading + body + property drawer per note, recording =:KEEP_ID:= and =:UPDATED:=, labels as tags, pinned-first). =cj/keep-refresh= regenerates it; auth via =auth-source=. A header line marks the file a read-only view. Degrades to a =display-warning= when the bridge, Python, gkeepapi, or token is missing — never errors at load.
** Phase 3 — Access UX + un-orphan
Keybindings (a Keep prefix), a dashboard entry, and the =(require 'google-keep-config)= in =init.el=. Optional: a dedicated read-only major mode for the buffer.
** Phase 4 (v2) — read-write
The immediate next increment after v1 lands. A write subcommand on the bridge (gkeepapi create/update), an elisp path that targets a note by =:KEEP_ID:= and checks =:UPDATED:= against Keep before overwriting (staleness guard), and entry points to create a note from a region/capture and edit one back. Specced in detail once v1's read model is proven on real notes; listed here so Phases 1-3 don't paint into a read-only corner.
(Later, not specced here, logged to todo.org: list/checkbox rendering; extract the core to a package.)
* Acceptance criteria
- [ ] =cj/keep-refresh= fetches the current Keep notes and writes them to the org page, one header per note with title/body/labels/metadata.
- [ ] Pinned notes sort to the top; labels render as org tags; Keep id/color/pinned/archived/updated land in a property drawer.
- [ ] Each note header carries a stable =:KEEP_ID:= and an =:UPDATED:= timestamp (the v2 targeting + staleness anchors).
- [ ] The master token is read from =authinfo.gpg= via =auth-source=; no secret is hardcoded.
- [ ] A missing bridge / Python / gkeepapi / token produces a clear =display-warning=, not a load error or a crash.
- [ ] =make validate-modules= + launch smoke clean with =google-keep-config= required.
* Readiness dimensions
- Data model & ownership: Keep is the source of truth; the org page is a generated read-only view (v1). Each note maps to one org header keyed by a stable =:KEEP_ID:=; the bridge JSON is the contract between Python and elisp, and the JSON-to-org transform is the round-trip contract v2 inverts.
- Errors, empty states & failure: auth failure, a broken gkeepapi, missing Python/token, or zero notes each degrade to a warning + an empty-or-stale page, never a crash. The unofficial API breaking is expected, not exceptional.
- Security & privacy: the master token lives in =authinfo.gpg= (gpg-encrypted), read via auth-source; note content lands in a local org file the user already trusts for org data. No secret in the repo. The token grants broad Google access — documented as a risk.
- Observability: the warning path names which piece is missing (Python / gkeepapi / token / bridge). The generated page's header shows the last refresh time.
- Performance & scale: one subprocess per manual refresh over N notes (tens to low hundreds); trivial. No background polling in v1.
- Reuse & lost opportunities: reuses org (rendering, search, agenda, links), auth-source (credentials), and the subprocess pattern. gkeepapi supplies the API client, so no endpoint code is written here.
- Architecture fit & weak points: three layers (Python bridge / elisp renderer / UX glue) with the fragile API isolated in layer 1. Weak point: gkeepapi maintenance and Google auth churn — mitigated by isolation, graceful degradation, and a swappable bridge.
- Config surface: a Keep org-file path, the auth-source host entry, and a keybinding prefix. No tuning knobs in v1.
- Documentation plan: a setup note (one-time master-token retrieval, =pip install gkeepapi=, the authinfo entry) and the module commentary. No user-migration doc (personal config).
- Dev tooling: the bridge is shell-testable with a JSON fixture; the renderer gets ERT over the JSON-to-org transform; =make validate-modules= + launch smoke for the module.
- Rollout, compatibility & rollback: additive — a new module + a require. Rollback = drop the require and delete the org page. No existing behavior changes.
- External APIs & deps: gkeepapi (PyPI) and the unofficial Google Keep mobile endpoint it wraps — the single load-bearing external dependency and the central risk. Python 3 on PATH. A Google master token.
* Risks, Rabbit Holes, and Drawbacks
- The central risk is gkeepapi breaking when Google changes auth or the private endpoint. It has a history of auth churn. With Takeout deferred there is no second read path, so the mitigations are: isolate gkeepapi in the bridge, degrade to a clear warning, keep the bridge swappable, and never block Emacs load on it.
- Credential risk: the master token grants broad account access. Keep it in =authinfo.gpg=, never the repo; document revocation.
- v2 staleness: because write follows immediately, the edit-back path must not overwrite a note that changed on the phone since the last refresh. The =:UPDATED:= anchor from v1 is what makes that check possible — a v2 rabbit hole if the field isn't carried correctly in v1.
- Scope creep: the pull toward full bidirectional sync, list fidelity, and real-time. v1 is a read-only org view and v2 is targeted read-write; richness and sync stay later, gated behind those landing.
* Review and iteration history
** 2026-06-24 Wed @ 22:40:00 -0400 — Claude — author
- What: initial draft.
- Why: Craig asked to spec the google-keep in-editor integration before building. It spans a fragile external API, an auth-source credential, a Python/elisp bridge, and a module-to-package trajectory, with real trade-offs on shape, direction, and data path — worth pinning before code.
- Artifacts: docs/specs/google-keep-emacs-integration-spec.org; the todo.org google-keep task.
** 2026-06-25 Thu @ 00:40:00 -0400 — Claude — author
- What: resolved all five decisions with Craig (one by one) and folded them in. Shape = org page (popup dropped from the spec entirely, a separate later discussion). Direction = read-only v1 then read-write v2 immediately after, so the read path now carries a round-trip-ready model + stable note-identity. Data path = gkeepapi sole bridge, Takeout deferred (stripped from scope/phases/acceptance). Auth = authinfo.gpg via auth-source. Structure = extractable core + glue. Added Phase 4 (v2) and the v2-staleness risk; Status moved to ready for review.
- Why: decisions resolved, so the spec can go to spec-review before implementation.
- Artifacts: this spec.
|