← back to Contact Mailer Ideas

generate-daily.sh

136 lines

#!/bin/bash
# generate-daily.sh — produce 20 fresh contact-mailer product ideas via claude CLI.
# Run by launchd com.steve.contact-mailer-daily at 08:00 PT daily.
# Triggered manually: ./generate-daily.sh [YYYY-MM-DD]
set -euo pipefail

ROOT="$HOME/Projects/contact-mailer-ideas"
DATE="${1:-$(date +%Y-%m-%d)}"
OUT="$ROOT/batches/${DATE}.json"
LOG="$ROOT/logs/${DATE}.log"
mkdir -p "$ROOT/batches" "$ROOT/logs"

# Per MEMORY: NEVER use Anthropic API key — strip both before claude CLI
unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN

# 2026-05-13 incident: launchd PATH didn't include ~/.local/bin → `claude` not found
# → 11 days of silent missing batches. Pin claude to absolute path and broaden PATH
# so jq, python3, scp, ssh, rsync resolve under launchd's minimal env.
CLAUDE_BIN="${CLAUDE_BIN:-$HOME/.local/bin/claude}"
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
if [ ! -x "$CLAUDE_BIN" ]; then
  echo "$(date -Iseconds) FATAL: claude CLI not found at $CLAUDE_BIN" >> "$LOG"
  exit 1
fi

# Idempotent: skip if today's batch already exists
if [ -f "$OUT" ]; then
  echo "$(date -Iseconds) batch ${DATE} already exists, skipping" >> "$LOG"
  exit 0
fi

# Existing idea names to avoid duplicates
EXISTING=$(jq -r '.ideas[].name' "$ROOT"/ideas.json "$ROOT"/batches/*.json 2>/dev/null \
  | sort -u | tr '\n' ',' | sed 's/,$//')

PROMPT="You are generating 20 NEW product ideas in the 'contact mailer' / email-marketing space (Constant Contact / Mailchimp / Beehiiv / Lemlist adjacent).

HARD RULES:
- 20 ideas, distinct from each other and from existing names: ${EXISTING}
- Each idea must be a real PRODUCT (not a feature), shippable by a 2-person team in ≤8 weeks.
- Mix angles: AI-native, anti-features, vertical (B2B trade / nonprofit / regulated industries), hardware-adjacent, time-shifted, monetization, privacy-first, network-effect, hybrid offline+digital. Avoid me-too 'better Mailchimp'.

OUTPUT FORMAT: pure JSON only, no markdown, no commentary. Shape exactly:
{
  \"ideas\": [
    {
      \"name\": \"ShortPascalCase\",
      \"angle\": \"one-line product angle\",
      \"target\": \"who buys this\",
      \"diff\": \"what makes it different in one sentence\",
      \"headline\": \"a 4-8 word hero headline\",
      \"action_items\": [
        {\"step\":\"VALIDATE\",\"what\":\"one concrete test to de-risk before building\"},
        {\"step\":\"MVP\",\"what\":\"minimum shippable scope + pricing hint\"},
        {\"step\":\"DISTRIBUTION\",\"what\":\"where to find first 10 customers\"}
      ]
    }
    // ... 20 items
  ]
}

Color palette suggestion is NOT needed. The downstream tool assigns colors.
Return ONLY the JSON object. No prose."

echo "$(date -Iseconds) running claude generator for ${DATE}" >> "$LOG"

# Try claude CLI; capture stdout
RAW="$("$CLAUDE_BIN" -p "$PROMPT" 2>>"$LOG" || true)"

# Extract JSON: prefer first {...} block; fall back to fenced code block if claude wrapped it
JSON=$(printf '%s' "$RAW" | python3 -c "
import sys, re, json
s = sys.stdin.read()
# Try fenced code block first
m = re.search(r'\`\`\`(?:json)?\s*(\{.*?\})\s*\`\`\`', s, re.DOTALL)
if m:
    s = m.group(1)
# Find first top-level JSON object
depth = 0
start = -1
for i, ch in enumerate(s):
    if ch == '{':
        if depth == 0: start = i
        depth += 1
    elif ch == '}':
        depth -= 1
        if depth == 0 and start >= 0:
            try:
                obj = json.loads(s[start:i+1])
                if 'ideas' in obj: print(json.dumps(obj)); sys.exit(0)
            except: pass
sys.exit(1)
" 2>>"$LOG" || true)

if [ -z "$JSON" ]; then
  echo "$(date -Iseconds) FAILED to parse claude output. raw saved to ${LOG}.raw" >> "$LOG"
  printf '%s' "$RAW" > "${LOG}.raw"
  exit 1
fi

COUNT=$(printf '%s' "$JSON" | jq '.ideas | length')
if [ "$COUNT" -lt 10 ]; then
  echo "$(date -Iseconds) too few ideas: ${COUNT}, aborting" >> "$LOG"
  exit 1
fi

# Compose full batch document with batch metadata + index renumbering
printf '%s' "$JSON" | jq --arg date "$DATE" --arg ts "$(date -Iseconds)" '{
  batch: ("contact-mailer-" + ($date | gsub("-"; ""))),
  batch_date: $date,
  vertical: "contact-mailer / email-marketing (Constant Contact-adjacent)",
  authority: "ideate-only",
  generated_at: $ts,
  generator: "claude-cli daily",
  ideas: [.ideas | to_entries[] | {
    idx: (.key + 1),
    name: .value.name,
    angle: .value.angle,
    target: .value.target,
    diff: .value.diff,
    headline: .value.headline,
    action_items: .value.action_items
  }]
}' > "$OUT"

# Append jsonl seeds (one per idea) so the existing ideas.jsonl ledger sees them
jq -r --arg date "$DATE" '.ideas[] | {batch: ("contact-mailer-" + ($date | gsub("-"; ""))), batch_date: $date, tick: 0, idx: .idx, name: .name, headline: .headline, angle: .angle, authority: "ideate-only"} | tojson' "$OUT" \
  >> ~/.claude/skills/idea-loop/data/ideas.jsonl

echo "$(date -Iseconds) wrote ${OUT} with ${COUNT} ideas" >> "$LOG"

# Sync to prod
"$ROOT/sync-to-prod.sh" >> "$LOG" 2>&1 || echo "$(date -Iseconds) sync-to-prod failed" >> "$LOG"

echo "$(date -Iseconds) done" >> "$LOG"