@feastalytics/cli 0.1.10 → 0.1.12

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/feast/SKILL.md CHANGED
@@ -9,41 +9,7 @@ Drive the Feastalytics platform from the terminal. The `feast` CLI exposes the s
9
9
 
10
10
  The CLI is the source of truth for *which* tools exist and *what* they accept — always discover that at runtime rather than assuming, because the tool set grows as new endpoints are tagged. Your job is to pick the right tool, scope it to the right organization, and hand it valid input.
11
11
 
12
- ## Prerequisites
13
-
14
- The `feast` CLI must be installed and on PATH:
15
-
16
- ```bash
17
- npm install -g @feastalytics/cli # or run ad-hoc with: npx @feastalytics/cli <command>
18
- ```
19
-
20
- Authenticate once — tokens are cached in `~/.config/feast-cli/credentials.json` and refreshed automatically:
21
-
22
- ```bash
23
- feast login # opens a browser to authorize (default)
24
- feast login --password [username] # headless / CI: username + password prompt, no browser
25
- ```
26
-
27
- If a command reports you're not logged in or the session expired, re-run `feast login`.
28
-
29
- ## Staying current
30
-
31
- Neither the CLI nor this skill updates itself. When a command prints an update notice on stderr:
32
-
33
- ```
34
- Update available: feast 0.1.1 → 0.2.0
35
- ```
36
-
37
- update both, then tell the user in one line that you did:
38
-
39
- ```bash
40
- npm install -g @feastalytics/cli@latest # only if `feast` is already on PATH from a global install
41
- npx skills add feastalytics/cli -g -a '*' -y # refresh this skill from the repo
42
- ```
43
-
44
- If you've been invoking the CLI through `npx` rather than a global install, skip the `npm install -g` and use `npx @feastalytics/cli@latest <command>` for the rest of the session instead — `npx` reuses a cached copy otherwise.
45
-
46
- Update the skill whenever you update the CLI: the two ship from the same repo but on different triggers, so a new CLI version usually means this skill's guidance has moved too. If the global install fails on permissions, don't retry with `sudo` — tell the user and fall back to `npx @feastalytics/cli@latest`.
12
+ Some environments hand you the CLI already installed, already authenticated, and pinned to one organization. If `feast tools` runs, you're set — otherwise, installing, logging in, and staying current are in `references/setup.md`.
47
13
 
48
14
  ## The core loop: discover → describe → call
49
15
 
@@ -59,17 +25,9 @@ Always `describe` an unfamiliar tool before calling it — the schema tells you
59
25
 
60
26
  ## Organizations: never let the API guess
61
27
 
62
- Most tools act on one organization. A user often belongs to several, so which one you target matters and must be explicit.
28
+ Most tools act on one organization, and which one must be explicit: pass it with `--org <organizationId>`. Acting on the wrong restaurant is worse than stopping to ask, so the CLI refuses rather than guessing when the target is ambiguous.
63
29
 
64
- ```bash
65
- feast whoami # shows the logged-in user and every org (with names) they can act on
66
- ```
67
-
68
- Pass the target org with `--org <organizationId>`:
69
-
70
- - If the user names an org, resolve it to its id with `feast whoami` and pass that id.
71
- - If the user belongs to exactly one org, the CLI uses it automatically — no flag needed.
72
- - If they belong to more than one and you omit `--org`, the CLI refuses and lists the orgs rather than silently picking one. That's intentional: acting on the wrong org is worse than stopping to ask. When this happens, surface the list to the user and confirm which one they mean.
30
+ If you weren't given an organization id, or a command reports you belong to several, `references/setup.md` has how to resolve one.
73
31
 
74
32
  ## Reads vs. writes
75
33
 
@@ -100,14 +58,19 @@ Many tasks are multi-step and have a required ordering the app normally enforces
100
58
  | Creating, cloning or configuring a campaign; promotions | `references/workflows/campaigns.md` |
101
59
  | Anything touching automations — creating, editing, simulating, promoting a draft | `references/workflows/automations.md` |
102
60
  | Editing funnel screens, applying a funnel template, staging a new screen | `references/workflows/funnels.md` |
103
- | Writing Meta ad copy — guest-facing or creator recruitment | `references/workflows/facebook.md` |
61
+ | Writing guest-facing Meta ad copy (`adCopy`) | `references/workflows/ad-copy-guest.md` |
62
+ | Writing creator-recruitment ad copy (`recruitmentAdCopy`) | `references/workflows/ad-copy-creator.md` |
104
63
  | Publishing, pausing, budgeting or diagnosing Meta ads | `references/workflows/ads.md` |
105
64
  | Creator sourcing — approving applicants, reviewing content, creatives, payouts | `references/workflows/creators.md` |
106
65
  | Members-program rewards; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
107
66
  | Working the onboarding taskboard; brand identity; phone, media, invites, billing | `references/workflows/onboarding.md` |
108
67
  | Searching guests/members; querying anything via the data catalog | `references/workflows/guests.md` |
109
68
 
