Skip to content

Latest commit

 

History

History
81 lines (64 loc) · 10.8 KB

File metadata and controls

81 lines (64 loc) · 10.8 KB

Agents Guide

mailing-list-cli is a single Rust binary for running a mailing list from the terminal. Campaigns, segments, templates, suppression, analytics, webhook ingestion — all exposed as JSON-emitting subcommands so an agent can drive them without an MCP server, schema file, or browser dashboard.

Current state

  • Version: v0.4.5 (design-gate enforcement on top of v0.4.4)
  • Hosted unsubscribe companion: web/ is SharpClap, a Next.js app on Vercel that serves the public /u/<token> unsubscribe page and the RFC 8058 one-click POST endpoint a local CLI cannot. unsubscribe sync mirrors its recorded opt-outs into local SQLite suppression (--endpoint, --api-key-env, --after, --limit, --max-pages, --dry-run). Token secret: MLC_UNSUBSCRIBE_SECRET; sync key read from MLC_UNSUBSCRIBE_SYNC_KEY, falling back to SYNC_API_KEY. See web/README.md.
  • Research: see /research for the five dossiers that informed the original design
  • Recent plans:
    • v0.2 rearchitecture (shipped as v0.2.0)
    • v0.3 production-grade 10k (shipped as v0.3.0)
    • v0.3.1 emergency hardening (shipped as v0.3.1)
    • v0.4 operator superpowers (shipped as v0.4.0)
    • v0.4.1 patch: explicit broadcast approval, safer template-render guidance, click payload passthrough, release automation
    • v0.4.2 patch: unsubscribe body links opt out of UTM rewriting, plain-text alternatives preserve anchor URLs
    • v0.4.3 patch: skill install / skill status install the embedded Codex/Claude/Gemini skill
    • v0.4.4 patch: embedded skill and agent-info include explicit email design rules plus template inspect for browser/design handoffs
    • v0.4.5 patch: template create --from-file and broadcast send preflight enforce the design + lint gate (override with --force / --allow-design-errors); JSX heuristic catches modern frameworks without an explicit React import; single design-rule scanner shared by inspect/create/send
    • v0.5 deliverability guard — planned, not shipped: send-ramp governor + bounce/complaint circuit breaker

Production hardening (v0.3.x)

What "production-grade" means in this codebase:

  • Send pipeline reliability: 429/5xx retry with exponential backoff [500ms, 1s, 2s, 4s], up to 4 retries; per-chunk DB transactions; preloaded suppression HashSet for O(1) lookups; resumable sends with atomic broadcast lock CAS (no double-send race even on concurrent invocation); v0.3.2 write-ahead broadcast_send_attempt table that records ESP acceptance BEFORE the local recipient UPDATE, so a crash between the two cannot cause duplicate sends on resume — it reconciles from the stored response.
  • Explicit send approval: real broadcast send and broadcast resume require --confirm. Use broadcast send <id> --dry-run first for recipient counts and preflight checks; --dry-run never sends and does not require confirmation.
  • Large-send behavior: broadcast send uses email-cli batch send chunks of 100 recipients. For a 1,000-recipient trial, target a separate 1,000-member list/segment; do not reuse the final full-list broadcast as a partial-send test.
  • Subprocess safety: every email-cli call has a 120-second default timeout (MLC_EMAIL_CLI_TIMEOUT_SEC env var to override). Hung subprocesses are killed via SIGKILL and surfaced as email_cli_timeout transient errors that feed the existing retry path.
  • Schema safety: Db::open fails fast (exit code 2, db_schema_too_new) when the on-disk schema version is newer than what this binary supports — no more silent column-mismatch errors after a binary downgrade.
  • Unsubscribe link security: MLC_UNSUBSCRIBE_SECRET is required (v0.3.2) — broadcast send refuses to sign tokens with a dev fallback. HMAC-SHA256.
  • GDPR compliance: contact erase --confirm writes a gdpr_erasure suppression tombstone before deleting the contact row (atomic transaction; the email is never momentarily absent from both).
  • Operator escape hatches: broadcast send <id> --confirm --force-unlock overrides a held send lock when the previous process is confirmed dead (use only after ps aux | grep mailing-list-cli).
  • Honest limitations: the 30-day complaint/bounce rate guards in broadcast send preflight are computed from the local event table, which is populated by webhook poll paginating email-cli email list by email ID and reading only last_event per row. The guards still fire and remain useful safety nets, but the exact percentages are best-effort approximations — see agent-info → known_limitations and the docstring on historical_send_rates. Proper fix is v0.5+ pending an upstream change to email-cli or a rolling-window snapshot diff. Source: GPT Pro F3.2 from 2026-04-09 hardening review.
  • Click reporting limitation: report show can count clicked emails from last_event=clicked. report links needs link payload from email-cli (click.link or link); the poll path stores it when present, but cannot infer the CTA URL from last_event alone.
  • Tracking workflow: run webhook poll / event poll after sends. The CLI shells out to email-cli email list, maps last_event into local event rows, stores click rows when link payload exists, then reports from SQLite via report show / report links.
  • Deliverability footer behavior: the send pipeline emits List-Unsubscribe and List-Unsubscribe-Post headers, and the body unsubscribe anchor is rendered with inline styling plus data-utm="off" so tracking parameters are not added to compliance links. The plain-text MIME alternative preserves anchor destinations as Label (URL) instead of dropping URLs.
  • Template quality warnings: template lint warns on unstyled <a href> links and fragile semantic layout tags such as <main>, because Gmail and other clients do not behave like full browsers.
  • Skill distribution: mailing-list-cli skill install writes the embedded mailing-list-cli skill to Codex, Claude, Gemini, and .agents skill roots. MLC_SKILL_ROOTS can override the install roots for tests or custom setups.
  • Agent-facing design guidance: agent-info.template_design_rules and the embedded skill tell agents to use table wrappers, visible margins, inline link styles, restrained typography, plain-text inspection, and broadcast preview before real sends.
  • Design handoff inspection: run template inspect --from-file <path> on browser/React/JSX/design-canvas handoffs before template create. A browser_prototype_needs_conversion verdict means the file is design direction only; convert it into standalone table-based inline HTML before linting, previewing, or sending.
  • Design + lint gate (v0.4.5): template create --from-file refuses imports whose verdict is browser_prototype_needs_conversion or whose lint reports any errors — error codes template_create_design_blocked / template_create_lint_blocked, override with --force. broadcast send re-runs the design check at preflight and refuses error-level findings — error code template_has_design_errors, override with --allow-design-errors or set [guards].block_design_errors = false in config.toml. The two error codes use distinct names so an agent can route a JSX handoff through conversion without confusing it with a substantive lint failure.
  • Hosted unsubscribe sync: unsubscribe clicks land on the SharpClap companion and stay in its Postgres until pulled. Run unsubscribe sync before every real send so hosted opt-outs reach local suppression; --dry-run verifies the endpoint and key without writing suppression or cursor state.

