# Uni1 Image Ad

> Use when the user wants to generate a Meta/Facebook image ad with Luma uni-1 and attach it to an existing Meta ad set. Triggers on phrases like "uni-1 ad", "make a Luma ad creative", "new image ad in <ad set name>", "uni-1 image ad for Meta", "generate uni-1 ad creative", "upload uni-1 image as Meta ad". Anchors on uni-1 + Meta together — does NOT trigger on a generic "make an ad" without the uni…

- **Type:** Skill
- **Install:** `agentstack add skill-krusemediallc-uni1-image-ad-uni1-image-ad`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [krusemediallc](https://agentstack.voostack.com/s/krusemediallc)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [krusemediallc](https://github.com/krusemediallc)
- **Source:** https://github.com/krusemediallc/uni1-image-ad/tree/main/skills/uni1-image-ad

## Install

```sh
agentstack add skill-krusemediallc-uni1-image-ad-uni1-image-ad
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# uni1-image-ad

Generate one or more uni-1 images and create paused Meta ads from them, attached to an existing ad set in the user's Meta ad account.

## Hard rules — never relax

These come from the user's Luma brand contract and from "don't spend money by accident":

1. **Model is `uni-1`.** Never substitute `photon-1`, `photon-flash-1`, or any other model. The brand contract requires every promotional image be generated by uni-1. The helper script enforces this; you must not work around it.
2. **Always clone from an existing ad.** The user must supply an existing **ad ID** to clone — page, ad set, link URL, and CTA are read from that ad. Never hand-pick a page or ad set independently. If the user does not provide an ad ID, ask for one before any other input.
3. **Claude writes the new ad copy.** Body and headline are generated fresh per variant, tailored to the new image. The cloned ad's copy is reference for tone/voice only — never copy it verbatim.
4. **Ad status is `PAUSED`.** Never pass `--status ACTIVE` or otherwise unpause an ad. The user reviews and launches manually in Ads Manager.
5. **No campaign or ad-set mutations.** New ads are attached to the cloned ad's existing ad set. The skill never creates campaigns or ad sets.
6. **Confirmation gate before every Meta CLI mutation.** Show the full command, wait for explicit approval, then run.
7. **Audit log first, then mutate.** Append the JSONL row when a creative is created, *before* attempting the ad. Orphaned creative IDs must be recoverable from `./generated/runs.jsonl`.

8. **No screenshot/platform chrome in generated images.** The output of `generate_image.py` must be the standalone ad creative — the static image the advertiser uploads. Never render iOS device chrome (status bar, home indicator), platform brand-row headers ("Sponsored"/"Saved"), post text/captions surrounding the ad, link-card footers ("URL.COM | headline | Learn more"), engagement rows (likes/comments/shares), or platform tab bars. The script auto-appends a no-chrome guard suffix to every prompt by default. If a use case genuinely requires screenshot-style chrome (rare — usually a UGC-aesthetic ad), pass `--allow-chrome` explicitly and explain why.

## Generation modes

The helper script supports two modes; pick based on what the user asks for.

| Mode | When to use | Required | Optional |
|---|---|---|---|
| `image` (default) | Generate a brand-new ad image. | `--prompt`, `--aspect-ratio` | `--image-ref` (up to 8) for product/style grounding |
| `image_edit` | Modify an existing image (swap colors, change background, add an element). | `--prompt`, `--source` | `--image-ref` (up to 8) for additional guidance |

**Reference images are how you keep brand fidelity.** When the user wants the ad to feature a specific product (jar, bottle, package), pass that product image via `--image-ref`. The model holds packaging, wordmarks, and label typography much more faithfully when it has the asset to ground on. Without a reference, output is "inspired by" — labels and wordmarks will be approximate.

Refs and source can be local file paths (PNG/JPG/JPEG/WEBP/GIF). The script base64-encodes them; no need to upload anywhere.

## Inputs the user must provide (or you must elicit)

The user-facing surface is small — most ad fields come from the cloned ad. Cache `last_clone_ad_id` per account in `~/.claude/skills/uni1-image-ad/state.json`.

| Input | Source | Notes |
|---|---|---|
| **Existing ad ID to clone** | User (required) | Ad must live in the configured `AD_ACCOUNT_ID`. Cache as default for next run. |
| Seed prompt | User | The creative direction in their words. You will rewrite it (see Phase 3). |
| Mode | User or inferred | `image` (default) or `image_edit`. Use `image_edit` only if the user is editing a specific source image. |
| Source image (`--source`) | User | Required only for `image_edit`. |
| Reference image(s) (`--image-ref`) | User | Optional but **strongly recommended** when the ad features a specific product. Up to 8. |
| Variant count `N` | User, default 1 | Cap at 5. |
| Aspect ratio | User | One of `1:1`, `1.91:1`, `4:5`, `9:16`. Reject anything else. Ignored in `image_edit` mode. |
| Ad name (base) | User, optional | Defaults to ` · uni-1 vN`. You suffix per variant. |
| CTA override | User, optional | Defaults to the cloned ad's CTA. Override only if user explicitly asks. |

**Derived from the cloned ad** (do NOT ask the user — read these via `meta ads ad get` + `meta ads creative get`):

| Field | Comes from |
|---|---|
| Ad set ID | `ad.adset_id` of the cloned ad |
| Page ID | `creative.object_story_spec.page_id` of the cloned creative |
| Link URL | `creative.object_story_spec.link_data.link` |
| CTA | `creative.object_story_spec.link_data.call_to_action.type` |
| Cloned body / title | Reference for tone only — used in Phase 3.5 to write new copy |

## Workflow

### Phase 1: Preflight

Verify in this exact order; bail on the first failure with a fix-it message.

1. The current working directory has a `.env` file containing `LUMA_API_KEY`, `ACCESS_TOKEN`, and `AD_ACCOUNT_ID`.
2. Run `meta auth status`. It must print `Authenticated`. On non-zero, instruct the user to refresh their token (see `references/meta-cli-flags.md` § Token refresh quick steps).
3. Read `~/.claude/skills/uni1-image-ad/state.json`. If `accounts[].last_clone_ad_id` exists, offer it as the default for "ad ID to clone".

### Phase 2: Gather inputs and clone the source ad

1. **Ask for the ad ID to clone** if not already supplied. Show cached `last_clone_ad_id` as the default. Refuse to proceed without one.

2. **Resolve the ad** via `meta -o json ads ad get `. From the response, capture:
   - `adset_id` — the destination for the new ad
   - `creative.id` — to fetch the cloned creative

3. **Resolve the creative** via `meta -o json ads creative get `. From the response, capture:
   - `object_story_spec.page_id` (or `object_story_spec.link_data.page_id`) → **page_id**
   - `object_story_spec.link_data.link` → **link_url**
   - `object_story_spec.link_data.call_to_action.type` → **CTA**
   - `object_story_spec.link_data.message` → cloned body (tone reference)
   - `object_story_spec.link_data.name` → cloned title (tone reference)

4. **Show the cloned values to the user** as a compact summary so they can sanity-check. Example:
   ```
   Cloning from ad 120246xxxxxxxxxxxx ("Old Headline · v3")
     Ad set:  120243594343360512  (Luma AI Testimonial Ads)
     Page:    1234567890           (Mr. Paid Social)
     Link:    https://lumalabs.ai/api
     CTA:     LEARN_MORE
     Body (cloned, for tone reference):
       "…"
   ```

5. **Then gather the new-image inputs**: seed prompt, mode (default `image`), `--source` if image_edit, `--image-ref` paths, variant count `N`, aspect ratio, optional ad-name base, optional CTA override.

6. Validate aspect ratio strictly. Reject anything not in `{"1:1", "1.91:1", "4:5", "9:16"}`.

### Phase 3: Prompt rewrite

This is where you add value. **First check the prompt library**, then either fill a template or write fresh.

#### 3a — Check the prompt library

Load `references/prompt-library.md`. It has 7 validated parameterizable templates:

| Tag | Format | When |
|---|---|---|
| **T1** | Apple Notes listicle | "Why I switched to X" / sentimental list of reasons |
| **T2** | Editorial article hero | Publication-co-signed credibility ad (FORBES, WIRED, Vogue, etc.) |
| **T3** | Story tweet+UGC composite | Authority quote + UGC photo for Stories/Reels |
| **T4** | Fake Google search mosaic | "Best X for Y" with publication logos as social proof |
| **T5** | Comparison table (light) | Brand vs. category competitor with feature checklist |
| **T6** | Comparison table (dark, hooky) | Stop-the-scroll dark-mode "this RUINS X" hook |
| **T7** | Sticky-note + product flatlay | Tactile UGC-style, 30-day-test reviewer voice |

If the user's seed prompt or brief maps onto one of these, use it: read the matching section in `prompt-library.md`, fill in the `{placeholder}` variables for the user's brand, and use that as the rewritten prompt. Tell the user which template you matched and why.

If nothing fits, write fresh — then propose adding the new pattern to the library after generation succeeds.

#### 3b — Fleshing out a fresh prompt (or filling a template)

When writing or completing a prompt, anchor on:

- Subject and pose
- Lighting and time of day
- Lens / framing
- Color palette / mood (pull from the brand's identity)
- Composition (rule of thirds, leading lines)
- **Negative space for text overlay** if the ad has body/headline copy
- **Reference roles** — if `--image-ref` is being used, name each reference's role explicitly in the prompt (e.g. "the product in image_ref[0]", "the lighting from image_ref[1]"). Luma's docs note that multi-reference quality improves when each reference is labeled in the prompt.
- **Standalone-creative scope** — never describe iOS chrome, Sponsored badges, engagement counts, or platform UI. The script's no-chrome guard catches violations, but write the prompt as if the rule is on you.

Show the rewritten prompt to the user as one block. Tell them which template (if any) it's based on. Ask: "Use this, edit it, or start over?" Loop until approved.

### Phase 4: Generate

Run the helper script. Always pass `--env-file` pointing at the project `.env`. Add `--image-ref ` once per reference (max 8). Switch to `--mode image_edit --source ` if the user is editing rather than creating from scratch.

```bash
# image mode (default), with optional brand reference
~/.claude/skills/uni1-image-ad/scripts/generate_image.py \
  --prompt "" \
  --aspect-ratio  \
  --n  \
  --image-ref  \
  [--image-ref ] \
  --out ./generated \
  --env-file .env

# image_edit mode (modify an existing image)
~/.claude/skills/uni1-image-ad/scripts/generate_image.py \
  --prompt "" \
  --mode image_edit \
  --source  \
  [--image-ref ] \
  --n  \
  --out ./generated \
  --env-file .env
```

Reference-grounded generations typically take ~40–60s (vs ~6s text-only). Don't bail early — the script's poll timeout is 240s.

Each line on stdout is JSON for one variant (`variant`, `path`, `generation_id`, `width`, `height`). Display the paths to the user.

If any variant comes back below 1080×1080, regenerate it (the script logs a warning to stderr but still emits the JSON line — you decide whether to keep it). If all variants failed, stop and report the error from stderr.

### Phase 5: Confirm variants

Show all paths and ask: "Use all / use these specific ones / regenerate / cancel." One confirmation covers all selected variants.

### Phase 5.5: Write new ad copy

Two sub-steps: first **read the data**, then **write copy informed by it**. Don't skip the data read — generic copy is the #1 failure mode of this skill.

#### 5.5a — Pull what's actually winning spend in this account

```bash
~/.claude/skills/uni1-image-ad/scripts/top_spending_ads.py \
  --limit 10 --days 30
```

That prints one JSON-per-line for the top-10 spending ads in the configured account, with `body`, `title`, `spend`, `impressions`, `ctr`, and `creative_id`. Read all of it, then derive a short pattern profile:

- **Voice** — first-person? founder? testimonial? brand-distant?
- **Length distribution** — what's the median body length of the top performers?
- **Bullet style** — `→`, `✓`, `•`, no bullets, emoji?
- **Hook structures** — counter-narrative, statistic, personal anecdote, threat?
- **Credibility anchors** — recurring numbers, names, claims?
- **CTA closers** — `Join us →`, `See the demo`, `Comment X`, no closer?

State the pattern profile to the user as a compact table before drafting copy. This is the diff between "I made up some copy" and "I matched what's working."

#### 5.5b — Write copy that matches the pattern profile

For each selected variant, you (Claude) write a **new** body and headline. Anchor on:

- **The new image** — what it actually shows. Lead with the visual hook.
- **The pattern profile from 5.5a** — match length distribution, voice, bullet style, CTA closer. The data is the spec.
- **The cloned ad's voice** — secondary signal; never copy phrasing verbatim.
- **A copy framework from `references/ad-copy-frameworks.md`** — load that file before writing. Pick a framework that fits the image + the dominant pattern in 5.5a.
- **For multi-variant runs**, use **different frameworks** per variant so the user A/B tests structure, not just wording.

**No hard character cap.** Meta truncates around the read-more fold, but if the top spenders run 400-700 char bodies and that's clearly working in this account, write to that length. The data decides, not a rule of thumb. (Exception: titles still fit under ~40 chars to avoid placement-level truncation.)

Show all variants' copy as a numbered table for review, including which framework each used and a one-line note on which top-spending ad it pattern-matches:

```
v1  [framework: First Person]  matches: 120245165245740512 ("Refresh. Duplicate. Pause. Pray.")
  title:  …
  body:   …
v2  [framework: Statistic]     matches: 120243595999050512 ("The math doesn't lie.")
  title:  …
  body:   …
```

Ask: "Use this copy / edit specific lines / regenerate." Loop until approved before any Meta CLI calls.

### Phase 6: Upload loop

For each selected variant `i`:

1. **Compose the creative-create command** using the cloned `page_id`, `link_url`, and `CTA` (or override) and the new copy from Phase 5.5. Use `references/meta-cli-flags.md` for the exact flag spelling. Quote all string values with embedded spaces.

   ```bash
   meta -o json ads creative create \
     --name " v" \
     --image  \
     --page-id  \
     --body "" \
     --title "" \
     --link-url "" \
     --call-to-action 
   ```

2. **Show the command, ask "run this?". Run on yes.** Capture the creative ID.

3. **Append to `./generated/runs.jsonl` immediately** (one JSON per line):

   ```json
   {"ts": "", "prompt": "", "variant": , "generation_id": "", "image_path": "", "creative_id": "", "creative_id_status": "created", "ad_id": null, "ad_id_status": "pending"}
   ```

4. **Compose the ad-create command** using the cloned `adset_id`:

   ```bash
   meta -o json ads ad create  \
     --name " v" \
     --creative-id 
   ```

5. **Show, confirm, run.** Capture the ad ID. Update the JSONL row in place (read all lines, replace this one, rewrite) with `ad_id` and `ad_id_status: "created"`.

6. If ad-create fails: leave `ad_id_status: "failed"` with an `error` field. Tell the user the creative was created (give the ID) and offer to retry just the ad-create step.

### Phase 7: Persist cache, report

1. Update `~/.claude/skills/uni1-image-ad/state.json`:
   ```json
   {
     "accounts": {
       "": {
         "last_clone_ad_id": "",
         "last_run_ts": ""
       }
     }
   }
   ```
2. For each created ad, print:
   - The ad ID
   - Deep link: `https://business.facebook.com/adsmanager/manage/ads?act=&selected_ad_ids=` (strip the `act_` prefix from `AD_ACCOUNT_ID`)
3. Remind the user: *all ads are PAUSED — review and launch manually.*

## Retry mode (partial-failure recovery)

If `runs.jsonl` already exists when the user re-runs the skill, scan it for rows where `creative_id_status == "created"` but `ad_id_status != "created"`. Offer:

> "I see  orphan creative(s) from a previous run. Do you want to retry the `ad create` step against those existing creatives, or generate a new image?"

If retry: skip Phases 3–5, jump to Phase 6 step 4 with the existing creative IDs.

## Dry-run mode

If the user says "dry run", "preview only", or "show me the commands", run Phases 1–4, then in Phase 6 just **print** every `meta` command without executing, and exit. Do not write to `runs.jsonl` in dry-run mode.

## Out of scope — fail clearly

If the user asks for any of these, stop and explain it's not supported:

- **Creating ads without cloning from an existing one.** The skill always clones. If the user ha

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [krusemediallc](https://github.com/krusemediallc)
- **Source:** [krusemediallc/uni1-image-ad](https://github.com/krusemediallc/uni1-image-ad)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-krusemediallc-uni1-image-ad-uni1-image-ad
- Seller: https://agentstack.voostack.com/s/krusemediallc
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