110
- Read more than one when a task spans them — a new campaign usually means `campaigns.md` plus `automations.md` and `funnels.md`, and publishing a recruitment ad means `creators.md` plus `facebook.md` and `ads.md`.
69
+ Every row names one file, and one file is the whole answer for that row — pick the row that matches what you're doing and read only it. The two ad-copy rows are mutually exclusive: you are writing to guests or to creators, never both in one piece of copy.
70
+
71
+ Read more than one file when a task genuinely spans steps — a new campaign usually means `campaigns.md` plus `automations.md` and `funnels.md`. Read each one as you reach that step rather than gathering them up front: a file stays in context for the rest of the session, so one you open speculatively is re-read on every later turn.
72
+
73
+ When the ask is a question rather than a change — where something lives, what a field means, which link to send — read the single file the table names and answer from it.
111
74
 
112
75
  Some things are deliberately **not exposed**: replying to a guest or a creator by SMS, firing an automation at a live member, pass image generation, ad-copy generation (write it yourself), and publishing creator content as partnership ads. The workflow files say which. Don't fabricate a call for a workflow whose tools aren't listed by `feast tools` — tell the user that part isn't available yet.
113
76
 
@@ -124,7 +87,7 @@ That file has the dashboard routes with their panel and tab names, the guest-fac
124
87
  User: "add a Free Dessert reward members can redeem for 100 points in my Plum Vietnamese org."
125
88
 
