From morning-ai
Generates platform-specific social media copy and images for X/Twitter and Xiaohongshu from AI news reports. Supports custom styles, personas, and constraints like character limits.
npx claudepluginhub octo-patch/morningai --plugin morning-aiThis skill uses the workspace's default tool permissions.
Transform the daily AI news report into platform-optimized social media content. Generates ready-to-post copy and adapted images for multiple platforms, with support for multiple accounts and styles per platform.
Generates platform-native content for X, LinkedIn, TikTok, YouTube, and newsletters from one idea. Useful for social posts, threads, video scripts, and content calendars.
Generates platform-specific social media posts for LinkedIn, Facebook, Instagram, Reddit from topics, content, or campaigns, respecting limits, hashtags, and best practices.
Generates platform-native content for X, LinkedIn, TikTok, YouTube, newsletters from source material. Builds social posts, threads, scripts, content calendars, and multi-platform adaptations.
Share bugs, ideas, or general feedback.
Transform the daily AI news report into platform-optimized social media content. Generates ready-to-post copy and adapted images for multiple platforms, with support for multiple accounts and styles per platform.
Phase 1 scope: Generate copy files + images. No auto-publish (future phase).
| Constraint | Value |
|---|---|
| Single tweet | 280 characters max |
| Thread | Unlimited tweets (recommended 5-15) |
| Images per tweet | Up to 4, 16:9 or 1:1 |
| Hashtags | 2-3 per tweet |
| Mentions | @handle format |
| Constraint | Value |
|---|---|
| Title | 20 characters max |
| Body | 1000 characters max |
| Images | Up to 9 per post, 3:4 or 1:1 (carousel) |
| Tags | 5-10 # tags at end of body |
| Emoji | Heavy use expected (platform culture) |
Templates define the voice, tone, and format for each channel. This repo ships with one example template per platform as a starting point:
templates/x/insider.md (Tech Insider persona)templates/xiaohongshu/educational.mdTo create your own persona or style:
# X persona
cp skills/gen-social/templates/x/insider.md skills/gen-social/templates/x/my-persona.md
# Xiaohongshu style
cp skills/gen-social/templates/xiaohongshu/educational.md skills/gen-social/templates/xiaohongshu/my-style.md
Edit the copy, then set your channel config style field to match the filename (e.g. "style": "my-persona"). Custom templates in these directories are gitignored — your personas stay local.
Each X style has a distinct persona — a consistent voice and perspective that makes the account feel like a real person, not a news feed.
Available styles:
| Style | Persona | Template | Tone | Default Items | Min Score |
|---|---|---|---|---|---|
briefing | Daily Briefing | templates/x/briefing.md | Clean scannable index + infographic image, neutral | 5 | 6 |
insider | Razor | templates/x/insider.md | Devastating one-liners, deadpan, says the quiet part loud | 3 | 7 |
commentary | Builder | templates/x/commentary.md | First-person practical, opinionated hot takes, @levelsio meets @karpathy | 3 | 8 |
thread | Hype-Free Analyst | templates/x/thread.md | Data-driven thread, contrarian, pattern recognition, @Benedict Evans | 5 | 6 |
Available styles:
| Style | Template | Tone | Default Items | Min Score |
|---|---|---|---|---|
news-briefing | templates/xiaohongshu/news-briefing.md | Telegraph speed-scan, priority indicators | 5-8 | 5 |
educational | templates/xiaohongshu/educational.md | Structured explainer, numbered, "one article to understand it all" | 3-5 | 6 |
recommendation | templates/xiaohongshu/recommendation.md | Enthusiastic discovery, heavy emoji, "must see!" | 3 | 7 |
A channel is the core abstraction: one channel = one platform + one style + one language + content selection rules. Each channel produces an independent set of output files.
Located at ~/.config/morning-ai/social_channels.json (or path set via SOCIAL_CHANNELS_FILE).
[
{
"id": "x_insider_en",
"platform": "x",
"style": "insider",
"lang": "en",
"items": 3,
"min_score": 7,
"image": true,
"image_aspect": "16:9"
},
{
"id": "x_thread_en",
"platform": "x",
"style": "thread",
"lang": "en",
"items": 5,
"min_score": 6,
"image": true,
"image_aspect": "16:9"
},
{
"id": "xhs_kepu_zh",
"platform": "xiaohongshu",
"style": "educational",
"lang": "zh",
"items": 5,
"min_score": 6,
"image": true,
"image_aspect": "3:4",
"image_count": 4
},
{
"id": "xhs_zhongcao_zh",
"platform": "xiaohongshu",
"style": "recommendation",
"lang": "zh",
"items": 3,
"min_score": 7,
"image": true,
"image_aspect": "3:4",
"image_count": 3
}
]
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique channel identifier, used in output filenames |
platform | string | Yes | x or xiaohongshu |
style | string | Yes | Style name (see Style Definitions above) |
lang | string | Yes | Language code: en, zh, ja, etc. |
items | number | No | Max items to include (default: from template) |
min_score | number | No | Minimum score threshold (default: from template) |
image | boolean | No | Generate images for this channel (default: false) |
image_aspect | string | No | Image aspect ratio: 16:9, 1:1, 3:4 (default: platform default) |
image_count | number | No | Number of carousel images for xiaohongshu (default: 1) |
image_style | string | No | Override image style: classic, dark, glassmorphism, newspaper, tech, cover-hook (default: persona's recommended style). Note: cover-hook is special — it is intended only for the FIRST image of a Xiaohongshu carousel (the thumbnail in the discovery feed). Subsequent images should use the channel's regular style. |
include_types | array | No | Filter by content types, e.g. ["model", "product"] (default: all) |
kol_voice_only | boolean | No | If true, only items with is_kol_voice: true are eligible for selection. Used by the dedicated KOL channel so that KOL original commentary becomes the post's main topic, not embedded inside an unrelated post. (default: false) |
conditional | string | No | Conditional run gate. Currently supported: viral_only (max score ≥ min_score) and kol_voice_available (at least one item with is_kol_voice: true and score ≥ min_score). If the gate fails, the channel produces an empty manifest entry (no copy file, no images) and downstream consumers (e.g. foreman publish adapter) should skip the run gracefully. |
When only one channel is needed, use env vars instead of a JSON file:
SOCIAL_PLATFORM=x
SOCIAL_STYLE=insider
SOCIAL_LANG=en
This creates a single channel with id {platform}_{style}_{lang}.
For each channel, select items from the daily report data:
importance >= min_scoreinclude_types is set, only those content typeskol_voice_only: true is set, only items where is_kol_voice: trueitems setting or template default)lang, translate content. Entity names (proper nouns) stay unchanged.This check runs BEFORE the 5 steps above. Some channels only make sense on certain days (e.g. a viral-only channel that should only post when there's a genuinely big story). These channels declare a conditional field — the value tells the pipeline what gate to apply:
conditional value | Gate logic |
|---|---|
| (omitted) | Always run (default) |
viral_only | Channel runs ONLY if max(item.importance for item in today's data) >= channel.min_score. If no item clears the threshold, the channel is skipped. |
kol_voice_available | Channel runs ONLY if at least one item has is_kol_voice: true AND score >= channel.min_score. If no qualifying KOL voice exists today, the channel is skipped. Used by the dedicated KOL channel so that on quiet KOL days the post is not forced. |
When a conditional channel is gated off:
skipped: true field and a skip_reason explaining why (e.g. "max score 7.8 < required 8.5"){
"id": "xhs_zhongcao_viral_zh",
"platform": "xiaohongshu",
"style": "single-topic",
"skipped": true,
"skip_reason": "max score 7.8 < required 8.5"
}
daily-publish-xhs-2-viral) detect skipped: true and mark their run as SKIPPED rather than FAILED.Why this matters: it lets us schedule a 12:00 viral-only post on the cron without generating empty/low-quality posts on slow news days. The viral channel only fires content when there's something worth firing about.
For each channel:
{SKILL_DIR}/skills/gen-social/templates/{platform}/{style}.mdlang settingSocial images reuse the existing image generation infrastructure (lib/image_gen.py) and the same providers (Gemini/GPT/MiniMax).
Image text language must match the target platform, not the report source language:
| Platform | Image Language | Header Text |
|---|---|---|
| X | English only | "MorningAI" |
| Xiaohongshu | Chinese only | "MorningAI" |
All card titles, bullet points, section headers, and any text rendered on the image must be in the platform's language. Entity names (proper nouns like OpenAI, DeepSeek) remain unchanged.
Each persona/style has a recommended image style that matches its voice. This overrides the global IMAGE_STYLE setting for social images.
| Persona | Recommended Image Style | Images | Why |
|---|---|---|---|
| briefing (Daily Briefing) | classic | 1 cover + per-category (model/product/benchmark/funding) | Clean editorial, professional — cover indexes the day, per-category images go deeper |
| insider (Tech Insider) | tech | 0 | Razor is pure text — images dilute the punchline |
| commentary (Builder) | dark | 0 | Builder takes stand alone — bold text, no visual crutch |
| thread (Hype-Free Analyst) | classic | 1 cover | Clean editorial magazine — serious, data-focused, no visual noise |
| Style | Default Image Style | Images | Why |
|---|---|---|---|
| recommendation | glassmorphism | 3 carousel | Frosted glass, warm tones — lifestyle/discovery aesthetic |
| educational | classic | 4 carousel | Clean editorial — structured, authoritative content |
| news-briefing | newspaper | 1 | Classic newsprint — information-dense, professional |
IMPORTANT: If the channel config has an explicit
image_stylefield, that takes absolute priority over the defaults above. Do NOT include style labels or content-type annotations in image prompts.
The briefing channel generates additional per-category images beyond the cover, mirroring how gen-infographic handles per-type infographics:
| Image | Filename | Generated When |
|---|---|---|
| Cover | social_{DATE}_x_briefing_en_cover.png | Always |
| Model | social_{DATE}_x_briefing_en_model.png | ≥1 Model item with score ≥ min_score |
| Product | social_{DATE}_x_briefing_en_product.png | ≥1 Product item with score ≥ min_score |
| Benchmark | social_{DATE}_x_briefing_en_benchmark.png | ≥1 Benchmark item with score ≥ min_score |
| Funding | social_{DATE}_x_briefing_en_funding.png | ≥1 Funding item with score ≥ min_score |
Per-category images:
items cap)The recommended style is a default — channels can override via image_style field in the channel config. If not set, the persona's recommended style is used. If no persona mapping exists, falls back to the global IMAGE_STYLE setting.
X images:
Xiaohongshu images:
image_count > 1):
{ASPECT} infographic, {HEADER_TEXT} {YYYY-MM-DD}, ALL text content in {LANG}.
Total news items: {N}
News cards (display EXACTLY {N} cards):
Card 1: {Entity name} {Event subject} {Core event verb phrase}
- {Point 1}
- {Point 2}
- {Point 3}
(... list according to actual item count ...)
CRITICAL RULES:
- ALL text on this image MUST be in {LANG} — titles, bullet points, headers, labels, everything
- Entity names are proper nouns (OpenAI, DeepSeek, Cursor) — keep as-is, do NOT translate
- Do NOT display ANY platform names on the image (no "小红书", "Xiaohongshu", "特刊", "Twitter", "X" etc.) — the image is content, not an ad for the platform
- Header text: "{HEADER_TEXT}"
- Each card title MUST include: Entity name + Event subject + Event description
- Display complete titles, do NOT truncate
- Do NOT display score numbers, score badges, or importance markers
- Do NOT invent items not listed
- Display ALL bullet points for each card
- Maximize content area — card titles and bullet points are the primary focus
ANTI-AI-SLOP RULES (mandatory — ref: huashu-design):
- NO purple/lavender/violet gradients — this is the #1 "AI-generated" tell
- NO emoji as bullet markers or icons — use typographic bullets (·, —, or numbered)
- NO rounded-corner cards with colored left border accent — the 2020-2024 AI slop archetype
- NO Inter/Roboto/Arial/system fonts as display — use serif display fonts (Noto Serif, Source Serif, Georgia)
- NO neon glow, glassmorphism, or frosted glass effects
- NO decorative icons per heading — if an icon doesn't carry information, remove it
- NO flat digital color blocks — use paper/material texture feel
- Color restraint: black + cream/white + ONE accent color max. Do NOT invent extra colors
- Typography IS the visual language — let font weight, size, and spacing create hierarchy, not color/shape decoration
- One detail at 120%, everything else at 80% — not uniformly polished, but sharp where it counts
- Describe mood, not pixel coordinates — "feels like a morning broadsheet" beats "title at top, 36pt, centered"
{STYLE_BLOCK}
{PLATFORM_STYLE_ADDON}
Variable substitution:
{HEADER_TEXT} → "MorningAI" for X, "MorningAI" for Xiaohongshu{LANG} → "English" for X, "Chinese" for Xiaohongshu{STYLE_BLOCK} → from the persona's recommended image style (see skills/gen-infographic/SKILL.md Style Presets){PLATFORM_STYLE_ADDON} for Xiaohongshu — use the matching style variant based on the channel's image_style field:
When image_style = glassmorphism:
Additional Xiaohongshu adaptation (glassmorphism):
- ALL text must be in Chinese (except entity proper nouns)
- Soft gradient background (lavender to pale rose)
- Semi-transparent frosted white cards with backdrop blur
- Use rounded corners (16px) on all cards
- Larger title font (22pt bold) for mobile readability
- Warm accent colors: coral (#FF6B6B), soft pink (#FFB4B4), lavender (#E8EAF6)
- Emoji bullet markers (colored dots or sparkle symbols)
- Clean, fresh, lifestyle-magazine aesthetic
- Generous padding and line spacing for mobile screens
When image_style = newspaper:
Additional Xiaohongshu adaptation (newspaper):
- ALL text must be in Chinese (except entity proper nouns)
- Mood: feels like unfolding a quality morning broadsheet — authoritative, calm, information-dense, zero decoration
- Warm cream background (#FFF8E7) with subtle paper grain texture — NOT flat digital white
- Typography IS the design: bold serif headers (Noto Serif CJK / Source Han Serif / Georgia feel), clean sans-serif body
- Typographic hierarchy through font weight (700 headers, 400 body) and size contrast — NOT through color or shape
- Thin black hairline rules (1px) between sections, like column dividers in a broadsheet
- NO card backgrounds, NO rounded corners, NO shadows, NO colored borders — pure typographic layout
- Color: cream + deep black + crimson (#DC143C) accent ONLY — crimson used sparingly (dateline, one key number, or a thin rule)
- Bullet markers: typographic only (· or —), NEVER emoji
- One signature detail at 120%: the headline typography should feel crafted, like a newspaper masthead
- Generous line spacing (1.6+) for mobile readability, but information-dense — fill the space with content, not decoration
- DO NOT use gradients, frosted glass, glow effects, or any "digital" aesthetic
- The image should look like it was typeset, not generated
When image_style = cover-hook: (FIRST image of XHS carousel ONLY — this is the discovery-feed thumbnail)
Additional Xiaohongshu adaptation (cover-hook):
- ALL text must be in Chinese (except entity proper nouns)
- Mood: a newspaper EXTRA edition headline — urgent, bold, one statement that demands attention, but with editorial class, not digital loudness
- 3:4 single card, super-large title text occupying ≥ 50% of canvas
- Title text = the post title or its strongest 1-line variant (e.g., "Cursor 估值飙到 500 亿")
- ONE single subtitle line below with the most striking concrete data point (e.g., "3 年达 20 亿 ARR · 史上最快 B2B")
- Newspaper headline poster aesthetic:
- Warm cream (#FFF8E7) with bold black serif title, OR deep charcoal (#1A1A1A) with cream/off-white serif title
- Bold serif typography (Noto Serif CJK / Source Han Serif / Georgia feel), weight ≥ 700
- Paper grain texture — NOT flat digital color blocks
- Thin crimson (#DC143C) hairline rule above or below the headline — the ONE signature detail at 120%
- Optional: small "MorningAI" masthead at very top in small caps, like a newspaper nameplate — restrained, not decorative
- NO bullet points, NO emoji, NO icons, NO small annotations — single statement only
- NO body text, NO list of items, NO date stamps — only the headline + one subtitle line
- NO gradients, NO glow, NO frosted glass, NO rounded corners — this is typeset, not generated
- The cover should look like a broadsheet front page that was photographed, not a Canva template
When image_style = classic:
Additional Xiaohongshu adaptation (classic):
- ALL text must be in Chinese (except entity proper nouns)
- Off-white background (#F5F5F0), clean editorial magazine layout
- Card-based layout with subtle shadows
- Navy (#1B2A4A), coral (#E8634A), teal (#2A9D8F) accent palette
- Use rounded corners (8px) on cards
- Larger title font for mobile readability
- Structured, authoritative visual hierarchy
- Generous padding and line spacing for mobile screens
{PLATFORM_STYLE_ADDON} for X (append to any base style):
Additional X/Twitter adaptation:
- ALL text must be in English (except entity proper nouns)
- Optimized for timeline scroll — key info visible at small preview size
- High contrast text for readability on mobile
Use the same methods as Step 4 (gen-infographic):
Option A — Native tool (if supported): Generate each image using built-in image generation capability.
Option B — Python script batch mode: Build a manifest JSON and run:
cd {SKILL_DIR} && python3 skills/gen-infographic/scripts/gen_infographic.py --batch {CWD}/social/manifest_images.json
Manifest entries support "aspect_ratio": "3:4" for Xiaohongshu images.
For Xiaohongshu channels with image_count >= 2 AND style recommendation or single-topic, the first image of the carousel is the discovery-feed thumbnail and MUST use the cover-hook style addon (regardless of the channel's regular image_style):
{PLATFORM_STYLE_ADDON} for cover-hook. Content = post title + ONE strongest data point. No bullet points, no list of items.image_style (e.g., newspaper). Content = the per-item news cards as before.This applies because the first image is what stops the scroll in the discovery feed; an information-dense newspaper card is unreadable as a thumbnail. Channels that already declare image_style: cover-hook explicitly should apply it to ALL images (rare — typically reserved for single-image posts).
All output goes to {CWD}/social/ directory.
social/
├── social_{DATE}_{channel_id}.md # Copy file
├── social_{DATE}_{channel_id}_cover.png # Single/cover image
├── social_{DATE}_{channel_id}_{N}.png # Carousel image N
└── social_{DATE}_manifest.json # Output index
MANDATORY SCHEMA — DO NOT IMPROVISE FIELD NAMES OR PATHS
The manifest is consumed by downstream tools that match on exact field names and assume flat filenames. Past LLM-generated output has drifted in ways that silently break consumers (
channel_idinstead ofid,copyinstead ofcopy_file, paths prefixed withsocial/). This section is the contract — produce it byte-for-byte as specified.
Required top-level shape:
channels MUST be a JSON array (list). Never a dict keyed by id.Required per-channel fields — use these exact names, no aliases:
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | Channel identifier. Field name is id, NOT channel_id. |
platform | string | Yes | x or xiaohongshu |
style | string | Yes | Style name |
lang | string | Yes | en, zh, ja, etc. |
copy_file | string | Conditional | Filename only (no directory prefix). Field name is copy_file, NOT copy. Omit this field if skipped: true. |
images | array | Conditional | Filenames only (no directory prefix). Empty list [] if no images. Omit this field if skipped: true. |
items_used | number | No | Count of source items included |
skipped | boolean | No | Set to true for conditional channels that did not meet their gate (e.g. viral_only with no item ≥ min_score). When true, omit copy_file and images. |
skip_reason | string | Conditional | Required when skipped: true. Short explanation, e.g. "max score 7.8 < required 8.5". |
Path rules — applies to copy_file and every entry in images:
social/ (e.g. social_2026-04-18_xhs_zixun_zh.md).social/, no ./, no absolute path."copy_file": "social/social_2026-04-18_xhs_zixun_zh.md" ❌"copy_file": "social_2026-04-18_xhs_zixun_zh.md" ✅Canonical example (copy this shape exactly):
{
"date": "2026-04-14",
"channels": [
{
"id": "x_insider_en",
"platform": "x",
"style": "insider",
"lang": "en",
"copy_file": "social_2026-04-14_x_insider_en.md",
"images": ["social_2026-04-14_x_insider_en_cover.png"],
"items_used": 3
},
{
"id": "xhs_kepu_zh",
"platform": "xiaohongshu",
"style": "educational",
"lang": "zh",
"copy_file": "social_2026-04-14_xhs_kepu_zh.md",
"images": [
"social_2026-04-14_xhs_kepu_zh_1.png",
"social_2026-04-14_xhs_kepu_zh_2.png",
"social_2026-04-14_xhs_kepu_zh_3.png",
"social_2026-04-14_xhs_kepu_zh_4.png"
],
"items_used": 5
},
{
"id": "xhs_zhongcao_viral_zh",
"platform": "xiaohongshu",
"style": "single-topic",
"lang": "zh",
"skipped": true,
"skip_reason": "max score 7.8 < required 8.5"
}
]
}
Forbidden variants (all of these have been observed in real output and break downstream consumers — do not produce them):
// ❌ WRONG — "channel_id" is not a recognized field
{ "channel_id": "xhs_zixun_zh", ... }
// ❌ WRONG — "copy" is not a recognized field
{ "id": "xhs_zixun_zh", "copy": "...", ... }
// ❌ WRONG — paths must be bare filenames
{ "copy_file": "social/social_2026-04-18_xhs_zixun_zh.md",
"images": ["social/social_2026-04-18_xhs_zixun_zh_1.png"] }
// ❌ WRONG — channels must be a list, not a dict keyed by id
{ "channels": { "xhs_zixun_zh": { ... } } }
Before writing the manifest file, mentally diff your output against the canonical example above. If any field name, structure, or path shape differs, fix it before writing.
SOCIAL_ENABLED=true)conditional field, evaluate the gate. If it fails, write a manifest entry with skipped: true + skip_reason and continue to the next channel — do NOT generate copy or images.
b. Read the channel's template file
c. Select top items from report data (score filter + item limit)
d. Generate copy following template rules and character limits
e. Write copy to social/{DATE}_{channel_id}.md
f. If image: true — build image prompts and generate platform-adapted images. For XHS carousels with style recommendation or single-topic, the FIRST image uses the cover-hook style addon; subsequent images use the channel's regular image_style.
g. Write images to social/{DATE}_{channel_id}_{N}.pngsocial/{DATE}_manifest.jsonlang setting for copyimage_style fieldlang setting