feat(email): integrate email + alias domain into wspc-cli#7
Merged
Conversation
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.
@
Summary
Integrates the Email domain (8 message routes + 4 alias routes) into
@wspc/cli, following the calendar-paved codegen-first pattern but adding two handwritten commands for cases the declarative codegen deliberately does not model: attachment upload (base64-encoded into the JSON body) and binary stream download.Depends on sadcoderlabs/wspc#365 — that PR ships the
x-cliannotations +display.dataPathfield this generator consumes. Merge order: server PR → prod deploy →npm run sync-spechere → release.Spec / plan in the wspc monorepo:
docs/superpowers/specs/2026-05-27-wspc-cli-email-design.mddocs/superpowers/plans/2026-05-27-wspc-cli-email-plan.mdCommands added (10 total)
Codegen-produced (8) —
email ls/show/read/unread/rm,alias add/ls/rm. All driven byx-clihints +displayin the OpenAPI spec; no per-command code in this repo.Handwritten (2):
email send— reads local file → detects MIME → base64-encodes → embeds inattachments[](or parseseml_xxx:idxas an inbound attachment reference). Validates fresh vs reply mode mutual exclusion. Per-attachment ≤ 5 MiB / total ≤ 25 MiB / text body ≤ 100 KiB client-side checks.email attachment <id> <idx>— uses newloadAuthedFetch()helper to get the OAuth-aware fetch + baseUrl, streamsapplication/octet-streamresponse into a file (--outputor filename derived fromContent-Disposition).Codegen improvements
_handwrittensentinel: routes flagged with{ command: "_handwritten", hidden: true }are skipped by codegen but stay in OpenAPI (SDK function +x-codeSamplesstill surface)emit.tsthat prevented--id <id> --id <id>from mapping tobody.ids: string[]—valueExprForOptiondid not honourmapsTo, conversion block had no branch for array-without-parser, and required-array cast wasstringinstead ofstring[]. Calendar--attendeeworked by accident because its parser branch happened to do the right thingmain()entry-point guard:tools/cli-codegen/main.tswas runningmain()at module-load time, which the unit test imports triggered — everynpm testre-emitted the entiresrc/generated/cli/tree as a side effect, polluting git status. Now guarded withfileURLToPath(import.meta.url) === process.argv[1].npm testafter this PR leaves the generated tree byte-for-byte stable.Renderer improvements
bool-badgeformat:is_readand similar booleans now render as✓ read(dim) vs● unread(bright) in pretty modedisplay.dataPathdrill: in pretty mode the renderer can drill into a wrapper key before rendering. Used byemail showwhose response is{ email, attachments }. JSON mode is unaffected —--jsonstill emits the full payloadNew utilities
parse-content-disposition.ts— extractsfilename=token from a Content-Disposition header. RFC 5987filename*=intentionally not supported (server does not emit it)mime-from-ext.ts— 13-entry static map, falls back toapplication/octet-stream. Nomime-typesdep added — keeping wspc-cli dependency-minimalAuth:
loadAuthedFetch()New named export from
src/handwritten/auth/load-sdk-client.ts. Returns{ fetch, baseUrl }wherefetchis the interceptor-wrapped version (handles OAuth token refresh transparently). Used byemail attachmentto bypass Hey API SDK for binary streams.Tests
_handwrittenskip + array passthrough tests)npm run typecheckpassesnpm run buildpasses (ESM + DTS)E2E verified (manual smoke against prod)
wspc alias add claude-smoke-001@wspc.app→ createdwspc email sendtext-only → received in Gmailwspc email send --attach <small.txt>→ received in Gmail with attachmentwspc email ls(pretty + JSON) → bool-badge column renders correctlywspc email show <id>(pretty) → drills intoemail, attachments metadata preserved in--jsonwspc email attachment <id> 0 --output got.png→ 2,364,849 bytes byte-for-byte match against originalwspc email attachment <id> 0(no--output) → filename derived from Content-Disposition (handles spaces, e.g._ mode _ Mobile.png)Implementation-time discoveries
email_alias_createbody field isemail(full address), notlocal_partas the plan template assumed — plan / spec updatedemail_alias_deletepath param is alsoemail, notid--include-deletedis exposed as<value>(user types--include-deleted true) because the server schema usesstringBool. UX should be a boolean flag — recorded as a codegen follow-up in the research docprocess.exitCodecannot be spied viavi.spyOn— tests use set-undefined-then-assert pattern instead@