126
89
  ```bash
127
- feast whoami # find the Plum Vietnamese org id
90
+ feast whoami # only if you weren't given the org id already
128
91
  feast describe createMembersProgramReward # learn the input shape (type: item vs name)
129
92
  feast call listMembersProgramRewards --org <orgId> # avoid duplicating an existing reward or catalog item
130
93
  feast call createMembersProgramReward --org <orgId> --input '{"type":"name","name":"Free Dessert","pointsCost":100}'
@@ -134,7 +97,7 @@ The pattern generalizes: identify the org, learn the tool, resolve any reference
134
97
 
135
98
  ## When something fails
136
99
 
137
- - "Not logged in / session expired" → `feast login` (or `feast login --password [username]` in a headless/CI context).
138
- - "You belong to multiple organizations" → pick one with `--org`, using `feast whoami` to get the id.
100
+ - "Not logged in / session expired", or `feast` isn't on PATH → `references/setup.md`.
101
+ - "You belong to multiple organizations" → pick one with `--org`; `references/setup.md` has how to find the id.
139
102
  - "Input does not match the tool schema" → re-read `feast describe <tool>` and fix the named fields.
140
103
  - A tool you expected isn't listed by `feast tools` → it may not be exposed yet; don't fabricate a call, tell the user.
@@ -0,0 +1,59 @@
1
+ # Setup, auth, and organizations
2
+
3
+ One-time and troubleshooting material: getting the `feast` CLI installed and logged in, keeping it and this skill current, and working out which organization to act on. The day-to-day loop lives in `SKILL.md`; you only need this file when something isn't working yet.
4
+
5
+ Some environments hand you a CLI that is already installed and authenticated, and pin you to a single organization. Nothing in this file applies there — if `feast tools` runs and your commands are going to the right restaurant, you are already set up.
6
+
7
+ ## Installing
8
+
9
+ The `feast` CLI must be installed and on PATH:
10
+
11
+ ```bash
12
+ npm install -g @feastalytics/cli # or run ad-hoc with: npx @feastalytics/cli <command>
13
+ ```
14
+
15
+ If the global install fails on permissions, don't retry with `sudo` — tell the user and fall back to `npx @feastalytics/cli@latest`.
16
+
17
+ ## Authenticating
18
+
19
+ Authenticate once — tokens are cached in `~/.config/feast-cli/credentials.json` and refreshed automatically:
20
+
21
+ ```bash
22
+ feast login # opens a browser to authorize (default)
23
+ feast login --password [username] # headless / CI: username + password prompt, no browser
24
+ ```
25
+
26
+ If a command reports you're not logged in or the session expired, re-run `feast login`.
27
+
28
+ ## Staying current
29
+
30
+ Neither the CLI nor this skill updates itself. When a command prints an update notice on stderr:
31
+
32
+ ```
33
+ Update available: feast 0.1.1 → 0.2.0
34
+ ```
35
+
36
+ update both, then tell the user in one line that you did:
37
+
38
+ ```bash
39
+ npm install -g @feastalytics/cli@latest # only if `feast` is already on PATH from a global install
40
+ npx skills add feastalytics/cli -g -a '*' -y # refresh this skill from the repo
41
+ ```
42
+
43
+ If you've been invoking the CLI through `npx` rather than a global install, skip the `npm install -g` and use `npx @feastalytics/cli@latest <command>` for the rest of the session instead — `npx` reuses a cached copy otherwise.
44
+
45
+ Update the skill whenever you update the CLI: the two ship from the same repo but on different triggers, so a new CLI version usually means this skill's guidance has moved too.
46
+
47
+ ## Which organization
48
+
49
+ Most tools act on one organization. A user often belongs to several, so which one you target matters and must be explicit.
50
+
51
+ ```bash
52
+ feast whoami # shows the logged-in user and every org (with names) they can act on
53
+ ```
54
+
55
+ Pass the target org with `--org <organizationId>`:
56
+
57
+ - If the user names an org, resolve it to its id with `feast whoami` and pass that id.
58
+ - If the user belongs to exactly one org, the CLI uses it automatically — no flag needed.
59
+ - If they belong to more than one and you omit `--org`, the CLI refuses and lists the orgs rather than silently picking one. That's intentional: acting on the wrong org is worse than stopping to ask. When this happens, surface the list to the user and confirm which one they mean.
@@ -0,0 +1,94 @@
1
+ # Creator-recruitment ad copy (`recruitmentAdCopy`)
2
+
3
+ > Part of the Feastalytics CLI workflows. Confirm a tool exists with `feast tools` before relying on it, and get its exact fields from `feast describe <tool>` — this file gives the *meaning* and *ordering* the schema can't.
4
+
5
+ This is the creator-facing half of Meta ad copy: `recruitmentAdCopy`, which sells a paid collaboration to a content creator shopping for brand deals.
6
+
7
+ **Writing to guests instead? Read `ad-copy-guest.md` and not this file.** `adCopy` sells the offer and the food to a hungry local scrolling past — a different audience and a completely different pitch. Conflating the two is the failure mode in this area, and it has happened repeatedly. A creator is not a customer; the food is their perk, not the pitch. If you catch yourself writing "claim your voucher" here, stop and start over.
8
+
9
+ For the creator program itself — applicants, visits, briefs, the decision loop — read `creators.md`. Publishing the finished ad is a separate job with its own loop — read `ads.md`.
10
+
11
+ **You write the copy yourself.** The dashboard has a "generate copy" button behind an LLM call; there is no CLI equivalent and you shouldn't want one, because it would be you calling an HTTP endpoint in order to call a model. The copy lands on a plain field of the campaign record, so saving it is trivial and covered at the bottom of this file. Everything between here and there is the part that's actually hard.
12
+
13
+ ## Variations: write a set, not a single
14
+
15
+ Meta's Advantage+ creative optimization tests combinations of headlines and primary texts against each other, so you're writing a *set* — several headlines and several primary texts that genuinely differ.
16
+
17
+ **Genuinely** is the load-bearing word. There is no required count. Three sharp variations that each take a real angle beat five where two are padding, and a set of near-identical rewrites teaches the optimizer nothing. Write as many as the collab actually supports. Judge it, and stop when the next variation would be filler.
18
+
19
+ The other reason to write more than one is that the restaurant may want to pick, and people form opinions by seeing alternatives rather than by being handed a single answer. So offering the set is usually the right move — but you have taste, and you should use it. Recommend the one you'd launch and say why. Cut the weak ones before anyone sees them rather than padding them in to look thorough. Three you'd defend beats five you wouldn't.
20
+
21
+ ## The audience
22
+
23
+ **Local food and lifestyle content creators** on Instagram and TikTok — someone scrolling for brand collabs, not a hungry person hunting a deal. The ad is decoupled from any consumer campaign the restaurant is running, *even if one is running right now*. Your job is to get the right creator to tap "Learn More" on a landing page that explains the collab in full — not to close the deal inside the ad.
24
+
25
+ ## Absolute rules — this is exactly where past generations went wrong
26
+
27
+ - **Never mention an offer, deal, voucher, promotion, discount, "claiming" anything, or pre-paying.** This is a collaboration, not a customer offer.
28
+ - **Don't pitch the food the way you'd pitch it to a diner.** The food is the perk; the collab is the pitch.
29
+ - **No customer-facing language** — "claim your voucher", "come hungry", "limited time offer", "this week only", "tap below to save".
30
+ - **Don't reuse the campaign's guest-facing framing** — banner copy, promotions, offer headlines. None of it belongs here, however good it is.
31
+ - **If there's a cash bonus, never imply it's automatic or guaranteed.** It is earned only if the restaurant selects the creator's reel to run as a paid ad. Phrasings like "earn a $100 bonus", "get a $100 bonus when you post", or "$100 bonus if you nail the brief" read as guaranteed-on-completion, and they have caused real creators to demand a bonus they hadn't earned. Always frame it conditionally: "a chance to earn", "up to", "if your reel gets picked to run as an ad".
32
+
33
+ ## What to pitch
34
+
35
+ - The restaurant is booking local food/lifestyle creators to come in, eat on the house, and post a short reel.
36
+ - **The creator gets:** a dining credit (order whatever they want), a creative brief with style direction but no script — they stay authentic — and, when acquisition is enabled, their reel boosted as a paid partnership ad alongside the restaurant's Instagram, which is free promotion to thousands of local foodies plus followers and engagement on their own page. Where a bonus exists, add it conditionally.
37
+ - **The creator gives:** one 30–60 second vertical reel (Instagram Reel / TikTok), filmed during the visit, submitted within 72 hours.
38
+ - **Eligibility:** an active food/lifestyle creator with a minimum local-area follower count on Instagram or TikTok.
39
+
40
+ ## Angles — rotate across the set
41
+
42
+ With a bonus: **get paid** ("Get paid to eat at X") → **free food + free promotion**, both sides of the exchange → **local creator call-out** ("Local foodies on IG — we want you") → **grow your page**, the boost and the new followers → **straightforward collab pitch**, no fluff: free meal + paid post + bonus.
43
+
44
+ Without a bonus, swap the first for **free food collab** ("Eat on us at X"), and grow-your-page for **brand partnership** (a collaboration, not a giveaway) or **behind-the-scenes** (be part of the restaurant's story).
45
+
46
+ Take as many of these as the collab genuinely supports rather than filling a quota.
47
+
48
+ ## Tone
49
+
50
+ Talk like a brand DM'ing a creator about a collab, not like a restaurant running an ad. Confident, peer-to-peer, slightly insider: "We're partnering with…", "We're booking creators for…", "Looking for local foodies who…".
51
+
52
+ Specifics over fluff — name the dollar amounts, the deliverable (one reel, 30–60s), the eligibility. Emoji fine in moderation (📸 🎥 🍴), don't spam. **Separate every sentence with a blank line (two newlines)** — each sentence has to stand alone visually, because a paragraph is a wall and a wall gets skipped.
53
+
54
+ **GOOD primary text:**
55
+
56
+ ```
57
+ Plum Vietnamese is booking local food creators this month. 📸
58
+
59
+ You get a $30 tab on us, a creative brief, and a chance to earn a $100 bonus if your reel gets run as a paid ad.
60
+
61
+ We'll also boost it as a partnership ad — free promo to thousands of local foodies. 1,000+ local IG/TikTok followers to apply.
62
+ ```
63
+
64
+ **GOOD headlines:** `Get paid to post about Plum` · `Local creators — eat free, post a reel` · `Foodies w/ 1,000+ followers, read this 👀`
65
+
66
+ **BAD — do not generate this:**
67
+
68
+ ```
69
+ Free meal at Plum Vietnamese this week! Claim your voucher and come hungry — you won't want to miss this deal. 🍴
70
+ ```
71
+
72
+ That's a customer offer wearing a creator ad's clothes. Every one of "free meal", "claim your voucher", "come hungry" and "this deal" is independently disqualifying. Note what the good version does instead: it names the restaurant as the one doing the booking, states the exchange in plain numbers, and gates on follower count — so the wrong reader self-selects out in the first line.
73
+
74
+ ## The terms are baked into the copy — record them
75
+
76
+ `foodCreditCents`, `creatorPayoutCents` and `minFollowerCount` on `recruitmentAdCopy` record the terms your copy actually stated. The dashboard compares them against the live creator board config and flags the copy as drifted when they diverge — so if you write "$30 tab" and leave them unset, nobody finds out when the credit later changes to $50 and the ad starts lying.
77
+
78
+ **Read the creator board config with `getInfluencerBoardConfig` first** — the dining credit, the bonus and the follower minimum live there and nowhere else. Write those exact numbers into the copy, and mirror them into these fields (in **cents** for the two money fields). Don't guess them, and don't ask the user for numbers the config already has.
79
+
80
+ The creator landing page is `/creator-landing` on the org's subdomain with `orgId`, `locId`, `campaignId` and UTM params — fiddly enough that you should reuse the existing `recruitmentAdCopy.landingPageUrl` when the campaign already has copy, rather than reconstructing it.
81
+
82
+ ## Saving it
83
+
84
+ One `updateCampaign` call writes `recruitmentAdCopy`. Run `feast describe updateCampaign` for the fields — alongside the headlines and primary texts it wants the landing page URL, the creative mix, a timestamp, and optional indices for the variation you're recommending.
85
+
86
+ **The one thing the schema won't tell you: `update.adCopy` replaces the whole object rather than merging into it.** Read the campaign with `getCampaign` first and send back everything you want kept, not just what changed.
87
+
88
+ ## Publishing
89
+
90
+ **CLI-drivable — read `ads.md`.** The loop is `listAdTemplates` → gather variables → `planAds` → `publishAds` (with its effects) → `getJob` → `setAdCampaignStatus`, and that file carries the ordering, the idempotency-key discipline, and the effect declarations that make the publish self-bookkeeping.
91
+
92
+ ---
93
+
94
+ > **Not exposed:** ad copy generation (write it yourself, per above). Read `references/links.md` before writing any dashboard link you hand over.
@@ -1,19 +1,14 @@
1
- # Meta ads
1
+ # Guest-facing ad copy (`adCopy`)
2
2
 
3
3
  > Part of the Feastalytics CLI workflows. Confirm a tool exists with `feast tools` before relying on it, and get its exact fields from `feast describe <tool>` — this file gives the *meaning* and *ordering* the schema can't.
4
4
 
5
- Two jobs live under Meta ads: **writing the ad copy** and **publishing the ad**. This file is the copywriting half; the publish loop (templates, planning, effects, activation) lives in `ads.md`.
5
+ This is the guest-facing half of Meta ad copy: `adCopy`, which sells the offer and the food to a hungry local scrolling past.
6
6
 
7
- **You write the copy yourself.** The dashboard has a "generate copy" button behind an LLM call; there is no CLI equivalent and you shouldn't want one, because it would be you calling an HTTP endpoint in order to call a model. The copy lands on a plain field of the campaign record, so saving it is trivial and covered at the bottom of this file. Everything between here and there is the part that's actually hard.
8
-
9
- ## First: which audience are you writing for?
10
-
11
- Two fields, two completely different pitches:
7
+ **Writing to creators instead? Read `ad-copy-creator.md` and not this file.** `recruitmentAdCopy` sells a paid collaboration to a content creator shopping for brand deals — a different audience and a completely different pitch. Conflating the two is the failure mode in this area, and it has happened repeatedly. A creator is not a customer; the food is their perk, not the pitch.
12
8
 
13
- - **`adCopy`** — guest-facing. Sells the offer and the food to a hungry local scrolling past.
14
- - **`recruitmentAdCopy`** — creator-facing. Sells a paid collaboration to a content creator shopping for brand deals.
9
+ Publishing the finished ad is a separate job with its own loop — read `ads.md` for that.
15
10
 
16
- **Conflating them is the failure mode in this area — it has happened repeatedly.** A creator is not a customer; the food is their perk, not the pitch. Decide which one you're writing before you write a word, and then read only that section below. If you catch yourself writing "claim your voucher" in a recruitment ad, stop and start over.
11
+ **You write the copy yourself.** The dashboard has a "generate copy" button behind an LLM call; there is no CLI equivalent and you shouldn't want one, because it would be you calling an HTTP endpoint in order to call a model. The copy lands on a plain field of the campaign record, so saving it is trivial and covered at the bottom of this file. Everything between here and there is the part that's actually hard.
17
12
 
18
13
  ## Variations: write a set, not a single
19
14
 
@@ -23,11 +18,7 @@ Meta's Advantage+ creative optimization tests combinations of headlines and prim
23
18
 
24
19
  The other reason to write more than one is that the restaurant may want to pick, and people form opinions by seeing alternatives rather than by being handed a single answer. So offering the set is usually the right move — but you have taste, and you should use it. Recommend the one you'd launch and say why. Cut the weak ones before anyone sees them rather than padding them in to look thorough. Three you'd defend beats five you wouldn't.
25
20
 
26
- ---
27
-
28
- ## Guest-facing copy (`adCopy`)
29
-
30
- ### Read before you write
21
+ ## Read before you write
31
22
 
32
23
  Generic copy is the failure mode, and specifics are the entire job. The difference between an ad that works and one that doesn't is almost never cleverness — it's whether the copy contains something only this restaurant could have said. So go get those things first:
33
24
 
@@ -40,7 +31,7 @@ The landing page URL is `https://{referrer}.feastalytics.com/campaign/{campaignI
40
31
 
