@aicopycoders/exodus 2026.7.200-beta.8 → 2026.8.1100
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.
- package/README.md +51 -0
- package/assets/docs/CLAUDE.md +3 -0
- package/assets/docs/PIPELINES.md +50 -0
- package/assets/skills/exodus/SKILL.md +6 -2
- package/assets/skills/exodus-meme/SKILL.md +44 -28
- package/assets/skills/exodus-winners/SKILL.md +239 -0
- package/assets/skills/exodus-workflow/SKILL.md +464 -63
- package/dist/bin/exodus.js +3 -0
- package/dist/commands/bank.d.ts +88 -0
- package/dist/commands/bank.js +346 -0
- package/dist/commands/browse.js +2 -1
- package/dist/commands/creative.js +2 -2
- package/dist/commands/genesis.js +16 -4
- package/dist/commands/keys.js +1 -0
- package/dist/commands/meme.js +9 -6
- package/dist/commands/primer.js +2 -1
- package/dist/commands/session.d.ts +55 -0
- package/dist/commands/session.js +202 -0
- package/dist/commands/status.js +4 -4
- package/dist/commands/swipe.js +4 -4
- package/dist/commands/template.js +2 -2
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +20 -0
- package/dist/commands/winners.js +13 -3
- package/dist/commands/workflow.d.ts +293 -8
- package/dist/commands/workflow.js +2396 -90
- package/dist/lib/client.d.ts +8 -1
- package/dist/lib/client.js +30 -6
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +52 -5
- package/dist/lib/intel-format.js +2 -1
- package/dist/lib/keys-sync.js +1 -0
- package/dist/lib/poll.js +7 -4
- package/dist/lib/route-support.d.ts +4 -0
- package/dist/lib/route-support.js +22 -0
- package/dist/lib/runStatus.d.ts +22 -0
- package/dist/lib/runStatus.js +139 -0
- package/dist/lib/workflowText.d.ts +3 -0
- package/dist/lib/workflowText.js +113 -0
- package/package.json +5 -2
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @aicopycoders/exodus
|
|
2
|
+
|
|
3
|
+
The Exodus CLI + Claude Code skills for the Copycoders ad pipeline. It drives the
|
|
4
|
+
Genesis creative bots, workflows, banks, and image/meme pipelines against your
|
|
5
|
+
brand — from your terminal and from Claude Code.
|
|
6
|
+
|
|
7
|
+
You don't install it globally. Run it with `npx` from your workspace folder.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 1. Scaffold (or refresh) the workspace in the current folder
|
|
13
|
+
npx -y @aicopycoders/exodus@beta init
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`init` creates the workspace (the `.claude/` skills, docs, and your brand
|
|
17
|
+
subfolder) and prints the next steps. The `@beta` tag is the dev/preview channel;
|
|
18
|
+
drop it (`@aicopycoders/exodus`) for the stable release channel.
|
|
19
|
+
|
|
20
|
+
### Connect your account
|
|
21
|
+
|
|
22
|
+
1. Open the Exodus dashboard → **Settings → Claude Code**.
|
|
23
|
+
2. Copy the `.env` snippet it shows you (your `EXODUS_API_KEY` + dashboard URL)
|
|
24
|
+
and paste it into a `.env` file in your workspace folder.
|
|
25
|
+
3. Pull your provider keys down so the local tools have them:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx -y @aicopycoders/exodus@beta keys pull
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This writes your dashboard provider + image keys into `.env` (it only upserts
|
|
32
|
+
those keys and preserves the rest of the file). Values are never printed.
|
|
33
|
+
|
|
34
|
+
### Check everything is wired up
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx -y @aicopycoders/exodus@beta doctor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`doctor` diagnoses local and dashboard-side issues in one shot — keys, workspace
|
|
41
|
+
health, CLI currency — and tells you exactly what to fix.
|
|
42
|
+
|
|
43
|
+
## Where to go next
|
|
44
|
+
|
|
45
|
+
- `npx -y @aicopycoders/exodus@beta --help` — the full command surface.
|
|
46
|
+
- `npx -y @aicopycoders/exodus@beta workflow --help` — run, build, and automate
|
|
47
|
+
saved multi-node workflows; resolve gate/repair parks from the inbox; read
|
|
48
|
+
banks and promote winners.
|
|
49
|
+
|
|
50
|
+
Inside Claude Code, just say **"exodus"** plus what you want
|
|
51
|
+
("exodus, write me some ads") and the skills route the rest.
|
package/assets/docs/CLAUDE.md
CHANGED
|
@@ -38,6 +38,7 @@ Any user can create and own unlimited brands — `npx @aicopycoders/exodus brand
|
|
|
38
38
|
- **Create a new brand:** `npx @aicopycoders/exodus brand create "<name>"` — creates it server-side, switches you in, and sets up its local subfolder
|
|
39
39
|
- **Switch brand:** `npx @aicopycoders/exodus brand use <slug>` — or just work from inside that brand's subfolder
|
|
40
40
|
- **New brand created on the dashboard?** `npx @aicopycoders/exodus@latest init` creates its subfolder and pulls its profile
|
|
41
|
+
- **Update the CLI + skills:** `npx @aicopycoders/exodus@latest update` — the only command that refreshes `.claude/skills/` and the workspace docs (`doctor` reports health but never rewrites them)
|
|
41
42
|
- **Brand voice / ICP / offer:** the active brand's `state/brand-profile.md` (inside its subfolder; at this root on older single-brand installs) — read this at session start and after any switch. If it doesn't match the active brand, the exodus-strategist skill will prompt to refresh.
|
|
42
43
|
|
|
43
44
|
Don't carry voice or proven angles across brands — each brand is its own creative universe.
|
|
@@ -58,8 +59,10 @@ These are the **Exodus** pipelines — the automatic system that drives the Gene
|
|
|
58
59
|
| **Creative** | `exodus-creative` | A specific creative-suite engine (native / copy-derived renders from copy, or ref-match to a reference image) when you've already picked one. Otherwise start at **Image**. |
|
|
59
60
|
| **Template** | `exodus-template` | Finished ad copy → many static ads across 50 structured formats (testimonial, hero, UGC…). Otherwise start at **Image**. |
|
|
60
61
|
| **Meme** | `exodus-meme` | Meme ads — recommends formats for a brief, then one batched run renders AI-image and classic Imgflip-template memes server-side. |
|
|
62
|
+
| **Workflow** | `exodus-workflow` | Run and build saved multi-node workflow automations from the CLI, resolve checkpoint/repair parks from the inbox, continue bot sessions, fire triggers, and read banks or promote winners. |
|
|
61
63
|
| **Browse** | `exodus-browse` | View history and retrieve past outputs |
|
|
62
64
|
| **Drive** | `exodus-drive` | Read/write Google Docs, Sheets, and Drive files via `npx @aicopycoders/exodus drive` (uses the dashboard's Google OAuth — no local CLI) |
|
|
65
|
+
| **Winners** | `exodus-winners` | Mine your own Meta ad account for winning ads (via the Meta Ads MCP) and import them into Exodus as generative fuel — interview, visual confirmation, `npx @aicopycoders/exodus winners import` |
|
|
63
66
|
|
|
64
67
|
For the operator-facing quick reference on how to invoke these in Claude Code, see `PIPELINES.md` in the workspace root.
|
|
65
68
|
|
package/assets/docs/PIPELINES.md
CHANGED
|
@@ -14,8 +14,10 @@ In Claude Code you don't click buttons. You say **"exodus"** plus what you want
|
|
|
14
14
|
| exodus-creative | Creative-Suite Engines (native / copy-derived / ref-match) — when you've already picked an engine |
|
|
15
15
|
| exodus-template | Ad-Type Format Variations across 50 structured formats |
|
|
16
16
|
| exodus-meme | Meme Ad Generator — recommend formats, one batched server-side run |
|
|
17
|
+
| exodus-workflow | Run/build saved multi-node workflows, resolve inbox gate/repair parks, continue sessions, fire triggers, read banks + promote winners |
|
|
17
18
|
| exodus-browse | View History and Surface the Right Run |
|
|
18
19
|
| exodus-drive | Google Drive, Docs, Sheets via the Dashboard's OAuth |
|
|
20
|
+
| exodus-winners | Mine your own Meta ad account for winners (needs the Meta Ads MCP) and import them into Exodus |
|
|
19
21
|
|
|
20
22
|
---
|
|
21
23
|
|
|
@@ -170,6 +172,31 @@ Returns: runId + dashboard URL /creative-suite/runs/<runId> (results land in the
|
|
|
170
172
|
|
|
171
173
|
---
|
|
172
174
|
|
|
175
|
+
## exodus-workflow
|
|
176
|
+
|
|
177
|
+
**What it does:** Run, chain, build, and edit saved multi-node Exodus Workflows from the CLI — the automations that wire Genesis bots, primers, briefs, and image nodes together on the dashboard canvas. Also resolves runs that park at a gate or repair step (from the inbox), continues bot chat sessions, enables/fires triggers, reads copy banks, and promotes a winner (which fires the Winner Flywheel).
|
|
178
|
+
|
|
179
|
+
```operator-guide
|
|
180
|
+
Say something like:
|
|
181
|
+
"exodus, run my launch workflow"
|
|
182
|
+
"exodus, what does this workflow need"
|
|
183
|
+
"exodus, check my workflow inbox"
|
|
184
|
+
"exodus, review that gate" / "exodus, approve the run"
|
|
185
|
+
"exodus, continue that session"
|
|
186
|
+
"exodus, promote this winner"
|
|
187
|
+
|
|
188
|
+
Representative commands:
|
|
189
|
+
exodus workflow list
|
|
190
|
+
exodus workflow run <wf> --wait
|
|
191
|
+
exodus workflow inbox
|
|
192
|
+
exodus workflow gate <runId> pick 3,7 | edit 4 | push "punchier" | approve | reject
|
|
193
|
+
exodus session list | show | chat
|
|
194
|
+
exodus bank list | show <key> | promote <key>
|
|
195
|
+
Every workflow/session/bank verb takes --json — that output is the machine API.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
173
200
|
## exodus-browse
|
|
174
201
|
|
|
175
202
|
**What it does:** View past pipeline runs and pull outputs from previous sessions.
|
|
@@ -200,3 +227,26 @@ For pipeline-run docs, use:
|
|
|
200
227
|
npx @aicopycoders/exodus read-doc <runId> — walks every tab, renders to markdown
|
|
201
228
|
Returns: structured JSON forwarded from Google's APIs (or markdown for read-doc)
|
|
202
229
|
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## exodus-winners
|
|
234
|
+
|
|
235
|
+
**What it does:** Mines your own Meta ad account for winning ads and imports them into Exodus as generative fuel. Your agent session needs the official Meta Ads MCP connected (`https://mcp.facebook.com/ads` — OAuth with your own Facebook login); the skill interviews you (no numeric questions), finds the ads carrying ~80% of your lifetime results, confirms each pick visually, then pushes a winner package through the CLI. Imported winners surface in generation selection with their verdict.
|
|
236
|
+
|
|
237
|
+
```operator-guide
|
|
238
|
+
Subcommands:
|
|
239
|
+
exodus winners import <file.json | -> Push a winner package (- reads stdin)
|
|
240
|
+
exodus winners status <importId> Re-poll an import later
|
|
241
|
+
exodus winners list Winners Exodus already holds
|
|
242
|
+
|
|
243
|
+
Import flags:
|
|
244
|
+
--dry-run Would-create vs would-update per winner; zero writes
|
|
245
|
+
--no-wait Return the importId immediately instead of polling
|
|
246
|
+
--json Machine-readable output
|
|
247
|
+
|
|
248
|
+
Requires: Meta Ads MCP in the session (mining) + your Scrape Creators key in
|
|
249
|
+
Settings → Keys (the own-page match scrape bills your account).
|
|
250
|
+
Say things like: "exodus, import my winning ads" · "exodus, mine my ad
|
|
251
|
+
account for winners" · "exodus, which winners does Exodus already hold?"
|
|
252
|
+
```
|
|
@@ -18,12 +18,16 @@ This skill ROUTES. Read what the user wants, name the destination skill, and han
|
|
|
18
18
|
| Static image ads ("make images", "statics", "render this ad") | `exodus-image` (the image front door — it menus engines and confirms before firing) |
|
|
19
19
|
| A specific image engine they already chose | `exodus-creative` (native / copy-derived / ref-match) or `exodus-template` (33 ad-type formats) |
|
|
20
20
|
| Meme ads ("exodus, meme this offer", "make me meme ads") | `exodus-meme` |
|
|
21
|
-
| Run / chain / build / edit a saved workflow ("run my workflow", "what does this workflow need", "build a workflow that…", "list my workflows") | `exodus-workflow` |
|
|
21
|
+
| Run / chain / build / edit a saved workflow, enable/fire its triggers, or roll back a version ("run my workflow", "what does this workflow need", "build a workflow that…", "list my workflows", "enable/fire the trigger", "roll back my workflow", "workflow versions") | `exodus-workflow` |
|
|
22
|
+
| Check the workflow inbox or review/resolve a parked run — checkpoint or repair ("check my workflow inbox", "approve that checkpoint", "approve the run", "repair that run") | `exodus-workflow` |
|
|
23
|
+
| Continue a bot chat session ("continue that session", "chat with the bot session", "list my sessions") | `exodus-workflow` |
|
|
24
|
+
| Read copy banks or promote a winner ("show my hook bank", "list my banks", "promote this winner") | `exodus-workflow` |
|
|
22
25
|
| Onboard a new brand / build the primer | `exodus-primer` (with winning ads) or `exodus-foundation` (no ads yet) |
|
|
23
26
|
| List/switch/troubleshoot brands | `exodus-brand` |
|
|
24
27
|
| See past runs / find an output | `exodus-browse` |
|
|
25
28
|
| Open or create Google Docs/Sheets | `exodus-drive` |
|
|
26
|
-
|
|
|
29
|
+
| Import their OWN winning ads from their Meta ad account ("import my winners", "mine my ad account for winners", "exodus winners") | `exodus-winners` |
|
|
30
|
+
| Update the CLI + skills ("update exodus", "run an exodus update") | run `npx @aicopycoders/exodus@latest update` — the ONLY command that refreshes skills; `doctor` checks health but never rewrites them |
|
|
27
31
|
|
|
28
32
|
If a custom pipeline skill is installed in this folder (any additional `exodus-*` directory under `.claude/skills/`), it routes the same way — match the user's words to that skill's description.
|
|
29
33
|
|
|
@@ -4,11 +4,11 @@ description: Generate meme-style ad creative for the active brand — recommends
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
```operator-guide
|
|
7
|
-
Subcommands (three; the batch renders server-side
|
|
7
|
+
Subcommands (three; the batch renders server-side):
|
|
8
8
|
exodus meme recommend --brief "<text>" [--avatar "<text>"]
|
|
9
9
|
exodus meme run --brief "<text>" --formats '<json>' [--formats-file <path>] [--avatar "<text>"] [--name "<label>"]
|
|
10
|
-
exodus meme regenerate --brief "<text>" --layer 1 --template-id <id> --template-name "<name>" --boxes <N>
|
|
11
|
-
exodus meme regenerate --brief "<text>" --layer 2|3 --format <formatId> [--hint "<text>"]
|
|
10
|
+
exodus meme regenerate --brief "<text>" --layer 1 --template-id <id> --template-name "<name>" --boxes <N>
|
|
11
|
+
exodus meme regenerate --brief "<text>" --layer 2|3 --format <formatId> [--hint "<text>"]
|
|
12
12
|
|
|
13
13
|
Flow:
|
|
14
14
|
1. recommend → { recommendations: [...] } — 15 picks (5 per layer), each with
|
|
@@ -17,14 +17,17 @@ Flow:
|
|
|
17
17
|
2. run → pass the picked recommendation objects straight into --formats (JSON
|
|
18
18
|
array, 1–50; the CLI normalizes them). Returns { runId }. One enqueue —
|
|
19
19
|
closing the session does NOT kill the batch.
|
|
20
|
-
3. Poll: exodus status --id <runId>
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
3. Poll: exodus workflow status --id <runId>
|
|
21
|
+
Memes render through the Image Rig, so a meme run IS a workflow run — the
|
|
22
|
+
workflow verb is the one that can read it, NOT `status --type creative`.
|
|
23
|
+
4. regenerate → re-render one miss, synchronously (returns image_url). Do not
|
|
24
|
+
pass its optional --id the run id from step 2 — that flag takes a
|
|
25
|
+
creative-suite run id, a different id space.
|
|
23
26
|
|
|
24
27
|
Keys (strict BYOK, refused server-side before anything starts):
|
|
25
28
|
layer 1 needs the member's Imgflip login · layers 2/3 need their Kie.ai key ·
|
|
26
29
|
captions always need their LLM key. `npx @aicopycoders/exodus doctor` checks all three.
|
|
27
|
-
Returns: runId + dashboard URL /
|
|
30
|
+
Returns: runId + dashboard URL /runs/<runId> (the visual review surface).
|
|
28
31
|
```
|
|
29
32
|
|
|
30
33
|
# Meme — Meme Ad Generator (Max's pipeline)
|
|
@@ -111,38 +114,43 @@ session and the run finishes anyway.
|
|
|
111
114
|
### 3. Poll
|
|
112
115
|
|
|
113
116
|
```bash
|
|
114
|
-
npx @aicopycoders/exodus status --id <runId>
|
|
117
|
+
npx @aicopycoders/exodus workflow status --id <runId>
|
|
115
118
|
```
|
|
116
119
|
|
|
120
|
+
Memes render through the Image Rig, so a meme run **is** a workflow run — the
|
|
121
|
+
workflow status verb reads it. `status --type creative` will not find it.
|
|
122
|
+
|
|
117
123
|
Poll roughly every 45 seconds while the user is waiting (or just hand them the
|
|
118
124
|
dashboard URL if they'd rather watch it live). Treat these statuses as
|
|
119
|
-
terminal: `
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
terminal: `succeeded`, `succeeded-with-warnings`, `failed`, `cancelled`. A
|
|
126
|
+
warnings verdict is the meme batch's old `partial-error` — some memes landed,
|
|
127
|
+
some missed, and the node detail names what missed.
|
|
122
128
|
|
|
123
129
|
### 4. Regenerate the misses
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
to the user and offer to re-render them — the format fields come from
|
|
127
|
-
recommend output you're still holding:
|
|
131
|
+
"Succeeded with warnings" means some renders failed and the rest landed. Name
|
|
132
|
+
the misses to the user and offer to re-render them — the format fields come from
|
|
133
|
+
the recommend output you're still holding:
|
|
128
134
|
|
|
129
135
|
```bash
|
|
130
136
|
# Classic (layer 1) miss:
|
|
131
137
|
npx @aicopycoders/exodus meme regenerate --brief "<brief>" --layer 1 \
|
|
132
|
-
--template-id <imgflip_template_id> --template-name "<name>" --boxes <imgflip_box_count>
|
|
138
|
+
--template-id <imgflip_template_id> --template-name "<name>" --boxes <imgflip_box_count>
|
|
133
139
|
|
|
134
140
|
# AI (layer 2/3) miss — --hint steers the re-roll:
|
|
135
141
|
npx @aicopycoders/exodus meme regenerate --brief "<brief>" --layer 2 --format <format_id> \
|
|
136
|
-
--hint "make the punchline land the product"
|
|
142
|
+
--hint "make the punchline land the product"
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
Regenerate is synchronous (caption + render server-side) and
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
Regenerate is synchronous (caption + render server-side) and returns the new
|
|
146
|
+
`image_url` directly. **Do not pass the run id from step 2 to `--id`** — that
|
|
147
|
+
flag attaches the fresh meme to a *creative-suite* run, and since memes moved
|
|
148
|
+
onto the Image Rig the id `run` prints is a workflow run id, a different id
|
|
149
|
+
space. Omit `--id` and hand the user the returned URL.
|
|
142
150
|
|
|
143
151
|
### 5. Report
|
|
144
152
|
|
|
145
|
-
Surface the dashboard URL (`/
|
|
153
|
+
Surface the dashboard URL (`/runs/<runId>`) + a 2-line
|
|
146
154
|
strategist take ("the classics carry the relatability, the Group Chat one is
|
|
147
155
|
the sleeper — review them in the library"). The dashboard is where the user
|
|
148
156
|
actually evaluates memes visually; your report gets them there with a point of
|
|
@@ -157,9 +165,9 @@ internals into chat.
|
|
|
157
165
|
`npx @aicopycoders/exodus doctor` to confirm, then send the user to **Settings → Keys** on
|
|
158
166
|
the dashboard. A batch with no layer-1 picks doesn't need Imgflip at all — if
|
|
159
167
|
the user lacks an Imgflip login, an AI-only batch is a legitimate fallback.
|
|
160
|
-
- **`
|
|
161
|
-
successes, name the misses from
|
|
162
|
-
- **`
|
|
168
|
+
- **`succeeded-with-warnings`** — not a failed run; some memes landed. Report the
|
|
169
|
+
successes, name the misses from the node detail, regenerate per step 4.
|
|
170
|
+
- **`failed` / `cancelled`** — the batch died; the status error says why.
|
|
163
171
|
Fix the cause (usually a key issue mid-run) before re-firing, don't just retry.
|
|
164
172
|
- **No progress for 15+ minutes** — presume the run dead and say so rather than
|
|
165
173
|
polling forever; the dashboard run page shows the same stall.
|
|
@@ -176,8 +184,16 @@ internals into chat.
|
|
|
176
184
|
|
|
177
185
|
## Admin
|
|
178
186
|
|
|
179
|
-
- Backend: `/api/meme/{recommend,run,regenerate}` (Bearer-auth).
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
`
|
|
183
|
-
-
|
|
187
|
+
- Backend: `/api/meme/{recommend,run,regenerate}` (Bearer-auth). Since #1142
|
|
188
|
+
`run` is an ADAPTER onto the Image Rig: it compiles the picked formats into a
|
|
189
|
+
firing plan and starts a hidden, per-workspace single-rig workflow, so the id
|
|
190
|
+
it returns names a `workflowRuns` row and every meme now carries the same
|
|
191
|
+
per-image provenance as any other rig render. That is why the poll verb is
|
|
192
|
+
`exodus workflow status`, not `exodus status --type creative` — the creative
|
|
193
|
+
status route reads `creativeSuiteRuns` for a Bearer caller.
|
|
194
|
+
- The rig's child image runs are still `creativeSuiteRuns` with engine `meme`,
|
|
195
|
+
so finished memes appear in the creative-suite library alongside other engines
|
|
196
|
+
and `exodus browse --agent meme` still covers them.
|
|
197
|
+
- `run` accepts only the two models the rig renders (`gpt-image-2`,
|
|
198
|
+
`nano-banana-pro`, or their Kie slugs); anything else is a 400 listing the
|
|
199
|
+
accepted values. It used to accept any value and silently ignore it.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: exodus-winners
|
|
3
|
+
description: Mine the member's OWN Meta ad account for its winning ads and import them into Exodus as generative fuel. The member's Claude Code session holds BOTH the official Meta Ads MCP (their Facebook login, their machine) and the Exodus CLI; this skill orchestrates the whole journey — point them at the Meta MCP, pick one ad account, run a NO-NUMBERS winner-definition interview, mine the account via the MCP, confirm each pick visually, then push a winner-package JSON with `npx @aicopycoders/exodus winners import` and run the two-phase video gap-filler. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("exodus, import my winning ads", "exodus winners", "exodus, mine my ad account for winners", "exodus, which of my own ads are winners"), named this skill or /exodus-winners, ran an `npx @aicopycoders/exodus winners` command, or the `exodus` hub skill routed here. Never claim generic asks ("what are my best ads", "pull my Meta ads", "analyze my Facebook ads") without Exodus context — in shared folders those may belong to the user's other tools; if the user did not say exodus, this skill is not for them. This is the member's OWN account's winners, NOT competitor swipes — competitor swipe mining lives on the dashboard's Swipe Mining surfaces, not here. The bare word "Genesis" without "exodus" refers to the member's own Genesis API key and personal bot recipes, NOT to Exodus.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```operator-guide
|
|
7
|
+
Subcommands:
|
|
8
|
+
exodus winners import <file.json | -> [--dry-run] [--no-wait] [--json] Push a winner package (- reads stdin)
|
|
9
|
+
exodus winners status <importId> [--json] Re-poll an import later
|
|
10
|
+
exodus winners list [--json] Winners Exodus already holds
|
|
11
|
+
|
|
12
|
+
Key flags:
|
|
13
|
+
--dry-run Local schema check + server dry-run: would-create vs would-update per winner,
|
|
14
|
+
plus a Scrape Creators key warning if it's missing. Zero writes, no media upload.
|
|
15
|
+
--no-wait Return the importId immediately instead of polling to outcomes
|
|
16
|
+
--json Machine-readable output
|
|
17
|
+
|
|
18
|
+
Facts baked into the CLI:
|
|
19
|
+
• Scopes to your active brand (exodus brand current). Switch with the exodus-brand skill.
|
|
20
|
+
• Requires the member's Scrape Creators key (Settings → Keys) — the own-page match scrape bills them.
|
|
21
|
+
• asset paths in the package resolve RELATIVE TO THE JSON FILE'S FOLDER.
|
|
22
|
+
• Re-push never duplicates rows: verdict snapshots replace wholesale; gap-filled rows
|
|
23
|
+
upgrade in place; winners absent from a re-push are untouched. It DOES re-run the
|
|
24
|
+
billed match scrape + enrichment — push updates, don't push as a retry reflex.
|
|
25
|
+
|
|
26
|
+
Meta side: the official Meta Ads MCP (https://mcp.facebook.com/ads) supplies all account data.
|
|
27
|
+
This skill NEVER calls the Meta Graph API directly and NEVER calls ads_library_search.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
# Winners — Mine Your Own Ad Account and Import the Champions
|
|
31
|
+
|
|
32
|
+
This is the agent-side journey for **Own-Brand Winners**: turn the member's real Meta ad account into a set of designated winners that Exodus stores as generative fuel. The session holds the member's **own Meta Ads MCP** (their Facebook OAuth) alongside the Exodus CLI — you drive both.
|
|
33
|
+
|
|
34
|
+
The member is a **creative strategist, not a media buyer**. This entire journey is data *collection and preparation* — the analysis happens later, server-side, on the stored winners. So the interview **never asks a single numeric question**: no budgets, no ROAS targets, no thresholds, no "top how many?". You infer the mechanics and confirm in plain language. Make that rule impossible to break — if you catch yourself about to ask for a number, stop and infer it instead.
|
|
35
|
+
|
|
36
|
+
The arc, in order: **MCP check → pick one account → winner-definition interview → mine via MCP → 30k-foot view → visual confirmation → write the package → dry-run → import → video gap-filler.** Persist the definition so re-mining re-asks nothing already settled.
|
|
37
|
+
|
|
38
|
+
## 0. Meta Ads MCP must be connected
|
|
39
|
+
|
|
40
|
+
Everything here reads from the **official Meta Ads MCP** — a remote MCP server at `https://mcp.facebook.com/ads`, OAuth'd in the user's browser with their own Facebook login. We don't own that flow; there is exactly one setup pointer and no troubleshooting beyond it.
|
|
41
|
+
|
|
42
|
+
**If the `ads_*` Meta tools are not present in this session, stop.** Tell the user to add `https://mcp.facebook.com/ads` as a remote MCP server and complete the browser OAuth, then come back. Do not attempt any workaround, any Graph API call, or any other connection method.
|
|
43
|
+
|
|
44
|
+
## 1. Account selection — the required first step, pinned forever
|
|
45
|
+
|
|
46
|
+
Call `ads_get_ad_accounts`. It returns **every** account the login can touch — often a dozen-plus across several businesses. For each: `ad_account_id`, `ad_account_name`, `business_name`, `is_queryable`, `not_queryable_reason`, currency.
|
|
47
|
+
|
|
48
|
+
- Show only **queryable** accounts (`is_queryable: true`). For any flagged one, surface its `not_queryable_reason` so the user understands why it's absent — don't silently drop it.
|
|
49
|
+
- **Account names can be empty strings** — fall back to the id and business name so the list is still legible.
|
|
50
|
+
- The user picks **one**. That account is **pinned** in the definition file (`adAccountId` + name) and is **never re-asked** and never allowed to silently drift. Multi-account handling beyond this single pin is out of scope — one account per definition file.
|
|
51
|
+
|
|
52
|
+
## 2. The winner-definition interview — prescriptive, no numbers
|
|
53
|
+
|
|
54
|
+
You are not collecting preferences here so much as confirming inferences. Four things get settled, and only one of them ever shows the user a choice.
|
|
55
|
+
|
|
56
|
+
**a) Objective → metric is invisible.** Ads group by their **self-labeled result type** (within objective — §2c); each group's success metric is the account's own label — Meta hands it to you already labeled (`results: {"value": "98 (Website appointments scheduled)"}`, `cost_per_result: {"value": "$91.64 USD (Website appointments scheduled)"}` — the string lives under `.value`). **Never ask the user to pick a metric.** The account already declared it. Offer one free-text escape hatch only — *"If your definition of a winner is different from 'the ads that drove the most results,' tell me in your own words."* — and record whatever they say **verbatim** as `customDefinition`, applied agent-side. Do not turn their words into a number.
|
|
57
|
+
|
|
58
|
+
**b) Funnel mapping is infer-then-confirm.** Propose, per campaign, an **awareness level** from the Schwartz five — `unaware` / `problem-aware` / `solution-aware` / `product-aware` / `most-aware` — inferred from the campaign objective plus the campaign/adset **names** (naming is read-only creative context; see §6). Present the whole proposed map **once** for a single confirm/adjust pass, then save it. On a re-mine, only **new** campaigns get proposed; already-mapped ones stay put.
|
|
59
|
+
|
|
60
|
+
**c) The winner rule is fixed — volume over efficiency, all-time, no caps.** State it, don't negotiate it:
|
|
61
|
+
|
|
62
|
+
> Within each result-type group, over the account's full lifetime (`date_preset: "maximum"`), the winners are the **smallest set of creatives that together carry ~80% of that group's lifetime results**, where each qualifying creative has **at least 10 results** (a materiality floor — one lucky conversion is not a winner). No top-N cap.
|
|
63
|
+
|
|
64
|
+
- **Groups key on the self-labeled result type, not on objective alone.** Real accounts mix objectives, and one objective can hide two currencies — OUTCOME_LEADS covers both a booked-call funnel (`"Website appointments scheduled"`) and an opt-in funnel (`"Website leads"`), and those must never share one 80% line (the cheap high-volume metric drowns the scarce valuable one). Partition by objective first, then by the parenthetical label on `results.value`; each distinct label is its own group with its own 80% line and its own floor.
|
|
65
|
+
- **The unit you rank is aggregated content, never the raw ad instance.** Advertisers duplicate: the same post runs in many ad sets and campaigns (same post id = the exact same content with its results split across instances), and the same video or image runs under several copy versions. Rank raw instances and a true winner shatters into sub-threshold fragments — or surfaces as several "different" winners that are one ad. Aggregate BEFORE applying the rule, and apply the **floor to the aggregate, never the instance** — four 6-result duplicates are one 24-result winner (§3 has the mechanics): collapse instances sharing an `effective_object_story_id` in every cut, and collapse further by creative asset (same video / same image across copy variants) within the format cuts.
|
|
66
|
+
- **Union with per-format cuts.** Run the same 80%/≥10 rule three ways: **overall** (post-level aggregates), **within video only**, and **within image only** (creative-level aggregates), then union the results. This guarantees both lanes populate even when one format dominates spend. The union dedupes on the aggregate's `accountAdId` (§3); a winner clearing multiple cuts is ONE row whose `selectionBasis` lists every cut it cleared (`overall` / `top-video` / `top-image`), and when a format cut is among them the stored verdict carries the **creative-level** aggregate's numbers and instance count (the broadest collapse it cleared).
|
|
67
|
+
- **Flat curve honesty.** If contribution is flat — no elbow, results spread evenly across hundreds of ads — say so plainly and show the top contributors instead of pretending there's a clean winner set. Never fabricate outliers to hit the 80% line.
|
|
68
|
+
|
|
69
|
+
**d) There are no losers at v1.** You designate winners only. Don't ask about, or invent, a "worst ads" pass.
|
|
70
|
+
|
|
71
|
+
The defaults behind this rule (`window: "maximum"`, `resultsFloor: 10`, `contributionLine: 0.80`) live in the definition file and are **edited in the file, never interrogated**. Selection is **format-agnostic** — you never ask "video or image?"; the per-format cuts already guarantee top images *and* top videos import.
|
|
72
|
+
|
|
73
|
+
## 3. Mining mechanics — the MCP facts that make or break this
|
|
74
|
+
|
|
75
|
+
Mine on `ads_get_ad_entities`. These are hard-won live-probe facts; encode them into how you call, not into a doc you can look up later:
|
|
76
|
+
|
|
77
|
+
- **Never mine account-wide — scope every metrics pull to one campaign.** Account-wide ad-level pulls that include `results` **500 consistently** on real accounts (mixed objectives, thousands of near-duplicate test ads) — no limit, no sort tweak, and no retry unsticks them, and `results` 500s at campaign level too. Enumerate campaigns first (attributes, `objective`, and spend all work fine at campaign level), then pull ads **campaign-by-campaign** with `results` over `date_preset: "maximum"` and aggregate agent-side into result-type groups (ads carry `campaign_id`; join `objective` from the campaign pull).
|
|
78
|
+
- **Sort server-side, tally compactly — never hold full blobs, and never floor raw rows.** Metric *filtering* on `ads_get_ad_entities` consistently **500s** — never filter by a metric. Metric *sorting* works. Per campaign: sort by **results descending**, page through, and retain one **compact tuple per result-bearing ad** — ad id, ad name, campaign id, creative id, created time, results, spend — discarding zero-result rows (they can't move any sum) and every other field. **The 10-result floor does NOT apply while paging**: sub-floor instances of one post can sum to a winner (§2c), so no row is ever dropped for being small. A creative-testing account runs 100+ near-duplicate ads per campaign; compact tuples keep the whole account to a few hundred short lines, where retaining or printing whole per-campaign blobs burns the session's context before the winner rule ever runs.
|
|
79
|
+
- **Aggregate across the whole result-type group, then floor, then rank.** A scoring group spans campaigns — finish paging **every** campaign that feeds a group before ranking it (the per-campaign loop is just how you gather; the group is the unit you rank, once). Then batch re-fetch creatives by `creative_ids` for the retained tuples — that yields each ad's `effective_object_story_id` (post identity), `image_hash` / video identity, and **format** (which the format-cut denominators need for every result-bearing row, not just candidates). Merge instances sharing a post id (every cut), then copy variants sharing the same creative asset (format cuts), summing results and spend as you merge. Only **now** apply the ≥10 floor — to aggregate totals, never to instances: four 6-result duplicates are one 24-result winner. Denominators: the overall 80% line divides by the group's total; each format cut divides by that format's own subtotal (`resultsShare` still reports share of the full group total).
|
|
80
|
+
- **The aggregate's identity must survive a re-mine.** Its `accountAdId` is its **earliest-created instance** — stable across re-mines (the top performer can change month to month; the oldest instance can't, and the server dedupes rows on this id — an unstable pick would duplicate the winner on the next import). The **highest-results instance** supplies the copy and `sourceNames`. When an aggregate spans more than one instance the verdict sentence says so plainly ("… across 4 ad instances in 3 campaigns"); summed counts are numbers **you** derived — own that in the sentence, and leave single-instance display strings verbatim.
|
|
81
|
+
- **Retry once on a 500 — but only where 500s are transient.** An identical retry usually clears a one-off 500 on a scoped call. It never clears the two structural 500s above (metric filtering, account-wide results) — don't burn calls proving that again.
|
|
82
|
+
- **Metric values are display strings, not numbers** — `"$8,980.38 USD"`, `"4.20%"`, `"Not available"` for zero-delivery ads. Keep them **verbatim** for the verdict snapshot. You still need to *rank* by them agent-side — parse a working copy for the math, but never mutate the string you store. (Account level also returns a numeric `amount_spent_cents` if you need a clean total.)
|
|
83
|
+
- **`results` / `cost_per_result` self-label the outcome** (`.value` = `"98 (Website appointments scheduled)"`). That label is the metric — carry it into `resultType`.
|
|
84
|
+
- **Hierarchy:** ads carry `campaign_id` + `adset_id`; `objective` lives on the **campaign**. There is no `campaign_name` at ad level — get campaign/adset names from a **campaign-level / adset-level call**, then join by id.
|
|
85
|
+
- **Always pass explicit `fields`** on every by-id lookup (`ads_get_creatives`, `ads_get_ad_images`, `ads_get_ad_videos`). The "omit fields → all fields" default is **broken** — a bare by-id lookup returns only `{id}`.
|
|
86
|
+
- **Creative content needs a re-fetch by `creative_ids`.** Listing mode returns ids/names only. A creative detail call gives `body` (primary text), `title` (headline), `call_to_action_type`, `image_hash`, `image_url` (signed CDN), and `effective_object_story_id` (`<pageId>_<postId>` — its **prefix is your page id**, the scrape target and own-brand row key).
|
|
87
|
+
- **`pageName` comes from the user, not from Meta.** The page tools can return `"(unknown)"` for the page name, and `ads_library_search` (which carries it) is off-limits — so once you have the page id, **confirm the page's name with the user in one line** ("This account posts as page 10715… — that's your <name> page, right?"). It names their own-brand entry in Exodus; don't guess it.
|
|
88
|
+
- **Non-US accounts need `country` set.** The package's `source.country` defaults to `US`, and a wrong country makes the server-side Ad Library match silently miss. Infer it from the account (currency, business) and confirm alongside the page name when it isn't clearly US.
|
|
89
|
+
- **Images: download the bytes at interview time, not push time.** `ads_get_ad_images` (by hash) returns a full-res signed CDN `url` that **expires** (watch the `oe=` param). Grab it while you're confirming the winner, save it locally, and reference the local file in the package.
|
|
90
|
+
- **Video: there is no download URL. Never promise one.** `ads_get_ad_videos` gives `title`, `length`, and a poster `picture` only — no source/download URL. The actual mp4 comes from the user in the gap-filler (§8).
|
|
91
|
+
- **Every video winner MUST carry its poster.** Download the `picture` from `ads_get_ad_videos` at confirmation time and attach it as `assets.posterPath` — for an unmatched video winner it is the **only** thumbnail the Own Brand gallery will ever have; skip it and the winner renders as a blank placeholder. The CLI dry-run warns on poster-less video winners: treat that warning as a stop, not a formality.
|
|
92
|
+
- **One page per package.** Winner↔library matching scrapes exactly `source.pageId` — a winner posted by any other page can never match and always gap-fills. If the account's winners span more than one Facebook page, split them into **one package per page** (each with that page's `pageId`/`pageName`); each import creates/updates its own own-brand entry and every winner keeps a real shot at matching.
|
|
93
|
+
- **One `breakdowns` value per call.** Stay on `ads_get_ad_entities` for raw data — the `ads_insights_*` tools are opinionated narrative wrappers (trend/funnel prose), not the numbers you're ranking.
|
|
94
|
+
- **Pass the user's real ask through `advertiser_request`** on each tool call — it's Meta's audit surface; give it the user's actual words.
|
|
95
|
+
- **Never call `ads_library_search`.** Winner↔Ad-Library matching happens **server-side in Exodus** after import; the old title-fingerprint approach is dead. Your job ends at the account data.
|
|
96
|
+
|
|
97
|
+
## 4. The 30,000-foot view first
|
|
98
|
+
|
|
99
|
+
Before you confirm a single ad, lead with the **account summary** — the strategist wants the shape before the specimens:
|
|
100
|
+
|
|
101
|
+
> "1,041 ad instances collapsing to 312 distinct creatives. In the Purchases group, 37 creatives produced ~80% of the 12,400 lifetime purchases — and 29 of those 37 are video. That format skew is itself a finding. (The account's second result type, Leads, gets its own line.)"
|
|
102
|
+
|
|
103
|
+
The instance-to-creative collapse, the format skew, the count of winners, the share they carry — that framing *is* strategic value. Deliver it, then move to confirmation.
|
|
104
|
+
|
|
105
|
+
## 5. Visual confirmation — the human gate
|
|
106
|
+
|
|
107
|
+
This is a HITL gate; treat it like one. For each proposed winner, render it with **`ads_get_ad_preview`** (an actual in-context creative image) and show it. The strategist:
|
|
108
|
+
|
|
109
|
+
- **strikes** picks that don't belong,
|
|
110
|
+
- **adds** any obvious winner the rule missed,
|
|
111
|
+
- and may attach an optional free-text **note** per winner (`strategistNote`).
|
|
112
|
+
|
|
113
|
+
Their edits win over the algorithm. Don't argue the rule against their eyes — they made these ads. Only after this pass do you compose verdicts and write the package.
|
|
114
|
+
|
|
115
|
+
## 6. Naming is read-only
|
|
116
|
+
|
|
117
|
+
Campaign / adset / ad names feed two things: your **awareness inference** (§2b) and the winner's `sourceNames` as creative context. That's all. **No naming-conventions guidance** — nobody is asked to rename anything, and the account's naming hygiene is not your concern.
|
|
118
|
+
|
|
119
|
+
## 7. The verdict snapshot & the winner-package JSON
|
|
120
|
+
|
|
121
|
+
Every winner carries a **verdict snapshot** composed to the schema below. Single-instance metrics stay as **display strings verbatim** — never parsed into numbers in what you store. An aggregate winner (§2c) is the one exception: its `results`/`spend` are sums you derived across its instances, and its sentence names the instance count so nobody mistakes a derived total for a Meta string. You compose one plain-language sentence per winner. A **fresh snapshot is written on every re-mine; snapshots are never patched.**
|
|
122
|
+
|
|
123
|
+
```jsonc
|
|
124
|
+
{
|
|
125
|
+
"version": 1,
|
|
126
|
+
"source": {
|
|
127
|
+
"adAccountId": "act_123…",
|
|
128
|
+
"adAccountName": "Ground Co",
|
|
129
|
+
"pageId": "10715…", // scrape target + own-brand row key (from effective_object_story_id prefix)
|
|
130
|
+
"pageName": "Ground Co",
|
|
131
|
+
"country": "US", // optional, default US — non-US accounts false-unmatch otherwise
|
|
132
|
+
"minedAt": "2026-07-14T18:30:00Z"
|
|
133
|
+
},
|
|
134
|
+
"winners": [
|
|
135
|
+
{
|
|
136
|
+
"accountAdId": "1202…", // the natural key; REQUIRED
|
|
137
|
+
"campaignId": "1201…",
|
|
138
|
+
"adSetId": "1202…",
|
|
139
|
+
"sourceNames": { "campaign": "…", "adSet": "…", "ad": "…" }, // verbatim, read-only
|
|
140
|
+
"createdTime": "2025-11-02T…",
|
|
141
|
+
"format": "video", // "video" | "image"; REQUIRED
|
|
142
|
+
"bodyText": "…", // verbatim; bodyText OR headline required
|
|
143
|
+
"headline": "…", // optional
|
|
144
|
+
"ctaType": "SHOP_NOW", // optional
|
|
145
|
+
"effectiveObjectStoryId": "pageId_postId", // optional
|
|
146
|
+
"verdict": {
|
|
147
|
+
"sentence": "Winner by contribution: 2,214 purchases (6.2% of group) @ $18.20 across 4 ad instances, all-time through Jul 2026", // REQUIRED — name the instance count when the winner aggregates >1
|
|
148
|
+
"resultType": "Purchases", // the self-label from results.value's parenthetical — also the scoring-group key (§2c)
|
|
149
|
+
"results": "2,214", // single instance: Meta's display string verbatim; aggregate: your derived sum (the sentence names the instance count)
|
|
150
|
+
"resultsShare": "6.2%", // YOU derive this (share of the result-type group's total)
|
|
151
|
+
"spend": "$40,297", // same rule as results: verbatim for single instances, your derived sum for aggregates
|
|
152
|
+
"costPerResult": "$18.20",
|
|
153
|
+
"asOf": "2026-07-14",
|
|
154
|
+
"objective": "OUTCOME_SALES",
|
|
155
|
+
"awarenessLevel": "product-aware", // from the confirmed mapping (§2b)
|
|
156
|
+
"selectionBasis": ["overall", "top-video"], // which cut(s) it cleared
|
|
157
|
+
"strategistNote": "…" // optional, from §5
|
|
158
|
+
},
|
|
159
|
+
"assets": {
|
|
160
|
+
"imagePath": "./media/1202….jpg", // image winners: downloaded at interview time
|
|
161
|
+
"videoPath": "./media/1202….mp4", // user-supplied file (gap-filler)
|
|
162
|
+
"posterPath": "./media/1202…-poster.jpg" // video winners: REQUIRED in practice — the gallery's only thumbnail (§3)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Verdict metrics are display strings verbatim (aggregates: derived sums, named as such — §7 intro). `assets` is optional in the schema, but a video winner without `posterPath` ships a blank gallery card (§3). A **malformed winner is rejected individually server-side** (with a reason in the ledger) — it never aborts the whole import.
|
|
170
|
+
|
|
171
|
+
## 8. Import — dry-run, push, then the video gap-filler
|
|
172
|
+
|
|
173
|
+
Run these in this exact order.
|
|
174
|
+
|
|
175
|
+
**1. Write the package to disk.** Put it at `state/own-brand-winners/import-<date>.json` (user-inspectable and re-runnable), with the interview-time downloaded media in a folder alongside it. **Asset paths are relative to the JSON file's folder** — so `./media/…jpg` next to `import-<date>.json` resolves correctly.
|
|
176
|
+
|
|
177
|
+
**2. Dry-run first, always:**
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npx @aicopycoders/exodus winners import state/own-brand-winners/import-<date>.json --dry-run --json
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Show the user the would-create vs would-update split. **If the dry-run reports the Scrape Creators key missing, surface it now** — a real import will fail without it; route them to Settings → Keys (scrapecreators.com). **If it warns that a video winner has no posterPath, go back and grab the poster (§3) before pushing** — that warning is the difference between a gallery of ads and a wall of blank placeholders. Fix any would-reject rows, then push for real:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npx @aicopycoders/exodus winners import state/own-brand-winners/import-<date>.json --json
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Default polling waits for outcomes. Present the **per-winner outcome table** it returns — each row is `matched` / `gap-filled` / `partial (missing: …)` / `rejected (reason)` — plus the created-vs-updated split.
|
|
190
|
+
|
|
191
|
+
**3. The two-phase video gap-filler.** Do **not** open with "find me 12 mp4 files." Push everything first; the report names exactly which video winners came back `partial (missing: video-file)` (expected — MCP has no video download). Then ask the user for **just those** files — they made the ads, they usually have them. Write a **second import JSON** containing only the partial winners: copy each winner's **full object** from the first import file (the server validates every push the same way — a stripped-down winner missing its `verdict`, `format`, or copy is rejected, not merged), same `source` block, and add the `videoPath`. Re-push it — the idempotent upgrade path completes those rows in place (the identical verdict re-replacing itself is harmless):
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npx @aicopycoders/exodus winners import state/own-brand-winners/gapfill-<date>.json --json
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Pick gap-fill candidates by their `missing` array **containing** `video-file` — an unmatched video winner usually lists `transcript` too. The re-push fills the file gap, but transcription runs as a **separate sweep after the import finalizes**, so the row typically still reads `partial (missing: transcript)` right away. That's not a failure — tell the user, and re-check `exodus winners list` later for the transcript to land.
|
|
198
|
+
|
|
199
|
+
Any winner the user can't supply a file for **stays amber and visible** in `exodus winners list` — that's fine, not a failure.
|
|
200
|
+
|
|
201
|
+
**4. "What does Exodus already hold?" is `exodus winners list`** — never keep the imported-winners ledger agent-side; it lives server-side and this is how you read it.
|
|
202
|
+
|
|
203
|
+
**5. Re-push safety** (from the CLI itself): re-pushing never duplicates rows — verdict snapshots replace wholesale, gap-filled rows upgrade in place, winners absent from a re-push are left untouched. But it is not free: every push re-uploads referenced assets, re-runs enrichment, and re-fires the Scrape Creators match scrape (billed to the member) — so re-push when there's something to update, not as a retry reflex.
|
|
204
|
+
|
|
205
|
+
## 9. Definition persistence
|
|
206
|
+
|
|
207
|
+
Persist the interview to `state/own-brand-winners.json`, keyed by **brand slug** (`state/` survives skill refreshes). It holds:
|
|
208
|
+
|
|
209
|
+
- pinned `adAccountId` + name (§1),
|
|
210
|
+
- `campaignAwarenessMap` (§2b),
|
|
211
|
+
- editable `defaults` — `{ "window": "maximum", "resultsFloor": 10, "contributionLine": 0.80 }`,
|
|
212
|
+
- `customDefinition` (the §2a escape-hatch text, verbatim),
|
|
213
|
+
- `lastMinedAt`.
|
|
214
|
+
|
|
215
|
+
Re-running re-asks **nothing already pinned** — only genuinely new campaigns get a confirm pass. Losing the file costs a short re-interview, never duplicate rows (dedup is server-side on `accountAdId`). Defaults are changed by editing the file, never by interrogating the user.
|
|
216
|
+
|
|
217
|
+
## Failure handling
|
|
218
|
+
|
|
219
|
+
- **Meta `ads_*` tools not in the session** — stop and point at `https://mcp.facebook.com/ads` (§0). No workaround exists here.
|
|
220
|
+
- **`ads_get_ad_entities` 500 with a metric `filtering`** — that's the known filter bug, not a transient. Drop the filter, sort instead, threshold agent-side (§3).
|
|
221
|
+
- **`ads_get_ad_entities` 500 on an account-wide (or campaign-level) `results` pull** — structural, not transient; retries never help. Scope to one campaign per call and aggregate agent-side (§3).
|
|
222
|
+
- **Any tool 500s once on a scoped call** — retry the identical call once before reporting it.
|
|
223
|
+
- **Dry-run warns "video winner has no posterPath"** — stop and grab the poster via `ads_get_ad_videos` (§3); pushing without it leaves a blank placeholder in the gallery.
|
|
224
|
+
- **A by-id lookup returns only `{id}`** — you omitted `fields`. Re-call with explicit `fields` (§3).
|
|
225
|
+
- **Signed image URL 403 / expired** — it lapsed since the interview. Re-fetch `ads_get_ad_images` and download again immediately.
|
|
226
|
+
- **Dry-run says Scrape Creators key missing** — real import will fail; Settings → Keys first.
|
|
227
|
+
- **`partial (missing: video-file)`** — expected for video winners; run the §8 gap-filler.
|
|
228
|
+
- **Import poll timed out** — the import is still running server-side; re-poll with `npx @aicopycoders/exodus winners status <importId>` rather than re-pushing.
|
|
229
|
+
- **Import auth / wrong brand** — `npx @aicopycoders/exodus doctor`, then confirm `npx @aicopycoders/exodus brand current` is the brand you mean (switch via the `exodus-brand` skill). Winners scope to the active brand.
|
|
230
|
+
|
|
231
|
+
## Not the right skill for
|
|
232
|
+
|
|
233
|
+
- **Competitor swipe mining** — this skill is the member's OWN ad account only. Mining competitors' ads lives on the dashboard's **Swipe Mining** surfaces (and the CLI swipe commands), not here.
|
|
234
|
+
- **Naming-conventions guidance** — names are read-only inputs; nobody is asked to rename anything.
|
|
235
|
+
- **Designating losers** — winners only at v1.
|
|
236
|
+
- **Scheduled / automated re-mining cadence** — re-run the skill when you want a fresh snapshot; there's no scheduler.
|
|
237
|
+
- **Multi-account handling beyond the single pin** — one account per definition file.
|
|
238
|
+
- **Meta MCP connection troubleshooting beyond the one URL**, and **any direct Meta Graph API call** — out of bounds; the MCP is the only Meta surface.
|
|
239
|
+
- **Writing copy or rendering images from these winners** — that happens downstream once they're stored (`exodus-genesis`, `exodus-image`); this skill only collects and imports.
|