Skip to content

feat(gmail): write primitives — bulk triage, filters, scope-aware status#106

Merged
uptonm merged 5 commits into
mainfrom
worktree-gmail-write-ops
Jul 20, 2026
Merged

feat(gmail): write primitives — bulk triage, filters, scope-aware status#106
uptonm merged 5 commits into
mainfrom
worktree-gmail-write-ops

Conversation

@uptonm

@uptonm uptonm commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What

Extends the gmail module from read-only to a triage tool: bulk archive/label/mark-read/trash, label creation, routing rules (filters), and a scope-aware status. Design spec: apps/home/docs/superpowers/specs/2026-07-20-gmail-write-primitives-design.md.

Why

The mailbox this targets has ~47k inbox messages (~24k unread) and only 2 user labels. Read-only search can't triage that — the only tractable approach is bulk action keyed on sender/pattern, plus filters so future mail self-sorts. This adds the write primitives to do that; the actual triage plan is driven externally (no baked-in triage engine — YAGNI).

Changes

  • core/google-auth — capture the refresh-grant scope and expose getGrantedScopes(). Shared, so gcal/gdrive can reuse it.
  • client — scopes become gmail.modify + gmail.settings.basic. New API fns: batchModifyMessages (chunked at Gmail's 1000-id limit), trashMessages (per-message — batchModify rejects the TRASH label, and there's no batch-trash endpoint), createLabel, list/create/deleteFilter, plus a pure chunk.
  • commands
    • messages modify — bulk archive / mark-read / trash / add / remove, selected by --q "<query>" or --ids a,b,c.
    • labels create --name
    • filters list | create | delete — routing rules for future mail (they do not backfill the existing backlog; messages modify handles that).
  • status — verifies granted scopes; an old read-only grant now returns insufficient_scope naming the gap and the fix, instead of surprising you with a 403 mid-triage.

Safety

  • Dry-run unless --yes. Every mutating command previews first: matched count, exact label deltas, and a from/subject sample — mutating nothing until you confirm.
  • Trash-only. "Delete" moves to Trash (recoverable ~30 days). No https://mail.google.com/ scope, so no permanent-delete or send.

Post-merge action

Scopes are baked into the refresh token — run home gmail configure once to re-consent to the broader scopes. Until then gmail status reports insufficient_scope (by design).

Testing

  • TDD throughout; 107/107 isolated test files pass, tsc clean. New: gmail-write, gmail-write-planners, gmail-write-commands, gmail-status + getGrantedScopes cases.
  • Driven live against the real account (read-only grant): status → correct insufficient_scope; messages modify --q … --archive --mark-read dry-run → correct preview with real sender/subject sample, zero mutation. Write-with---yes paths are covered by unit tests over mocked fetch (can't be driven live without re-consent + mutating the real mailbox).

🤖 Generated with Claude Code

uptonm and others added 2 commits July 20, 2026 19:16
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ware status

Extend the gmail module from read-only to a triage tool:

- core/google-auth: capture the refresh-grant scope and expose
  getGrantedScopes(), so a module can verify its token still carries the
  scopes it needs instead of failing later with a 403.
- client: scopes go to gmail.modify + gmail.settings.basic; add
  batchModifyMessages (chunked at 1000), trashMessages (per-message, since
  batchModify rejects TRASH), createLabel, and list/create/deleteFilter.
- commands: 'messages modify' (bulk archive/label/mark-read/trash by --q or
  --ids), 'labels create', and 'filters list/create/delete'. All mutating
  commands are dry-run unless --yes — the preview shows matched count, label
  deltas, and a from/subject sample.
- status now verifies granted scopes and reports insufficient_scope (naming
  the gap and the fix) when an old read-only grant can't write.

Delete is trash-only (recoverable); no permanent-delete or send scope.
Requires re-running 'home gmail configure' to grant the broader scopes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
home Ready Ready Preview, Comment Jul 20, 2026 8:22pm

Request Review

Bulk trash is now CLI-reversible. Adds:
- client: messageUntrashUrl + untrashMessages (per-message messages.untrash
  under bounded concurrency, mirroring trashMessages).
- command: 'messages untrash' — inverse of 'messages modify --trash'. Selects
  by --q (auto-scoped to in:trash, includeSpamTrash) or --ids; dry-run unless
  --yes. collectMessageIds/previewSample now take includeSpamTrash so trashed
  mail is selectable.

Untrash recovers to All Mail, not the inbox: Gmail drops INBOX on trash and
neither untrash nor batchModify restores it (verified live), so the command
documents that re-inboxing is an explicit 'messages modify --add INBOX'.

Verified live end-to-end: single trash+untrash round-trip, and a reversible
bulk run of 103 messages (trash -> untrash -> restore INBOX), all counts exact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the label lifecycle. Adds:
- client: deleteLabel (DELETE on the label resource; only user labels are
  deletable, messages the label was on remain).
- command: 'labels delete <id>' — dry-run unless --yes, since deleting a label
  strips it from every message and is not recoverable.

Verified live: dry-run previews, --yes deletes, subsequent get 404s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@uptonm
uptonm merged commit 52127d7 into main Jul 20, 2026
3 checks passed
@uptonm
uptonm deleted the worktree-gmail-write-ops branch July 20, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant