@feastalytics/cli 0.1.17 → 0.1.19

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 CHANGED
@@ -6,6 +6,15 @@ Ships with an [agent skill](#agent-skill) so Claude Code, Codex, and other agent
6
6
 
7
7
  ## Install
8
8
 
9
+ Feastalytics ships as a plugin (the `feast` skill plus the hosted MCP server at `https://mcp.feast-api.com/mcp`), as a standalone skill, and as this CLI. The MCP server signs you in with OAuth the first time you connect; there is no client id or API key to enter.
10
+
11
+ - **Claude Code**: `/plugin marketplace add feastalytics/cli`, then `/plugin install feastalytics@feast`.
12
+ - **claude.ai and Claude Desktop**: add a custom connector with the URL `https://mcp.feast-api.com/mcp`. A directory listing is to follow.
13
+ - **ChatGPT**: `npm run build:chatgpt` writes `dist/feastalytics-chatgpt.zip` (`plugin/plugin.json`, `plugin/mcp.json`, the icon and `plugin/skills/`).
14
+ - **Skill only** (Claude Code, Codex, Cursor and other agents): `npx skills add feastalytics/cli`.
15
+
16
+ ### CLI
17
+
9
18
  ```bash
10
19
  npm install -g @feastalytics/cli
11
20
  ```
@@ -66,7 +75,7 @@ Mutations additionally require `--org` and print the server-resolved org before
66
75
 
67
76
  ## Agent skill
68
77
 
69
- The `feast/` directory is an [agent skill](https://www.skills.sh) that teaches an agent to operate Feastalytics through its tools, either this CLI or the hosted MCP server at `https://mcp.feast-api.com/mcp` (same tools, same names). Install it into your agent(s):
78
+ The `plugin/skills/feast/` directory is an [agent skill](https://www.skills.sh) that teaches an agent to operate Feastalytics through its tools, either this CLI or the hosted MCP server at `https://mcp.feast-api.com/mcp` (same tools, same names). Install it into your agent(s):
70
79
 
71
80
  ```bash
72
81
  npx skills add feastalytics/cli
@@ -86,7 +95,7 @@ npx skills add feastalytics/cli -g -a '*' -y
86
95
  - `-a '*'` re-links **all** agents (Claude Code, Codex, …) so each picks up the new version.
87
96
  - `-y` skips the confirmation prompts.
88
97
 
89
- To refresh from a local checkout instead of GitHub, run `npx skills add ./feast -g -a '*' -y` from the repo root.
98
+ To refresh from a local checkout instead of GitHub, run `npx skills add . -g -a '*' -y` from the repo root.
90
99
 
91
100
  ### Playbook skills from Feastalytics
92
101
 
package/dist/cli.js CHANGED
@@ -9045,6 +9045,35 @@ var CLI_MANIFEST = {
9045
9045
  "$schema": "http://json-schema.org/draft-07/schema#"
9046
9046
  }
9047
9047
  },
9048
+ {
9049
+ "id": "approveVideo",
9050
+ "domain": "ads",
9051
+ "description": "Approve a video and save its MP4 to the campaign's creative library, returning the libraryKey. Pass the projectId; runId is optional and defaults to the latest run that has an export (the one getVideo's exportUrl plays), so pass it only to approve an earlier run from listCampaignVideos. Fails until that run's export is ready (getVideo pipeline.status ready). Only once the human has watched the video and approved it.",
9052
+ "type": "mutation",
9053
+ "path": [
9054
+ "api",
9055
+ "bevyl",
9056
+ "approveProject"
9057
+ ],
9058
+ "inputJsonSchema": {
9059
+ "type": "object",
9060
+ "properties": {
9061
+ "projectId": {
9062
+ "type": "string",
9063
+ "minLength": 1
9064
+ },
9065
+ "runId": {
9066
+ "type": "string",
9067
+ "minLength": 1
9068
+ }
9069
+ },
9070
+ "required": [
9071
+ "projectId"
9072
+ ],
9073
+ "additionalProperties": false,
9074
+ "$schema": "http://json-schema.org/draft-07/schema#"
9075
+ }
9076
+ },
9048
9077
  {
9049
9078
  "id": "awardReward",
9050
9079
  "domain": "membersProgram",
@@ -13530,7 +13559,7 @@ var CLI_MANIFEST = {
13530
13559
  {
13531
13560
  "id": "createRecruitmentCreatives",
13532
13561
  "domain": "creators",
13533
- "description": "Generate the five AI recruitment ad creatives for a location. Pass campaignId (or offerId) so they attach to its recruitment offer. Only missing types are generated; force deletes and regenerates the whole set. foodCredit from getInfluencerBoardConfig.",
13562
+ "description": "Render the five recruitment ad images for a location from fixed templates, using its own photo, logo and food credit (no AI image generation). Pass campaignId (or offerId) so they attach to its recruitment offer. Only missing types are generated; force deletes and regenerates the whole set. foodCredit from getInfluencerBoardConfig.",
13534
13563
  "type": "mutation",
13535
13564
  "path": [
13536
13565
  "api",
@@ -13843,6 +13872,90 @@ var CLI_MANIFEST = {
13843
13872
  "$schema": "http://json-schema.org/draft-07/schema#"
13844
13873
  }
13845
13874
  },
13875
+ {
13876
+ "id": "generateVideo",
13877
+ "domain": "ads",
13878
+ "description": "Start a Bevyl ad video for a campaign from its b-roll. Returns { projectId, pipelineStatus } at once; the server uploads the clips, waits for Bevyl to process them, creates the video and exports it. Poll getVideo with the projectId every 30 seconds or so until pipeline.status is ready (exportUrl is the MP4) or failed (message says why). Spends Bevyl generation credits, so only with the human's explicit approval. Build prompt from getVideoPromptOptions (its defaultPrompt, edited or not): Bevyl reads it verbatim, up to 5000 characters. Set format yourself (talking-head, voiceover, trending-sounds which needs trendId, or no-audio); the chosen direction's suggestedFormat is a good default. Optional: angleId from listCampaignVideos to add a version to an existing angle (otherwise angleTitle names a new one, defaulting to the concept's first line), voiceoverProfileId, backgroundMusicTrackId, durationSeconds 10 to 80 in steps of 5, and brollKeys (S3 keys from listMedia scope creativeLibraryBroll; omit to use every clip already synced to Bevyl).",
13879
+ "type": "mutation",
13880
+ "path": [
13881
+ "api",
13882
+ "bevyl",
13883
+ "generate"
13884
+ ],
13885
+ "inputJsonSchema": {
13886
+ "type": "object",
13887
+ "properties": {
13888
+ "campaignId": {
13889
+ "type": "string",
13890
+ "minLength": 1
13891
+ },
13892
+ "angleId": {
13893
+ "type": "string",
13894
+ "minLength": 1,
13895
+ "maxLength": 100
13896
+ },
13897
+ "angleTitle": {
13898
+ "type": "string",
13899
+ "minLength": 1,
13900
+ "maxLength": 300
13901
+ },
13902
+ "prompt": {
13903
+ "type": "string",
13904
+ "minLength": 1,
13905
+ "maxLength": 5e3
13906
+ },
13907
+ "format": {
13908
+ "type": "string",
13909
+ "enum": [
13910
+ "talking-head",
13911
+ "voiceover",
13912
+ "trending-sounds",
13913
+ "no-audio"
13914
+ ],
13915
+ "default": "talking-head"
13916
+ },
13917
+ "voiceoverProfileId": {
13918
+ "type": "string",
13919
+ "minLength": 1
13920
+ },
13921
+ "trendId": {
13922
+ "type": "string",
13923
+ "minLength": 1
13924
+ },
13925
+ "backgroundMusicTrackId": {
13926
+ "type": "string",
13927
+ "minLength": 1
13928
+ },
13929
+ "durationSeconds": {
13930
+ "type": "integer",
13931
+ "minimum": 10,
13932
+ "maximum": 80,
13933
+ "multipleOf": 5
13934
+ },
13935
+ "brollKeys": {
13936
+ "type": "array",
13937
+ "items": {
13938
+ "type": "string",
13939
+ "minLength": 1
13940
+ },
13941
+ "maxItems": 100
13942
+ },
13943
+ "bevylVideoIds": {
13944
+ "type": "array",
13945
+ "items": {
13946
+ "type": "string",
13947
+ "minLength": 1
13948
+ },
13949
+ "maxItems": 100
13950
+ }
13951
+ },
13952
+ "required": [
13953
+ "campaignId"
13954
+ ],
13955
+ "additionalProperties": false,
13956
+ "$schema": "http://json-schema.org/draft-07/schema#"
13957
+ }
13958
+ },
13846
13959
  {
13847
13960
  "id": "getAutomationDraft",
13848
13961
  "domain": "automations",
@@ -14805,6 +14918,56 @@ var CLI_MANIFEST = {
14805
14918
  "$schema": "http://json-schema.org/draft-07/schema#"
14806
14919
  }
14807
14920
  },
14921
+ {
14922
+ "id": "getVideo",
14923
+ "domain": "ads",
14924
+ "description": "Read one Content Studio video by projectId. pipeline.status moves uploading, processing, creating, rendering, exporting, then ready (exportUrl is the MP4 to watch) or failed (message says why). pipeline is null for videos made before the server pipeline; use listCampaignVideos for those. Reads Feastalytics only, so it is cheap to poll.",
14925
+ "type": "query",
14926
+ "path": [
14927
+ "api",
14928
+ "bevyl",
14929
+ "getProject"
14930
+ ],
14931
+ "inputJsonSchema": {
14932
+ "type": "object",
14933
+ "properties": {
14934
+ "projectId": {
14935
+ "type": "string",
14936
+ "minLength": 1
14937
+ }
14938
+ },
14939
+ "required": [
14940
+ "projectId"
14941
+ ],
14942
+ "additionalProperties": false,
14943
+ "$schema": "http://json-schema.org/draft-07/schema#"
14944
+ }
14945
+ },
14946
+ {
14947
+ "id": "getVideoPromptOptions",
14948
+ "domain": "ads",
14949
+ "description": "The building blocks for a generateVideo prompt for one campaign. defaultPrompt is what Content Studio would send with its default picks: plain text in four sections (Campaign, Concept, Creative direction, CTA), each a heading line followed by its text. campaign, concept, direction and cta list the alternatives for each section ({ id, label, text }); concept options also carry the reference video they were distilled from, and direction options a suggestedFormat to pass as generateVideo's format. Send defaultPrompt as is, or swap a section's text for another option's, or rewrite it entirely: generateVideo sends the prompt to Bevyl verbatim. Reads Feastalytics only.",
14950
+ "type": "query",
14951
+ "path": [
14952
+ "api",
14953
+ "bevyl",
14954
+ "getVideoPromptOptions"
14955
+ ],
14956
+ "inputJsonSchema": {
14957
+ "type": "object",
14958
+ "properties": {
14959
+ "campaignId": {
14960
+ "type": "string",
14961
+ "minLength": 1
14962
+ }
14963
+ },
14964
+ "required": [
14965
+ "campaignId"
14966
+ ],
14967
+ "additionalProperties": false,
14968
+ "$schema": "http://json-schema.org/draft-07/schema#"
14969
+ }
14970
+ },
14808
14971
  {
14809
14972
  "id": "inviteUser",
14810
14973
  "domain": "core",
@@ -14965,6 +15128,31 @@ var CLI_MANIFEST = {
14965
15128
  ],
14966
15129
  "inputJsonSchema": null
14967
15130
  },
15131
+ {
15132
+ "id": "listCampaignVideos",
15133
+ "domain": "ads",
15134
+ "description": "Every Content Studio video for a campaign, newest first: projectId, angleId and angleTitle (videos are grouped into angles), the concept and direction they were made from, latestRun (the Bevyl run, whose runId approveVideo needs), approvedRunId, edits, and pipeline (status, message, exportUrl) for videos made through generateVideo. Calls Bevyl to read run status, so prefer getVideo to poll a single video.",
15135
+ "type": "query",
15136
+ "path": [
15137
+ "api",
15138
+ "bevyl",
15139
+ "listCampaignProjects"
15140
+ ],
15141
+ "inputJsonSchema": {
15142
+ "type": "object",
15143
+ "properties": {
15144
+ "campaignId": {
15145
+ "type": "string",
15146
+ "minLength": 1
15147
+ }
15148
+ },
15149
+ "required": [
15150
+ "campaignId"
15151
+ ],
15152
+ "additionalProperties": false,
15153
+ "$schema": "http://json-schema.org/draft-07/schema#"
15154
+ }
15155
+ },
14968
15156
  {
14969
15157
  "id": "listCreatives",
14970
15158
  "domain": "creators",
@@ -15545,30 +15733,6 @@ var CLI_MANIFEST = {
15545
15733
  "$schema": "http://json-schema.org/draft-07/schema#"
15546
15734
  }
15547
15735
  },
15548
- {
15549
- "id": "purchaseAndConfigurePhoneNumber",
15550
- "domain": "core",
15551
- "description": "Buys a real number from Twilio for the organization and bills the account. Nothing here undoes that. Pick a number geographically close to the restaurant: guests answer a local area code and read a distant one as spam, so search by the restaurant's own postal code or coordinates, never a guessed area code. If you don't know where the restaurant is, establish it first from its POS location, its Google Place, or by asking. Don't buy until you do.",
15552
- "type": "mutation",
15553
- "path": [
15554
- "api",
15555
- "onboarding",
15556
- "purchaseAndConfigurePhoneNumber"
15557
- ],
15558
- "inputJsonSchema": {
15559
- "type": "object",
15560
- "properties": {
15561
- "phoneNumber": {
15562
- "type": "string"
15563
- }
15564
- },
15565
- "required": [
15566
- "phoneNumber"
15567
- ],
15568
- "additionalProperties": false,
15569
- "$schema": "http://json-schema.org/draft-07/schema#"
15570
- }
15571
- },
15572
15736
  {
15573
15737
  "id": "queryData",
15574
15738
  "domain": "data",
@@ -15734,6 +15898,41 @@ var CLI_MANIFEST = {
15734
15898
  "$schema": "http://json-schema.org/draft-07/schema#"
15735
15899
  }
15736
15900
  },
15901
+ {
15902
+ "id": "requestVideoEdit",
15903
+ "domain": "ads",
15904
+ "description": `Ask Bevyl to remake a video with one change, for example "make the hook punchier" or "use the patio shots first". Send only the change in note (600 characters at most); Bevyl builds from the video's current state. On-screen text cannot be moved or resized. Starts a new run: getVideo moves back to rendering, then ready with the new export. Spends Bevyl generation credits, so only with the human's explicit approval.`,
15905
+ "type": "mutation",
15906
+ "path": [
15907
+ "api",
15908
+ "bevyl",
15909
+ "regenerateProject"
15910
+ ],
15911
+ "inputJsonSchema": {
15912
+ "type": "object",
15913
+ "properties": {
15914
+ "projectId": {
15915
+ "type": "string",
15916
+ "minLength": 1
15917
+ },
15918
+ "note": {
15919
+ "type": "string",
15920
+ "minLength": 1,
15921
+ "maxLength": 600
15922
+ },
15923
+ "requestId": {
15924
+ "type": "string",
15925
+ "format": "uuid"
15926
+ }
15927
+ },
15928
+ "required": [
15929
+ "projectId",
15930
+ "note"
15931
+ ],
15932
+ "additionalProperties": false,
15933
+ "$schema": "http://json-schema.org/draft-07/schema#"
15934
+ }
15935
+ },
15737
15936
  {
15738
15937
  "id": "saveAutomationEdits",
15739
15938
  "domain": "automations",
@@ -15783,62 +15982,6 @@ var CLI_MANIFEST = {
15783
15982
  "$schema": "http://json-schema.org/draft-07/schema#"
15784
15983
  }
15785
15984
  },
15786
- {
15787
- "id": "searchAvailablePhoneNumbers",
15788
- "domain": "core",
15789
- "description": "Lists Twilio numbers available to buy for texting guests. Free and read-only. Search by the restaurant's own postal code or latitude/longitude: proximity matters, and a guessed area code lands a number in the wrong town.",
15790
- "type": "query",
15791
- "path": [
15792
- "api",
15793
- "onboarding",
15794
- "searchAvailablePhoneNumbers"
15795
- ],
15796
- "inputJsonSchema": {
15797
- "anyOf": [
15798
- {
15799
- "type": "object",
15800
- "properties": {
15801
- "areaCode": {
15802
- "type": "string"
15803
- }
15804
- },
15805
- "required": [
15806
- "areaCode"
15807
- ],
15808
- "additionalProperties": false
15809
- },
15810
- {
15811
- "type": "object",
15812
- "properties": {
15813
- "postalCode": {
15814
- "type": "string"
15815
- }
15816
- },
15817
- "required": [
15818
- "postalCode"
15819
- ],
15820
- "additionalProperties": false
15821
- },
15822
- {
15823
- "type": "object",
15824
- "properties": {
15825
- "latitude": {
15826
- "type": "number"
15827
- },
15828
- "longitude": {
15829
- "type": "number"
15830
- }
15831
- },
15832
- "required": [
15833
- "latitude",
15834
- "longitude"
15835
- ],
15836
- "additionalProperties": false
15837
- }
15838
- ],
15839
- "$schema": "http://json-schema.org/draft-07/schema#"
15840
- }
15841
- },
15842
15985
  {
15843
15986
  "id": "searchGooglePlaces",
15844
15987
  "domain": "core",
@@ -22401,23 +22544,6 @@ var CLI_MANIFEST = {
22401
22544
  },
22402
22545
  "additionalProperties": false
22403
22546
  },
22404
- "openTableConfig": {
22405
- "type": "object",
22406
- "properties": {
22407
- "baseUrl": {
22408
- "type": "string"
22409
- },
22410
- "numDaysCanReserveAhead": {
22411
- "type": "number"
22412
- }
22413
- },
22414
- "required": [
22415
- "baseUrl",
22416
- "numDaysCanReserveAhead"
22417
- ],
22418
- "additionalProperties": false,
22419
- "description": "Deprecated: Use openTableConfigs instead"
22420
- },
22421
22547
  "openTableConfigs": {
22422
22548
  "type": "array",
22423
22549
  "items": {
@@ -23683,13 +23809,6 @@ var CLI_MANIFEST = {
23683
23809
  "maxItems": 20,
23684
23810
  "description": "Additional email addresses for scheduled, rescheduled, confirmed and cancelled visit emails and Calendar invitations. No Feast account is required. Send an empty array to remove all email-only recipients; omit to preserve them. Requires the organization's booking notification feature to be enabled."
23685
23811
  },
23686
- "passConfigured": {
23687
- "type": "boolean"
23688
- },
23689
- "calendarConfigured": {
23690
- "type": "boolean",
23691
- "description": "Ignored. Kept while older clients still send it."
23692
- },
23693
23812
  "maxBookingDaysOut": {
23694
23813
  "anyOf": [
23695
23814
  {
@@ -23914,9 +24033,6 @@ var CLI_MANIFEST = {
23914
24033
  "completionStatus": {
23915
24034
  "type": "object",
23916
24035
  "properties": {
23917
- "preProductOnboardingComplete": {
23918
- "type": "boolean"
23919
- },
23920
24036
  "postProductOnboardingComplete": {
23921
24037
  "type": "boolean"
23922
24038
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feastalytics/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "feast"
11
+ "!dist/*.zip",
12
+ "plugin/skills"
12
13
  ],
13
14
  "author": "Feastalytics",
14
15
  "license": "UNLICENSED",
@@ -28,7 +29,9 @@
28
29
  "dev": "tsx src/cli.ts",
29
30
  "feast": "tsx src/cli.ts",
30
31
  "typecheck": "tsc --noEmit",
31
- "prepublishOnly": "npm run build"
32
+ "prepublishOnly": "npm run build",
33
+ "version": "node scripts/sync-plugin-version.mjs && git add plugin/.claude-plugin/plugin.json plugin/plugin.json",
34
+ "build:chatgpt": "sh scripts/build-chatgpt-zip.sh"
32
35
  },
33
36
  "devDependencies": {
34
37
  "@trpc/client": "^10.45.2",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: feast
3
- description: Operate a Feastalytics organization (campaigns, automations, funnels, members-program rewards, the wallet pass, creator sourcing, Meta ads, texting, onboarding, and read-only data queries) through the Feastalytics tools, either the `feast` CLI or the Feastalytics MCP server. 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", "approve this creator", "publish the recruitment ad", "text this guest back", "query my guests", "update the members program", or any request to script, batch or automate Feastalytics operations. Reach for it even when the user doesn't name the CLI or the MCP server: if the task is reading or changing Feastalytics data, these are the tools.
3
+ description: >-
4
+ Operate a Feastalytics organization (campaigns, automations, funnels, members-program rewards, the wallet pass, creator sourcing, Meta ads, texting, onboarding, and read-only data queries) through the Feastalytics tools, either the `feast` CLI or the Feastalytics MCP server. 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", "approve this creator", "publish the recruitment ad", "text this guest back", "query my guests", "update the members program", or any request to script, batch or automate Feastalytics operations. Reach for it even when the user doesn't name the CLI or the MCP server: if the task is reading or changing Feastalytics data, these are the tools.
4
5
  ---
5
6
 
6
7
  # Feast
@@ -53,10 +54,9 @@ That last point matters most for the tools that reach the real world rather than
53
54
 
54
55
  - `sendText` texts a guest or creator immediately, one person per call, with no scheduling and no undo.
55
56
  - Approving or denying a creator visit (`updateCreatorVisit`) or deciding a submission (`decideCreatorSubmission`) texts that person. `updateCreatorVisit` can preview its texts with `dryRun: true` or skip them with `sideEffects: false`; `decideCreatorSubmission` can skip its text with `skipApprovalText`.
56
- - Paying a creator's bonus (`createInfluencerPayout`) charges the organization's card.
57
+ - Paying a creator's bonus (`createInfluencerPayout`, on the CLI) charges the organization's card. Over the MCP server that tool is not available, so the client pays bonuses in the dashboard.
57
58
  - `awardReward` puts a real reward in a member's wallet pass, and a retried call grants a second one.
58
59
  - `inviteUser` sends a real email.
59
- - Buying a phone number bills the account.
60
60
  - Publishing a campaign puts it live, and pricing a recurring promotion creates real Stripe products.
61
61
  - Activating a Meta campaign spends real ad budget.
62
62
  - Saving automation edits changes what guests receive.
@@ -85,6 +85,7 @@ Many tasks are multi-step and have a required ordering the app normally enforces
85
85
  | Writing guest-facing Meta ad copy (`adCopy`) | `references/workflows/ad-copy-guest.md` |
86
86
  | Writing creator-recruitment ad copy (`recruitmentAdCopy`) | `references/workflows/ad-copy-creator.md` |
87
87
  | Publishing, pausing, budgeting or diagnosing Meta ads | `references/workflows/ads.md` |
88
+ | Making a video ad with Bevyl: prompt, generate, edit, approve | `references/workflows/videos.md` |
88
89
  | Creator sourcing: approving applicants, reviewing content, creatives, payouts, reimbursements, texting a creator | `references/workflows/creators.md` |
89
90
  | Members-program rewards, granting a reward to one member; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
90
91
  | Working the onboarding taskboard; brand identity; phone, media, invites, billing | `references/workflows/onboarding.md` |
@@ -41,6 +41,10 @@ Restaurants recruit local content creators to visit and post. One config per loc
41
41
 
42
42
  A template-driven publish pipeline: `listAdTemplates` → `planAds` → `publishAds` → `getJob` → `setAdCampaignStatus`, plus `ads_*` tools for reading and steering what's already on the ad account. See `workflows/ads.md`.
43
43
 
44
+ ## Video ads (Content Studio)
45
+
46
+ Bevyl edits a campaign's b-roll into ad videos, grouped into angles. One `projectId` per video: `getVideoPromptOptions` → `generateVideo` → poll `getVideo` → `requestVideoEdit` or `approveVideo`. See `workflows/videos.md`.
47
+
44
48
  ## The data catalog
45
49
 
46
50
  `describeData` / `queryData` expose a read-only, org-scoped query surface over the data model: guests, orders, menu items, texts, creator visits, payouts. When no purpose-built tool answers a read question, the catalog usually does; `describeData` with no arguments is the index.
@@ -30,7 +30,7 @@ If the global install fails on permissions, don't retry with `sudo`. Tell the us
30
30
 
31
31
  ### Authenticating
32
32
 
33
- Authenticate once. Tokens are cached in `~/.config/feast-cli/credentials.json` and refreshed automatically:
33
+ Authenticate once. The CLI keeps you signed in and refreshes the session itself:
34
34
 
35
35
  ```bash
36
36
  feast login # opens a browser to authorize (default)
@@ -64,7 +64,7 @@ Feastalytics publishes further skills that build on this one (campaign diagnosis
64
64
 
65
65
  ```bash
66
66
  feast skill list # what is published for your organization
67
- feast skill install feast-playbooks # install or refresh one into ~/.claude/skills/
67
+ feast skill install feast-playbooks # install or refresh one for your agent
68
68
  ```
69
69
 
70
70
  Install what `feast skill list` offers when the user asks for a playbook this skill does not cover, and re-run the install when the CLI prints an update notice.
@@ -55,8 +55,4 @@ An effect that reports `error` in the job is a case for the dashboard, not for p
55
55
  - `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: those are published paused at all three levels, so activating the campaign alone would spend nothing.
56
56
  - `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. That layout config value is what makes the funnel fire the pixel and what the onboarding task reads.
57
57
 
58
- ### Reference scripts for video ads
59
-
60
- `listReferenceScripts` returns the reference ad scripts Content Studio offers as Concept presets for Bevyl videos, each distilled from an ad that performed: `description` (what the video shows), `videoUrl` (a public MP4 preview), `structure` (the ordered beats), `keyPhrases` (lines to adapt, with `<placeholders>` filled from the campaign's facts) and `concept` (the exact text Content Studio sends to Bevyl). Copy the structure and pacing, not the words. The list is the same for every organization.
61
-
62
58
  > **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.
@@ -89,12 +89,14 @@ When an automation's trigger is `receiveAutomation`, ask the user whether it sho
89
89
 
90
90
  ### Text-content best practices (rules when creating, checklist when reviewing)
91
91
 
92
- 1. **Descriptive names**: "Day 2: Visit Reminder with Pass Link", not "Reminder 1".
93
- 2. **Lead with the pass link**: the first post-signup text MUST include it ("add your pass: {{pass link}}").
94
- 3. **Always `https://`** on every link (carriers block bare/protocol-less links).
92
+ 1. **Descriptive names**: "Day 2: Visit Reminder with Wallet Link", not "Reminder 1".
93
+ 2. **Lead with the wallet link**: the first post-signup text MUST include the link that adds the guest's card to Apple or Google Wallet, written with the `passLink` text variable (see the variables below).
94
+ 3. **Every link starts with https**: carriers block links written without it.
95
95
  4. **Mobile Google Maps links only**: `https://maps.app.goo.gl/...`, never desktop `maps.google.com`.
96
96
  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.
97
- 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.
97
+ 6. **Personalize** with the `firstName` variable; **vary** tone/wording across automations; **re-share** useful info (wallet link, hours, maps, reservation) in reminders; keep **empty lines** between blocks for readability.
98
98
  7. **Align offer expirations with open hours**: never expire an offer while the restaurant is closed.
99
99
 
100
+ **Text variables.** A text action fills these in per guest when it sends. Write each one as its name wrapped in double curly braces (two opening braces, the name, two closing braces), exactly as spelled: `firstName`, `lastName`, `memberNumber`, `serialNumber`, `progress`, `passLink` (the guest's wallet link), `referralLink` (the campaign referral link), `googleMapsLink`, `membersProgramLink`, `visitCount`, `offer`. Any other name is sent to the guest as literal text.
101
+
100
102
  ---
@@ -52,7 +52,7 @@ Three tools, all keyed by the Feast campaign `id` from `listCampaigns` (a UUID),
52
52
 
53
53
  **Units.** `count`; `percent` as 0 to 100 (not 0 to 1); `usd` in dollars (not cents); `days`; `multiple` for ROAS (2 means 2x). In the breakdown: sessions, visitors, signups, impressions and reach are counts; every `*Rate`, `thumbStopRatio`, `holdRate` and `uniqueClickthrough` are percents; spend, cpm, revenue, costPerSignup and revenuePerSignup are USD; averageTimeToShow is days from signup to first scan.
54
54
 
55
- **Headline numbers: `getCampaignKpis`** with `{ "campaignId": "...", "start"?: ..., "end"?: ... }`. Pass both `start` and `end` for a date range; with either missing it covers all time. `"isPrimaryOnly": true` returns only the primary metrics. Returns one `{ id, type, value, unit }` row per metric, covering ad performance (spend, impressions, hook rate, hold rate, CTR, from synced Facebook data, so ROAS is revenue divided by spend), the funnel, automations and results. Rate metrics with a target band also carry `benchmark: { min, good, great }` in the same unit. A metric whose value would be zero is left out rather than returned as 0. So a missing spend row means no spend or no Facebook sync yet, never a confirmed $0.
55
+ **Headline numbers: `getCampaignKpis`** with `{ "campaignId": "...", "start"?: ..., "end"?: ... }`. Send both `start` and `end` for a date range; with either missing it covers all time. `"isPrimaryOnly": true` returns only the primary metrics. Returns one `{ id, type, value, unit }` row per metric, covering ad performance (spend, impressions, hook rate, hold rate, CTR, from synced Facebook data, so ROAS is revenue divided by spend), the funnel, automations and results. Rate metrics with a target band also carry `benchmark: { min, good, great }` in the same unit. A metric whose value would be zero is left out rather than returned as 0. So a missing spend row means no spend or no Facebook sync yet, never a confirmed $0.
56
56
 
57
57
  **Grading: `getCampaignBenchmarks`** (no input) returns `{ id, label, unit, description, formula, benchmark }` for every metric id. Call it once and reuse it; it is the same for every campaign. Grade a value green at or above `good`, yellow at or above `min`, red below `min`; `great` is a stretch level (null for ROAS). `benchmark` is null when a metric has no target band. The bands are fleet percentiles (P25/P50/P75 of campaigns with over 500 visitors), rounded, not per organization; ROAS is anchored at 1x break-even.
58
58
 
@@ -69,10 +69,12 @@ The ads that bring applicants in are tool-drivable end to end:
69
69
  The same tool is how you reschedule and how you record what happened. `startTime` set to a date texts the creator a confirmation and alerts the approver; `null` clears the time and texts the creator asking for a new one. `startTime` is rejected while the row is `pending_approval` and in any call that passes `status: "approved"`, so approve first, then set the time in a second call (`status: "pending_approval"` clears the time itself; don't pass `startTime` with it). `status` also accepts `confirmed`, `visited`, `missed`, `issue` and `cancelled`; of these only `cancelled` texts the creator. `locationId` moves the visit to another location with a creator program and texts no one, so tell the creator yourself. `notes` sets staff notes shown on the scanner, never sent to the creator. Pass `sideEffects: false` to make any update silent (same field writes, but no creator text, no allowance spend, no post-approval automation), which is what you want when correcting a record after the fact rather than making the decision now.
70
70
  3. The creator books, visits, and submits content on their own; none of that is driven from here.
71
71
  4. `listCreatorSubmissions` with `{ "status": "submitted" }` (and `"revision_requested"`): the content review queue. Submissions are stored outside the queryable data model, so this tool is the only way to read them.
72
- 5. `decideCreatorSubmission`: `approved`, `rejected`, `revision_requested`, or `under_review`. **Approving texts the creator too**, unless you pass `skipApprovalText: true` (use that only for silent record corrections). `revision_requested` always texts: it sends your `feedbackMessage` verbatim plus a resubmit link, so write it as something the creator will read, not an internal note. **Always pass `approvalType` explicitly when approving**, because an omitted one means `"ad"`: `"ad"` means the content may run in paid ads, stamps the board's bonus on the submission and marks it pending (paid later through `createInfluencerPayout`), and is rejected when the board's bonus is $0; `"organic"` is for content only on their own channels, and earns no payout. Re-approving an approved submission is rejected, except upgrading an `organic` approval to `ad`.
72
+ 5. `decideCreatorSubmission`: `approved`, `rejected`, `revision_requested`, or `under_review`. **Approving texts the creator too**, unless you send `skipApprovalText: true` (use that only for silent record corrections). `revision_requested` always texts: it sends your `feedbackMessage` verbatim plus a resubmit link, so write it as something the creator will read, not an internal note. **Always send `approvalType` explicitly when approving**, because an omitted one means `"ad"`: `"ad"` means the content may run in paid ads, stamps the board's bonus on the submission and marks it pending (paid later through `createInfluencerPayout`), and is rejected when the board's bonus is zero; `"organic"` is for content only on their own channels, and earns no payout. Re-approving an approved submission is rejected, except upgrading an `organic` approval to `ad`.
73
73
 
74
74
  ### Paying the bonus
75
75
 
76
+ Over the MCP server `createInfluencerPayout` is not available: the client pays creator bonuses in the dashboard, so point them there. On the CLI it remains available, as follows.
77
+
76
78
  `createInfluencerPayout` with `{ "eventId": "..." }` charges the organization's card and starts the creator's bonus on its way. **Never call it on your own initiative**: every call needs the client's explicit, fresh approval to pay this specific creator; a standing instruction doesn't count. The endpoint enforces its own preconditions (a submission approved with `approvalType: "ad"`, no payout already active for the visit: one per visit). The amount defaults to the bonus stamped on the submission when it was approved (falling back to the board config), grossed up to cover the Stripe fee; pass `amountCents` only when the client explicitly asks to pay this one creator a different amount. It applies to this payout only, is written back to the submission so reporting matches what was paid, and leaves the board config unchanged. A visit whose only attempts are FAILED or REFUNDED may be retried, which voids the earlier attempt's open invoice first. After the charge, Stripe webhooks carry it to the creator with no further action from you. Follow progress in `queryData` `creators.creatorPayout`, joined to the visit on `visitEventId`.
77
79
 
78
80
  ### Reimbursing boards
@@ -26,7 +26,7 @@ The recipient is always named by id, never by phone number, and the type must ma
26
26
 
27
27
  - `describeData` with no arguments returns the index of every queryable object type plus the full query grammar; narrowed by schema or object type it returns full column detail (type, enum values, nullability, description, and the link names `pivot` and `join` take). Pass `includeGrammar: false` once you have the grammar. Never guess column names.
28
28
  - `queryData` is read-only and always scoped to the organization; never filter on organizationId yourself.
29
- - Writing a query: `commands` run in order (`filter`, `pivot`, `join`, `aggregate`), and `pivot` and `join` must come before any `aggregate`. A filter leaf is one column, written as the column name prefixed with `$`; combine leaves with `{ "type": "and" | "or", "filters": [...] }`. Use `{ "strings": [...] }` for any-of rather than a large `or`. Pass `args.fields` to return only the columns you need on wide object types, and page by passing the returned `nextCursor` back as `args.cursor` (no `nextCursor` means no more rows).
29
+ - Writing a query: `commands` run in order (`filter`, `pivot`, `join`, `aggregate`), and `pivot` and `join` must come before any `aggregate`. A filter leaf is one column, written as the column name prefixed with `$`; combine leaves with `{ "type": "and" | "or", "filters": [...] }`. Use `{ "strings": [...] }` for any-of rather than a large `or`. Send `args.fields` to return only the columns you need on wide object types, and page by sending the returned `nextCursor` back as `args.cursor` (no `nextCursor` means no more rows).
30
30
  - Example, opted-in members with more than 5 visits, newest first:
31
31
  ```json
32
32
  { "schemaName": "core", "objectTypeName": "guest",
@@ -5,7 +5,7 @@
5
5
  `getTaskboard` is the single "what needs fixing or finishing" surface: one `entries` list discriminated by `kind`. `task` entries are the org's onboarding tasks; `issue` entries are live-computed misconfigurations (placeholder content, inactive automations, a missing "Text STOP" opt-out, unawarded rewards, wallet pass and pixel problems), each with a `severity`, a human `message` and a `fixHint`. Funnel checks cover only screens reachable from the funnel's start screen, so an orphaned screen raises no issue. Scope with `{"scope":{"type":"onboarding"}}` for tasks only, `{"type":"task","task":{"taskId":"..."}}` for one task, or leave the default `all`.
6
6
 
7
7
  - **Start from `completionInstructions`, not guesswork.** Every task entry says exactly what completes it and whether it needs a human in a browser. Trust it over inferring from the task name.
8
- - **Split the work accordingly.** Campaigns, automations, funnel fixes, rewards, brand identity, the phone number, image uploads and the onboarding form are all completable through the tools, so do them. Tasks that need OAuth (Facebook, POS), physical device setup, or in-restaurant staff training cannot be: hand the user that task's **`completionUrl`**, a page where they complete exactly that task. Paste the URL directly in your reply so the user can open it.
8
+ - **Split the work accordingly.** Campaigns, automations, funnel fixes, rewards, brand identity, image uploads and the onboarding form are all completable through the tools, so do them. Tasks that need OAuth (Facebook, POS), physical device setup, or in-restaurant staff training cannot be: hand the user that task's **`completionUrl`**, a page where they complete exactly that task. Paste the URL directly in your reply so the user can open it.
9
9
  - **Never claim a task complete or try to mark one.** Statuses are derived from live data by a recompute (triggered by every taskboard read, ~30s lag). Do the underlying work, then re-read the taskboard to confirm the checkmark flipped.
10
10
  - Working through onboarding = repeat: `getTaskboard` (scope `onboarding`) → do the tool-doable incomplete required tasks → hand over completionUrls for the rest → re-read to verify.
11
11
 
@@ -27,7 +27,7 @@ Browser-only: the brand *import* intelligence (auto-extracting a usable palette
27
27
 
28
28
  ## Plumbing the taskboard leans on
29
29
 
30
- - **Phone number**: `searchAvailablePhoneNumbers` (free; search by the restaurant's own postal code or coordinates, since proximity beats a memorable area code) then `purchaseAndConfigurePhoneNumber`, which **bills the account irreversibly**. Establish where the restaurant actually is before buying.
30
+ - **Texting number**: there is no tool to search for or buy one. The restaurant's texting number is bought automatically from its location when its Google place is set (see `updateBrandIdentity` above), or the client chooses one in the dashboard's "Choose texting number" task. If neither has happened, hand the user that task's `completionUrl`.
31
31
  - **Media**: `getMediaUploadUrl` (PUT the bytes to the presigned URL, then reference the returned key), `listMedia`, `deleteMedia`. This is how logos and offer images get in through the tools.
32
32
  - **Team**: `inviteUser` sends a real email immediately and **defaults to OWNER** (full billing access), so always pass `role` explicitly; VIEWER is read-only, SCANNER is for staff running the scanner app. A new person gets an invitation valid for 14 days; someone with a Feast account gets a login reminder and is added right away. Re-inviting an email cancels its pending invites and sends a fresh one. Only an OWNER can invite.
33
33
  - **Billing**: `getBillingStatus`, read-only: `hasAccess` answers "can they use the product," `needsPayment` flags the states worth acting on and is what the dashboard reads to put the app behind a payment form. `currentTier` and `subscriptionStatus` describe the plan. `existingOrganizations` lists every organization billed under the same billing admin's subscription (this one included when it is on that subscription), with names and tiers; it is absent for per-organization billing. Every billing write stays in the dashboard.
@@ -0,0 +1,14 @@
1
+ # Video ads (Bevyl)
2
+
3
+ ## Making a video
4
+
5
+ One video is one `projectId`, from Generate to the finished MP4. The loop:
6
+
7
+ 1. **`getVideoPromptOptions({ campaignId })`**. `defaultPrompt` is what Content Studio would send: four plain-text sections (Campaign, Concept, Creative direction, CTA), each a heading line then its text. `campaign`, `concept`, `direction` and `cta` list the alternatives for each section (`{ id, label, text }`). Concepts carry the reference video they came from (`videoUrl`, `description`), and directions a `suggestedFormat`. Show the human the prompt you plan to send. Swap a section for another option's text or rewrite it as they ask.
8
+ 2. **`generateVideo`** with `campaignId`, `prompt` (sent to Bevyl verbatim, 5,000 characters at most) and `format` (`voiceover`, `talking-head`, `trending-sounds` with a `trendId`, or `no-audio`; the chosen direction's `suggestedFormat` is the default). Pass `angleId` from `listCampaignVideos` to add a version to an existing angle; otherwise a new angle is named from the Concept's first line, or from `angleTitle`. Footage is every clip already synced to Bevyl unless you pass `brollKeys` (S3 keys from `listMedia` scope `creativeLibraryBroll`). **This spends Bevyl credits: only with the human's explicit go-ahead.**
9
+ 3. **`getVideo({ projectId })`** about every 30 seconds. `pipeline.status` moves uploading, processing, creating, rendering, exporting, then `ready` (`exportUrl` is the MP4) or `failed` (`message` says why). A first video takes several minutes.
10
+ 4. Send the human `exportUrl` to watch. For one change, **`requestVideoEdit`** with only that change in `note` (credits again; `getVideo` goes back to rendering). When they approve it, **`approveVideo({ projectId })`** saves the MP4 to the campaign's creative library and returns its `libraryKey`.
11
+
12
+ ## Reference scripts
13
+
14
+ `listReferenceScripts` returns the reference ad scripts Content Studio offers as Concept presets for Bevyl videos, each distilled from an ad that performed: `description` (what the video shows), `videoUrl` (a public MP4 preview), `structure` (the ordered beats), `keyPhrases` (lines to adapt, with `<placeholders>` filled from the campaign's facts) and `concept` (the exact text Content Studio sends to Bevyl). Copy the structure and pacing, not the words. The list is the same for every organization. Use one to explain a concept option or to write a concept of your own for `prompt`.