@feastalytics/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/dist/cli.js +18252 -0
- package/feast/SKILL.md +94 -0
- package/feast/references/domains.md +36 -0
- package/feast/references/workflows.md +188 -0
- package/package.json +42 -0
package/feast/SKILL.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feast
|
|
3
|
+
description: Operate a Feastalytics organization from the terminal — list, create, and update campaigns, automations, offers, funnels, and members-program rewards — via the `feast` CLI. Use this skill whenever the user wants to inspect or change Feastalytics data outside the dashboard — "list my campaigns", "create an automation for org X", "what offers does this location have", "clone this campaign", "update the members program", or any request to script/batch/automate Feastalytics operations. Reach for it even when the user doesn't say "CLI" — if the task is reading or changing Feastalytics campaign/automation/offer/reward data, this is the tool.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Feast CLI
|
|
7
|
+
|
|
8
|
+
Drive the Feastalytics platform from the terminal. The `feast` CLI exposes the same tool surface the in-app AI agent uses (campaigns, automations, offers, funnels, members program) as plain commands that hit the production API as the logged-in user.
|
|
9
|
+
|
|
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
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
The `feast` CLI must be installed and on PATH:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @feast/cli # or run ad-hoc with: npx @feast/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 <username>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If a command reports you're not logged in or the session expired, re-run `feast login`.
|
|
27
|
+
|
|
28
|
+
## The core loop: discover → describe → call
|
|
29
|
+
|
|
30
|
+
Don't guess tool names or input shapes. Introspect the live CLI:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
feast tools # list every available tool, its domain, and whether it mutates
|
|
34
|
+
feast describe <tool> # full description + input JSON schema for one tool
|
|
35
|
+
feast call <tool> --org <organizationId> --input '<json>'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Always `describe` an unfamiliar tool before calling it — the schema tells you the exact required fields, and the CLI validates your `--input` against it locally before sending anything, so a bad payload fails fast with a clear message instead of a confusing server error.
|
|
39
|
+
|
|
40
|
+
## Organizations: never let the API guess
|
|
41
|
+
|
|
42
|
+
Most tools act on one organization. A user often belongs to several, so which one you target matters and must be explicit.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
feast whoami # shows the logged-in user and every org (with names) they can act on
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Pass the target org with `--org <organizationId>`:
|
|
49
|
+
|
|
50
|
+
- If the user names an org, resolve it to its id with `feast whoami` and pass that id.
|
|
51
|
+
- If the user belongs to exactly one org, the CLI uses it automatically — no flag needed.
|
|
52
|
+
- 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.
|
|
53
|
+
|
|
54
|
+
## Reads vs. writes
|
|
55
|
+
|
|
56
|
+
Query tools (listing, describing, reading) are safe and read-only. Mutation tools (create, update, clone, delete, apply) change production data, so the CLI adds guards:
|
|
57
|
+
|
|
58
|
+
- Mutations require `--org` explicitly.
|
|
59
|
+
- Before running, the CLI verifies the server-resolved org and prompts for `y/N` confirmation.
|
|
60
|
+
- In a non-interactive context where the user has already told you to proceed, add `--yes` to skip the prompt. Only do this when the user's intent is unambiguous — the confirmation exists to prevent acting on the wrong org or with the wrong payload.
|
|
61
|
+
|
|
62
|
+
Prefer reading before writing: e.g. `listCampaigns` to find the right `campaignId` before `updateCampaign`, or `describe`/`listAutomationFlows` before creating a flow.
|
|
63
|
+
|
|
64
|
+
## Building good input
|
|
65
|
+
|
|
66
|
+
`--input` takes a JSON string (or `--input-file <path>` for larger payloads). Construct it from the schema you got via `describe`. When a tool references another entity by id (a campaign id, location id, flow id), look that id up first with the relevant list/read tool rather than inventing it.
|
|
67
|
+
|
|
68
|
+
For the domain-specific meaning of fields — how automations chain, what a funnel screen contains, how offers are structured — consult the guidance in `references/domains.md` when the schema alone isn't enough.
|
|
69
|
+
|
|
70
|
+
## Workflows
|
|
71
|
+
|
|
72
|
+
Many tasks are multi-step and have a required ordering the app normally enforces. The most important rule: **automations live inside flows — always find a flow (`listAutomationFlows`) or create one (`createAutomationFlow`) before adding automations; never create an orphan automation.** The same "resolve the parent/ids first, then act" shape recurs across campaigns, funnels, and offers.
|
|
73
|
+
|
|
74
|
+
For the ordered steps and domain rules of each common workflow, read `references/workflows.md`. It covers what's **fully doable** — creating/cloning a campaign, authoring automations end-to-end (create/edit/delete/simulate, with the trigger, condition, send-time, and chaining rules that make a flow professional), editing funnel screens (the draft → preview → promote loop), creating offers, and exploring users — and what's **not yet exposed** — members-program reward creation, brand identity, and replying to guests by SMS. 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.
|
|
75
|
+
|
|
76
|
+
## Worked example
|
|
77
|
+
|
|
78
|
+
User: "add a $5-off offer to the Plum location in my Plum Vietnamese org."
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
feast whoami # find the Plum Vietnamese org id
|
|
82
|
+
feast describe dfyCreateOffer # learn the offer input shape
|
|
83
|
+
feast call dfyGetMenuHierarchy --org <orgId> --input '{"locationId":"<id>"}' # find the location/menu ids
|
|
84
|
+
feast call dfyCreateOffer --org <orgId> --input '{ ... }' # create it (confirms first)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The pattern generalizes: identify the org, learn the tool, resolve any referenced ids, then act.
|
|
88
|
+
|
|
89
|
+
## When something fails
|
|
90
|
+
|
|
91
|
+
- "Not logged in / session expired" → `feast login <username>`.
|
|
92
|
+
- "You belong to multiple organizations" → pick one with `--org`, using `feast whoami` to get the id.
|
|
93
|
+
- "Input does not match the tool schema" → re-read `feast describe <tool>` and fix the named fields.
|
|
94
|
+
- 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,36 @@
|
|
|
1
|
+
# Feastalytics domain model
|
|
2
|
+
|
|
3
|
+
Background for constructing tool input correctly. This is the conceptual map; the authoritative field list for any tool always comes from `feast describe <tool>`.
|
|
4
|
+
|
|
5
|
+
## Organizations
|
|
6
|
+
|
|
7
|
+
The top-level tenant. Nearly every tool is scoped to one organization via `--org`. An org has one or more POS locations (Toast/Square/Clover); many tools that operate on menus or offers need a `locationId`, which you get from `loadCurrentOrganization` (it returns the org's `locations`) — not the organization id.
|
|
8
|
+
|
|
9
|
+
## Campaigns (acquisition)
|
|
10
|
+
|
|
11
|
+
A campaign is an acquisition effort. It bundles:
|
|
12
|
+
- a **funnel** (the screens a new guest sees),
|
|
13
|
+
- **automations** (see below) scoped to that campaign,
|
|
14
|
+
- **promotions/offers** attached to it.
|
|
15
|
+
|
|
16
|
+
Typical flow: `createCampaign` (set `isCreating: true` if you'll finish it with `populateCampaign`), then `populateCampaign`, then choose a funnel template. `cloneCampaign` duplicates an existing one (funnel + automations + offers) — it needs the source campaign id and a `referrer` (a subdomain from the org's `subdomains2`).
|
|
17
|
+
|
|
18
|
+
## Automations and flows
|
|
19
|
+
|
|
20
|
+
- An **automation** is one trigger → action unit (e.g. "on checkout, award reward").
|
|
21
|
+
- A **flow** is a named grouping of automations. A flow belongs to *either* a campaign *or* the members program (never both).
|
|
22
|
+
- `listAutomationFlows` scopes with input: `{ campaignId }` returns that campaign's flows; `{ scope: "membersProgram" }` returns members-program flows (those with no campaign). `listAutomations` returns every automation in the org, ordered by execution priority.
|
|
23
|
+
- **Authoring:** `createAutomationFlow` makes a flow; `batchEditAutomations` creates/updates/deletes automations in one atomic batch (create ops **require** a `flowId`); `updateAutomationFlow` / `deleteAutomationFlow` manage the flow itself; `simulateAutomations` dry-runs a flow with no real sends. See `workflows.md` for the ordering and the trigger/condition/send-time rules.
|
|
24
|
+
- Templates: `listAutomationTemplates` → `loadTemplateAutomations` (preview) → `applyAutomationTemplate`. Only apply a template to a campaign/members-program that has no existing flows.
|
|
25
|
+
|
|
26
|
+
## Offers (DFY strategy)
|
|
27
|
+
|
|
28
|
+
Offers live in the organization's strategy backlog. `dfyCreateOffer` needs a `framework` — one of `free`, `combo`, or `experience` — a name, headline, description, and `items` (`[{ name, price }]`). `offerPrice` is null for the `free` framework. Browse the menu to source real item names/prices with `dfyGetMenuHierarchy` (needs a `locationId`). `dfyListOffers` shows the current backlog with status and priority.
|
|
29
|
+
|
|
30
|
+
## Members program (retention)
|
|
31
|
+
|
|
32
|
+
The retention counterpart to campaigns: rewards and pass configuration for returning guests. Members-program automations are the flows with no `campaignId` (`scope: "membersProgram"` above).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
Maintenance note: this file is currently hand-authored. The richest source of this domain knowledge is the in-app agent's prompt files (`src/agent-core/src/prompts/` — AutomationsPrompt, CampaignsPrompt, LayoutEnginePrompt, MembersProgramPrompt, OfferPrompt). A future improvement is to generate this reference from those, so the skill and the in-app agent never drift.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Common workflows
|
|
2
|
+
|
|
3
|
+
Multi-step tasks in Feastalytics have a required ordering the UI normally enforces for you. Driving the API directly, you have to sequence the calls yourself. Each workflow below gives the ordered steps and the domain rules that make the result *good*, not just valid — and flags where a step is **not yet exposed to the CLI** so you don't fabricate a call.
|
|
4
|
+
|
|
5
|
+
Always confirm a tool exists with `feast tools` before relying on it; if a workflow's tool is missing, tell the user that part isn't available via the CLI yet rather than inventing it. The exact field list for any tool always comes from `feast describe <tool>` — this file gives you the *meaning* and *ordering* the schema can't.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Creating a campaign
|
|
10
|
+
|
|
11
|
+
Fully doable via the CLI. The server does the heavy lifting (id generation, default config, the funnel prerequisite) — you sequence the calls.
|
|
12
|
+
|
|
13
|
+
1. `loadCurrentOrganization` — read the org to get valid **referrers** (subdomains, from `subdomains2[].subdomain`) and location ids.
|
|
14
|
+
2. `createCampaign` with `{ "campaign": { "name": "...", "isCreating": true, "fbCampaigns": [], "attributionRules": [] } }` — keep the returned campaign **id** (a UUID). It comes back `isCreating: true`.
|
|
15
|
+
3. `populateCampaign` with the `campaignId` and a **`funnelType`**:
|
|
16
|
+
- `"reservation"` — no extra config.
|
|
17
|
+
- `"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).
|
|
18
|
+
- `"prepay"` — needs `prepayConfig` with `promotionName`, `price`, and an image (`imageUrl`).
|
|
19
|
+
4. (optional) `chooseFunnelTemplate` — the **acquisition** half: the funnel screens a guest sees. Requires a fresh campaign whose funnel is unset; resolves the referrer from the campaign.
|
|
20
|
+
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` / `loadTemplateAutomations`.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
**Reading a campaign back:** `getCampaign` returns the full config for one campaign (funnel/offer config, referrers, status); `listCampaigns` is the summary list; `getCampaignKpis` is performance metrics. Read with `getCampaign` before any `updateCampaign`.
|
|
25
|
+
|
|
26
|
+
**Cloning:** `cloneCampaign` with `sourceCampaignId`, `newCampaignName`, and a `referrer` (subdomain) duplicates funnel + automations + offers and returns a `newCampaignId`. **Gotcha:** the cloned automations still contain the *source* campaign's reservation links. After cloning, review the new campaign's automations and rewrite any reservation link to the new campaign's shorthand — the format is `https://{subdomain}.feastalytics.com/i/{new-shorthand}/reservation`.
|
|
27
|
+
|
|
28
|
+
The one-shot text→campaign endpoints (`createWithOffer` / `parseCampaignDescription`) aren't exposed to the CLI — use the steps above.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Setting up automations
|
|
33
|
+
|
|
34
|
+
**Now fully authorable from the CLI** (create / edit / delete / dry-run). This is the richest workflow — the ordering is simpler than the app's, but the domain rules below are what separate a professional flow from a carrier-blocked mess. Follow them when creating, and use them as a checklist when reviewing.
|
|
35
|
+
|
|
36
|
+
### The model: automations live inside flows
|
|
37
|
+
|
|
38
|
+
- An **automation** is one trigger → (conditions) → action unit (e.g. "on checkout, send a text").
|
|
39
|
+
- A **flow** groups automations by a shared trigger, and belongs to *either* a campaign *or* the members program — never both.
|
|
40
|
+
- **The rule that matters most: every automation needs a `flowId`. `batchEditAutomations` throws on a create op without one.** So you must resolve the flow *before* creating. Never invent a flowId.
|
|
41
|
+
|
|
42
|
+
### The CLI loop (simpler than the app — no navigate/stage/save split)
|
|
43
|
+
|
|
44
|
+
Unlike the in-app agent (which scaffolds an automation, then edits it, then explicitly saves), the CLI sends a **complete** automation in one operation and it persists immediately.
|
|
45
|
+
|
|
46
|
+
1. `listAutomationFlows` — find an existing flow. Pass `{ "campaignId": "<id>" }` for a campaign's flows, or `{ "scope": "membersProgram" }` for members-program flows. Reuse a matching flow when one fits.
|
|
47
|
+
2. If none fits, `createAutomationFlow` to make one. If the campaign/members-program has **no flows at all**, strongly prefer `applyAutomationTemplate` (then customize) over building from scratch. Only apply a template when there are no existing flows.
|
|
48
|
+
3. `listAutomations` with `{ "flowId": "<id>" }` to see the automations already in that flow before editing (omit the input to list every automation in the org).
|
|
49
|
+
4. `batchEditAutomations` with a list of `operations`:
|
|
50
|
+
- `{ "type": "create", "automation": { ...full automation..., "flowId": "<id>" } }` — generate a fresh UUID for the automation's id, set the `flowId`, and include triggers, conditions, actions, send time, and a descriptive title all at once. The server validates it and (create ops) requires the flowId.
|
|
51
|
+
- `{ "type": "update", "automationId": "<id>", "automation": { ...changed fields... } }`
|
|
52
|
+
- `{ "type": "delete", "automationId": "<id>" }` — blocked if the automation already has sends.
|
|
53
|
+
5. `simulateAutomations` — dry-run the flow against a synthetic event timeline with **no real sends**, to confirm the right automations fire before or after you save. It can also preview un-saved `edits` before you commit them.
|
|
54
|
+
|
|
55
|
+
`updateAutomationFlow` renames/retitles a flow; `deleteAutomationFlow` removes a flow and its automations (blocked at ≥20 sends — turn it off instead).
|
|
56
|
+
|
|
57
|
+
> **Not exposed:** actually *firing* an automation at a live member (the app's "run") is intentionally not a CLI tool — it sends a real SMS. Use `simulateAutomations` for verification; real sends stay in the app.
|
|
58
|
+
|
|
59
|
+
### Choosing the trigger
|
|
60
|
+
|
|
61
|
+
- **Campaign flows: prefer `viewCampaign` over `signUp`.** A guest viewing the campaign page is the natural entry point — it captures new sign-ups *and* returning guests. Use `signUp` only for members-program welcome flows or a fire-once-at-registration moment.
|
|
62
|
+
- **`offerExpiration` is rarely a *flow* trigger.** Use it on an individual automation inside an expiration nurture chain, not as a standalone flow's trigger type.
|
|
63
|
+
|
|
64
|
+
### Conditions: the nested event/occur shape
|
|
65
|
+
|
|
66
|
+
Event conditions nest the event and its timing. The `occur` object uses `match` (GTE/LTE/EQ) and `duration` (milliseconds):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "type": "event",
|
|
70
|
+
"event": { "event": { "type": "signUp" },
|
|
71
|
+
"occur": { "match": "GTE", "duration": 86400000 } } }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Positive duration = past** (event already happened) — for `signUp`, `addPass`, `visit`, `offerRedemption`, etc.
|
|
75
|
+
- **Negative duration = future** — only for `offerExpiration` (e.g. "expires within 2 hours" → `LTE`, `-7200000`).
|
|
76
|
+
- `EQ` matches within the whole increment (day/week/hour).
|
|
77
|
+
|
|
78
|
+
### Send times & prime texting windows
|
|
79
|
+
|
|
80
|
+
Send times are `immediate`, `relativeDelay` (`delayMs` after the trigger), or `absoluteDelay` (`utcHour`/`utcMinute`, optional `utcDayOfWeek` or `utcDay`). Think in the **org's local timezone**, then convert to UTC.
|
|
81
|
+
|
|
82
|
+
**Always schedule inside a prime window — never arbitrary times, never before 8 AM or after 9 PM:**
|
|
83
|
+
- Morning: **8:00–11:30 AM** (org timezone)
|
|
84
|
+
- Afternoon: **4:00–6:00 PM** (org timezone)
|
|
85
|
+
|
|
86
|
+
Which window depends on meal service: breakfast/lunch-only → all morning; dinner-only → mostly afternoon, at most one morning; both → roughly 50/50. Determine meal service from existing automations/funnel/settings, or ask the user. **Vary the minutes** so no two automations in a flow share a send time (e.g. 9:03, 9:17, 4:22). Recommend specific times rather than asking.
|
|
87
|
+
|
|
88
|
+
### Chaining vs. keeping independent
|
|
89
|
+
|
|
90
|
+
Chain with the `receiveAutomation` trigger (automation B fires because A was received) **only when B always follows A**.
|
|
91
|
+
|
|
92
|
+
- **Good:** welcome → follow-up tips 2 days later; expiration nurture reminders (per-guest timeline).
|
|
93
|
+
- **Do NOT linearly chain a calendar countdown** ("1 week before" → "3 days before" → "day of"). If an early step fails or the guest joins late, every later step is blocked. Instead **fan out from a shared parent**: every countdown message uses `receiveAutomation` → the same entry automation, each with its own `absoluteDelay` date. Then no single message can block the rest.
|
|
94
|
+
- **Expiration loops are valid** when gated by user action + a state change: e.g. `... → expired → (guest texts EXTEND, a reply-trigger automation runs extendReward) → re-enters "expires in 3 days"`. The loop is safe because EXTEND gates re-entry and `extendReward` moves the expiration date so conditions re-evaluate. A loop with no user action or no state change is invalid (infinite).
|
|
95
|
+
|
|
96
|
+
### Backfill (chained automations against past recipients)
|
|
97
|
+
|
|
98
|
+
When an automation's trigger is `receiveAutomation`, ask the user whether it should apply only going forward or also to everyone who already received the upstream automation:
|
|
99
|
+
|
|
100
|
+
- Default `applyToHistorical: false` (going forward only).
|
|
101
|
+
- For past recipients, set `applyToHistorical: true` as a **top-level sibling** of `automation` on the create/update op (never inside a trigger; it isn't stored — it only enqueues a one-shot backfill on that save).
|
|
102
|
+
- Before confirming, call `countParentAutomationRecipients` and tell the user the audience size; warn if > 1000. Only backfill after explicit confirmation.
|
|
103
|
+
|
|
104
|
+
### Rewards inside automations
|
|
105
|
+
|
|
106
|
+
- **Checkout auto-creates the reward.** For a checkout-triggered automation, do NOT add an `awardReward` action — the reward is already granted. Checkout flows only send texts. Use `awardReward` for non-checkout flows (visit milestones, sign-up rewards).
|
|
107
|
+
- **Members-program `awardReward` defaults to a 30-day expiration**: `{ "type": "relative", "relative": { "offsetMs": 2592000000 } }`. Mention it in your summary; omit only if the user says the reward shouldn't expire. This does not apply to campaign offers.
|
|
108
|
+
|
|
109
|
+
### Text-content best practices (rules when creating, checklist when reviewing)
|
|
110
|
+
|
|
111
|
+
1. **Descriptive names** — "Day 2 – Visit Reminder with Pass Link", not "Reminder 1".
|
|
112
|
+
2. **Lead with the pass link** — the first post-signup text MUST include it ("add your pass: {{pass link}}").
|
|
113
|
+
3. **Always `https://`** on every link (carriers block bare/protocol-less links).
|
|
114
|
+
4. **Mobile Google Maps links only** — `https://maps.app.goo.gl/...`, never desktop `maps.google.com`.
|
|
115
|
+
5. **Correct reservation links** — `https://{subdomain}.feastalytics.com/i/{shorthand}/reservation` using the *current* campaign's shorthand (from `listCampaigns`) and a valid subdomain. Never reuse another campaign's link.
|
|
116
|
+
6. **Personalize** with `{{firstName}}`; **vary** tone/wording across automations; **re-share** useful info (pass link, hours, maps, reservation) in reminders; keep **empty lines** between blocks for readability.
|
|
117
|
+
7. **Align offer expirations with open hours** — never expire an offer while the restaurant is closed.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Creating offers (strategy backlog)
|
|
122
|
+
|
|
123
|
+
Fully doable from the CLI. Offers live in the organization's strategy backlog (the offer queue), sourced from real menu data.
|
|
124
|
+
|
|
125
|
+
1. `loadCurrentOrganization` → get the **`locationId`** (from `locations`) — offer tools key on the location, **never** the organizationId.
|
|
126
|
+
2. `dfyGetMenuHierarchy` with that `locationId` — browse real menu items and prices.
|
|
127
|
+
3. `dfyListOffers` — see the current backlog; avoid duplicates.
|
|
128
|
+
4. `dfyCreateOffer` — create it. `dfyUpdateOffer` / `dfyDeleteOffer` to revise.
|
|
129
|
+
|
|
130
|
+
Every offer picks one **framework**:
|
|
131
|
+
|
|
132
|
+
- **`free`** — give away a low-cost item (appetizer, side, drink, small dessert) with no purchase. Maximizes signups. `offerPrice: null`; `items` = the single free item at its menu price. Headline: "A Complimentary [Item]" / "Free [Item]".
|
|
133
|
+
- **`combo`** — bundle to lift the ticket. *Pattern A* "Buy X, Get Y Free" (`offerPrice` = purchased item only) — best for quick-service. *Pattern B* fixed-price bundle "[Item] & [Item] for $XX" (`offerPrice` = bundle price) — preserves brand equity for upscale.
|
|
134
|
+
- **`experience`** — a curated multi-item tasting/pairing/prix-fixe with **no discount** (`offerPrice` = sum of item prices). MUST have 2+ items.
|
|
135
|
+
|
|
136
|
+
`dfyCreateOffer` needs `name` (internal label, no restaurant name), `headline` (states what the guest gets + price, using real item names), a short `description` (context the headline can't carry), `framework`, `items` (`[{name, price}]` from the menu), `offerPrice`, and `locationId`. Always frame as "offers," never "discounts" or "deals."
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Members program (retention)
|
|
141
|
+
|
|
142
|
+
The retention counterpart to campaigns — flows with no `campaignId`.
|
|
143
|
+
|
|
144
|
+
- **Automations are authorable** (see the automations workflow): use `listAutomationFlows` with `{ "scope": "membersProgram" }`, then the same create/edit loop. Remember the members-program 30-day `awardReward` default.
|
|
145
|
+
- **Creating members-program rewards themselves is NOT exposed.** Reward creation in the app writes a catalog item + reward link through generic object-query mutations with no dedicated tool, and orchestrates catalog-item-create-or-reuse plus awarding-automation scaffolding client-side. There's no tagged `createReward`. If asked, tell the user this needs the app (or a new endpoint).
|
|
146
|
+
- **Pass builder** (wallet pass design) is not exposed to the CLI.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Exploring users (guests / members)
|
|
151
|
+
|
|
152
|
+
**Reading is now available** via `searchUsers`. It returns a page of recent member activity — one event per member, each carrying the member's `serialNumber` plus the event (type, time, related object).
|
|
153
|
+
|
|
154
|
+
- Filter with `query` (free-text name), `eventTypes` (e.g. `sentText`, `receivedText`, `scan`, `order`, `rewardAwarded`, `rewardRedeemed`, `checkout`, the `*Attribution` types), `progressMinBound`/`progressMaxBound` (visit-count range), `isUnread: true` (members with unanswered inbound texts), `orderBy` (ASC|DESC by event time).
|
|
155
|
+
- Paginate with `limit` (default 100) and `cursor` (pass back the `cursor` from the previous call; an undefined cursor means no more pages).
|
|
156
|
+
|
|
157
|
+
**Replying by SMS is NOT exposed, deliberately.** The send primitive enforces opt-out, quiet-hours, dedup, and rate limits *downstream* (not at the endpoint), and opt-in is currently gated only by a UI control. If a reply capability is ever exposed, it must run with confirmation and must not bypass those guardrails. For now, tell the user that replying to guests is done in the app.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
**Applying a funnel template** expands a whole screen tree server-side in one call: `chooseFunnelTemplate` (needs the campaign's funnel unset — a fresh campaign — and resolves the referrer from the campaign). `deleteFunnel` tears one down.
|
|
162
|
+
|
|
163
|
+
**Editing individual funnel screens is now CLI-drivable** through a **draft → preview → promote** loop. You never apply edits locally: you stage them on an off-prod draft, preview the result at a stable URL, then save. Tools: `listFunnelScreens`, `createFunnelDraft`, `stageFunnelEdit`, `getFunnelDraft`, `listFunnelDrafts`, `discardFunnelDraft`, `saveFunnelEdits`.
|
|
164
|
+
|
|
165
|
+
### The loop
|
|
166
|
+
|
|
167
|
+
1. **`listFunnelScreens`** `{ "referrer": "<subdomain>", "campaignId": "<id>" }` — read the funnel's screens to get each `screenId` and its renderables' `id`s + content. **Read before any `update` edit** — an update replaces a renderable by id, so you need its current shape. (Omit `campaignId` for a base/members-program funnel.)
|
|
168
|
+
2. **`createFunnelDraft`** `{ "referrer": "...", "campaignId": "..." }` — creates an off-prod overlay; keep the returned `draftId`. Nothing is live yet. **Immediately inspect the funnel's current state**: open the draft's preview URL (see step 4 — with no edits staged yet it renders the live funnel as-is) so you have a visual baseline of what you're about to change. If you have a browser/screenshot tool, open and screenshot it now; if you can't view it yourself, share the URL with the user before editing.
|
|
169
|
+
3. **`stageFunnelEdit`** `{ "draftId": "...", "screenId": "...", "edit": <RenderableEdit> }` — one renderable edit per call; the server validates it against the current screen. Repeat per change. A `RenderableEdit` is a discriminated union (`describe stageFunnelEdit` for the full schema):
|
|
170
|
+
- `{ "type": "update", "id": "<renderableId>", "renderable": { ...clone of what you read, with your changes... } }` — keep the same `id`.
|
|
171
|
+
- `{ "type": "create", "renderable": { "id": "<new-uuid>", ... }, "targetId": "<sibling id>", "position": "before" | "after" | "inside" }` — generate a fresh UUID.
|
|
172
|
+
- `{ "type": "delete", "id": "<renderableId>" }` and `{ "type": "move", "id": "...", "targetId": "...", "position": "..." }`.
|
|
173
|
+
4. **Preview** (no CLI call): open `https://{referrer}.feastalytics.com/preview/{draftId}/{campaignId}` (drop `/{campaignId}` for a members-program draft). It renders the whole funnel as a flow diagram with the draft's edits applied. Screenshot it, then iterate — re-run `listFunnelScreens` **with the `draftId`** to read the funnel *with* the staged edits, stage more, re-preview — until it's right.
|
|
174
|
+
5. **`saveFunnelEdits`** `{ "draftId": "..." }` — **promotes to prod** (mutation, confirms first): applies the draft's edits to the live funnel and marks the draft `promoted`. To abandon instead, `discardFunnelDraft`.
|
|
175
|
+
|
|
176
|
+
`saveFunnelEdits` can also take an inline `{ "referrer", "campaignId", "edits": [ { "screenId", "edit" } ] }` array instead of a `draftId` — a one-shot save with no persisted draft (you lose the preview step, so prefer the draft loop when the change is visual).
|
|
177
|
+
|
|
178
|
+
### Domain rules
|
|
179
|
+
- **Base screens vs campaign screens.** A campaign-owned screen is edited in place; a **base/shared screen edited in a campaign context becomes a campaign *override*** — the shared screen is left untouched. `saveFunnelEdits` decides this automatically from the draft's `campaignId`, so a change scoped to one campaign only affects that campaign.
|
|
180
|
+
- **Read before every `update`.** Construct the edit from what `listFunnelScreens` returned (renderable ids are stable), never from memory.
|
|
181
|
+
- **One edit per `stageFunnelEdit`**, staged incrementally; each is validated as it lands.
|
|
182
|
+
- **Drift guard.** `saveFunnelEdits` rejects the promote if the live funnel changed since the draft was created — re-create the draft in that case.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Establishing brand identity
|
|
187
|
+
|
|
188
|
+
**Not exposed to the CLI yet.** Brand setup (logo, colors, fonts, subdomain look) writes to several untagged endpoints, and the intelligent part — auto-extracting a usable palette from a scraped brand with WCAG-contrast derivation and logo selection — lives entirely in the browser, not the API. A brand-import endpoint exists only as a raw scrape (unfiltered logos/colors). If asked, tell the user brand setup isn't CLI-drivable yet.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feastalytics/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line client for the Feastalytics platform — list, create, and update campaigns, automations, offers, and members-program rewards from the terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"feast": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"feast"
|
|
12
|
+
],
|
|
13
|
+
"author": "Feastalytics",
|
|
14
|
+
"license": "UNLICENSED",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/feastalytics/cli.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/feastalytics/cli#readme",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"dev": "tsx src/cli.ts",
|
|
29
|
+
"feast": "tsx src/cli.ts",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@trpc/client": "^10.45.2",
|
|
35
|
+
"@types/node": "^20.14.0",
|
|
36
|
+
"ajv": "^8.17.1",
|
|
37
|
+
"superjson": "^2.2.2",
|
|
38
|
+
"tsup": "^8.3.0",
|
|
39
|
+
"tsx": "^4.19.4",
|
|
40
|
+
"typescript": "^5.5.0"
|
|
41
|
+
}
|
|
42
|
+
}
|