@orbitant/brain-marketing 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orbitant-marketing",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Marketing team skills for content creation and review",
5
5
  "author": {
6
6
  "name": "Orbitant"
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Marketing team skills for content creation and review.
4
4
 
5
- **v1.5.0** · vertical `marketing` · 8 skills
5
+ **v1.6.0** · vertical `marketing` · 9 skills
6
6
 
7
7
  ## Install
8
8
 
@@ -36,7 +36,7 @@ Named imports work too: `import { skills, meta } from '@orbitant/brain-marketing
36
36
 
37
37
  ## Keys
38
38
 
39
- - **skills** — `orbitant-blog-post-create`, `orbitant-blog-post-review`, `orbitant-blog-post-translate`, `orbitant-image-creation`, `orbitant-linkedin-post`, `orbitant-newsletter`, `orbitant-tone`, `orbitant-yt-description`
39
+ - **skills** — `orbitant-blog-post-create`, `orbitant-blog-post-review`, `orbitant-blog-post-translate`, `orbitant-image-creation`, `orbitant-linkedin-post`, `orbitant-newsletter`, `orbitant-tone`, `orbitant-x-thread`, `orbitant-yt-description`
40
40
 
41
41
  ## Versioning
42
42
 
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "name": "orbitant-marketing",
4
- "version": "1.5.0",
4
+ "version": "1.6.0",
5
5
  "vertical": "marketing"
6
6
  },
7
7
  "skills": [
@@ -86,8 +86,8 @@
86
86
  {
87
87
  "name": "orbitant-image-creation",
88
88
  "folder": "image-creation",
89
- "description": "Generates blog post thumbnail images for Orbitant following the brand's visual\nidentity, using Google's Imagen API (Nano Banana 2). Activates when creating\nblog images, generating thumbnails, designing featured images for articles, or\nwhen someone needs a visual for an Orbitant insight/blog post. Use this skill\neven if the user just says \"I need an image for this article\", \"create a\nthumbnail\", \"generate a hero image\", or \"make a featured image\". Also triggers\nwhen the user mentions \"Nano Banana 2\", \"image generation\", or asks for a\nprompt for an AI image tool.",
90
- "version": "1.0.0",
89
+ "description": "Generates blog post thumbnail images for Orbitant following the brand's visual\nidentity, using Google's Gemini image API (Nano Banana 2). Activates when creating\nblog images, generating thumbnails, designing featured images for articles, or\nwhen someone needs a visual for an Orbitant insight/blog post. Use this skill\neven if the user just says \"I need an image for this article\", \"create a\nthumbnail\", \"generate a hero image\", or \"make a featured image\". Also triggers\nwhen the user mentions \"Nano Banana 2\", \"image generation\", or asks for a\nprompt for an AI image tool.",
90
+ "version": "1.0.1",
91
91
  "tags": [
92
92
  "marketing",
93
93
  "image",
@@ -99,12 +99,12 @@
99
99
  "ai-image",
100
100
  "imagen"
101
101
  ],
102
- "content": "\n## Overview\n\nYou are a visual prompt engineer and image generator for the Orbitant engineering blog. Your job is to:\n\n1. Craft a prompt matching Orbitant's visual identity\n2. Generate the image by running the bundled script against Google's Imagen API\n3. Deliver the final image file to the user\n\nRespond in the same language as the user's request.\n\n---\n\n## When to Use This Skill\n\nActivate when the user:\n- Needs a featured image or thumbnail for a blog post\n- Asks for an image prompt for an Orbitant article\n- Wants to generate visuals matching Orbitant's brand style\n- Mentions Nano Banana 2 or AI image generation for blog content\n\n---\n\n## Prerequisites\n\n### Dependencies\n\n- **Node.js 18+**\n- **`@google/genai`** and **`sharp`** packages:\n\n```bash\nnpm install @google/genai sharp\n```\n\n### Google API Key\n\nYou need a `GOOGLE_API_KEY` to call the Imagen API. There are two ways to get one:\n\n| Option | How | Cost |\n|--------|-----|------|\n| **Google AI Studio** | Go to <https://aistudio.google.com/apikey> and create a key with your personal Google account | Free tier with daily limits |\n| **Google Workspace** | Same link, but sign in with your organization's Workspace account. Many Workspace plans include Gemini/Imagen API access with generous quotas (check your admin console under **Apps → Additional Google services → Google AI Studio**) | Included in Workspace plans that have Gemini enabled |\n\n> **Tip:** If your organization uses Google Workspace with Gemini enabled, you likely already have API access at no extra cost — ask your Workspace admin if unsure.\n\n### Setting the API Key\n\nPick **one** of the following methods:\n\n**Option A — `.env` file (recommended, stays local and git-ignored):**\n\nCreate `plugins/orbitant-marketing/skills/image-creation/scripts/.env`:\n\n```env\nGOOGLE_API_KEY=your-key-here\n```\n\n**Option B — Environment variable (current shell session only):**\n\n```bash\nexport GOOGLE_API_KEY=\"your-key-here\"\n```\n\n**Option C — Shell profile (persistent across sessions):**\n\nAdd to your `~/.bashrc`, `~/.zshrc`, or equivalent:\n\n```bash\nexport GOOGLE_API_KEY=\"your-key-here\"\n```\n\n> **Note:** The `.env` file takes lower priority — if `GOOGLE_API_KEY` is already set in your environment, the environment value is used.\n\n### Quick Verification\n\nRun a single test image to confirm everything works:\n\n```bash\nnode plugins/orbitant-marketing/skills/image-creation/scripts/generate-image.mjs \\\n --prompt \"A single white ceramic cube on a white surface, soft studio lighting, shallow depth of field, minimalist, monochrome\" \\\n --output /tmp/orbitant-test.png \\\n --count 1\n```\n\nIf the API key is not set, the skill will craft the prompt and show it to the user so they can use it manually in AI Studio or another tool.\n\n> **Note:** The `--negative` flag is accepted by the script but **not supported by the current Imagen API** (`imagen-4.0-generate-001`). Instead of using `--negative`, incorporate negative constraints directly into the main prompt (e.g., \"No red, orange, or yellow fire. No text, no words, no logos.\").\n\n### Reference Images Setup\n\nBefore crafting prompts, check if `assets/reference/` contains images. These are real blog thumbnails from orbitant.com that show the target visual style by example.\n\nIf the folder is **empty or missing**, ask the user to run:\n\n```bash\nnode scripts/scrape-insights-images.mjs\n```\n\nThis downloads a curated set of ~26 reference images. It is safe to re-run — existing files are skipped. Use `--force` to re-download everything.\n\nOnce available, **browse a few reference images** from `assets/reference/` before crafting prompts. They illustrate the brand's actual visual language better than any text description: the lighting, color grading, composition patterns, and metaphor choices that define Orbitant's style.\n\n---\n\n## Available Scripts\n\n- **`scripts/generate-image.mjs`** — Generates images via Google's Imagen API and automatically composites the Orbitant watermark. Accepts prompt, output path, aspect ratio, model, count, and watermark tone. Returns JSON with file paths on success.\n- **`scripts/scrape-insights-images.mjs`** — Downloads curated reference images from orbitant.com into `assets/reference/`. Skips existing files. Use `--force` to re-download.\n\nRun `node scripts/generate-image.mjs --help` or `node scripts/scrape-insights-images.mjs --help` for full usage.\n\n## Available Assets\n\n- **`assets/watermark-white.svg`** — White Orbitant watermark (compass + text) for dark backgrounds\n- **`assets/watermark-black.svg`** — Black Orbitant watermark for light backgrounds\n\nThe script auto-detects which watermark to use based on the bottom strip brightness of the generated image. Override with `--watermark white|black|none`.\n\n---\n\n## Workflow\n\n### Step 0 — Load Visual References\n\n1. Read `references/visual-identity.md` for the brand rules.\n2. Check if `assets/reference/` contains images. If empty, ask the user to run `node scripts/scrape-insights-images.mjs` and wait before continuing.\n3. Browse 3–5 reference images from `assets/reference/` to calibrate your sense of the brand's visual style.\n\n### Step 1 — Choose the Category\n\nBased on the article topic, decide between:\n- **Category A — Conceptual Metaphor** (default, ~70% of images): AI-generated scenes using a physical metaphor\n- **Category B — Real Photography** (~30%): Team photos for culture/event articles — cannot be generated, tell the user to pick from their photo library\n\n### Step 2 — Find the Metaphor (Category A only)\n\nIdentify a **physical object or scene** that metaphorically represents the article's core concept:\n- Immediately recognizable (not too abstract)\n- Visually simple (one subject, not a collage)\n- Compatible with a studio-lit, minimalist aesthetic\n- Not a stock-photo cliche (no handshakes, gears, lightbulbs, jigsaw pieces)\n\n### Step 3 — Choose the Background Tone\n\n- **Light background** (white/light gray): methodology, best practices, architecture, design, product, business\n- **Dark background** (charcoal/black): security, debugging, infrastructure, AI, data, DevOps, low-level engineering\n\n### Step 4 — Craft the Prompt\n\nUse this structure:\n\n```\nA [object/scene metaphor] representing [concept], shot with a [lens mm] lens\nat f/[aperture], [lighting type] from [direction]. [Background color] studio\nbackground. Color palette: [colors]. Minimalist composition with generous\nnegative space. [Additional details]. Clean empty bottom-center area with no\nelements or objects. Photorealistic quality, 16:9 aspect ratio.\n```\n\nIncorporate negative constraints directly into the prompt itself (the `--negative` flag is not supported by the current API). Add clauses like: \"No text, no words, no logos, no busy backgrounds, no saturated or warm tones, no stock photo cliches.\"\n\n### Step 5 — Generate the Image\n\nRun the script from the skill directory:\n\n```bash\nnode scripts/generate-image.mjs \\\n --prompt \"THE CRAFTED PROMPT\" \\\n --output ./output/ARTICLE-SLUG.png \\\n --aspect 16:9\n```\n\nTo generate multiple variants for the user to choose from:\n\n```bash\nnode scripts/generate-image.mjs \\\n --prompt \"THE CRAFTED PROMPT\" \\\n --output ./output/ARTICLE-SLUG.png \\\n --aspect 16:9 \\\n --count 3\n```\n\n### Step 6 — Present Results\n\nShow the user:\n1. The **category** and **metaphor reasoning**\n2. The **prompt** used (also saved as `.prompt.json` next to the images for reuse)\n3. The **generated image(s)** — read the output file(s) so the user can see them\n4. Note that the **Orbitant watermark was automatically composited** (unless `--watermark none` was used)\n\nThe `.prompt.json` file stores the full prompt, model, aspect ratio, and generation timestamp so the user can reproduce or tweak the image later without the skill.\n\n---\n\n## Orbitant Visual Identity Rules\n\n> Full visual identity spec (colors, watermark, signature look, proven metaphors) is in **`references/visual-identity.md`**. Read it before crafting prompts.\n\nKey points:\n- **Color palette**: Monochrome-dominant, teal (#00BFA5) accent only, avoid saturated/warm tones\n- **Watermark**: Composited automatically — never include text/logos in the prompt, always leave clean bottom-center space\n- **Signature look**: Shallow DoF, minimalist, studio-lit, desaturated premium aesthetic\n- **Format**: 16:9 landscape, 1440x810, PNG from API\n\n---\n\n## Example\n\n**Article**: \"5 Tips for Successful Legacy Migrations\"\n\n**Category**: A — Conceptual Metaphor\n**Metaphor**: Layered architectural model being deconstructed, representing careful extraction of legacy systems\n**Background**: Light (methodology/best-practices topic)\n\n```bash\nnode scripts/generate-image.mjs \\\n --prompt \"A detailed white architectural model of a classic building being carefully deconstructed layer by layer, with some layers floating slightly above, shot with a 85mm lens at f/2.8, soft directional lighting from the left. Clean white studio background. Color palette: monochrome whites and light grays with subtle shadows. Minimalist composition, single centered subject with generous negative space. Clean bottom-center area reserved for brand watermark. No text, no words, no logos, no busy backgrounds. Photorealistic 3D render quality, 16:9 aspect ratio.\" \\\n --output ./output/legacy-migrations.png \\\n --count 2\n```\n\n---\n\n## Important Rules\n\n- **NEVER include text, words, or logos in the prompt** — the watermark is composited separately.\n- **One subject, one metaphor** — Orbitant images are minimalist.\n- **Respect the color palette** — desaturated, monochrome-dominant, teal accents only.\n- **Always specify shallow depth of field** in the prompt.\n- **Always reserve bottom-center space** for the logo overlay.\n- **Avoid stock photo cliches** — no handshakes, gears, lightbulbs, jigsaw pieces, globes.\n- **Match background tone to topic** — light for constructive topics, dark for technical/deep topics.\n- **If the article is about team/culture**, recommend a real photo (Category B) instead of generating.\n- **If `GOOGLE_API_KEY` is not available**, output the prompt for manual use and tell the user how to set up the key.\n",
102
+ "content": "\n## Overview\n\nYou are a visual prompt engineer and image generator for the Orbitant engineering blog. Your job is to:\n\n1. Craft a prompt matching Orbitant's visual identity\n2. Generate the image by running the bundled script against Google's Gemini image API\n3. Deliver the final image file to the user\n\nRespond in the same language as the user's request.\n\n---\n\n## When to Use This Skill\n\nActivate when the user:\n- Needs a featured image or thumbnail for a blog post\n- Asks for an image prompt for an Orbitant article\n- Wants to generate visuals matching Orbitant's brand style\n- Mentions Nano Banana 2 or AI image generation for blog content\n\n---\n\n## Prerequisites\n\n### Dependencies\n\n- **Node.js 18+**\n- **`@google/genai`** and **`sharp`** packages:\n\n```bash\nnpm install @google/genai sharp\n```\n\n### Google API Key\n\nYou need a `GOOGLE_API_KEY` to call the Gemini image API. There are two ways to get one:\n\n| Option | How | Cost |\n|--------|-----|------|\n| **Google AI Studio** | Go to <https://aistudio.google.com/apikey> and create a key with your personal Google account | Free tier with daily limits |\n| **Google Workspace** | Same link, but sign in with your organization's Workspace account. Many Workspace plans include Gemini API access with generous quotas (check your admin console under **Apps → Additional Google services → Google AI Studio**) | Included in Workspace plans that have Gemini enabled |\n\n> **Tip:** If your organization uses Google Workspace with Gemini enabled, you likely already have API access at no extra cost — ask your Workspace admin if unsure.\n\n### Setting the API Key\n\nPick **one** of the following methods:\n\n**Option A — `.env` file (recommended, stays local and git-ignored):**\n\nCreate `plugins/orbitant-marketing/skills/image-creation/scripts/.env`:\n\n```env\nGOOGLE_API_KEY=your-key-here\n```\n\n**Option B — Environment variable (current shell session only):**\n\n```bash\nexport GOOGLE_API_KEY=\"your-key-here\"\n```\n\n**Option C — Shell profile (persistent across sessions):**\n\nAdd to your `~/.bashrc`, `~/.zshrc`, or equivalent:\n\n```bash\nexport GOOGLE_API_KEY=\"your-key-here\"\n```\n\n> **Note:** The `.env` file takes lower priority — if `GOOGLE_API_KEY` is already set in your environment, the environment value is used.\n\n### Quick Verification\n\nRun a single test image to confirm everything works:\n\n```bash\nnode plugins/orbitant-marketing/skills/image-creation/scripts/generate-image.mjs \\\n --prompt \"A single white ceramic cube on a white surface, soft studio lighting, shallow depth of field, minimalist, monochrome\" \\\n --output /tmp/orbitant-test.png \\\n --count 1\n```\n\nIf the API key is not set, the skill will craft the prompt and show it to the user so they can use it manually in AI Studio or another tool.\n\n> **Note:** The `--negative` flag is accepted by the script but **not supported by Gemini image models** (`gemini-3.1-flash-image`) and is ignored with a warning. Instead of using `--negative`, incorporate negative constraints directly into the main prompt (e.g., \"No red, orange, or yellow fire. No text, no words, no logos.\").\n\n### Reference Images Setup\n\nBefore crafting prompts, check if `assets/reference/` contains images. These are real blog thumbnails from orbitant.com that show the target visual style by example.\n\nIf the folder is **empty or missing**, ask the user to run:\n\n```bash\nnode scripts/scrape-insights-images.mjs\n```\n\nThis downloads a curated set of ~26 reference images. It is safe to re-run — existing files are skipped. Use `--force` to re-download everything.\n\nOnce available, **browse a few reference images** from `assets/reference/` before crafting prompts. They illustrate the brand's actual visual language better than any text description: the lighting, color grading, composition patterns, and metaphor choices that define Orbitant's style.\n\n---\n\n## Available Scripts\n\n- **`scripts/generate-image.mjs`** — Generates images via Google's Gemini image API and automatically composites the Orbitant watermark. Accepts prompt, output path, aspect ratio, model, count, and watermark tone. Returns JSON with file paths on success.\n- **`scripts/scrape-insights-images.mjs`** — Downloads curated reference images from orbitant.com into `assets/reference/`. Skips existing files. Use `--force` to re-download.\n\nRun `node scripts/generate-image.mjs --help` or `node scripts/scrape-insights-images.mjs --help` for full usage.\n\n## Available Assets\n\n- **`assets/watermark-white.svg`** — White Orbitant watermark (compass + text) for dark backgrounds\n- **`assets/watermark-black.svg`** — Black Orbitant watermark for light backgrounds\n\nThe script auto-detects which watermark to use based on the bottom strip brightness of the generated image. Override with `--watermark white|black|none`.\n\n---\n\n## Workflow\n\n### Step 0 — Load Visual References\n\n1. Read `references/visual-identity.md` for the brand rules.\n2. Check if `assets/reference/` contains images. If empty, ask the user to run `node scripts/scrape-insights-images.mjs` and wait before continuing.\n3. Browse 3–5 reference images from `assets/reference/` to calibrate your sense of the brand's visual style.\n\n### Step 1 — Choose the Category\n\nBased on the article topic, decide between:\n- **Category A — Conceptual Metaphor** (default, ~70% of images): AI-generated scenes using a physical metaphor\n- **Category B — Real Photography** (~30%): Team photos for culture/event articles — cannot be generated, tell the user to pick from their photo library\n\n### Step 2 — Find the Metaphor (Category A only)\n\nIdentify a **physical object or scene** that metaphorically represents the article's core concept:\n- Immediately recognizable (not too abstract)\n- Visually simple (one subject, not a collage)\n- Compatible with a studio-lit, minimalist aesthetic\n- Not a stock-photo cliche (no handshakes, gears, lightbulbs, jigsaw pieces)\n\n### Step 3 — Choose the Background Tone\n\n- **Light background** (white/light gray): methodology, best practices, architecture, design, product, business\n- **Dark background** (charcoal/black): security, debugging, infrastructure, AI, data, DevOps, low-level engineering\n\n### Step 4 — Craft the Prompt\n\nUse this structure:\n\n```\nA [object/scene metaphor] representing [concept], shot with a [lens mm] lens\nat f/[aperture], [lighting type] from [direction]. [Background color] studio\nbackground. Color palette: [colors]. Minimalist composition with generous\nnegative space. [Additional details]. Clean empty bottom-center area with no\nelements or objects. Photorealistic quality, 16:9 aspect ratio.\n```\n\nIncorporate negative constraints directly into the prompt itself (the `--negative` flag is not supported by the current API). Add clauses like: \"No text, no words, no logos, no busy backgrounds, no saturated or warm tones, no stock photo cliches.\"\n\n### Step 5 — Generate the Image\n\nRun the script from the skill directory:\n\n```bash\nnode scripts/generate-image.mjs \\\n --prompt \"THE CRAFTED PROMPT\" \\\n --output ./output/ARTICLE-SLUG.png \\\n --aspect 16:9\n```\n\nTo generate multiple variants for the user to choose from:\n\n```bash\nnode scripts/generate-image.mjs \\\n --prompt \"THE CRAFTED PROMPT\" \\\n --output ./output/ARTICLE-SLUG.png \\\n --aspect 16:9 \\\n --count 3\n```\n\n### Step 6 — Present Results\n\nShow the user:\n1. The **category** and **metaphor reasoning**\n2. The **prompt** used (also saved as `.prompt.json` next to the images for reuse)\n3. The **generated image(s)** — read the output file(s) so the user can see them\n4. Note that the **Orbitant watermark was automatically composited** (unless `--watermark none` was used)\n\nThe `.prompt.json` file stores the full prompt, model, aspect ratio, and generation timestamp so the user can reproduce or tweak the image later without the skill.\n\n---\n\n## Orbitant Visual Identity Rules\n\n> Full visual identity spec (colors, watermark, signature look, proven metaphors) is in **`references/visual-identity.md`**. Read it before crafting prompts.\n\nKey points:\n- **Color palette**: Monochrome-dominant, teal (#00BFA5) accent only, avoid saturated/warm tones\n- **Watermark**: Composited automatically — never include text/logos in the prompt, always leave clean bottom-center space\n- **Signature look**: Shallow DoF, minimalist, studio-lit, desaturated premium aesthetic\n- **Format**: 16:9 landscape, 1440x810, PNG from API\n\n---\n\n## Example\n\n**Article**: \"5 Tips for Successful Legacy Migrations\"\n\n**Category**: A — Conceptual Metaphor\n**Metaphor**: Layered architectural model being deconstructed, representing careful extraction of legacy systems\n**Background**: Light (methodology/best-practices topic)\n\n```bash\nnode scripts/generate-image.mjs \\\n --prompt \"A detailed white architectural model of a classic building being carefully deconstructed layer by layer, with some layers floating slightly above, shot with a 85mm lens at f/2.8, soft directional lighting from the left. Clean white studio background. Color palette: monochrome whites and light grays with subtle shadows. Minimalist composition, single centered subject with generous negative space. Clean bottom-center area reserved for brand watermark. No text, no words, no logos, no busy backgrounds. Photorealistic 3D render quality, 16:9 aspect ratio.\" \\\n --output ./output/legacy-migrations.png \\\n --count 2\n```\n\n---\n\n## Important Rules\n\n- **NEVER include text, words, or logos in the prompt** — the watermark is composited separately.\n- **One subject, one metaphor** — Orbitant images are minimalist.\n- **Respect the color palette** — desaturated, monochrome-dominant, teal accents only.\n- **Always specify shallow depth of field** in the prompt.\n- **Always reserve bottom-center space** for the logo overlay.\n- **Avoid stock photo cliches** — no handshakes, gears, lightbulbs, jigsaw pieces, globes.\n- **Match background tone to topic** — light for constructive topics, dark for technical/deep topics.\n- **If the article is about team/culture**, recommend a real photo (Category B) instead of generating.\n- **If `GOOGLE_API_KEY` is not available**, output the prompt for manual use and tell the user how to set up the key.\n",
103
103
  "frontmatter": {
104
104
  "name": "orbitant-image-creation",
105
- "description": "Generates blog post thumbnail images for Orbitant following the brand's visual\nidentity, using Google's Imagen API (Nano Banana 2). Activates when creating\nblog images, generating thumbnails, designing featured images for articles, or\nwhen someone needs a visual for an Orbitant insight/blog post. Use this skill\neven if the user just says \"I need an image for this article\", \"create a\nthumbnail\", \"generate a hero image\", or \"make a featured image\". Also triggers\nwhen the user mentions \"Nano Banana 2\", \"image generation\", or asks for a\nprompt for an AI image tool.\n",
105
+ "description": "Generates blog post thumbnail images for Orbitant following the brand's visual\nidentity, using Google's Gemini image API (Nano Banana 2). Activates when creating\nblog images, generating thumbnails, designing featured images for articles, or\nwhen someone needs a visual for an Orbitant insight/blog post. Use this skill\neven if the user just says \"I need an image for this article\", \"create a\nthumbnail\", \"generate a hero image\", or \"make a featured image\". Also triggers\nwhen the user mentions \"Nano Banana 2\", \"image generation\", or asks for a\nprompt for an AI image tool.\n",
106
106
  "license": "MIT",
107
- "version": "1.0.0",
107
+ "version": "1.0.1",
108
108
  "metadata": {
109
109
  "author": "orbitant",
110
110
  "tags": "marketing, image, thumbnail, blog, visual, prompt, nano-banana-2, ai-image, imagen"
@@ -191,6 +191,33 @@
191
191
  },
192
192
  "relDir": "skills/tone"
193
193
  },
194
+ {
195
+ "name": "orbitant-x-thread",
196
+ "folder": "x-thread",
197
+ "description": "X (Twitter) thread writer for Orbitant. Turns a published blog post OR native\nmaterial — a Knowledge Sharing transcript, a Slack thread, a feature just\nshipped, a build-in-public moment — into a thread ready to post: an opening\npost that stands alone, body posts of one idea each, and a closing post,\nevery one annotated with its character count against X's 280 limit.\n\nActivate when the user asks for a thread, an X post or a Twitter post, or says\n\"haz un hilo\", \"convierte esto en un hilo\", \"esto da para un hilo\", \"thread\nthis\", \"algo para X\", \"para Twitter\". Also trigger when the user shares a blog\npost, a transcript or a Slack conversation and asks for social copy while\nmentioning X, Twitter, threads or @WeOrbitant — even if they never say the\nword \"thread\". This skill writes for X only; it is the wrong skill for\nLinkedIn, YouTube or the newsletter.",
198
+ "version": "1.0.0",
199
+ "tags": [
200
+ "marketing",
201
+ "x",
202
+ "twitter",
203
+ "thread",
204
+ "social-media",
205
+ "content",
206
+ "engagement"
207
+ ],
208
+ "content": "\n# Orbitant X Thread Skill\n\nA thread is one argument spread across several posts. But X distributes **single posts**: the timeline shows them on their own, out of order, and people quote and screenshot individual posts without the rest.\n\nSo a thread has two jobs at once. It has to hold together as one argument, and every post in it has to survive alone. Every rule below follows from that.\n\n---\n\n## What X imposes\n\nPlatform facts. They are not style preferences and you cannot write around them.\n\n- **280 characters per post, hard.** A post at 281 does not post. Count every one.\n- **No text formatting.** X renders no markdown. `**bold**` shows up as literal asterisks, `##` as literal hashes. The line break is the only formatting tool you have.\n- **Posts are read out of order and out of context**, pulled into a timeline between two unrelated things.\n- **Links cost reach.** A link in the opening post suppresses distribution. One link per thread, in the closing post.\n- **Hashtags do nothing.** See the Hashtags section.\n- **Code does not survive as text.** No monospace, and indentation is stripped. Code has to be an image.\n\n---\n\n## Input\n\n### Source material\n\nRead it in full before writing anything, and identify which kind you have — it decides the closing post.\n\n| Source | What it is | Closing post |\n|---|---|---|\n| **Blog-derived** | A published post in Markdown, with a URL | Ends with the link |\n| **Native** | KS transcript, Slack thread, a feature just shipped, a build-in-public moment, a decision you made | Ends with a statement, no link |\n\nYour job is never to summarise the source. It is to find the one argument inside it that earns a thread.\n\nIf the material holds no real argument — only an announcement, or a single fact — say so. A thread is the wrong format for it, and one good post beats a padded thread.\n\n### Account and voice\n\nAsk which account posts if it is not stated and the answer would change the copy. Default to the company account.\n\n| Account | Pronoun | Self-mention | Sign-off |\n|---|---|---|---|\n| **Company** (`@WeOrbitant`) | \"we\" | Never — the account *is* Orbitant | None |\n| **Personal** (a team member) | \"I\" | One `@WeOrbitant` mention, in the closing post | The lived experience is the sign-off |\n\nOn a personal account the thread must read as something that person actually did, decided or measured — first person singular. Do not write a company announcement and paste a name on it.\n\n### Language\n\n**Follow the language of the source.** Spanish material, Spanish thread. English material, English thread. One version — never both, and never a translation alongside.\n\nIf the source mixes languages, use the language of the bulk of it. Keep technical English terms in English even in a Spanish thread: *framework*, *pipeline*, *deployment*, *token*.\n\n---\n\n## Step 1 — Fix the spine\n\nBefore writing any post, write the **spine** in one sentence: the single argument the whole thread makes.\n\n**The spine is the concept, the principle or the pattern — never the tool, the client or the implementation.** If the source uses a real project, a framework or a specific stack to illustrate the point, those are evidence. They are never the subject.\n\n> Spine: \"Dependencies pointing inward is what makes business logic survive a framework change.\"\n>\n> Spine: \"The cost of a slow pipeline is not the wait — it is the batching behaviour it forces on the team.\"\n\n**Test:** if you cannot state the spine in one sentence, you do not have a thread yet. You have two threads, or a blog post.\n\n---\n\n## Step 2 — The opening post\n\nX surfaces this post on its own. Assume most people read only this one. **It has to deliver value standing alone** — someone who never opens the thread should still walk away with something.\n\n### Rules\n\n- **A statement, never a rhetorical question.** A question invites \"no\" and a scroll.\n- **No link.** It goes in the closing post, once.\n- **No thread announcement.** No \"🧵\", no \"A thread:\", no \"👇\", no \"Let me explain:\". A post that has to announce it is a thread is not a hook.\n- **Target ~200 of the 280 characters.** Room to breathe reads as confidence; a post packed to 279 reads as cramming.\n- **Do not open with \"We\", \"Our\" or \"Orbitant\".** Open with the insight. Exception: featuring a teammate by name is encouraged, to give them credit.\n- **Concrete beats clever.** A number, a measurement or a named consequence outperforms wordplay.\n\n### Examples\n\nDo:\n\n- `We measured it: with hexagonal architecture the agent needed 3 to 5x more context to make the same change.`\n- `Our CI took 22 minutes. One integration test was eating 60% of that, because it span up a real Postgres on every run.`\n- `AI is writing malicious npm packages now, and they are harder to spot than the ones humans wrote.`\n\nDon't:\n\n- `Have you ever wondered why your CI is so slow? 🧵` — rhetorical question, plus thread emoji\n- `Frontend architecture: a thread 👇` — announces itself, delivers nothing\n- `Here is everything we learned about pipeline optimisation. Let me explain: 1/12` — growth-hacker voice, and a hardcoded total you will want to change\n\n---\n\n## Step 3 — The body posts (1 to 8)\n\n### Rules\n\n- **One idea per post.** If a post carries two ideas, it is two posts.\n- **Every post must parse alone.** No post may depend on having read the previous one. Openers that dangle — \"And that is when we realised\", \"This is why it matters\" — are meaningless in a timeline.\n- **Every post should be quotable.** Assume any one of them gets quoted with no context attached.\n- **Report the character count** for each, and keep every one under 280.\n- **Vary the length on purpose.** Length is a rhythm instrument: a 90-character post lands a beat, a 250-character one carries the detail an argument needs. Do not let every post settle into the same band — a thread where all posts measure roughly the same reads as something generated, not written. Put the short ones where you want the reader to stop.\n- **Use short lines and blank lines** as the structure. That is the whole formatting toolkit.\n- **Lists inside a post:** one item per line, three items maximum, no bullet characters. More than three means it should be several posts.\n- **Numbering:** only if the thread is genuinely enumerable. Numbering a narrative turns it into a listicle. Never hardcode a total (`1/7`) — you will add or cut a post.\n- **Code:** one short snippet at most, as an image.\n- **No links.** One link per thread, in the closing post.\n\nGive one post to the honest limitation — where the idea stops working. A thread that says so is trusted more than one that does not.\n\n### Example\n\n> Don't: `And that was when we found the real problem.`\n>\n> Do: `The real problem was the test, not the pipeline. It span up a real Postgres for every run — 13 of the 22 minutes.`\n\n---\n\n## Step 4 — The closing post\n\n**Blog-derived** — the link, phrased naturally. No \"click here\", no exclamation marks.\n\n- `Full breakdown, with the numbers: [url]`\n- `We wrote up the whole migration here: [url]`\n\n**Native** — a statement that encapsulates the spine. Memorable, not a generic call to action. `Building with intention, not chaos` — not `Read more on our blog`.\n\nOn a personal account, this is where the single `@WeOrbitant` mention goes, if the work was Orbitant's.\n\n**No engagement bait.** Not \"Follow for more\", not \"Bookmark this\", not \"What do you think?\", not \"Agree?\", not \"RT if you have lived this\".\n\n---\n\n## Step 5 — Suggest one visual\n\nPropose **one** image, for the single post that most needs it — one line saying what it shows and which post it attaches to. Not a brief per post.\n\nAn image earns its slot when it carries:\n\n- a system, a flow or a relationship between components (a diagram)\n- a before/after comparison\n- a code snippet (the only way to put code on X)\n- a real result — the output, the dashboard, the timing\n\nA thread with one well-placed image outperforms both a thread with none and a thread with an image on every post. If nothing in that list applies, say no image is needed. A decorative image costs attention and returns nothing.\n\n---\n\n## Hashtags\n\n**Do not use hashtags on X.** Not one. The single exception is a live event tag when there is a real convening hashtag people are actively clicking — a conference Orbitant is at or sponsoring.\n\nThey have lost their discovery function. The timeline surfaces content by semantic relevance and engagement graph, not by hashtag index. Two or more now read as bot behaviour and correlate with *less* reach.\n\nHashtag conventions from Orbitant's other channels do not carry over to X. This is deliberate — do not add them back for consistency.\n\nOn X the brand appears as **`@WeOrbitant`**, which is a mention, and mentions do work.\n\n---\n\n## Output format\n\nPlain text, ready to paste into X, Typefully or n8n. No markdown in the post bodies.\n\n**The `POST n — c/280` line is the delimiter, and that is a contract.** One header line per post, one blank line between posts, nothing else at that level: a scheduler or an n8n step splits the thread deterministically on it, and a reviewer reads the same artifact. Do not emit a second, undivided copy of the thread alongside it — the split IS the work, and two copies drift the moment a reviewer corrects one.\n\n**Never hand a thread to a tool that splits by length.** An auto-splitter cuts at 280 characters wherever that falls, which undoes every rule above: posts stop being one idea each, stop parsing alone and stop being quotable. The boundaries here are decided, not incidental.\n\n```text\nTHREAD — [spine in one sentence]\n\nAccount: @WeOrbitant (company) | personal — [name]\nLanguage: [es | en]\nSource: [blog post URL | native: what it came from]\n\nPOST 1 — [n]/280\n[opening post]\n\nPOST 2 — [n]/280\n[body post]\n\nPOST 3 — [n]/280\n[body post]\n\nPOST 4 — [n]/280\n[body post]\n\nPOST 5 — [n]/280\n[closing post]\n\nIMAGE — attach to POST [n]\n[what it shows, one line]\n```\n\n**Length is whatever the spine needs.** Three posts is a thread; ten is the ceiling, and more than that means the spine is carrying too much. There is no floor to reach: if the argument is spent after one body post, the thread is three posts. If it fits in a single post, deliver one post and say it is not a thread.\n\n### Self-check before delivering\n\nVerify each of these. Do not deliver a thread that fails one.\n\n- [ ] A character count reported for every post, none over 280\n- [ ] The post lengths vary; they have not all settled into the same band\n- [ ] The opening post has no link\n- [ ] The opening post delivers value standing alone\n- [ ] Every body post parses with no prior context\n- [ ] Zero hashtags, or one real event tag\n- [ ] No markdown syntax anywhere in the post bodies\n- [ ] Exactly one link, in the closing post — or none, if native\n- [ ] The spine is a concept or principle, not a tool or a client\n- [ ] The pronoun matches the account\n- [ ] The thread language matches the source language\n- [ ] No more than 10 posts, and none of them padding\n\n---\n\n## Tone\n\nRefer to the `tone` skill for Orbitant's voice. On X specifically:\n\n- **Compress hard.** 280 characters is not a trimmed paragraph, it is a different sentence length. One clause where you would write two.\n- **Concrete over abstract.** Numbers, measurements, named consequences. X punishes vagueness faster than any other channel.\n- **Dry humour is welcome** when it fits naturally. Human, not polished — not forced, not performative.\n- **No buzzwords:** \"game-changing\", \"innovative\", \"cutting-edge\", \"empower\", \"leverage\".\n- **No growth-hacker voice.** The fastest way to lose a technical audience: \"Here is everything you need to know\", \"10 lessons I learned\", \"Most developers get this wrong\", \"This changed how I think about X forever\", \"Steal this\".\n- **In Spanish, the em dash only marks a two-sided aside.** A single-sided dash used as a continuation is an anglicism.\n\n---\n\n## What to avoid\n\n- Recycling copy written for another channel. Start from the source material, never from an existing post.\n- Summarising the source instead of extracting one argument from it.\n- An opening post that only makes sense once you have read the thread.\n- Posts that only make sense once you have read the blog post.\n- Hashtags. Any.\n- A link in the opening post, or links scattered through the body.\n- Markdown syntax — X renders none of it.\n- Code as text.\n- `🧵`, `👇`, \"A thread:\", and every other way of announcing a thread instead of starting one.\n- Engagement bait of any kind, including the closing question.\n- Padding a thin idea to reach a post count. A thread that runs out of argument at post 4 should be four posts, or one.\n",
209
+ "frontmatter": {
210
+ "name": "orbitant-x-thread",
211
+ "description": "X (Twitter) thread writer for Orbitant. Turns a published blog post OR native\nmaterial — a Knowledge Sharing transcript, a Slack thread, a feature just\nshipped, a build-in-public moment — into a thread ready to post: an opening\npost that stands alone, body posts of one idea each, and a closing post,\nevery one annotated with its character count against X's 280 limit.\n\nActivate when the user asks for a thread, an X post or a Twitter post, or says\n\"haz un hilo\", \"convierte esto en un hilo\", \"esto da para un hilo\", \"thread\nthis\", \"algo para X\", \"para Twitter\". Also trigger when the user shares a blog\npost, a transcript or a Slack conversation and asks for social copy while\nmentioning X, Twitter, threads or @WeOrbitant — even if they never say the\nword \"thread\". This skill writes for X only; it is the wrong skill for\nLinkedIn, YouTube or the newsletter.\n",
212
+ "version": "1.0.0",
213
+ "license": "MIT",
214
+ "metadata": {
215
+ "author": "orbitant",
216
+ "tags": "marketing, x, twitter, thread, social-media, content, engagement"
217
+ }
218
+ },
219
+ "relDir": "skills/x-thread"
220
+ },
194
221
  {
195
222
  "name": "orbitant-yt-description",
196
223
  "folder": "yt-description",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbitant/brain-marketing",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Marketing team skills for content creation and review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  # Image Creation Skill
2
2
 
3
- Generates blog post thumbnails for the Orbitant engineering blog using Google's Imagen API, following the brand's visual identity system.
3
+ Generates blog post thumbnails for the Orbitant engineering blog using Google's Gemini image API (Nano Banana 2), following the brand's visual identity system.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -32,7 +32,7 @@ Or just ask Claude: _"Generate a blog image about microservices"_ — the skill
32
32
  | `--aspect` | Aspect ratio: `1:1`, `3:4`, `4:3`, `9:16`, `16:9` | `16:9` |
33
33
  | `--count` | Number of variants (1–4) | `3` |
34
34
  | `--watermark` | `white`, `black`, `none`, or `auto` | `auto` |
35
- | `--model` | Imagen model ID | `imagen-4.0-generate-001` |
35
+ | `--model` | Gemini image model ID | `gemini-3.1-flash-image` |
36
36
 
37
37
  > **Note:** `--negative` is accepted but not supported by the current API. Bake negative constraints directly into the prompt instead.
38
38
 
@@ -2,7 +2,7 @@
2
2
  name: orbitant-image-creation
3
3
  description: |
4
4
  Generates blog post thumbnail images for Orbitant following the brand's visual
5
- identity, using Google's Imagen API (Nano Banana 2). Activates when creating
5
+ identity, using Google's Gemini image API (Nano Banana 2). Activates when creating
6
6
  blog images, generating thumbnails, designing featured images for articles, or
7
7
  when someone needs a visual for an Orbitant insight/blog post. Use this skill
8
8
  even if the user just says "I need an image for this article", "create a
@@ -10,7 +10,7 @@ description: |
10
10
  when the user mentions "Nano Banana 2", "image generation", or asks for a
11
11
  prompt for an AI image tool.
12
12
  license: MIT
13
- version: "1.0.0"
13
+ version: "1.0.1"
14
14
  metadata:
15
15
  author: orbitant
16
16
  tags: marketing, image, thumbnail, blog, visual, prompt, nano-banana-2, ai-image, imagen
@@ -21,7 +21,7 @@ metadata:
21
21
  You are a visual prompt engineer and image generator for the Orbitant engineering blog. Your job is to:
22
22
 
23
23
  1. Craft a prompt matching Orbitant's visual identity
24
- 2. Generate the image by running the bundled script against Google's Imagen API
24
+ 2. Generate the image by running the bundled script against Google's Gemini image API
25
25
  3. Deliver the final image file to the user
26
26
 
27
27
  Respond in the same language as the user's request.
@@ -51,12 +51,12 @@ npm install @google/genai sharp
51
51
 
52
52
  ### Google API Key
53
53
 
54
- You need a `GOOGLE_API_KEY` to call the Imagen API. There are two ways to get one:
54
+ You need a `GOOGLE_API_KEY` to call the Gemini image API. There are two ways to get one:
55
55
 
56
56
  | Option | How | Cost |
57
57
  |--------|-----|------|
58
58
  | **Google AI Studio** | Go to <https://aistudio.google.com/apikey> and create a key with your personal Google account | Free tier with daily limits |
59
- | **Google Workspace** | Same link, but sign in with your organization's Workspace account. Many Workspace plans include Gemini/Imagen API access with generous quotas (check your admin console under **Apps → Additional Google services → Google AI Studio**) | Included in Workspace plans that have Gemini enabled |
59
+ | **Google Workspace** | Same link, but sign in with your organization's Workspace account. Many Workspace plans include Gemini API access with generous quotas (check your admin console under **Apps → Additional Google services → Google AI Studio**) | Included in Workspace plans that have Gemini enabled |
60
60
 
61
61
  > **Tip:** If your organization uses Google Workspace with Gemini enabled, you likely already have API access at no extra cost — ask your Workspace admin if unsure.
62
62
 
@@ -101,7 +101,7 @@ node plugins/orbitant-marketing/skills/image-creation/scripts/generate-image.mjs
101
101
 
102
102
  If the API key is not set, the skill will craft the prompt and show it to the user so they can use it manually in AI Studio or another tool.
103
103
 
104
- > **Note:** The `--negative` flag is accepted by the script but **not supported by the current Imagen API** (`imagen-4.0-generate-001`). Instead of using `--negative`, incorporate negative constraints directly into the main prompt (e.g., "No red, orange, or yellow fire. No text, no words, no logos.").
104
+ > **Note:** The `--negative` flag is accepted by the script but **not supported by Gemini image models** (`gemini-3.1-flash-image`) and is ignored with a warning. Instead of using `--negative`, incorporate negative constraints directly into the main prompt (e.g., "No red, orange, or yellow fire. No text, no words, no logos.").
105
105
 
106
106
  ### Reference Images Setup
107
107
 
@@ -121,7 +121,7 @@ Once available, **browse a few reference images** from `assets/reference/` befor
121
121
 
122
122
  ## Available Scripts
123
123
 
124
- - **`scripts/generate-image.mjs`** — Generates images via Google's Imagen API and automatically composites the Orbitant watermark. Accepts prompt, output path, aspect ratio, model, count, and watermark tone. Returns JSON with file paths on success.
124
+ - **`scripts/generate-image.mjs`** — Generates images via Google's Gemini image API and automatically composites the Orbitant watermark. Accepts prompt, output path, aspect ratio, model, count, and watermark tone. Returns JSON with file paths on success.
125
125
  - **`scripts/scrape-insights-images.mjs`** — Downloads curated reference images from orbitant.com into `assets/reference/`. Skips existing files. Use `--force` to re-download.
126
126
 
127
127
  Run `node scripts/generate-image.mjs --help` or `node scripts/scrape-insights-images.mjs --help` for full usage.
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Generates Orbitant blog thumbnails using Google's Imagen API
5
- * and composites the Orbitant watermark automatically.
4
+ * Generates Orbitant blog thumbnails using Google's Gemini image
5
+ * generation API (Nano Banana) and composites the Orbitant watermark
6
+ * automatically.
6
7
  *
7
8
  * Usage:
8
9
  * node scripts/generate-image.mjs --prompt "..." --output path/to/image.png [OPTIONS]
@@ -12,7 +13,7 @@
12
13
  * --output FILE Output file path, .png (required)
13
14
  * --negative TEXT Negative prompt (optional)
14
15
  * --aspect RATIO 1:1, 3:4, 4:3, 9:16, 16:9 (default: 16:9)
15
- * --model MODEL Model ID (default: imagen-4.0-generate-001)
16
+ * --model MODEL Model ID (default: gemini-3.1-flash-image)
16
17
  * --count N Number of images, 1-4 (default: 1)
17
18
  * --watermark TONE Watermark variant: white, black, none (default: auto-detect)
18
19
  * --help Show this help
@@ -62,7 +63,7 @@ Options:
62
63
  --output FILE Output file path, .png (required)
63
64
  --negative TEXT Negative prompt (optional)
64
65
  --aspect RATIO 1:1, 3:4, 4:3, 9:16, 16:9 (default: 16:9)
65
- --model MODEL Model ID (default: imagen-4.0-generate-001)
66
+ --model MODEL Model ID (default: gemini-3.1-flash-image)
66
67
  --count N Number of images, 1-4 (default: 1)
67
68
  --watermark TONE white, black, or none (default: auto-detect from image brightness)
68
69
  --help Show this help
@@ -77,7 +78,7 @@ function parseCliArgs() {
77
78
  output: { type: "string" },
78
79
  negative: { type: "string", default: "" },
79
80
  aspect: { type: "string", default: "16:9" },
80
- model: { type: "string", default: "imagen-4.0-generate-001" },
81
+ model: { type: "string", default: "gemini-3.1-flash-image" },
81
82
  count: { type: "string", default: "3" },
82
83
  watermark: { type: "string", default: "auto" },
83
84
  help: { type: "boolean", default: false },
@@ -224,38 +225,46 @@ async function main() {
224
225
  console.error(`Aspect ratio: ${args.aspect}`);
225
226
  console.error(`Prompt: ${args.prompt.substring(0, 120)}...`);
226
227
 
227
- let response;
228
- try {
229
- const config = {
230
- numberOfImages: count,
231
- aspectRatio: args.aspect,
232
- };
233
- if (args.negative) {
234
- config.negativePrompt = args.negative;
235
- }
236
- response = await ai.models.generateImages({
237
- model: args.model,
238
- prompt: args.prompt,
239
- config,
240
- });
241
- } catch (err) {
242
- console.error(`API error: ${err.message}`);
243
- process.exit(3);
244
- }
245
-
246
- if (!response.generatedImages || response.generatedImages.length === 0) {
228
+ if (args.negative) {
247
229
  console.error(
248
- "Error: API returned no images. The prompt may have been blocked by safety filters."
230
+ "Warning: --negative is not supported by Gemini image models and will be ignored. " +
231
+ "Put negative constraints directly in the prompt instead."
249
232
  );
250
- process.exit(3);
233
+ }
234
+
235
+ // Gemini image models return a single image per generateContent call,
236
+ // so request `count` times to keep the multi-image contract.
237
+ const imageBuffers = [];
238
+ for (let i = 0; i < count; i++) {
239
+ try {
240
+ const response = await ai.models.generateContent({
241
+ model: args.model,
242
+ contents: args.prompt,
243
+ config: {
244
+ responseModalities: ["IMAGE"],
245
+ imageConfig: { aspectRatio: args.aspect },
246
+ },
247
+ });
248
+ const parts = response.candidates?.[0]?.content?.parts ?? [];
249
+ const imagePart = parts.find((part) => part.inlineData?.data);
250
+ if (!imagePart) {
251
+ console.error(
252
+ "Error: API returned no images. The prompt may have been blocked by safety filters."
253
+ );
254
+ process.exit(3);
255
+ }
256
+ imageBuffers.push(Buffer.from(imagePart.inlineData.data, "base64"));
257
+ } catch (err) {
258
+ console.error(`API error: ${err.message}`);
259
+ process.exit(3);
260
+ }
251
261
  }
252
262
 
253
263
  await mkdir(dirname(args.output), { recursive: true });
254
264
 
255
265
  const results = [];
256
- for (let i = 0; i < response.generatedImages.length; i++) {
257
- const imgBytes = response.generatedImages[i].image.imageBytes;
258
- const originalBuffer = Buffer.from(imgBytes, "base64");
266
+ for (let i = 0; i < imageBuffers.length; i++) {
267
+ const originalBuffer = imageBuffers[i];
259
268
 
260
269
  // Determine output paths
261
270
  const basePath =
@@ -0,0 +1,269 @@
1
+ ---
2
+ name: orbitant-x-thread
3
+ description: |
4
+ X (Twitter) thread writer for Orbitant. Turns a published blog post OR native
5
+ material — a Knowledge Sharing transcript, a Slack thread, a feature just
6
+ shipped, a build-in-public moment — into a thread ready to post: an opening
7
+ post that stands alone, body posts of one idea each, and a closing post,
8
+ every one annotated with its character count against X's 280 limit.
9
+
10
+ Activate when the user asks for a thread, an X post or a Twitter post, or says
11
+ "haz un hilo", "convierte esto en un hilo", "esto da para un hilo", "thread
12
+ this", "algo para X", "para Twitter". Also trigger when the user shares a blog
13
+ post, a transcript or a Slack conversation and asks for social copy while
14
+ mentioning X, Twitter, threads or @WeOrbitant — even if they never say the
15
+ word "thread". This skill writes for X only; it is the wrong skill for
16
+ LinkedIn, YouTube or the newsletter.
17
+ version: "1.0.0"
18
+ license: MIT
19
+ metadata:
20
+ author: orbitant
21
+ tags: marketing, x, twitter, thread, social-media, content, engagement
22
+ ---
23
+
24
+ # Orbitant X Thread Skill
25
+
26
+ A thread is one argument spread across several posts. But X distributes **single posts**: the timeline shows them on their own, out of order, and people quote and screenshot individual posts without the rest.
27
+
28
+ So a thread has two jobs at once. It has to hold together as one argument, and every post in it has to survive alone. Every rule below follows from that.
29
+
30
+ ---
31
+
32
+ ## What X imposes
33
+
34
+ Platform facts. They are not style preferences and you cannot write around them.
35
+
36
+ - **280 characters per post, hard.** A post at 281 does not post. Count every one.
37
+ - **No text formatting.** X renders no markdown. `**bold**` shows up as literal asterisks, `##` as literal hashes. The line break is the only formatting tool you have.
38
+ - **Posts are read out of order and out of context**, pulled into a timeline between two unrelated things.
39
+ - **Links cost reach.** A link in the opening post suppresses distribution. One link per thread, in the closing post.
40
+ - **Hashtags do nothing.** See the Hashtags section.
41
+ - **Code does not survive as text.** No monospace, and indentation is stripped. Code has to be an image.
42
+
43
+ ---
44
+
45
+ ## Input
46
+
47
+ ### Source material
48
+
49
+ Read it in full before writing anything, and identify which kind you have — it decides the closing post.
50
+
51
+ | Source | What it is | Closing post |
52
+ |---|---|---|
53
+ | **Blog-derived** | A published post in Markdown, with a URL | Ends with the link |
54
+ | **Native** | KS transcript, Slack thread, a feature just shipped, a build-in-public moment, a decision you made | Ends with a statement, no link |
55
+
56
+ Your job is never to summarise the source. It is to find the one argument inside it that earns a thread.
57
+
58
+ If the material holds no real argument — only an announcement, or a single fact — say so. A thread is the wrong format for it, and one good post beats a padded thread.
59
+
60
+ ### Account and voice
61
+
62
+ Ask which account posts if it is not stated and the answer would change the copy. Default to the company account.
63
+
64
+ | Account | Pronoun | Self-mention | Sign-off |
65
+ |---|---|---|---|
66
+ | **Company** (`@WeOrbitant`) | "we" | Never — the account *is* Orbitant | None |
67
+ | **Personal** (a team member) | "I" | One `@WeOrbitant` mention, in the closing post | The lived experience is the sign-off |
68
+
69
+ On a personal account the thread must read as something that person actually did, decided or measured — first person singular. Do not write a company announcement and paste a name on it.
70
+
71
+ ### Language
72
+
73
+ **Follow the language of the source.** Spanish material, Spanish thread. English material, English thread. One version — never both, and never a translation alongside.
74
+
75
+ If the source mixes languages, use the language of the bulk of it. Keep technical English terms in English even in a Spanish thread: *framework*, *pipeline*, *deployment*, *token*.
76
+
77
+ ---
78
+
79
+ ## Step 1 — Fix the spine
80
+
81
+ Before writing any post, write the **spine** in one sentence: the single argument the whole thread makes.
82
+
83
+ **The spine is the concept, the principle or the pattern — never the tool, the client or the implementation.** If the source uses a real project, a framework or a specific stack to illustrate the point, those are evidence. They are never the subject.
84
+
85
+ > Spine: "Dependencies pointing inward is what makes business logic survive a framework change."
86
+ >
87
+ > Spine: "The cost of a slow pipeline is not the wait — it is the batching behaviour it forces on the team."
88
+
89
+ **Test:** if you cannot state the spine in one sentence, you do not have a thread yet. You have two threads, or a blog post.
90
+
91
+ ---
92
+
93
+ ## Step 2 — The opening post
94
+
95
+ X surfaces this post on its own. Assume most people read only this one. **It has to deliver value standing alone** — someone who never opens the thread should still walk away with something.
96
+
97
+ ### Rules
98
+
99
+ - **A statement, never a rhetorical question.** A question invites "no" and a scroll.
100
+ - **No link.** It goes in the closing post, once.
101
+ - **No thread announcement.** No "🧵", no "A thread:", no "👇", no "Let me explain:". A post that has to announce it is a thread is not a hook.
102
+ - **Target ~200 of the 280 characters.** Room to breathe reads as confidence; a post packed to 279 reads as cramming.
103
+ - **Do not open with "We", "Our" or "Orbitant".** Open with the insight. Exception: featuring a teammate by name is encouraged, to give them credit.
104
+ - **Concrete beats clever.** A number, a measurement or a named consequence outperforms wordplay.
105
+
106
+ ### Examples
107
+
108
+ Do:
109
+
110
+ - `We measured it: with hexagonal architecture the agent needed 3 to 5x more context to make the same change.`
111
+ - `Our CI took 22 minutes. One integration test was eating 60% of that, because it span up a real Postgres on every run.`
112
+ - `AI is writing malicious npm packages now, and they are harder to spot than the ones humans wrote.`
113
+
114
+ Don't:
115
+
116
+ - `Have you ever wondered why your CI is so slow? 🧵` — rhetorical question, plus thread emoji
117
+ - `Frontend architecture: a thread 👇` — announces itself, delivers nothing
118
+ - `Here is everything we learned about pipeline optimisation. Let me explain: 1/12` — growth-hacker voice, and a hardcoded total you will want to change
119
+
120
+ ---
121
+
122
+ ## Step 3 — The body posts (1 to 8)
123
+
124
+ ### Rules
125
+
126
+ - **One idea per post.** If a post carries two ideas, it is two posts.
127
+ - **Every post must parse alone.** No post may depend on having read the previous one. Openers that dangle — "And that is when we realised", "This is why it matters" — are meaningless in a timeline.
128
+ - **Every post should be quotable.** Assume any one of them gets quoted with no context attached.
129
+ - **Report the character count** for each, and keep every one under 280.
130
+ - **Vary the length on purpose.** Length is a rhythm instrument: a 90-character post lands a beat, a 250-character one carries the detail an argument needs. Do not let every post settle into the same band — a thread where all posts measure roughly the same reads as something generated, not written. Put the short ones where you want the reader to stop.
131
+ - **Use short lines and blank lines** as the structure. That is the whole formatting toolkit.
132
+ - **Lists inside a post:** one item per line, three items maximum, no bullet characters. More than three means it should be several posts.
133
+ - **Numbering:** only if the thread is genuinely enumerable. Numbering a narrative turns it into a listicle. Never hardcode a total (`1/7`) — you will add or cut a post.
134
+ - **Code:** one short snippet at most, as an image.
135
+ - **No links.** One link per thread, in the closing post.
136
+
137
+ Give one post to the honest limitation — where the idea stops working. A thread that says so is trusted more than one that does not.
138
+
139
+ ### Example
140
+
141
+ > Don't: `And that was when we found the real problem.`
142
+ >
143
+ > Do: `The real problem was the test, not the pipeline. It span up a real Postgres for every run — 13 of the 22 minutes.`
144
+
145
+ ---
146
+
147
+ ## Step 4 — The closing post
148
+
149
+ **Blog-derived** — the link, phrased naturally. No "click here", no exclamation marks.
150
+
151
+ - `Full breakdown, with the numbers: [url]`
152
+ - `We wrote up the whole migration here: [url]`
153
+
154
+ **Native** — a statement that encapsulates the spine. Memorable, not a generic call to action. `Building with intention, not chaos` — not `Read more on our blog`.
155
+
156
+ On a personal account, this is where the single `@WeOrbitant` mention goes, if the work was Orbitant's.
157
+
158
+ **No engagement bait.** Not "Follow for more", not "Bookmark this", not "What do you think?", not "Agree?", not "RT if you have lived this".
159
+
160
+ ---
161
+
162
+ ## Step 5 — Suggest one visual
163
+
164
+ Propose **one** image, for the single post that most needs it — one line saying what it shows and which post it attaches to. Not a brief per post.
165
+
166
+ An image earns its slot when it carries:
167
+
168
+ - a system, a flow or a relationship between components (a diagram)
169
+ - a before/after comparison
170
+ - a code snippet (the only way to put code on X)
171
+ - a real result — the output, the dashboard, the timing
172
+
173
+ A thread with one well-placed image outperforms both a thread with none and a thread with an image on every post. If nothing in that list applies, say no image is needed. A decorative image costs attention and returns nothing.
174
+
175
+ ---
176
+
177
+ ## Hashtags
178
+
179
+ **Do not use hashtags on X.** Not one. The single exception is a live event tag when there is a real convening hashtag people are actively clicking — a conference Orbitant is at or sponsoring.
180
+
181
+ They have lost their discovery function. The timeline surfaces content by semantic relevance and engagement graph, not by hashtag index. Two or more now read as bot behaviour and correlate with *less* reach.
182
+
183
+ Hashtag conventions from Orbitant's other channels do not carry over to X. This is deliberate — do not add them back for consistency.
184
+
185
+ On X the brand appears as **`@WeOrbitant`**, which is a mention, and mentions do work.
186
+
187
+ ---
188
+
189
+ ## Output format
190
+
191
+ Plain text, ready to paste into X, Typefully or n8n. No markdown in the post bodies.
192
+
193
+ **The `POST n — c/280` line is the delimiter, and that is a contract.** One header line per post, one blank line between posts, nothing else at that level: a scheduler or an n8n step splits the thread deterministically on it, and a reviewer reads the same artifact. Do not emit a second, undivided copy of the thread alongside it — the split IS the work, and two copies drift the moment a reviewer corrects one.
194
+
195
+ **Never hand a thread to a tool that splits by length.** An auto-splitter cuts at 280 characters wherever that falls, which undoes every rule above: posts stop being one idea each, stop parsing alone and stop being quotable. The boundaries here are decided, not incidental.
196
+
197
+ ```text
198
+ THREAD — [spine in one sentence]
199
+
200
+ Account: @WeOrbitant (company) | personal — [name]
201
+ Language: [es | en]
202
+ Source: [blog post URL | native: what it came from]
203
+
204
+ POST 1 — [n]/280
205
+ [opening post]
206
+
207
+ POST 2 — [n]/280
208
+ [body post]
209
+
210
+ POST 3 — [n]/280
211
+ [body post]
212
+
213
+ POST 4 — [n]/280
214
+ [body post]
215
+
216
+ POST 5 — [n]/280
217
+ [closing post]
218
+
219
+ IMAGE — attach to POST [n]
220
+ [what it shows, one line]
221
+ ```
222
+
223
+ **Length is whatever the spine needs.** Three posts is a thread; ten is the ceiling, and more than that means the spine is carrying too much. There is no floor to reach: if the argument is spent after one body post, the thread is three posts. If it fits in a single post, deliver one post and say it is not a thread.
224
+
225
+ ### Self-check before delivering
226
+
227
+ Verify each of these. Do not deliver a thread that fails one.
228
+
229
+ - [ ] A character count reported for every post, none over 280
230
+ - [ ] The post lengths vary; they have not all settled into the same band
231
+ - [ ] The opening post has no link
232
+ - [ ] The opening post delivers value standing alone
233
+ - [ ] Every body post parses with no prior context
234
+ - [ ] Zero hashtags, or one real event tag
235
+ - [ ] No markdown syntax anywhere in the post bodies
236
+ - [ ] Exactly one link, in the closing post — or none, if native
237
+ - [ ] The spine is a concept or principle, not a tool or a client
238
+ - [ ] The pronoun matches the account
239
+ - [ ] The thread language matches the source language
240
+ - [ ] No more than 10 posts, and none of them padding
241
+
242
+ ---
243
+
244
+ ## Tone
245
+
246
+ Refer to the `tone` skill for Orbitant's voice. On X specifically:
247
+
248
+ - **Compress hard.** 280 characters is not a trimmed paragraph, it is a different sentence length. One clause where you would write two.
249
+ - **Concrete over abstract.** Numbers, measurements, named consequences. X punishes vagueness faster than any other channel.
250
+ - **Dry humour is welcome** when it fits naturally. Human, not polished — not forced, not performative.
251
+ - **No buzzwords:** "game-changing", "innovative", "cutting-edge", "empower", "leverage".
252
+ - **No growth-hacker voice.** The fastest way to lose a technical audience: "Here is everything you need to know", "10 lessons I learned", "Most developers get this wrong", "This changed how I think about X forever", "Steal this".
253
+ - **In Spanish, the em dash only marks a two-sided aside.** A single-sided dash used as a continuation is an anglicism.
254
+
255
+ ---
256
+
257
+ ## What to avoid
258
+
259
+ - Recycling copy written for another channel. Start from the source material, never from an existing post.
260
+ - Summarising the source instead of extracting one argument from it.
261
+ - An opening post that only makes sense once you have read the thread.
262
+ - Posts that only make sense once you have read the blog post.
263
+ - Hashtags. Any.
264
+ - A link in the opening post, or links scattered through the body.
265
+ - Markdown syntax — X renders none of it.
266
+ - Code as text.
267
+ - `🧵`, `👇`, "A thread:", and every other way of announcing a thread instead of starting one.
268
+ - Engagement bait of any kind, including the closing question.
269
+ - Padding a thin idea to reach a post count. A thread that runs out of argument at post 4 should be four posts, or one.