feat(gmail): write primitives — bulk triage, filters, scope-aware status#106
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Extends the
gmailmodule from read-only to a triage tool: bulk archive/label/mark-read/trash, label creation, routing rules (filters), and a scope-awarestatus. 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-grantscopeand exposegetGrantedScopes(). Shared, sogcal/gdrivecan reuse it.client— scopes becomegmail.modify+gmail.settings.basic. New API fns:batchModifyMessages(chunked at Gmail's 1000-id limit),trashMessages(per-message —batchModifyrejects theTRASHlabel, and there's no batch-trash endpoint),createLabel,list/create/deleteFilter, plus a purechunk.messages modify— bulk archive / mark-read / trash / add / remove, selected by--q "<query>"or--ids a,b,c.labels create --namefilters list | create | delete— routing rules for future mail (they do not backfill the existing backlog;messages modifyhandles that).insufficient_scopenaming the gap and the fix, instead of surprising you with a 403 mid-triage.Safety
--yes. Every mutating command previews first: matched count, exact label deltas, and a from/subject sample — mutating nothing until you confirm.https://mail.google.com/scope, so no permanent-delete or send.Post-merge action
Scopes are baked into the refresh token — run
home gmail configureonce to re-consent to the broader scopes. Until thengmail statusreportsinsufficient_scope(by design).Testing
tscclean. New:gmail-write,gmail-write-planners,gmail-write-commands,gmail-status+getGrantedScopescases.status→ correctinsufficient_scope;messages modify --q … --archive --mark-readdry-run → correct preview with real sender/subject sample, zero mutation. Write-with---yespaths are covered by unit tests over mocked fetch (can't be driven live without re-consent + mutating the real mailbox).🤖 Generated with Claude Code