@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/dist/cli.js +1163 -69
- package/feast/SKILL.md +13 -50
- package/feast/references/setup.md +59 -0
- package/feast/references/workflows/ad-copy-creator.md +94 -0
- package/feast/references/workflows/{facebook.md → ad-copy-guest.md} +12 -90
- package/feast/references/workflows/ads.md +4 -4
- package/feast/references/workflows/campaigns.md +9 -1
- package/feast/references/workflows/creators.md +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 #
|
|
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"
|
|
138
|
-
- "You belong to multiple organizations" → pick one with `--org
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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:
|
|
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:
|
|
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 — `
|
|
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)
|
|
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 `
|
|
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.
|
|
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": {
|