41
32
  Mine all of it for things a human would actually remember: opening dates, the street, menu item names, sweepstakes mechanics, numbers, proper nouns. **If the source has real specifics and your copy says "taco time!", you did it wrong.** When you finish a draft, check that you couldn't paste it onto a different restaurant's campaign without anyone noticing.
42
33
 
43
- ### Headlines — each ≤ 40 characters
34
+ ## Headlines — each ≤ 40 characters
44
35
 
45
36
  The headline appears *below* the image or video. Forty characters is a hard ceiling; Meta truncates past it, and a headline that dies mid-word looks broken.
46
37
 
@@ -54,7 +45,7 @@ Each headline in your set should take a **different angle**. These five are the
54
45
 
55
46
  No generic marketing language. Write like a person, not a brand.
56
47
 
57
- ### Primary text — each 2–4 sentences
48
+ ## Primary text — each 2–4 sentences
58
49
 
59
50
  The primary text appears *above* the image or video. It's the first thing anyone reads, and it's read in a fast scroll on a phone.
60
51
 
@@ -65,7 +56,7 @@ The primary text appears *above* the image or video. It's the first thing anyone
65
56
  - Emoji are welcome when they add energy or visual punch. **At most one per sentence**, never forced. A well-placed emoji > no emoji > emoji spam.