Conventions

This project follows the agent-cli-framework patterns:

  • Structured JSON output, auto-detected via IsTerminal
  • Semantic exit codes: 0 success, 1 transient (retry), 2 config (fix setup), 3 bad input, 4 rate limited
  • Self-describing via agent-info — one command returns the full capability manifest
  • No interactive prompts, ever. v0.2 removed the v0.1 template edit command because it violated this invariant; agents use Write/Edit tools directly on files passed via template create --from-file.
  • Local-first state under ~/.local/share/mailing-list-cli/
  • Config under ~/.config/mailing-list-cli/config.toml
  • Cache under ~/.cache/mailing-list-cli/ (always safe to rm -rf)
  • Integrated preview. template preview <name> writes rendered HTML to disk and optionally opens it in the default browser. This is the core iteration primitive — it replaces every "catch the mistake upfront" safety net the v0.1 system had.
  • Release automation. Pushing a vX.Y.Z tag runs .github/workflows/release.yml, verifies the tag matches Cargo.toml, publishes crates.io, updates the Homebrew tap, and creates/updates a GitHub release. Required repo secrets are listed in docs/release.md.
  • Template render is not send-ready stdout. template render <name> emits the full JSON envelope. If you need the rendered HTML for custom tooling, extract .data.html after checking status == "success" and data.lint_errors == 0; never pass the whole stdout to email-cli --html. Prefer broadcast preview <id> --to <email> for inbox tests because it runs the same strict render/compliance path as a real send.
  • Template inspect is the handoff gate. template inspect <name> or template inspect --from-file <path> classifies HTML/JSX/browser prototypes as email_ready, email_candidate_with_warnings, not_send_ready, or browser_prototype_needs_conversion, and returns concrete conversion steps for agents.

Discovery

mailing-list-cli agent-info

Returns a JSON manifest of every subcommand, every flag, every exit code. No documentation drift, no MCP server, no schema file an agent has to load up front.

Required dependency: email-cli

mailing-list-cli does not talk to Resend directly. Every send, every audience operation, every event read goes through email-cli, which is the sole Resend API client. Both binaries must be on $PATH.

This split exists so neither tool has to do the other's job:

  • email-cli owns the Resend API surface, accounts, profiles, transports, the inbox, the webhook listener.
  • mailing-list-cli owns campaigns, segmentation, templates, suppression, analytics, revenue attribution, and the hosted-unsubscribe sync.

For an agent: use email-cli for personal correspondence, mailing-list-cli for newsletters and campaigns. They cooperate on the same Resend account but each one stays in its lane.