Cap-go/automations email↔Discord bridge). The complexity concentrated in the bridge integration — making a programmatically-sent ticket behave correctly (avoiding the ownedOutboundCopy heuristic, the double-delivery race from multiple bridge-routed @capgo.app recipients, and two-party reply threading). The cleanest robust path (a dedicated POST /builder-ticket endpoint in the automations worker + team-in-Discord) was judged not worth the cost now. Preserved so the analysis isn't lost. No implementation plan was produced (§8 is the closest skeleton).Scope: Capgo builder CLI flows only. Not the rest of the CLI; no standalone command.
Unified help menu at three points. "Ask Capgo support" is always first.
| Trigger | Build log? | Menu options (in order) |
|---|---|---|
| Build failure | Yes (jobId + log) | 📨 Ask Capgo support🤖 Ask AI🔄 Try again❌ Exit |
| Onboarding error | No | 📨 Ask Capgo support🔄 Try again / ↩️ Restart❌ Exit |
| Unhandled error (raw provider API error) | No | 📨 Ask Capgo support🔄 Try again❌ Exit |
AI is offered only when a build log exists. Onboarding/unhandled errors get support only.
Independent choices. After AI returns its analysis and the user is still stuck, the AI result screen adds 📨 Still stuck — send this to Capgo support carrying the same logs + the AI analysis into the email (no re-gathering).
Reuses the proven path the AI feature already uses (CLI → ${apiHost}/build/* → builder worker). Backend is a thin proxy owning identity + rate limiting; the worker sends. Compression (gzip -9) happens in the CLI, so the worker is a near pass-through.
CLI (user machine)
builder run writes a hidden verbose internal log as it goes
on failure/error → unified help menu
user picks "Ask Capgo support"
• assemble ONE combined bundle (labeled sections):
DIAGNOSTICS / INTERNAL CLI LOG / BUILD LOG (fastlane)? / AI ANALYSIS? / USER MESSAGE?
• redact secrets (CLI-only) + TELL the user we do it
• GZIP -9 + BASE64 in the CLI (Node — no mem/CPU limits)
• "We'll send your request to Capgo & email you a copy at <email>. Secrets removed."
Send / View logs first / Cancel
└ View logs first → Show path (copies to clipboard) / Open preview (combined bundle)
• on Send → POST { appId, jobId?, bundleB64, … } → ${apiHost}/build/support (capgkey)
│
▼
capgo-new backend public/build/support.ts (THIN PROXY, JSON only)
1. auth capgkey → resolve user → VERIFIED email
2. RATE LIMIT here (1/60s + 10/day per user) — NOT in the worker
3. To = resolved email; reject if client-sent email differs
4. forward JSON { bundleB64, verifiedEmail, appId, jobId? }
▼
capgo_builder worker /support
1. NO gzip, NO DO pull — drop bundleB64 into the attachment
2. env.EMAIL.send(...) ← Cloudflare Email Service binding
from: "Capgo Builder Support" <capgo_builder_support@capgo.app>
to: <verified user email> (the USER — clean, well-titled email)
cc: michael@capgo.app (internal, NON-routed → safe; NOT martin@)
bcc: capgo_builder_support@capgo.app (ONLY bridge-routed addr; invisible)
(martin@ excluded — it IS bridge-routed → would race; NO replyTo)
attachments: [{ content: bundleB64, filename: …log.gz, type: application/gzip }]
▼
Cap-go/automations bridge → ingests the BCC copy → R2 attachment → Discord [SUPPORT] thread
(team works in Discord, not email)
team replies → To = capgo_builder_support@ (routed → dropped as self-copy),
CC = buildReplyAllCc = user + michael@ (originalTo+originalCc) → both get every reply → no loop/race
Split: CLI = capture + combined bundle + redact(+inform) + gzip-9/base64 · backend = identity + verified email + rate limit + forwarding · worker = attach blob + send.
Use Cloudflare Email Service (transactional, onboarded for capgo.app) — not the legacy Email Routing binding. Resend is not used. Capgo is on a paid CF plan (confirmed) — required to send to arbitrary recipients (the user).
"Capgo Builder Support" <capgo_builder_support@capgo.app>.capgo_builder_support@capgo.app — the single bridge-routed recipient and the only envelope address that reaches the bridge; stripped from headers → invisible. Routed via ForwardEmail alias → support@usecapgo.com.michael@capgo.app). A non-routed CC adds an inbox recipient without a second routed one, and the bridge includes it on replies (buildReplyAllCc reads originalCc). martin@capgo.app is excluded — it is bridge-routed → would race. CC list in worker config; every entry must be a verified non-routed mailbox.support@ not used.capgo_builder_support@. To/CC may include non-routed addresses (the user; non-routed internal mailboxes like michael@), but never a second bridge-routed address (support@, martin@). Safety hinges on CC'd addresses staying non-routed — verify michael@ before shipping, never add a routed address to the CC config. Rare edge: if the builder user's own email is a routed @capgo.app address, the To copy also routes in → detect and drop the BCC in that case, or accept the dedup fallback.
Two bridge-routed recipients ⇒ the bridge receives the mail twice. Its Message-ID dedup (index.ts:605-621) is get-then-put, non-atomic, on eventually-consistent KV across separate worker invocations — so simultaneous double-delivery (e.g. a reply-all hitting two routed addresses) could slip through and double-post. One routed recipient (the BCC) makes the race impossible by construction.
Cap-go/automations/email/index.ts)capgo_builder_support@), parses header To/CC → originalTo = [user], originalSender = From.to: originalSender (capgo_builder_support@, routed → dropped as self-copy via x-bridge-source) and cc: buildReplyAllCc = originalTo + originalCc = the user + the non-routed CC (michael@) (index.ts:185/1577/1660) → both receive every reply (those copies land in normal inboxes → no re-ingest).capgo_builder_support@ → routed → threaded into their existing Discord thread (In-Reply-To). Bidirectional; user only ever sees clean emails.capgo_builder_support@, and the bridge drops its own x-bridge-source copies.| Limit | Value | Implication |
|---|---|---|
| Total message size | 5 MiB | Hard ceiling on the base64'd email |
| … for verified destination addrs only | 25 MiB | Recipient verified in the CF account (not the sender). The user isn't one → 5 MiB tier. Moot (measured 28 KB). |
| Attachments | must be base64 (~+33%) | 5 MiB budget is the encoded size |
| Worker memory | 128 MB / isolate | Not a constraint — the "10 MB" is script bundle size |
| Email-binding CPU | ~50 ms / request | Why we don't gzip in the worker |
| Recipients (to+cc+bcc) | 50 | Fine (we use 2) |
logs_72018286007.zip) = 123 KB raw → 28 KB gzip(-9)+base64 = ~4.4× shrink, 188× under the 5 MiB ceiling; the ~4.5 MiB encoded budget holds ~20 MiB raw. gzip level 9, run in the CLI.
// builder worker wrangler.jsonc (prod + preprod)
"send_email": [
{ "name": "EMAIL", "remote": true,
"allowed_sender_addresses": ["capgo_builder_support@capgo.app"] }
]
Sender setup: the capgo.app domain onboarding (SPF/DKIM) is the sender verification — any @capgo.app address sends immediately.
Today only ~12 in-memory lines exist. Add a full, hidden, on-disk log per builder run.
cli/src/support/internal-log.ts (new).~/.capgo-credentials/support/internal-<appId>-<ts>.log, append-as-you-go — survives crashes & unhandled errors.debug; every external API request+response (method, URL, status, body — including raw provider API errors: Apple App Store Connect AND Google Play, plus Gradle/CocoaPods/signing); every shell command (cap add ios/android, pod install, Gradle, …) with stdout/stderr; diagnostics (OS/arch, Node, CLI version, package manager, Capgo/Capacitor versions, appId, platform, channel, step).On Ask Capgo support, internal log + build log are combined into one bundle file with labeled sections (DIAGNOSTICS / INTERNAL CLI LOG / BUILD LOG (fastlane) / AI ANALYSIS / USER MESSAGE). One file = one attachment, one preview, one gzip. Built by extending the existing writeOnboardingSupportBundle, then gzipped(-9)+base64'd in the CLI.
User-facing email: addressed To: the user (CC'd to non-routed internal addrs like michael@); the Discord bridge is triggered by an invisible BCC. Preview is never shown by default — only via "View logs first".
To email is derived server-side from the capgkey owner; reject if client-sent email differs. The routed BCC and any internal CC (non-routed only, e.g. michael@) are worker config; never CC a bridge-routed address (support@, martin@) → would race.send_email restricted via allowed_sender_addresses.public/build/support.ts using the resolved capgkey owner.E_CONTENT_TOO_LARGE) into graceful degradation. Iterative but cheap (CLI, no 50 ms limit): gzip-9+base64 once → if ≤ 4.5 MiB send, no loop; else keep diagnostics header, estimate raw budget from measured ratio, cut to the tail, refine with linear ~200 KB steps (capped ~8), + a "truncated" notice. Emit support_truncated telemetry → the trigger to reconsider R2.cli/srcsupport/internal-log.ts — verbose logger + CLI-only redaction; wire into builder/onboarding + API/shell sites (capture raw Apple and Google errors).writeOnboardingSupportBundle → combined labeled bundle.support/contact-support.ts — assemble, gzip-9+base64, encoded-size guard, preview/path/clipboard UX (incl. "secrets removed" disclosure), POST, success/failure.build/onboarding/ui/steps/ios-shared.tsx & init/command.ts (support-first); add AI escalation option.sendEvent() (same as ai/telemetry.ts). Events: CLI Builder Support Requested / Sent / Failed / Truncated + Escalated from AI. Tags = closed-enum + metadata only (app_id, platform, job_id?, trigger, has_build_log, has_ai_analysis, result, error_status?, truncated, raw_bytes, encoded_bytes). Never send log content or the user's description. Respect CAPGO_DISABLE_TELEMETRY / CAPGO_DISABLE_POSTHOG.public/buildsupport.ts — mirror ai_analyze.ts: auth → verified email → rate limit (1/60s + 10/day) → reject mismatch → validate size → proxy JSON.public/build/index.ts./support route — drop bundleB64 into attachment; compose env.EMAIL.send with To = user, BCC = capgo_builder_support@. No gzip in worker.wrangler.jsonc — add send_email binding (prod + preprod, remote: true, sender allowlist).capgo.app onboarded/verified in Email Service (SPF + DKIM) — done.capgo_builder_support@capgo.app into the bridge (ForwardEmail alias → support@usecapgo.com) — used only as BCC. A CC is allowed only for NON-routed internal mailboxes (e.g. michael@); never CC a bridge-routed address (support@, martin@) or the race returns. Verify each CC address's routing before adding it (and if routing rules change). Discord forum stays the team's primary view (optional role @mention).413) → distinct errors.code (E_CONTENT_TOO_LARGE, E_RATE_LIMIT_EXCEEDED, E_DAILY_LIMIT_EXCEEDED, E_SENDER_DOMAIN_NOT_AVAILABLE) → surface to CLI fallback.E_CONTENT_TOO_LARGE; verify To=user + BCC addressing.capgo.app.support_truncated telemetry as the trigger to reconsider.capgo_builder_support@capgo.app = the single bridge-routed recipient that opens the thread (invisible). CC = non-routed internal addrs only (e.g. michael@) — safe, and included on replies via buildReplyAllCc; martin@ excluded (bridge-routed → would race). No Reply-To; support@ not used. User + michael@ get every reply; single routed recipient + x-bridge-source drop ⇒ no loop, no race. Hinges on CC'd addrs staying non-routed.writeOnboardingSupportBundle.