aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/google-keep-emacs-integration-spec.org
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-25 00:37:45 -0400
committerCraig Jennings <c@cjennings.net>2026-06-25 00:37:45 -0400
commit2160442636f05ff04f09dcd561de1be49d6486e0 (patch)
tree8f8f65d40e20de608127c84938d1f42381def5f7 /docs/specs/google-keep-emacs-integration-spec.org
parent27ef68af51df06dc34ed9a1bc0568eec9cc2f4ac (diff)
downloaddotemacs-2160442636f05ff04f09dcd561de1be49d6486e0.tar.gz
dotemacs-2160442636f05ff04f09dcd561de1be49d6486e0.zip
docs(spec): resolve google-keep decisions (read-only v1 to write v2, gkeepapi, authinfo)
Diffstat (limited to 'docs/specs/google-keep-emacs-integration-spec.org')
-rw-r--r--docs/specs/google-keep-emacs-integration-spec.org103
1 files changed, 51 insertions, 52 deletions
diff --git a/docs/specs/google-keep-emacs-integration-spec.org b/docs/specs/google-keep-emacs-integration-spec.org
index 0b57f731f..df0cf3a0b 100644
--- a/docs/specs/google-keep-emacs-integration-spec.org
+++ b/docs/specs/google-keep-emacs-integration-spec.org
@@ -4,7 +4,7 @@
#+TODO: TODO | DONE SUPERSEDED CANCELLED
* Metadata
-| Status | draft |
+| Status | ready for review (decisions resolved) |
|----------+------------------------------------------------------------|
| Owner | Craig |
|----------+------------------------------------------------------------|
@@ -14,7 +14,7 @@
* 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 eventually editable, with a path to publish the result as a standalone package.
+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:
@@ -28,41 +28,42 @@ Researched 2026-06-24: there is no in-editor Emacs Google Keep package on MELPA
- 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).
-- A path to read-write (create/edit notes from Emacs) without making v1 wait on it.
+- 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 via the chosen data path 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.
-- Out of scope: write-back to Keep, list/checkbox fidelity, label/color/pin *editing*, the org-capture-style popup, package extraction.
-- vNext: read-write (create a note from a region or capture; edit a note back to Keep), the org-capture-style quick-note popup, list/checkbox rendering, and extracting the core to a standalone package.
+- 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.
+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. This is the one place the unofficial API lives, isolated so a break is contained and swappable. A Takeout-import path is the no-auth fallback (parse a Takeout dump into the same JSON shape).
-2. *Org renderer (elisp).* Runs the bridge as a subprocess, parses its JSON, and writes the org page (heading + body + properties per note), with =cj/keep-refresh= as the entry point. Reads the master token via =auth-source=.
-3. *Access UX (elisp).* Keybindings, a dashboard entry, and (vNext) a dedicated buffer/mode or the org-capture-style popup.
+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.
-- Neutral, because it is the right tool for an archival snapshot, the wrong one for daily use. Kept as the v1 fallback / bootstrap, not the primary.
+- 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 vNext, write-back) from inside Emacs, with the full note model.
+- 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 and the Takeout fallback still works.
+- 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.
@@ -74,67 +75,60 @@ Three layers, cleanly separable so the core can later be a package:
- 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 [0/5]
-
-** TODO Presentation shape: org page of headers (v1), popup deferred
-- Owner / by-when: Craig / before implementation
-- Context: Craig named two shapes — an org-capture-style popup and a separate org page with each note as a header.
-- Decision: We will render notes as one *org page*, each note a top-level header (title + body + metadata properties, labels as tags), in v1; the org-capture-style quick-note *popup* is vNext.
-- Consequences: easier — reuses org search/agenda/grep/links, nothing bespoke to render, and read-only is safe. Harder — a popup-first quick-capture flow waits for vNext, and a large Keep collection makes a long file (mitigated by pinned-first sort and org folding).
-
-** TODO Direction: read-only in v1, read-write in vNext
-- Owner / by-when: Craig / before implementation
-- Context: the bridge can read and (later) write Keep; doing both in v1 raises the risk surface.
-- Decision: We will ship v1 read-only (fetch + render + refresh); create/edit-back-to-Keep is vNext.
-- Consequences: easier — no accidental Keep mutation while the integration is young, and value ships fast. Harder — editing a note still means the phone/web until vNext; the org file is a view, not a source of truth.
-
-** TODO Data path: gkeepapi subprocess bridge, Takeout import as fallback
-- Owner / by-when: Craig / before implementation
-- Context: the MCP is agent-only; the live options are gkeepapi or a Takeout import.
-- Decision: We will use a small Python gkeepapi bridge that emits JSON as the primary path, with a Takeout-import parser into the same JSON shape as the no-auth fallback. The MCP is not in the data path.
-- Consequences: easier — live notes now, write-back later, one isolated fragile component. Harder — a Python + gkeepapi dependency and a maintained bridge; the unofficial API can break and needs a master token.
-
-** TODO Auth: master token in authinfo.gpg via auth-source
-- Owner / by-when: Craig / before implementation
-- Context: gkeepapi authenticates with a Google *master token* (obtained once), not the account password.
-- 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.
+* 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.
-** TODO Structure: google-keep-config.el glue + extractable core
-- Owner / by-when: Craig / before implementation
-- Context: Craig wants a module-to-package trajectory.
-- 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.
+** 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, plus a Takeout-import path producing the same JSON shape. Standalone and testable from the shell with a fixture; no Emacs yet. Tree stays working (new files only).
+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=: run the bridge as a subprocess, parse the JSON, and write the org page (heading + body + property drawer per note, labels as tags, pinned-first). =cj/keep-refresh= regenerates it; auth via =auth-source=. A header line marks the file read-only-view. Degrades to a =display-warning= when the bridge, Python, gkeepapi, or token is missing — never errors at load.
+=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.
-(vNext phases — not specced here, logged to todo.org: read-write create/edit; the org-capture-style popup; list/checkbox rendering; extract the core to a package.)
+** 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.
-- [ ] The Takeout-import fallback produces the same org page from a Takeout dump with no auth.
- [ ] =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; the bridge JSON is the contract between Python and elisp.
+- 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 the Takeout fallback.
+- 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.
@@ -142,12 +136,17 @@ Keybindings (a Keep prefix), a dashboard entry, and the =(require 'google-keep-c
- 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. Mitigations: isolate it in the bridge, degrade to a warning, keep the Takeout-import fallback working, and never block Emacs load on it.
+- 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.
-- Scope creep: the pull toward full bidirectional sync, list fidelity, and real-time. v1 is a read-only org view on purpose; write-back and richness are vNext, gated behind the org-page landing first.
+- 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 (org page vs popup), direction (read vs read-write), and data path (gkeepapi vs Takeout vs MCP) — worth pinning before code.
-- Artifacts: docs/specs/google-keep-emacs-integration-spec.org; the todo.org google-keep task (to be cross-linked at hand-off).
+- 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.