66
57
  - Each variation takes a different approach, but all of them must work whether the viewer sees a static image or a video (see `creativeMix` below).
67
58
 
68
- ### Voice
59
+ ## Voice
69
60
 
70
61
  Write like you're texting a friend about a spot you're genuinely hyped about. Not like a brand's social media manager. Not like a restaurant's About page.
71
62
 
@@ -108,7 +99,7 @@ Claim your voucher and come see what the hype is about.
108
99
 
109
100
  Study what actually changes between them. The bad versions aren't wrong on the facts — they carry the same information. They fail because they *announce* where the good ones *react*. "We are giving away" is a press release; "Yeah, you read that right" is a person. The good versions also front-load the hook into the first line, break every sentence onto its own visual row, and trade a complete sentence for a fragment wherever the fragment hits harder. Notice too that neither good version is longer than the bad one it replaces — this is compression, not decoration.
110
101
 
111
- ### `creativeMix` changes what the copy may assume
102
+ ## `creativeMix` changes what the copy may assume
112
103
 
113
104
  Set this to what's actually true of the assets that will run, then write to it:
114
105
 
@@ -118,82 +109,15 @@ Set this to what's actually true of the assets that will run, then write to it:
118
109
 
119
110
  Copy that only makes sense next to a visible offer, running as `mixed`, will quietly underperform for half the audience.
