← 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"