Skip to content

blacklogos/sigil

Repository files navigation

🪶 sigil

Seal the room after you leave it.

License: MIT Companion to cc4.marketing

A small CLI for sending personalized, human-feeling follow-up emails after a talk, meeting, or event. One email per recipient, with a per-recipient signed PDF link. Built for VIP-list size sends (≤50), not bulk marketing.

Vietnamese-first template (anh / em / bạn tone field built in). Cloudflare Email Service for sending, R2 + Workers for per-recipient PDF link delivery, KV for click tracking. No mailchimp, no API key for content generation — rewrites happen in Claude Code via the /email-rewrite slash command.

What it looks like

$ sig init
sigil init — campaign workspace setup
workspace: /Users/you/Documents/talk-2026-05

? CF API token *                            ••••••••…7a25
? CF account ID *                           1a2b3c…
? From name *                               Trí Võ
? From email *                              tri@mtri.me
? Event name *                              Sài Gòn AI · May
? Event day phrase                          sáng thứ Năm
? Email subject *                           Slides PDF + lời cảm ơn
? Test email addresses (comma-separated)    tri@mtri.me, qa@biplus.com.vn
? Agent shims to install                    claude, antigravity

Validating CF token … ok (id=8f2c4a1d…)
.env changes:
  + CF_API_TOKEN=<masked …7a25>
  + EVENT_NAME=Sài Gòn AI · May
  + EMAIL_SUBJECT=Slides PDF + lời cảm ơn
  + TEST_EMAILS=tri@mtri.me, qa@biplus.com.vn
? Apply changes? Yes
✓ wrote .env
✓ .claude/commands/email-rewrite.md
✓ .antigravity/commands/email-rewrite.md

$ gog sheets get 1A2b… 'A1:Z' --plain | sig ingest --stdin
Wrote 23 leads to data/leads.json

$ sig review --table
┌──┬──────────────────────┬───────────┬──────┬─────────┬──────────┬────────┬────┬────┐
│ #│ email                │ name      │ tone │ optin   │ token    │ sent   │test│    │
├──┼──────────────────────┼───────────┼──────┼─────────┼──────────┼────────┼────┼────┤
│ 1│ ai@example.com       │ Ai        │ bạn  │ Có      │ Kqdqs…   │   —    │ —  │    │
│ 2│ trieu@example.com    │ Triều     │ anh- │ Có      │ aB91p…   │   —    │ —  │    │
│ …│ … 21 more …          │           │      │         │          │        │    │    │
└──┴──────────────────────┴───────────┴──────┴─────────┴──────────┴────────┴────┴────┘
23 leads · 23 unsent · 23 no-sentence · 0 no-token

$ sig prompt
Reviewing 23 lead(s).  Actions: <Enter>=keep · /default · ?<prompt> · <new sentence> · /quit
────────────────────────────────────────────────────────────────────────────────
1/23  Ai  <ai@example.com>  tone=bạn
> ?reference her question about RAG retrieval latency in production
2/23  Triều  <trieu@example.com>  tone=anh-em
> /default
…
Saved.  kept=4  default=9  overwritten=2  queued=8
8 rewrite(s) queued in data/rewrite-queue.json.
Run `/email-rewrite` in Claude Code chat to process.

# (in Claude Code chat:)
/email-rewrite
  → walks 8 queued rows, per-row diff + keep/regenerate/skip,
    writes back to data/leads.json, clears queue

$ sig prep
Minted 0 new token(s). Total leads: 23
Wrote worker/tokens.json (23 tokens)
Updated TOKENS_JSON in worker/wrangler.toml