120
111
 
121
- ### Video-led campaigns: the one thing you can't do
112
+ ## Video-led campaigns: the one thing you can't do
122
113
 
123
114
  The in-app generator **feeds the video assets to the model as multimodal input** and mines them for quotes, moments and on-screen specifics that end up in the copy. **You cannot watch a video from the CLI.**
124
115
 
125
116
  So for a `video_only` or `mixed` campaign, either work from a description or transcript the user gives you — saying plainly that's what you're working from — or write what you can from the landing page and tell the user the in-app dialog will do better here, because it can see the footage. Don't quietly produce video-campaign copy that never references the video and present it as equivalent. It isn't.
126
117
 
127
- ---
128
-
129
- ## Creator-recruitment copy (`recruitmentAdCopy`)
130
-
131
- **Read this section only when writing `recruitmentAdCopy`.**
132
-
133
- The audience is **local food and lifestyle content creators** on Instagram and TikTok — someone scrolling for brand collabs, not a hungry person hunting a deal. The ad is decoupled from any consumer campaign the restaurant is running, *even if one is running right now*. Your job is to get the right creator to tap "Learn More" on a landing page that explains the collab in full — not to close the deal inside the ad.
134
-
135
- ### Absolute rules — this is exactly where past generations went wrong
136
-
137
- - **Never mention an offer, deal, voucher, promotion, discount, "claiming" anything, or pre-paying.** This is a collaboration, not a customer offer.
138
- - **Don't pitch the food the way you'd pitch it to a diner.** The food is the perk; the collab is the pitch.
139
- - **No customer-facing language** — "claim your voucher", "come hungry", "limited time offer", "this week only", "tap below to save".
140
- - **Don't reuse the campaign's guest-facing framing** — banner copy, promotions, offer headlines. None of it belongs here, however good it is.
141
- - **If there's a cash bonus, never imply it's automatic or guaranteed.** It is earned only if the restaurant selects the creator's reel to run as a paid ad. Phrasings like "earn a $100 bonus", "get a $100 bonus when you post", or "$100 bonus if you nail the brief" read as guaranteed-on-completion, and they have caused real creators to demand a bonus they hadn't earned. Always frame it conditionally: "a chance to earn", "up to", "if your reel gets picked to run as an ad".
142
-
143
- ### What to pitch
144
-
145
- - The restaurant is booking local food/lifestyle creators to come in, eat on the house, and post a short reel.
146
- - **The creator gets:** a dining credit (order whatever they want), a creative brief with style direction but no script — they stay authentic — and, when acquisition is enabled, their reel boosted as a paid partnership ad alongside the restaurant's Instagram, which is free promotion to thousands of local foodies plus followers and engagement on their own page. Where a bonus exists, add it conditionally.
147
- - **The creator gives:** one 30–60 second vertical reel (Instagram Reel / TikTok), filmed during the visit, submitted within 72 hours.
148
- - **Eligibility:** an active food/lifestyle creator with a minimum local-area follower count on Instagram or TikTok.
149
-
150
- ### Angles — rotate across the set
151
-
152
- With a bonus: **get paid** ("Get paid to eat at X") → **free food + free promotion**, both sides of the exchange → **local creator call-out** ("Local foodies on IG — we want you") → **grow your page**, the boost and the new followers → **straightforward collab pitch**, no fluff: free meal + paid post + bonus.
153
-
154
- Without a bonus, swap the first for **free food collab** ("Eat on us at X"), and grow-your-page for **brand partnership** (a collaboration, not a giveaway) or **behind-the-scenes** (be part of the restaurant's story).
155
-
156
- As with guest copy, take as many of these as the collab genuinely supports rather than filling a quota.
157
-
158
- ### Tone
159
-
160
- Talk like a brand DM'ing a creator about a collab, not like a restaurant running an ad. Confident, peer-to-peer, slightly insider: "We're partnering with…", "We're booking creators for…", "Looking for local foodies who…".
161
-
162
- Specifics over fluff — name the dollar amounts, the deliverable (one reel, 30–60s), the eligibility. Emoji fine in moderation (📸 🎥 🍴), don't spam. The blank-line-between-sentences rule applies here too.
163
-
164
- **GOOD primary text:**
165
-
166
- ```
167
- Plum Vietnamese is booking local food creators this month. 📸
168
-
169
- You get a $30 tab on us, a creative brief, and a chance to earn a $100 bonus if your reel gets run as a paid ad.
170
-
171
- We'll also boost it as a partnership ad — free promo to thousands of local foodies. 1,000+ local IG/TikTok followers to apply.
172
- ```
173
-
174
- **GOOD headlines:** `Get paid to post about Plum` · `Local creators — eat free, post a reel` · `Foodies w/ 1,000+ followers, read this 👀`
175
-
176
- **BAD — do not generate this:**
177
-
178
- ```
179
- Free meal at Plum Vietnamese this week! Claim your voucher and come hungry — you won't want to miss this deal. 🍴
180
- ```
181
-
182
- That's a customer offer wearing a creator ad's clothes. Every one of "free meal", "claim your voucher", "come hungry" and "this deal" is independently disqualifying. Note what the good version does instead: it names the restaurant as the one doing the booking, states the exchange in plain numbers, and gates on follower count — so the wrong reader self-selects out in the first line.
183
-
184
- ### The terms are baked into the copy — record them
185
-
186
- `foodCreditCents`, `creatorPayoutCents` and `minFollowerCount` on `recruitmentAdCopy` record the terms your copy actually stated. The dashboard compares them against the live creator board config and flags the copy as drifted when they diverge — so if you write "$30 tab" and leave them unset, nobody finds out when the credit later changes to $50 and the ad starts lying.
187
-
188
- **Read the creator board config with `getInfluencerBoardConfig` first** — the dining credit, the bonus and the follower minimum live there and nowhere else. Write those exact numbers into the copy, and mirror them into these fields (in **cents** for the two money fields). Don't guess them, and don't ask the user for numbers the config already has.
189
-
190
- The creator landing page is `/creator-landing` on the org's subdomain with `orgId`, `locId`, `campaignId` and UTM params — fiddly enough that you should reuse the existing `recruitmentAdCopy.landingPageUrl` when the campaign already has copy, rather than reconstructing it.
191
-
192
- ---
193
-
194
118
  ## Saving it
195
119
 
196
- One `updateCampaign` call writes `adCopy` or `recruitmentAdCopy`. Run `feast describe updateCampaign` for the fields — alongside the headlines and primary texts it wants the landing page URL, the creative mix, a timestamp, and optional indices for the variation you're recommending.
120
+ One `updateCampaign` call writes `adCopy`. Run `feast describe updateCampaign` for the fields — alongside the headlines and primary texts it wants the landing page URL, the creative mix, a timestamp, and optional indices for the variation you're recommending.
197
121
 
198
122
  **The one thing the schema won't tell you: `update.adCopy` replaces the whole object rather than merging into it.** Read the campaign with `getCampaign` first and send back everything you want kept, not just what changed.
199
123
 
@@ -204,5 +128,3 @@ One `updateCampaign` call writes `adCopy` or `recruitmentAdCopy`. Run `feast des
204
128
  ---
205
129
 
206
130
  > **Not exposed:** ad copy generation (write it yourself, per above). Read `references/links.md` before writing any dashboard link you hand over.
207
-
208
- ---
@@ -4,7 +4,7 @@
4
4
 
5
5
  Publishing is CLI-drivable end to end: resolve a template, plan, publish, activate. The planner is the only door in — **never hand-assemble Meta campaign parameters**; `publishAds` re-derives everything from the template variables and refuses anything else.
6
6
 
7
- For the *words* in the ads, read `facebook.md` first — copywriting is its own discipline with its own failure modes.
7
+ For the *words* in the ads, read the copywriting file for your audience first — `ad-copy-guest.md` for guest-facing offer ads, `ad-copy-creator.md` for creator recruitment. Copywriting is its own discipline with its own failure modes.
8
8
 
9
9
  ### The model: template → plan → publish → activate
10
10
 
@@ -36,8 +36,8 @@ An effect that reports `error` in the job is a case for the dashboard, not for p
36
36
 
37
37
  ### Which template
38
38
 
39
- - **`directOffer`** — guest-facing offer ads for a campaign. Copy rules: the `adCopy` half of `facebook.md`.
40
- - **`recruitment`** — creator-recruitment ads. An always-on trickle with an enforced budget floor and ceiling. Creatives come from `createRecruitmentCreatives` → `listCreatives` (pass each creative's `imageKey` as a `libraryAsset` reference); copy rules: the `recruitmentAdCopy` half of `facebook.md`; program context: `creators.md`.
39
+ - **`directOffer`** — guest-facing offer ads for a campaign. Copy rules: `ad-copy-guest.md`.
40
+ - **`recruitment`** — creator-recruitment ads. An always-on trickle with an enforced budget floor and ceiling. Creatives come from `createRecruitmentCreatives` → `listCreatives` (pass each creative's `imageKey` as a `libraryAsset` reference); copy rules: `ad-copy-creator.md`; program context: `creators.md`.
41
41
  - **`addAds`** — add fresh creatives to an ad set that is already running. Copy the settings the new ads must match from an existing ad via `ads_get_ad_entities` — its description carries the exact field-by-field recipe, and Meta will happily publish a mismatched ad rather than reject it.
42
42
 
43
43
  ### Reading and steering what's live
@@ -47,4 +47,4 @@ An effect that reports `error` in the job is a case for the dashboard, not for p
47
47
  - `ads_activate_entity` — go-live for structures Feastalytics did *not* publish. No cascade: activate top-down and check `willDeliver`; a child under a paused parent is live in name only. For campaigns Feastalytics published, `setAdCampaignStatus` cascades and is the right tool.
48
48
  - `ads_get_datasets` / `ads_create_dataset` — pixel checks and creation. The pixel a campaign should optimise against is the one its funnel actually fires (from the layout config), not whichever pixel looks plausible on the account. After creating one, write its id back with `updateBrandIdentity` — creation alone connects nothing.
49
49
 
50
- > **Not exposed:** ad-copy generation (write it yourself — `facebook.md`), creative *content* editing on Meta (immutable there), and publishing creator content as partnership ads.
50
+ > **Not exposed:** ad-copy generation (write it yourself — `ad-copy-guest.md` / `ad-copy-creator.md`), creative *content* editing on Meta (immutable there), and publishing creator content as partnership ads.
@@ -13,7 +13,15 @@ Fully doable via the CLI. The server does the heavy lifting (id generation, defa
13
13
  - `"reservation"` — no extra config.
14
14
  - `"simpleRewards"` — needs `simpleRewardsConfig` with a `promotionName` and an image. Pass a public `imageUrl` string (the CLI can't do the app's file-upload path).
15
15
  - `"prepay"` — needs `prepayConfig` with `promotionName`, `price`, and an image (`imageUrl`).
16
- 4. (optional) `applyFunnelTemplate` — the **acquisition** half: the funnel screens a guest sees. Requires a fresh campaign whose funnel is unset; resolves the referrer from the campaign.
16
+ 4. (optional) the funnel — the **acquisition** half: the funnel screens a guest sees. Same list → pick → apply shape as automations:
17
+ - `listFunnelTemplates` with the `campaignId` — the template catalog with per-campaign `eligible`/`ineligibleReason`, a `recommended` id, and each template's guest `journey`. Read this before applying; never guess a template id.
18
+ - Pick by what the guest should experience, not by whether the offer has a price:
19
+ - `offer-basic` — Sign Up goes straight to the offer wallet. **No payment step.** The template for any offer redeemed in person, priced or not.
20
+ - `offer-prepay` / `offer-direct-prepay` — a Stripe payment screen is part of the funnel. Only eligible when the promotion has `canPrePay: true` **and** a `price`; anything else is rejected with `PRECONDITION_FAILED`.
21
+ - `reservation-offer-basic` / `reservation-offer-prepay` / `reservation-offer-direct-prepay` / `reservation-only` — the reservation variants of the same split.
22
+ - `applyFunnelTemplate` with `{ "campaignId": ..., "templateId": ... }`. Requires a fresh campaign whose funnel is unset; resolves the referrer from the campaign.
23
+ - The promotion's `canPrePay` flag does **not** change what a template builds — it only gates eligibility. A "no prepay" request means `offer-basic` (or another no-payment template), full stop.
24
+ - After applying, confirm with `listFunnelScreens` that the journey matches intent — for a no-prepay offer there must be no `payment` screen.
17
25
  5. (optional) `applyAutomationTemplate` — the **retention** half: the follow-up messaging. Provisions the campaign's flow *and* its automations in one call, so you don't hand-build a flow for this path. Preview options first with `listAutomationTemplates` / `listTemplateAutomations`.
18
26
 
19
27
  Steps 4 and 5 are the two independent halves of a working campaign — the funnel (what the guest sees) and the automations (the messaging that follows). A fully working campaign has a funnel with no screen errors and at least one automation flow.
@@ -57,7 +57,7 @@ The ads that bring applicants in are CLI-drivable end to end:
57
57
  1. `createRecruitmentCreatives` with the `campaignId` — it resolves (or creates) the campaign's recruitment offer itself, which is what groups the creatives and carries the monthly sourcing cap. Each run calls an image model per missing type; `force` deletes and regenerates the whole set, so don't pass it casually.
58
58
  2. `listCreatives` — each creative's `imageKey` is the reference `planAds` takes as a `libraryAsset`; `staleCreativeIds` flags creatives generated from an older version of their offer.
59
59
  3. Publish through the `recruitment` template in `ads.md`, declaring the **`linkRecruitmentOffer` effect** — the publish is refused without it. The effect stamps the creatives, links the offer (which the sourcing cap and dashboard spend read), and texts the program's approver that sourcing is live.
60
- 4. Copy rules for the ad live in `facebook.md` (`recruitmentAdCopy` — the creator-facing half; conflating it with guest copy is the classic failure).
60
+ 4. Copy rules for the ad live in `ad-copy-creator.md` (`recruitmentAdCopy`; conflating it with guest copy is the classic failure).
61
61
 
62
62
  ### The decision loop
63
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feastalytics/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Command-line client for the Feastalytics platform — list, create, and update campaigns, automations, offers, and members-program rewards from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {