Files
claude-skills-reference/marketing-skill/x-twitter-growth/scripts/tweet_composer.py
Leo 66dcf674c5 feat(marketing): add x-twitter-growth skill with 5 Python tools
- Profile auditor (bio quality, posting patterns, growth readiness)
- Tweet composer (hooks, threads, validation, 30+ proven patterns)
- Content planner (weekly calendars with format mix)
- Competitor analyzer (competitive intel via data import)
- Growth tracker (snapshot-based progress tracking + milestone projection)
- Algorithm reference doc (ranking signals, timing, format performance)
- 226-line SKILL.md with practical playbook (no fluff)
- Security audit: PASS (0 findings)
2026-03-10 17:52:02 +01:00

291 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Tweet Composer — Generate structured tweets and threads with proven hook patterns.
Provides templates, character counting, thread formatting, and hook generation
for different content types. No API required — pure content scaffolding.
Usage:
python3 tweet_composer.py --type tweet --topic "AI in healthcare"
python3 tweet_composer.py --type thread --topic "lessons from scaling" --tweets 8
python3 tweet_composer.py --type hooks --topic "startup mistakes" --count 10
python3 tweet_composer.py --validate "your tweet text here"
"""
import argparse
import json
import sys
import textwrap
from dataclasses import dataclass, field, asdict
from typing import Optional
MAX_TWEET_CHARS = 280
HOOK_PATTERNS = {
"listicle": [
"{n} {topic} that changed how I {verb}:",
"The {n} biggest mistakes in {topic}:",
"{n} {topic} most people don't know about:",
"I spent {time} studying {topic}. Here are {n} lessons:",
"{n} signs your {topic} needs work:",
],
"contrarian": [
"Unpopular opinion: {claim}",
"Hot take: {claim}",
"Everyone says {common_belief}. They're wrong.",
"Stop {common_action}. Here's what to do instead:",
"The {topic} advice you keep hearing is backwards.",
],
"story": [
"I {did_thing} and it completely changed my {outcome}.",
"Last {timeframe}, I made a mistake with {topic}. Here's what happened:",
"3 years ago I was {before_state}. Now I'm {after_state}. Here's the playbook:",
"I almost {near_miss}. Then I discovered {topic}.",
"The best {topic} advice I ever got came from {unexpected_source}.",
],
"observation": [
"{topic} is underrated. Here's why:",
"Nobody talks about this part of {topic}:",
"The gap between {thing_a} and {thing_b} is where the money is.",
"If you're struggling with {topic}, you're probably {mistake}.",
"The secret to {topic} isn't what you think.",
],
"framework": [
"The {name} framework for {topic} (save this):",
"How to {outcome} in {timeframe} (step by step):",
"{topic} explained in 60 seconds:",
"The only {n} things that matter for {topic}:",
"A simple system for {topic} that actually works:",
],
"question": [
"What's the most underrated {topic}?",
"If you could only {do_one_thing} for {topic}, what would it be?",
"What {topic} advice would you give your younger self?",
"Real question: why do most people {common_mistake}?",
"What's one {topic} that completely changed your perspective?",
],
}
THREAD_STRUCTURE = """
Thread Outline: {topic}
{'='*50}
Tweet 1 (HOOK — most important):
Pattern: {hook_pattern}
Draft: {hook_draft}
Chars: {hook_chars}/280
Tweet 2 (CONTEXT):
Purpose: Set up why this matters
Suggestion: "Here's what most people get wrong about {topic}:"
OR: "I spent [time] learning this. Here's the breakdown:"
Tweets 3-{n} (BODY — one idea per tweet):
{body_suggestions}
Tweet {n_plus_1} (CLOSE):
Purpose: Summarize + CTA
Suggestion: "TL;DR:\\n\\n[3 bullet summary]\\n\\nFollow @handle for more on {topic}"
Reply to Tweet 1 (ENGAGEMENT BAIT):
Purpose: Resurface the thread
Suggestion: "What's your experience with {topic}? Drop it below 👇"
"""
@dataclass
class TweetDraft:
text: str
char_count: int
over_limit: bool
warnings: list = field(default_factory=list)
def validate_tweet(text: str) -> TweetDraft:
"""Validate a tweet and return analysis."""
char_count = len(text)
over_limit = char_count > MAX_TWEET_CHARS
warnings = []
if over_limit:
warnings.append(f"Over limit by {char_count - MAX_TWEET_CHARS} characters")
# Check for links in body
import re
if re.search(r'https?://\S+', text):
warnings.append("Contains URL — consider moving link to reply (hurts reach)")
# Check for hashtags
hashtags = re.findall(r'#\w+', text)
if len(hashtags) > 2:
warnings.append(f"Too many hashtags ({len(hashtags)}) — max 1-2, ideally 0")
elif len(hashtags) > 0:
warnings.append(f"Has {len(hashtags)} hashtag(s) — consider removing for cleaner look")
# Check for @mentions at start
if text.startswith('@'):
warnings.append("Starts with @ — will be treated as reply, not shown in timeline")
# Readability
lines = text.strip().split('\n')
long_lines = [l for l in lines if len(l) > 70]
if long_lines:
warnings.append("Long unbroken lines — add line breaks for mobile readability")
return TweetDraft(text=text, char_count=char_count, over_limit=over_limit, warnings=warnings)
def generate_hooks(topic: str, count: int = 10) -> list:
"""Generate hook variations for a topic."""
hooks = []
for pattern_type, patterns in HOOK_PATTERNS.items():
for p in patterns:
hook = p.replace("{topic}", topic).replace("{n}", "7").replace(
"{time}", "6 months").replace("{timeframe}", "month").replace(
"{claim}", f"{topic} is overrated").replace(
"{common_belief}", f"{topic} is simple").replace(
"{common_action}", f"overthinking {topic}").replace(
"{outcome}", "approach").replace("{verb}", "think").replace(
"{name}", "3-Step").replace("{did_thing}", f"changed my {topic} strategy").replace(
"{before_state}", "stuck").replace("{after_state}", "thriving").replace(
"{near_miss}", f"gave up on {topic}").replace(
"{unexpected_source}", "a complete beginner").replace(
"{thing_a}", "theory").replace("{thing_b}", "execution").replace(
"{mistake}", "overcomplicating it").replace(
"{common_mistake}", f"ignore {topic}").replace(
"{do_one_thing}", "change one thing").replace(
"{common_action}", f"overthinking {topic}")
hooks.append({"type": pattern_type, "hook": hook, "chars": len(hook)})
if len(hooks) >= count:
return hooks
return hooks[:count]
def generate_thread_outline(topic: str, num_tweets: int = 8) -> str:
"""Generate a thread structure outline."""
hooks = generate_hooks(topic, 3)
best_hook = hooks[0]["hook"] if hooks else f"Everything I know about {topic}:"
body = []
suggestions = [
"Key insight or surprising fact",
"Common mistake people make",
"The counterintuitive truth",
"A practical example or case study",
"The framework or system",
"Implementation steps",
"Results or evidence",
"The nuance most people miss",
]
for i, s in enumerate(suggestions[:num_tweets - 3], 3):
body.append(f" Tweet {i}: [{s}]")
body_text = "\n".join(body)
return f"""
{'='*60}
THREAD OUTLINE: {topic}
{'='*60}
Tweet 1 (HOOK):
"{best_hook}"
Chars: {len(best_hook)}/280
Tweet 2 (CONTEXT):
"Here's what most people get wrong about {topic}:"
{body_text}
Tweet {num_tweets - 1} (CLOSE):
"TL;DR:
• [Key takeaway 1]
• [Key takeaway 2]
• [Key takeaway 3]
Follow for more on {topic}"
Reply to Tweet 1 (BOOST):
"What's your biggest challenge with {topic}? 👇"
{'='*60}
RULES:
- Each tweet must stand alone (people read out of order)
- Max 3-4 lines per tweet (mobile readability)
- No filler tweets — cut anything that doesn't add value
- Hook tweet determines 90%% of thread performance
{'='*60}
"""
def main():
parser = argparse.ArgumentParser(
description="Generate tweets, threads, and hooks with proven patterns",
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--type", choices=["tweet", "thread", "hooks", "validate"],
default="hooks", help="Content type to generate")
parser.add_argument("--topic", default="", help="Topic for content generation")
parser.add_argument("--tweets", type=int, default=8, help="Number of tweets in thread")
parser.add_argument("--count", type=int, default=10, help="Number of hooks to generate")
parser.add_argument("--validate", nargs="?", const="", help="Tweet text to validate")
parser.add_argument("--json", action="store_true", help="Output JSON")
args = parser.parse_args()
if args.type == "validate" or args.validate is not None:
text = args.validate or args.topic
if not text:
print("Error: provide tweet text to validate", file=sys.stderr)
sys.exit(1)
result = validate_tweet(text)
if args.json:
print(json.dumps(asdict(result), indent=2))
else:
icon = "🔴" if result.over_limit else ""
print(f"\n {icon} {result.char_count}/{MAX_TWEET_CHARS} characters")
if result.warnings:
for w in result.warnings:
print(f" ⚠️ {w}")
else:
print(" No issues found.")
print()
elif args.type == "hooks":
if not args.topic:
print("Error: --topic required for hook generation", file=sys.stderr)
sys.exit(1)
hooks = generate_hooks(args.topic, args.count)
if args.json:
print(json.dumps(hooks, indent=2))
else:
print(f"\n{'='*60}")
print(f" HOOK IDEAS: {args.topic}")
print(f"{'='*60}\n")
for i, h in enumerate(hooks, 1):
print(f" {i:2d}. [{h['type']:<12}] {h['hook']}")
print(f" ({h['chars']} chars)")
print()
elif args.type == "thread":
if not args.topic:
print("Error: --topic required for thread generation", file=sys.stderr)
sys.exit(1)
outline = generate_thread_outline(args.topic, args.tweets)
print(outline)
elif args.type == "tweet":
if not args.topic:
print("Error: --topic required", file=sys.stderr)
sys.exit(1)
hooks = generate_hooks(args.topic, 5)
print(f"\n 5 tweet drafts for: {args.topic}\n")
for i, h in enumerate(hooks, 1):
print(f" {i}. {h['hook']}")
print(f" ({h['chars']} chars)\n")
if __name__ == "__main__":
main()