$ cd worker && npx wrangler deploy && cd ..
✓ Deployed pdf-sigil triggers (https://pdf.mtri.me/*)

$ sig send --test
[DRY=false] TEST → tri@mtri.me, qa@biplus.com.vn | 23 lead(s) × 2 addr = 46 send(s)
✓ ai@example.com  → tri@mtri.me  cf-msg-id-…
✓ ai@example.com  → qa@biplus.com.vn  cf-msg-id-…
✓ trieu@example.com  → tri@mtri.me  …
… (44 more) …

$ sig send
PROD | 23 lead(s)
About to send 23 live email(s) (ai@example.com … omar@example.com). Continue? [y/N] y
✓ ai@example.com  → ai@example.com  cf-msg-id-…
… (22 more) …

$ sig status
EMAIL                          SENT (UTC)             CLICKS  LAST CLICK
ai@example.com                 ✓ 2026-05-16 14:22Z    3       2026-05-16 14:24Z
trieu@example.com              ✓ 2026-05-16 14:22Z    1       2026-05-16 15:01Z
…

Same surface, scriptable:

sig --version                            # 0.2.0
sig review --json | jq '.[] | select(.sentAt == null) | .email'
sig status --json | jq '[.leads[] | {email, clicks: .clickStats.count}]'
sig send --yes --only=foo@bar.com        # bypass interactive confirm
NO_COLOR=1 sig review --table            # zero ANSI in captured output

Install

git clone <this-repo> ~/Documents/sigil
cd ~/Documents/sigil
npm install
npm link        # makes `sig` globally available

Use

Per campaign, in any working directory:

# 1. Set up
sig init                                      # English TUI; writes .env, drops agent shims
cp ~/Documents/sigil/worker/wrangler.example.toml worker/wrangler.toml
# edit wrangler.toml (worker name, route, KV/R2 bindings)

# put your Google Forms CSV at data/leads.csv (TSV from Sheets paste works)
# or pipe directly:
#   gog sheets get <ID> 'A1:Z' --plain | sig ingest --stdin

# 2. Workflow
sig ingest                                    # parse data/leads.csv → leads.json
sig review                                    # cards (--table compact, --diff vs last send)
sig prompt                                    # interactive per-row review
                                              # ? prompts queued for /email-rewrite
sig prep                                      # mint tokens, rewrite wrangler.toml block
cd worker && npx wrangler deploy && cd ..
sig preview --email=foo@bar.com               # render check (warns on unresolved {{var}})
sig send --test=you@example.com               # land all personalized emails in one inbox
sig send                                      # real send (asks y/N first)
sig status                                    # sent + clicks + delivery

Multi-lang campaigns: set BODY_TEMPLATE=templates/body.en.md in .env to use the bundled English template. Leave unset for the default Vietnamese body. Fork templates/body.md into your own file for a custom layout — single file with YAML frontmatter subject: then text body, then --- html ---, then HTML body.

In Claude Code chat (after sig prompt queued some ? prompts):

/email-rewrite

…walks the queue with per-row approval + diff, writes rewrites back to data/leads.json.

Why sigil

A sigil is a personal seal — pressed into wax to close a letter, marking authorship. Across cultures it's the mark of "this came from me." That's the product promise: every email is sealed, identifiable, and addressed to one person. Not a blast.

Two strong syllables. Easy on a sales call. Distinct from mailer SaaS.

What sigil is not

  • Not bulk email. There's no list of 10k. Quota gates are real but small (CF Email Service: 1000/day on Workers Paid).
  • Not a CRM. State lives in one JSON file per campaign.
  • Not deliverability magic. Reputation is your sending domain's job.
  • Not English-only — but the default template is Vietnamese. To swap, fork cli/lib/template.ts.

Architecture

   data/leads.csv ─▶ leads.json ──┬─▶ sig review / preview
                                  │
                                  ├─▶ sig prep (mint token per row,
                                  │            write worker/tokens.json + wrangler.toml block)
                                  │
                                  └─▶ sig send  ──▶ Cloudflare Email Service API
                                                       │
   pdf.<your-domain>/p/:token ◀────────── recipient ◀──┘
        │
        ├─ Worker: token in TOKENS_JSON?
        ├─ KV.put(token, {firstClickAt, lastClickAt, count, lastUa, lastIp})
        └─ stream PDF from R2 binding PDF_BUCKET

Commands

sig … Purpose
init TUI workspace setup (or --non-interactive + flags); writes .env, drops agent shims
ingest parse data/leads.csvdata/leads.json (also --stdin, --json)
review card view (--table compact, --diff vs last send, --json)
preview --email=X / --all fully rendered email; warns on unresolved {{var}}
prompt interactive review — keep / default / overwrite / queue ?prompt
prep mint tokens, write worker/tokens.json + inject TOKENS_JSON into wrangler.toml
send --test use TEST_EMAILS from .env (or --test=a@x.com,b@y.com)
send real send (asks y/N; --yes bypasses)
send --force resend even if already sentAt (use with care)
send --only=X single recipient
status sent + per-token PDF clicks + CF feedback (--json available)

Global flags: -h/--help, -v/--version, --no-color (also honors NO_COLOR, TERM=dumb), --no-input.

Examples

Human-paced run:

sig ingest                          # parse data/leads.csv
sig review --diff                   # inspect, see what would change vs last send
sig prompt                          # walk each row; type ?<hint> to queue a rewrite
# (in Claude Code chat:)  /email-rewrite
sig prep                            # mint tokens, write worker bindings
cd worker && npx wrangler deploy && cd ..
sig send --test=you@example.com     # land all N personalized copies in your inbox
sig send                            # asks y/N first, then sends for real
sig status                          # sent + clicks + delivery feedback

Scripted / agent-callable run (no human at the keyboard):

sig --version                       # capability gate
gog sheets get <ID> 'A1:Z' --plain | sig ingest --json | jq 'length'
sig review --json | jq '.[] | select(.sentAt == null) | .email'
sig send --yes                      # bypass the confirm gate
sig send --only=foo@bar.com --yes
sig status --json | jq '.leads[] | {email, sent: (.sentAt != null), clicks: .clickStats.count}'
NO_COLOR=1 sig review --table       # ANSI stripped

Exit codes: 0 ok · 1 runtime/API · 2 usage/validation · 3 env missing.

Slash commands (any agent)

Canonical instructions live at prompts/email-rewrite.md (agent-neutral). Each agent's slash-command shim is a one-line pointer dropped by sig init --agents=….

Agent Where the shim lands Surface
Claude Code .claude/commands/email-rewrite.md /email-rewrite
Antigravity .antigravity/commands/email-rewrite.md /email-rewrite
Codex none (reads AGENTS.md) do email-rewrite

What it does: processes data/rewrite-queue.json with per-row approval + diff, writes to leads.json, clears queue entry.

Cloudflare setup

Each campaign needs:

  1. Email Service onboarded for the sending domain (tri@mtri.me-style address verified). Workers Paid plan ($5/mo) to send to non-verified destinations.
  2. R2 bucket for the PDF.
  3. Worker at a route like pdf.example.com (custom domain).
  4. KV namespace for click stats.

See docs/howto.md for the full step-by-step. API token needs:

  • Email Sending: Edit (for /send)
  • Email Sending: Read (for /feedback in status)
  • Workers KV Storage: Read (for click stats in status)
  • Workers Scripts: Edit + Workers R2 Storage: Edit (for wrangler deploys)

File layout

bin/sig.mjs                       single-entry dispatcher
cli/
  init.ts ingest.ts review.ts preview.ts prompt.ts prep.ts send.ts status.ts
  lib/    csv  state  token  template  email-cf  cli-format  cf-api  argv
          init-fields  init-writers
templates/
  body.md                         default VN template (mustache {{vars}})
  body.en.md                      English example template
prompts/
  email-rewrite.md                canonical agent-neutral rewrite-queue prompt
.claude/commands/email-rewrite.md     shim → prompts/email-rewrite.md
.antigravity/commands/email-rewrite.md  shim → same
worker/
  wrangler.example.toml           starter — copy to wrangler.toml per campaign
  src/index.ts                    GET /p/:token → log + KV + R2 stream
docs/howto.md
.env.example                      copy to .env (or run `sig init`)
AGENTS.md                         briefing for any agent session

Companion project

Built alongside cc4.marketing — a free interactive course on using Claude Code for marketing workflows. Sigil is the post-event follow-up tool the course's curriculum gestures at, shipped as something you can actually run. The /email-rewrite slash command + per-row handcrafting is exactly the kind of agent-in-the-loop pattern the course teaches.

If you want help setting sigil up for your own event (CF onboarding, custom template, your first send), reach out at mtri.me — done-with-you setup is a thing I'll do.

License

MIT © 2026 Minh Tri Vo.

Use it, fork it, ship something with it. Pull requests welcome but not expected — sigil is a personal tool that just happens to be open source. If you build something on top, I'd love to hear about it.