@feastalytics/cli 0.1.15 → 0.1.17

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 CHANGED
@@ -3221,8 +3221,8 @@ var require_utils = __commonJS({
3221
3221
  }
3222
3222
  return ind;
3223
3223
  }
3224
- function removeDotSegments(path3) {
3225
- let input = path3;
3224
+ function removeDotSegments(path5) {
3225
+ let input = path5;
3226
3226
  const output = [];
3227
3227
  let nextSlash = -1;
3228
3228
  let len = 0;
@@ -3474,8 +3474,8 @@ var require_schemes = __commonJS({
3474
3474
  wsComponent.secure = void 0;
3475
3475
  }
3476
3476
  if (wsComponent.resourceName) {
3477
- const [path3, query] = wsComponent.resourceName.split("?");
3478
- wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
3477
+ const [path5, query] = wsComponent.resourceName.split("?");
3478
+ wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
3479
3479
  wsComponent.query = query;
3480
3480
  wsComponent.resourceName = void 0;
3481
3481
  }
@@ -6836,22 +6836,22 @@ var TRPC_ERROR_CODES_BY_NUMBER = invert(TRPC_ERROR_CODES_BY_KEY);
6836
6836
  var TRPC_ERROR_CODES_BY_NUMBER2 = invert(TRPC_ERROR_CODES_BY_KEY);
6837
6837
  var noop = () => {
6838
6838
  };
6839
- function createInnerProxy(callback, path3) {
6839
+ function createInnerProxy(callback, path5) {
6840
6840
  const proxy = new Proxy(noop, {
6841
6841
  get(_obj, key) {
6842
6842
  if (typeof key !== "string" || key === "then") {
6843
6843
  return void 0;
6844
6844
  }
6845
6845
  return createInnerProxy(callback, [
6846
- ...path3,
6846
+ ...path5,
6847
6847
  key
6848
6848
  ]);
6849
6849
  },
6850
6850
  apply(_1, _2, args) {
6851
- const isApply = path3[path3.length - 1] === "apply";
6851
+ const isApply = path5[path5.length - 1] === "apply";
6852
6852
  return callback({
6853
6853
  args: isApply ? args.length >= 2 ? args[1] : [] : args,
6854
- path: isApply ? path3.slice(0, -1) : path3
6854
+ path: isApply ? path5.slice(0, -1) : path5
6855
6855
  });
6856
6856
  }
6857
6857
  });
@@ -7263,13 +7263,13 @@ function createHTTPBatchLink(requester) {
7263
7263
  if (maxURLLength === Infinity) {
7264
7264
  return true;
7265
7265
  }
7266
- const path3 = batchOps.map((op) => op.path).join(",");
7266
+ const path5 = batchOps.map((op) => op.path).join(",");
7267
7267
  const inputs = batchOps.map((op) => op.input);
7268
7268
  const url = getUrl({
7269
7269
  ...resolvedOpts,
7270
7270
  runtime,
7271
7271
  type,
7272
- path: path3,
7272
+ path: path5,
7273
7273
  inputs
7274
7274
  });
7275
7275
  return url.length <= maxURLLength;
@@ -7327,11 +7327,11 @@ function createHTTPBatchLink(requester) {
7327
7327
  }
7328
7328
  var batchRequester = (requesterOpts) => {
7329
7329
  return (batchOps) => {
7330
- const path3 = batchOps.map((op) => op.path).join(",");
7330
+ const path5 = batchOps.map((op) => op.path).join(",");
7331
7331
  const inputs = batchOps.map((op) => op.input);
7332
7332
  const { promise, cancel } = jsonHttpRequester({
7333
7333
  ...requesterOpts,
7334
- path: path3,
7334
+ path: path5,
7335
7335
  inputs,
7336
7336
  headers() {
7337
7337
  if (!requesterOpts.opts.headers) {
@@ -7365,12 +7365,12 @@ function httpLinkFactory(factoryOpts) {
7365
7365
  return (opts) => {
7366
7366
  const resolvedOpts = resolveHTTPLinkOptions(opts);
7367
7367
  return (runtime) => ({ op }) => observable((observer) => {
7368
- const { path: path3, input, type } = op;
7368
+ const { path: path5, input, type } = op;
7369
7369
  const { promise, cancel } = factoryOpts.requester({
7370
7370
  ...resolvedOpts,
7371
7371
  runtime,
7372
7372
  type,
7373
- path: path3,
7373
+ path: path5,
7374
7374
  input,
7375
7375
  headers() {
7376
7376
  if (!opts.headers) {
@@ -7416,13 +7416,13 @@ var httpLink = httpLinkFactory({
7416
7416
 
7417
7417
  // node_modules/@trpc/client/dist/index.mjs
7418
7418
  var TRPCUntypedClient = class {
7419
- $request({ type, input, path: path3, context = {} }) {
7419
+ $request({ type, input, path: path5, context = {} }) {
7420
7420
  const chain$ = createChain({
7421
7421
  links: this.links,
7422
7422
  op: {
7423
7423
  id: ++this.requestId,
7424
7424
  type,
7425
- path: path3,
7425
+ path: path5,
7426
7426
  input,
7427
7427
  context
7428
7428
  }
@@ -7442,28 +7442,28 @@ var TRPCUntypedClient = class {
7442
7442
  });
7443
7443
  return abortablePromise;
7444
7444
  }
7445
- query(path3, input, opts) {
7445
+ query(path5, input, opts) {
7446
7446
  return this.requestAsPromise({
7447
7447
  type: "query",
7448
- path: path3,
7448
+ path: path5,
7449
7449
  input,
7450
7450
  context: opts?.context,
7451
7451
  signal: opts?.signal
7452
7452
  });
7453
7453
  }
7454
- mutation(path3, input, opts) {
7454
+ mutation(path5, input, opts) {
7455
7455
  return this.requestAsPromise({
7456
7456
  type: "mutation",
7457
- path: path3,
7457
+ path: path5,
7458
7458
  input,
7459
7459
  context: opts?.context,
7460
7460
  signal: opts?.signal
7461
7461
  });
7462
7462
  }
7463
- subscription(path3, input, opts) {
7463
+ subscription(path5, input, opts) {
7464
7464
  const observable$ = this.$request({
7465
7465
  type: "subscription",
7466
- path: path3,
7466
+ path: path5,
7467
7467
  input,
7468
7468
  context: opts?.context
7469
7469
  });
@@ -7535,10 +7535,10 @@ function createTRPCClientProxy(client) {
7535
7535
  if (key === "__untypedClient") {
7536
7536
  return client;
7537
7537
  }
7538
- return createRecursiveProxy(({ path: path3, args }) => {
7538
+ return createRecursiveProxy(({ path: path5, args }) => {
7539
7539
  const pathCopy = [
7540
7540
  key,
7541
- ...path3
7541
+ ...path5
7542
7542
  ];
7543
7543
  const procedureType = clientCallTypeToProcedureType(pathCopy.pop());
7544
7544
  const fullPath = pathCopy.join(".");
@@ -7647,12 +7647,12 @@ var streamingJsonHttpRequester = (opts, onSingle) => {
7647
7647
  var streamRequester = (requesterOpts) => {
7648
7648
  const textDecoder = getTextDecoder(requesterOpts.opts.textDecoder);
7649
7649
  return (batchOps, unitResolver) => {
7650
- const path3 = batchOps.map((op) => op.path).join(",");
7650
+ const path5 = batchOps.map((op) => op.path).join(",");
7651
7651
  const inputs = batchOps.map((op) => op.input);
7652
7652
  const { cancel, promise } = streamingJsonHttpRequester({
7653
7653
  ...requesterOpts,
7654
7654
  textDecoder,
7655
- path: path3,
7655
+ path: path5,
7656
7656
  inputs,
7657
7657
  headers() {
7658
7658
  if (!requesterOpts.opts.headers) {
@@ -7866,7 +7866,7 @@ var isURL = (payload) => payload instanceof URL;
7866
7866
 
7867
7867
  // node_modules/superjson/dist/pathstringifier.js
7868
7868
  var escapeKey = (key) => key.replace(/\\/g, "\\\\").replace(/\./g, "\\.");
7869
- var stringifyPath = (path3) => path3.map(String).map(escapeKey).join(".");
7869
+ var stringifyPath = (path5) => path5.map(String).map(escapeKey).join(".");
7870
7870
  var parsePath = (string, legacyPaths) => {
7871
7871
  const result = [];
7872
7872
  let segment = "";
@@ -8113,26 +8113,26 @@ var getNthKey = (value, n) => {
8113
8113
  }
8114
8114
  return keys.next().value;
8115
8115
  };
8116
- function validatePath(path3) {
8117
- if (includes(path3, "__proto__")) {
8116
+ function validatePath(path5) {
8117
+ if (includes(path5, "__proto__")) {
8118
8118
  throw new Error("__proto__ is not allowed as a property");
8119
8119
  }
8120
- if (includes(path3, "prototype")) {
8120
+ if (includes(path5, "prototype")) {
8121
8121
  throw new Error("prototype is not allowed as a property");
8122
8122
  }
8123
- if (includes(path3, "constructor")) {
8123
+ if (includes(path5, "constructor")) {
8124
8124
  throw new Error("constructor is not allowed as a property");
8125
8125
  }
8126
8126
  }
8127
- var getDeep = (object, path3) => {
8128
- validatePath(path3);
8129
- for (let i = 0; i < path3.length; i++) {
8130
- const key = path3[i];
8127
+ var getDeep = (object, path5) => {
8128
+ validatePath(path5);
8129
+ for (let i = 0; i < path5.length; i++) {
8130
+ const key = path5[i];
8131
8131
  if (isSet(object)) {
8132
8132
  object = getNthKey(object, +key);
8133
8133
  } else if (isMap(object)) {
8134
8134
  const row = +key;
8135
- const type = +path3[++i] === 0 ? "key" : "value";
8135
+ const type = +path5[++i] === 0 ? "key" : "value";
8136
8136
  const keyOfRow = getNthKey(object, row);
8137
8137
  switch (type) {
8138
8138
  case "key":
@@ -8148,14 +8148,14 @@ var getDeep = (object, path3) => {
8148
8148
  }
8149
8149
  return object;
8150
8150
  };
8151
- var setDeep = (object, path3, mapper) => {
8152
- validatePath(path3);
8153
- if (path3.length === 0) {
8151
+ var setDeep = (object, path5, mapper) => {
8152
+ validatePath(path5);
8153
+ if (path5.length === 0) {
8154
8154
  return mapper(object);
8155
8155
  }
8156
8156
  let parent = object;
8157
- for (let i = 0; i < path3.length - 1; i++) {
8158
- const key = path3[i];
8157
+ for (let i = 0; i < path5.length - 1; i++) {
8158
+ const key = path5[i];
8159
8159
  if (isArray(parent)) {
8160
8160
  const index = +key;
8161
8161
  parent = parent[index];
@@ -8165,12 +8165,12 @@ var setDeep = (object, path3, mapper) => {
8165
8165
  const row = +key;
8166
8166
  parent = getNthKey(parent, row);
8167
8167
  } else if (isMap(parent)) {
8168
- const isEnd = i === path3.length - 2;
8168
+ const isEnd = i === path5.length - 2;
8169
8169
  if (isEnd) {
8170
8170
  break;
8171
8171
  }
8172
8172
  const row = +key;
8173
- const type = +path3[++i] === 0 ? "key" : "value";
8173
+ const type = +path5[++i] === 0 ? "key" : "value";
8174
8174
  const keyOfRow = getNthKey(parent, row);
8175
8175
  switch (type) {
8176
8176
  case "key":
@@ -8182,7 +8182,7 @@ var setDeep = (object, path3, mapper) => {
8182
8182
  }
8183
8183
  }
8184
8184
  }
8185
- const lastKey = path3[path3.length - 1];
8185
+ const lastKey = path5[path5.length - 1];
8186
8186
  if (isArray(parent)) {
8187
8187
  parent[+lastKey] = mapper(parent[+lastKey]);
8188
8188
  } else if (isPlainObject(parent)) {
@@ -8197,7 +8197,7 @@ var setDeep = (object, path3, mapper) => {
8197
8197
  }
8198
8198
  }
8199
8199
  if (isMap(parent)) {
8200
- const row = +path3[path3.length - 2];
8200
+ const row = +path5[path5.length - 2];
8201
8201
  const keyToRow = getNthKey(parent, row);
8202
8202
  const type = +lastKey === 0 ? "key" : "value";
8203
8203
  switch (type) {
@@ -8244,16 +8244,16 @@ function traverse(tree, walker2, version, origin = []) {
8244
8244
  walker2(nodeValue, origin);
8245
8245
  }
8246
8246
  function applyValueAnnotations(plain, annotations, version, superJson) {
8247
- traverse(annotations, (type, path3) => {
8248
- plain = setDeep(plain, path3, (v) => untransformValue(v, type, superJson));
8247
+ traverse(annotations, (type, path5) => {
8248
+ plain = setDeep(plain, path5, (v) => untransformValue(v, type, superJson));
8249
8249
  }, version);
8250
8250
  return plain;
8251
8251
  }
8252
8252
  function applyReferentialEqualityAnnotations(plain, annotations, version) {
8253
8253
  const legacyPaths = enableLegacyPaths(version);
8254
- function apply(identicalPaths, path3) {
8255
- const object = getDeep(plain, parsePath(path3, legacyPaths));
8256
- identicalPaths.map((path4) => parsePath(path4, legacyPaths)).forEach((identicalObjectPath) => {
8254
+ function apply(identicalPaths, path5) {
8255
+ const object = getDeep(plain, parsePath(path5, legacyPaths));
8256
+ identicalPaths.map((path6) => parsePath(path6, legacyPaths)).forEach((identicalObjectPath) => {
8257
8257
  plain = setDeep(plain, identicalObjectPath, () => object);
8258
8258
  });
8259
8259
  }
@@ -8271,12 +8271,12 @@ function applyReferentialEqualityAnnotations(plain, annotations, version) {
8271
8271
  return plain;
8272
8272
  }
8273
8273
  var isDeep = (object, superJson) => isPlainObject(object) || isArray(object) || isMap(object) || isSet(object) || isError(object) || isInstanceOfRegisteredClass(object, superJson);
8274
- function addIdentity(object, path3, identities) {
8274
+ function addIdentity(object, path5, identities) {
8275
8275
  const existingSet = identities.get(object);
8276
8276
  if (existingSet) {
8277
- existingSet.push(path3);
8277
+ existingSet.push(path5);
8278
8278
  } else {
8279
- identities.set(object, [path3]);
8279
+ identities.set(object, [path5]);
8280
8280
  }
8281
8281
  }
8282
8282
  function generateReferentialEqualityAnnotations(identitites, dedupe) {
@@ -8287,7 +8287,7 @@ function generateReferentialEqualityAnnotations(identitites, dedupe) {
8287
8287
  return;
8288
8288
  }
8289
8289
  if (!dedupe) {
8290
- paths = paths.map((path3) => path3.map(String)).sort((a, b) => a.length - b.length);
8290
+ paths = paths.map((path5) => path5.map(String)).sort((a, b) => a.length - b.length);
8291
8291
  }
8292
8292
  const [representativePath, ...identicalPaths] = paths;
8293
8293
  if (representativePath.length === 0) {
@@ -8306,10 +8306,10 @@ function generateReferentialEqualityAnnotations(identitites, dedupe) {
8306
8306
  return isEmptyObject(result) ? void 0 : result;
8307
8307
  }
8308
8308
  }
8309
- var walker = (object, identities, superJson, dedupe, path3 = [], objectsInThisPath = [], seenObjects = /* @__PURE__ */ new Map()) => {
8309
+ var walker = (object, identities, superJson, dedupe, path5 = [], objectsInThisPath = [], seenObjects = /* @__PURE__ */ new Map()) => {
8310
8310
  const primitive = isPrimitive(object);
8311
8311
  if (!primitive) {
8312
- addIdentity(object, path3, identities);
8312
+ addIdentity(object, path5, identities);
8313
8313
  const seen = seenObjects.get(object);
8314
8314
  if (seen) {
8315
8315
  return dedupe ? {
@@ -8343,7 +8343,7 @@ var walker = (object, identities, superJson, dedupe, path3 = [], objectsInThisPa
8343
8343
  if (index === "__proto__" || index === "constructor" || index === "prototype") {
8344
8344
  throw new Error(`Detected property ${index}. This is a prototype pollution risk, please remove it from your object.`);
8345
8345
  }
8346
- const recursiveResult = walker(value, identities, superJson, dedupe, [...path3, index], [...objectsInThisPath, object], seenObjects);
8346
+ const recursiveResult = walker(value, identities, superJson, dedupe, [...path5, index], [...objectsInThisPath, object], seenObjects);
8347
8347
  transformedValue[index] = recursiveResult.transformedValue;
8348
8348
  if (isArray(recursiveResult.annotations)) {
8349
8349
  innerAnnotations[escapeKey(index)] = recursiveResult.annotations;
@@ -8654,7 +8654,8 @@ function errorMessage(error) {
8654
8654
  }
8655
8655
 
8656
8656
  // src/main.ts
8657
- import fs2 from "fs";
8657
+ import fs3 from "fs";
8658
+ import path3 from "path";
8658
8659
 
8659
8660
  // src/generated/manifest.ts
8660
8661
  var CLI_MANIFEST = {
@@ -8668,7 +8669,7 @@ var CLI_MANIFEST = {
8668
8669
  {
8669
8670
  "id": "ads_activate_entity",
8670
8671
  "domain": "ads",
8671
- "description": "Take a Meta campaign, ad set or ad from PAUSED to ACTIVE. This spends real money \u2014 call it only after the human has explicitly confirmed they want this live. Pausing is the other tool: ads_update_entity with status PAUSED. Activating a parent does NOT activate its children, so a structure that was published paused has to be activated from the top down \u2014 campaign, then ad set, then ad. Activating a child whose parent is still paused succeeds at Meta and delivers nothing, so the response reports pausedAncestors and willDeliver; if willDeliver is false the entity is live in name only and you should say so rather than reporting success. setAdCampaignStatus is the shortcut for a campaign Feastalytics published, because that one cascades to every level at once.",
8672
+ "description": "Take one Meta campaign, ad set or ad from PAUSED to ACTIVE. Spends real money: only after the human explicitly confirms. Does not cascade, so activate top-down; willDeliver false (a paused parent, listed in pausedAncestors) means live in name only, so say so. For a campaign Feastalytics published, use setAdCampaignStatus, which cascades.",
8672
8673
  "type": "mutation",
8673
8674
  "path": [
8674
8675
  "api",
@@ -8708,7 +8709,7 @@ var CLI_MANIFEST = {
8708
8709
  {
8709
8710
  "id": "ads_create_dataset",
8710
8711
  "domain": "ads",
8711
- "description": "Create a dataset (Meta pixel) on an ad account, for a restaurant that has none. Call ads_get_datasets first: an account usually already has one, a second dataset splits a funnel's events in two, and datasets cannot be deleted. Creating it connects it to nothing \u2014 write the returned id to the organization's layout config with updateBrandIdentity, which is what makes the funnel fire it and what the onboarding task reads.",
8712
+ "description": "Create a dataset (Meta pixel) on an ad account. Datasets cannot be deleted and a second one splits a funnel's events, so check ads_get_datasets first. Creating connects nothing: write the returned id to the layout config with updateBrandIdentity.",
8712
8713
  "type": "mutation",
8713
8714
  "path": [
8714
8715
  "api",
@@ -8739,7 +8740,7 @@ var CLI_MANIFEST = {
8739
8740
  {
8740
8741
  "id": "ads_get_ad_accounts",
8741
8742
  "domain": "ads",
8742
- "description": "List the ad accounts this organization can publish to. The Meta token reaches every ad account of every business it was connected for, so the list is narrowed to accounts publishAds will accept; includeUnassigned returns the rest for diagnosing a missing account, and those are not publishable.",
8743
+ "description": "List the ad accounts you can publish to. The Meta token reaches every ad account of every business it was connected for, so the list is narrowed to accounts publishAds will accept; includeUnassigned returns the rest for diagnosing a missing account, and those are not publishable.",
8743
8744
  "type": "query",
8744
8745
  "path": [
8745
8746
  "api",
@@ -8762,7 +8763,7 @@ var CLI_MANIFEST = {
8762
8763
  {
8763
8764
  "id": "ads_get_ad_entities",
8764
8765
  "domain": "ads",
8765
- "description": "Read the campaigns, ad sets or ads already on an ad account. Ads come back with their creative attached.\n\nlevel says what kind of thing comes back; the ids say where to look. An id at its own level fetches that one object, and at a lower level lists that object's children \u2014 so campaignId with level campaign returns that campaign, with level adSet returns its ad sets, and with level ad returns every ad in it across all of its ad sets. Ids can only point downward: an adSetId at level campaign is an error rather than being ignored. Where several apply, the narrowest wins.\n\nTo add new creatives to an ad set that is already running, use this to copy the settings the new ads must match, then publish with the addAds template. Read level ad with that adSetId, skip ads whose effectiveStatus is DELETED or ARCHIVED, take the first one left, and pull from its creative:\n- pageId: objectStorySpec.page_id\n- instagramAccountId: objectStorySpec.instagram_actor_id, or instagram_user_id if that is absent \u2014 both spellings occur\n- urlTags: urlTags\n- headline, primaryText and landingUrl live in whichever of three shapes the ad uses. assetFeedSpec, if present, wins: titles[0].text, bodies[0].text, link_urls[0].website_url. Otherwise objectStorySpec.link_data for an image ad: name, message, link. Otherwise objectStorySpec.video_data for a video ad: title, message, call_to_action.value.link.\n\nA mismatch here is not rejected by Meta \u2014 it publishes an ad pointing somewhere different from its siblings \u2014 so copy the values rather than inventing them.",
8766
+ "description": "Read the campaigns, ad sets or ads on an ad account; ads include their creative. level is what comes back; a campaignId, adSetId or adId narrows to that object or its children (an id below level is an error). Before an addAds publish, copy a live ad's settings as the ads workflow describes.",
8766
8767
  "type": "query",
8767
8768
  "path": [
8768
8769
  "api",
@@ -8822,7 +8823,7 @@ var CLI_MANIFEST = {
8822
8823
  {
8823
8824
  "id": "ads_get_custom_audiences",
8824
8825
  "domain": "ads",
8825
- "description": "List the custom audiences on an ad account, for the customAudienceIds and excludedCustomAudienceIds template variables. isReadyForUse false means Meta will not deliver to it, usually for too few matched people, so an ad set targeting it reaches nobody, and the size bounds are approximate and go stale while an audience is being updated. An audience belongs to the ad account, so the ids from one account are rejected by another. Nothing here says how fresh the underlying list is: a customer-list audience is a snapshot of whatever was last uploaded, and timeUpdated is when that happened.",
8826
+ "description": "List the custom audiences on an ad account, for the customAudienceIds and excludedCustomAudienceIds template variables. Skip any with isReadyForUse false: Meta delivers nothing to it. Audience ids belong to one ad account and are rejected by another.",
8826
8827
  "type": "query",
8827
8828
  "path": [
8828
8829
  "api",
@@ -8848,7 +8849,7 @@ var CLI_MANIFEST = {
8848
8849
  {
8849
8850
  "id": "ads_get_datasets",
8850
8851
  "domain": "ads",
8851
- "description": "List the datasets (Meta pixels) on an ad account, with lastFiredTime so you can see which are receiving events. This is for checking a pixel rather than choosing one: the pixelId a campaign should optimise against is the pixel its funnel actually fires, which comes from the organization's layout config. A pixel picked from this list because it looks plausible may receive no traffic from that funnel.",
8852
+ "description": "List the datasets (Meta pixels) on an ad account, with lastFiredTime. For checking a pixel, not choosing one: a campaign should optimise against the pixel its funnel fires (from the organization's layout config), or it may get no traffic.",
8852
8853
  "type": "query",
8853
8854
  "path": [
8854
8855
  "api",
@@ -8874,7 +8875,7 @@ var CLI_MANIFEST = {
8874
8875
  {
8875
8876
  "id": "ads_get_ig_accounts",
8876
8877
  "domain": "ads",
8877
- "description": "List the Instagram identities a Page can run ads as. kind is business for a real Instagram business account, or pageBacked for the shadow identity Meta creates for a Page without one \u2014 both are valid values for the instagramAccountId template variable. Takes a pageId because the ad identity follows the Page.",
8878
+ "description": "List the Instagram identities a Page can run ads as. kind is business for a real Instagram business account, or pageBacked for the shadow identity Meta creates for a Page without one; both are valid for the instagramAccountId template variable. pageId comes from ads_get_user_pages.",
8878
8879
  "type": "query",
8879
8880
  "path": [
8880
8881
  "api",
@@ -8900,7 +8901,7 @@ var CLI_MANIFEST = {
8900
8901
  {
8901
8902
  "id": "ads_get_user_pages",
8902
8903
  "domain": "ads",
8903
- "description": "List the Facebook Pages available for advertising. The Meta token also reaches Pages belonging to other businesses, and nothing stops publishAds from using one, so prefer a Page with usedByOrganization true \u2014 those are the Pages this organization has already run ads from. instagramBusinessAccountId is included when the Page has one; use ads_get_ig_accounts for the full identity list.",
8904
+ "description": "List the Facebook Pages available for advertising, with any linked Instagram business account. The token also reaches other businesses' Pages and publishAds accepts them, so prefer a Page with usedByOrganization true (one this organization has run ads from).",
8904
8905
  "type": "query",
8905
8906
  "path": [
8906
8907
  "api",
@@ -8918,7 +8919,7 @@ var CLI_MANIFEST = {
8918
8919
  {
8919
8920
  "id": "ads_update_entity",
8920
8921
  "domain": "ads",
8921
- "description": "Change budget, name or pause state on a Meta campaign, ad set or ad. This is the lever the re-evaluation loop turns: scale a winner or throttle a loser by moving its daily budget. adAccountId must be the account that actually owns the entity, and is rejected otherwise. Budgets are integer cents and replace the current value rather than adjusting it \u2014 read the entity with ads_get_ad_entities first, and confirm the new number with the human, because it starts spending differently the moment it lands. An entity carries either a daily or a lifetime budget, never both. status only accepts PAUSED: pausing one ad is what this is for, while going live spends money and belongs to setAdCampaignStatus, which cascades. Creative content cannot be edited at all \u2014 Meta creatives are immutable, so new copy or media means a new ad.",
8922
+ "description": "Rename, re-budget or pause a Meta campaign, ad set or ad. Budgets are integer cents and replace the current value, and spend changes the moment it lands: read the entity with ads_get_ad_entities and confirm the new number with the human first. adAccountId must own the entity. status only accepts PAUSED; going live is setAdCampaignStatus or ads_activate_entity. Creatives cannot be edited.",
8922
8923
  "type": "mutation",
8923
8924
  "path": [
8924
8925
  "api",
@@ -9009,7 +9010,7 @@ var CLI_MANIFEST = {
9009
9010
  {
9010
9011
  "id": "applyFunnelTemplate",
9011
9012
  "domain": "campaigns",
9012
- "description": "Applies a funnel template to an existing campaign that has no funnel set up yet (override.initialScreenId is null). Pass a templateId from listFunnelTemplates: offer-basic (Sign Up routes straight to the offer wallet \u2014 no payment step; use this for offers redeemed in person), offer-prepay / offer-direct-prepay (Sign Up / landing routes to a Stripe payment screen), reservation-offer-basic (Sign Up routes to reservation-or-wallet), reservation-offer-prepay / reservation-offer-direct-prepay (payment then reservation), reservation-only. The promotion's canPrePay flag does NOT change what gets built \u2014 prepay templates always insert the payment screen, and are rejected unless the promotion has canPrePay: true and a price.",
9013
+ "description": "Builds a campaign's funnel screens from a template. Only works on a campaign with no funnel yet (deleteFunnel resets one). templateId comes from listFunnelTemplates. Prepay templates always add a Stripe payment screen, so use offer-basic for an offer redeemed in person, priced or not.",
9013
9014
  "type": "mutation",
9014
9015
  "path": [
9015
9016
  "api",
@@ -9047,7 +9048,7 @@ var CLI_MANIFEST = {
9047
9048
  {
9048
9049
  "id": "awardReward",
9049
9050
  "domain": "membersProgram",
9050
- "description": "Give one member a reward they can redeem, the way the dashboard's Give Reward button does. This is a real grant that lands in the guest's wallet pass \u2014 it is not the same as createMembersProgramReward, which only defines a reward the program offers. serialNumber identifies the member (get one from searchUsers) and itemId the catalog item they get; both are checked against this organization and a wrong id is rejected rather than granted. Expiry is optional and a reward with none never expires: expiresInDays sets it to the end of that day in the restaurant's timezone, which is what a guest reads '14 days' to mean, and expiresAt takes an exact ISO 8601 instant \u2014 pass one or the other. locationId restricts redemption to one participating location and is otherwise left open. Awarding recomputes the member's progress, which also re-evaluates their automations, so a flow triggered by earning a reward will fire. There is no undo and no idempotency key, so a retried call grants a second reward.",
9051
+ "description": "Give one member a reward in their wallet pass now, like the dashboard's Give Reward button (createMembersProgramReward only defines a reward the program offers). serialNumber from searchUsers, itemId from listMembersProgramRewards. Re-evaluates the member's automations, so a flow triggered by earning a reward fires. No undo and no idempotency key: a retried call grants a second reward, so confirm with the user and call once per member.",
9051
9052
  "type": "mutation",
9052
9053
  "path": [
9053
9054
  "api",
@@ -9089,7 +9090,7 @@ var CLI_MANIFEST = {
9089
9090
  {
9090
9091
  "id": "batchEditAutomations",
9091
9092
  "domain": "automations",
9092
- "description": "Write automation changes straight to production in one atomic batch. PREFER THE DRAFT FLOW: createAutomationDraft plus stageAutomationEdits let a human preview and sign off first, and saveAutomationEdits promotes through this same code path \u2014 reach for this tool only when an immediate live write is explicitly wanted. Every automation lives inside a flow, so `create` ops require a flowId and throw without one: call listAutomationFlows to find and reuse a matching flow, or createAutomationFlow to make one. Never invent a flowId. Delete is blocked for automations that already have sends.",
9093
+ "description": "Write automation changes straight to production in one atomic batch; real guests receive the result. Use only when the user explicitly wants an immediate live write; otherwise use the draft loop (createAutomationDraft, stageAutomationEdits, saveAutomationEdits). `create` ops need a flowId from listAutomationFlows or createAutomationFlow. An `update` replaces each field it sends, so send the full current `actions` array.",
9093
9094
  "type": "mutation",
9094
9095
  "path": [
9095
9096
  "api",
@@ -10947,7 +10948,7 @@ var CLI_MANIFEST = {
10947
10948
  {
10948
10949
  "id": "cloneCampaign",
10949
10950
  "domain": "campaigns",
10950
- "description": "Clones an existing campaign including its funnel screens, automations, and offers. Requires sourceCampaignId, newCampaignName, and referrer (subdomain from organization.subdomains2). Cloned automations keep the source campaign's reservation links \u2014 after cloning, rewrite any reservation link in the new campaign's automations to the new campaign's shorthand.",
10951
+ "description": "Copies a campaign's funnel screens, automations and offers into a new campaign. referrer is a subdomain from getOrganization (subdomains2). The copied automations keep the source campaign's reservation links; rewrite them to the new campaign's shorthand.",
10951
10952
  "type": "mutation",
10952
10953
  "path": [
10953
10954
  "api",
@@ -10980,7 +10981,7 @@ var CLI_MANIFEST = {
10980
10981
  {
10981
10982
  "id": "countParentAutomationRecipients",
10982
10983
  "domain": "automations",
10983
- "description": "How many distinct members already received an automation \u2014 the audience size a receiveAutomation-triggered child would reach if backfilled with applyToHistorical: true. Call this before any backfill, tell the user the number, and warn when it exceeds 1000; only backfill after they confirm. Read-only and safe to call while deciding.",
10984
+ "description": "Count distinct members who already received an automation: the audience a receiveAutomation child reaches if backfilled with applyToHistorical: true. Read-only. Before any backfill, tell the user this number, warn above 1000, and backfill only after they confirm.",
10984
10985
  "type": "query",
10985
10986
  "path": [
10986
10987
  "api",
@@ -11004,7 +11005,7 @@ var CLI_MANIFEST = {
11004
11005
  {
11005
11006
  "id": "createAutomationDraft",
11006
11007
  "domain": "automations",
11007
- "description": "Start a draft of automation changes \u2014 an off-prod overlay a human can preview and sign off before anything goes live. This is the DEFAULT way to change automations: create a draft, add changes with stageAutomationEdits, share a preview link, promote with saveAutomationEdits. `title` is what the reviewer sees. Optionally seed with `operations` in the format batchEditAutomations takes. Returns the draft including `previewUrls` \u2014 links to send for sign-off.",
11008
+ "description": "Start a draft of automation changes for a human to preview before they go live; the default way to change automations. `title` is what the reviewer sees. Optional `operations` use the batchEditAutomations format. Keep the returned draftId (drafts cannot be listed); share `previewUrls` for sign-off.",
11008
11009
  "type": "mutation",
11009
11010
  "path": [
11010
11011
  "api",
@@ -12865,7 +12866,7 @@ var CLI_MANIFEST = {
12865
12866
  {
12866
12867
  "id": "createAutomationFlow",
12867
12868
  "domain": "automations",
12868
- "description": "Create an automation flow \u2014 the container grouping automations by a shared trigger. Automations always live inside a flow, so find one with listAutomationFlows before creating another.",
12869
+ "description": "Create an automation flow: the container grouping automations by a shared trigger. Automations always live inside a flow, so find one with listAutomationFlows before creating another.",
12869
12870
  "type": "mutation",
12870
12871
  "path": [
12871
12872
  "api",
@@ -12928,7 +12929,7 @@ var CLI_MANIFEST = {
12928
12929
  {
12929
12930
  "id": "createAvailability",
12930
12931
  "domain": "creators",
12931
- "description": "Open a window creators can book visits in. A block is either type 'once' with a utcStart and utcEnd, or type 'weekly' with start and end hour/minute, the utcDaysOfWeek it repeats on, and blockUtcStart for when the repetition begins. All times are UTC, and the restaurant thinks in local time \u2014 convert before writing. For weekly blocks utcDaysOfWeek is the day of week IN UTC, so an evening local window that crosses midnight UTC lands on the following day: 9pm Friday New York is 02:00 Saturday UTC, and writing Friday there opens the wrong night. Set campaignId as well as locationId: the Set booking windows task only completes when a window carries the first campaign's id, so a window without one works for booking but leaves the task open.",
12932
+ "description": "Open a window creators can book visits in, as a 'once' or 'weekly' block. Times and weekly days are UTC: convert the restaurant's local day and time together (see the creators workflow). Set campaignId as well as locationId, or the Set booking windows task stays open.",
12932
12933
  "type": "mutation",
12933
12934
  "path": [
12934
12935
  "api",
@@ -13060,7 +13061,7 @@ var CLI_MANIFEST = {
13060
13061
  {
13061
13062
  "id": "createBrandIdentity",
13062
13063
  "domain": "core",
13063
- "description": "Create the restaurant's branded website: a subdomain, its layout config, and a full default screen tree. Gates everything funnel-shaped downstream, since screens are addressed by referrer. referrer is the subdomain label only \u2014 letters and numbers, no dots \u2014 and is lowercased; it is claimed across all organizations, so a name another restaurant already uses is rejected. Get logoUrl from getMediaUploadUrl. Confirm the name and subdomain with the customer first; this is their branding decision.",
13064
+ "description": "Create the restaurant's branded site: a subdomain (referrer), its layout config and default screens. Funnels are addressed by referrer, so they need one. The subdomain is claimed across all restaurants: confirm the name and subdomain with the customer first. Get logoUrl from getMediaUploadUrl.",
13064
13065
  "type": "mutation",
13065
13066
  "path": [
13066
13067
  "api",
@@ -13282,7 +13283,7 @@ var CLI_MANIFEST = {
13282
13283
  {
13283
13284
  "id": "createCreativeStrategy",
13284
13285
  "domain": "creators",
13285
- "description": "Generate a creator strategy. An awareness strategy is built from a fixed template and is saved before this returns, with generationStatus complete and a null jobId. A CTA strategy is generated by an LLM in the background \u2014 this returns immediately with generationStatus generating, so poll getCreativeStrategy with the returned strategyId until it reads complete or failed before using the brief. The jobId and jobType that come back track the same run through getJob, but getCreativeStrategy is the simpler poll \u2014 reach for getJob only when the strategy reads failed and you want the job's errorMessage. Edit the result with updateCreativeStrategy. Passing a strategyId that is not a draft is rejected rather than overwritten.",
13286
+ "description": "Generate a creator brief (creative strategy). An awareness brief is saved before this returns; a CTA brief generates in the background, so poll getCreativeStrategy with the returned strategyId until generationStatus is complete or failed before using it. Edit it with updateCreativeStrategy.",
13286
13287
  "type": "mutation",
13287
13288
  "path": [
13288
13289
  "api",
@@ -13434,7 +13435,7 @@ var CLI_MANIFEST = {
13434
13435
  {
13435
13436
  "id": "createInfluencerPayout",
13436
13437
  "domain": "creators",
13437
- "description": "Charges the organization's card to pay a creator's content bonus. NEVER call this on your own initiative or as part of an automated flow \u2014 every call needs the client's explicit, fresh approval to pay this specific creator, given to you directly; a standing instruction or an inferred intent does not count. The endpoint enforces its own preconditions and refuses otherwise: the visit must have a content submission approved as a paid ad (decideCreatorSubmission with approvalType 'ad' \u2014 organic approvals earn no payout), and no payout may already exist for the visit in any active status \u2014 one payout per visit, so a second call while one is pending, funding, onboarding, or paid is rejected. A visit whose only attempts are FAILED or REFUNDED may be retried; the retry voids the earlier attempt's open Stripe invoice first and recomputes the default amount the same way. The bonus amount defaults to the one stamped on the submission when it was approved (falling back to the location's board config), and is grossed up so the org covers the Stripe fee. Pass amountCents only when the client explicitly asks to pay this one creator a different amount; it is written back to the submission so reporting matches what was paid. After the charge, Stripe webhooks carry it to the creator (FUNDED \u2192 onboarding if needed \u2192 PAID) with no further action from you; follow progress in queryData creators.creatorPayout.",
13438
+ "description": "Charge the organization's card to pay a creator's content bonus. Never call it on your own initiative or in an automated flow: every call needs the client's explicit, fresh approval to pay this specific creator, and a standing instruction does not count. Needs a submission approved with approvalType 'ad'; one payout per visit. Pass amountCents only when the client asks to pay this creator a different amount. eventId is the visit's eventId.",
13438
13439
  "type": "mutation",
13439
13440
  "path": [
13440
13441
  "api",
@@ -13465,7 +13466,7 @@ var CLI_MANIFEST = {
13465
13466
  {
13466
13467
  "id": "createMembersProgramReward",
13467
13468
  "domain": "membersProgram",
13468
- "description": "Create a members program reward over a catalog item. type 'item' promotes an existing item by itemId \u2014 get one from listMembersProgramRewards or a catalog query, and prefer it whenever the item already exists in the POS. type 'name' looks the name up across the Feast, Toast, Square and Clover catalogs and creates a new Feast item only if nothing matches; the match is exact, so a near-miss silently creates a duplicate of a menu item the restaurant already has. When several items share a name a Feast one wins, and otherwise you get a CONFLICT listing the candidates so you can pass itemId instead. staffInstructions are stored only on Feast items and are rejected for a POS-sourced one. Pass pointsCost for a reward guests redeem with points; omit it for one granted by an automation. A reward with neither a pointsCost nor an awardReward automation can never reach a guest, so pair it with an automation.",
13469
+ "description": "Create a members program reward over a catalog item. Prefer type 'item' with an existing itemId (from a catalog query). type 'name' matches the name exactly across the Feast and POS catalogs and creates a new Feast item when nothing matches, so a near-miss silently duplicates a menu item.",
13469
13470
  "type": "mutation",
13470
13471
  "path": [
13471
13472
  "api",
@@ -13529,7 +13530,7 @@ var CLI_MANIFEST = {
13529
13530
  {
13530
13531
  "id": "createRecruitmentCreatives",
13531
13532
  "domain": "creators",
13532
- "description": "Generate the five AI recruitment ad creatives a restaurant runs to source creators. Pass campaignId and the tool resolves the campaign's recruitment offer itself, creating one only if the campaign has none \u2014 that offer is what groups the creatives, stamps the Meta campaign onto the strategy, and carries the monthly sourcing cap, so creatives generated without it land in a shared bucket and are attached to nothing. Pass offerId instead only when you already have the exact offer; one of the two is required, because without an offer the creatives would be generated, charged for, and attached to nothing. force regenerates every type, deleting and replacing the existing creatives rather than filling gaps; without it only missing types are generated. Each generation calls an image model per missing type, so force costs a full set.",
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.",
13533
13534
  "type": "mutation",
13534
13535
  "path": [
13535
13536
  "api",
@@ -13576,7 +13577,7 @@ var CLI_MANIFEST = {
13576
13577
  {
13577
13578
  "id": "decideCreatorSubmission",
13578
13579
  "domain": "creators",
13579
- "description": "Decide on a creator's content submission. THIS TEXTS THE CREATOR unless skipApprovalText \u2014 approving tells them they're done, revision_requested sends your feedbackMessage plus a resubmit link, so write feedbackMessage for the creator to read rather than as an internal note. Approving also queues their bonus payout. approvalType 'ad' means the content may run in paid ads and is rejected when the board's bonus is $0; use 'organic' otherwise. Get submissionIds from listCreatorSubmissions.",
13580
+ "description": "Decide on a creator's content submission. Approving and revision_requested text the creator (skipApprovalText silences only the approval text); revision_requested sends your feedbackMessage, so write it for the creator. Always pass approvalType when approving: omitted means 'ad' (may run in paid ads, sets the bonus pending, rejected when the bonus is $0); 'organic' earns no bonus. submissionId from listCreatorSubmissions.",
13580
13581
  "type": "mutation",
13581
13582
  "path": [
13582
13583
  "api",
@@ -13623,7 +13624,7 @@ var CLI_MANIFEST = {
13623
13624
  {
13624
13625
  "id": "deleteAutomationFlow",
13625
13626
  "domain": "automations",
13626
- "description": "Delete an automation flow and the automations inside it. Pass flowId. Blocked if the flow's automations have 20 or more sends \u2014 turn the flow off instead of deleting it in that case. This is destructive; prefer disabling over deleting when unsure.",
13627
+ "description": "Delete an automation flow and the automations inside it. Pass flowId. Blocked if the flow's automations have 20 or more sends; turn the flow off instead of deleting it in that case. This is destructive; prefer disabling over deleting when unsure.",
13627
13628
  "type": "mutation",
13628
13629
  "path": [
13629
13630
  "api",
@@ -13733,7 +13734,7 @@ var CLI_MANIFEST = {
13733
13734
  {
13734
13735
  "id": "deleteMembersProgramReward",
13735
13736
  "domain": "membersProgram",
13736
- "description": "Remove a members program reward. This is how an orphan gets cleaned up \u2014 a reward no automation's awardReward grants can never reach a guest and holds the members program short of complete. The catalog item is left alone, because it may be a real POS menu item or be used by another reward. Guests who already redeemed keep what they were given; this only stops it being offered again.",
13737
+ "description": "Remove a members program reward so it is no longer offered (the cleanup for an orphan no automation grants). rewardId from listMembersProgramRewards. The catalog item is left alone, and guests who already redeemed keep what they were given.",
13737
13738
  "type": "mutation",
13738
13739
  "path": [
13739
13740
  "api",
@@ -13758,7 +13759,7 @@ var CLI_MANIFEST = {
13758
13759
  {
13759
13760
  "id": "describeData",
13760
13761
  "domain": "data",
13761
- "description": "Describe the queryable data model, then read it with queryData. Call with no arguments first \u2014 that returns an index of every queryable object type plus the full query grammar. Narrowing by schema or object type returns full column detail: type, enum values, nullability, description, and the link names you pass to a pivot or join. Prefer the 'interface' schema, whose object types are POS-agnostic and return the same shape whichever POS the organization runs.",
13762
+ "description": "Describe the queryable data model, then read it with queryData. With no arguments it returns an index of every object type plus the query grammar; narrowing by schema or object type returns full column detail, including the link names pivot and join take. Prefer the POS-agnostic 'interface' schema.",
13762
13763
  "type": "query",
13763
13764
  "path": [
13764
13765
  "api",
@@ -13845,7 +13846,7 @@ var CLI_MANIFEST = {
13845
13846
  {
13846
13847
  "id": "getAutomationDraft",
13847
13848
  "domain": "automations",
13848
- "description": "Read an automation draft \u2014 its staged operations, the flows it touches, the resulting automations after those operations are merged onto current live state, and the preview links that show those changes highlighted. `resultingAutomations` is what each touched automation will actually look like if the draft is saved \u2014 always check it for fields that silently changed or disappeared, not just the fields the operations explicitly mention. Use the returned `operations` as `edits` on simulateAutomations to preview what the draft would do.",
13849
+ "description": "Read an automation draft by draftId: its staged `operations`, the flows it touches, `previewUrls`, and `resultingAutomations` (each touched automation as it will look after the save). Check `resultingAutomations` for fields that changed or disappeared, not only the ones the operations mention.",
13849
13850
  "type": "query",
13850
13851
  "path": [
13851
13852
  "api",
@@ -13869,7 +13870,7 @@ var CLI_MANIFEST = {
13869
13870
  {
13870
13871
  "id": "getBillingStatus",
13871
13872
  "domain": "core",
13872
- "description": 'Whether this organization is paid up, and on what. hasAccess is the one to read for "can they use the product": it means paid and not mid-charge. needsPayment is its inverse for the states worth acting on \u2014 PENDING, FAILED, or a charge in flight \u2014 and is what the dashboard polls to decide whether to block the app behind a payment form. currentTier and subscriptionStatus describe the plan; initialDepositCents is in cents. Read-only: every write here moves real money and stays in the dashboard. existingOrganizations lists *other* organizations billed under the same billing admin, with their names and tiers \u2014 they are not this organization, and it is empty for per-organization billing.',
13873
+ "description": 'Read-only billing state for the organization. hasAccess answers "can they use the product" (paid, no charge in flight); needsPayment flags PENDING, FAILED or a charge in flight. initialDepositCents is in cents. Every billing change stays in the dashboard.',
13873
13874
  "type": "query",
13874
13875
  "path": [
13875
13876
  "api",
@@ -13881,7 +13882,7 @@ var CLI_MANIFEST = {
13881
13882
  {
13882
13883
  "id": "getCampaign",
13883
13884
  "domain": "campaigns",
13884
- "description": "Full configuration for one acquisition campaign \u2014 name, status, funnel and landing-page config, ad config, offers, settings. campaignId is the Feast campaign's `id` from listCampaigns (a UUID), NOT the nested Meta campaignId. Read this before editing a campaign. For performance metrics use getCampaignKpis.",
13885
+ "description": "Full configuration for one acquisition campaign: name, status, funnel and landing page config, ad config, offers, settings. campaignId is the Feast campaign id from listCampaigns (a UUID), not the nested Meta campaignId. Read before updateCampaign. For performance use getCampaignKpis.",
13885
13886
  "type": "query",
13886
13887
  "path": [
13887
13888
  "api",
@@ -13906,7 +13907,7 @@ var CLI_MANIFEST = {
13906
13907
  {
13907
13908
  "id": "getCampaignBenchmarks",
13908
13909
  "domain": "campaigns",
13909
- "description": "The definition and target band of every campaign metric id that getCampaignKpis and getCampaignBreakdown return. One entry per id: { id, label, unit, description, formula, benchmark }, where unit is count, percent (0-100), usd (dollars), days or multiple (ROAS, 2 = 2x), and benchmark is { min, good, great } in that unit or null when the metric has no target band. Grade a value as green at or above good, yellow at or above min, red below min; great is a stretch level (null for ROAS). The bands are fleet percentiles (P25/P50/P75 of campaigns with over 500 visitors) rounded to clean numbers, not per-organization; ROAS is anchored at 1x break-even. Call once and reuse; the table does not change per campaign.",
13910
+ "description": "The definition (label, unit, description, formula) and target band of every metric id that getCampaignKpis and getCampaignBreakdown return. Bands are fleet-wide, not per organization. Call once and reuse; the table is the same for every campaign.",
13910
13911
  "type": "query",
13911
13912
  "path": [
13912
13913
  "api",
@@ -13919,11 +13920,7 @@ var CLI_MANIFEST = {
13919
13920
  {
13920
13921
  "id": "getCampaignBreakdown",
13921
13922
  "domain": "campaigns",
13922
- "description": `A campaign's metrics broken down by channel, Facebook campaign, ad set, ad, referrer and creator, as a tree of nodes, loaded a batch at a time. Pass node refs, get back each node's metrics plus the refs of its children (identifiers only, no metrics). Load children by passing those refs back in.
13923
-
13924
- Start from { type: "campaign", campaign: { campaignId } }, where campaignId is the Feast campaign id from listCampaigns. It returns the campaign totals and its channel refs. The tree is: campaign \u2192 channel (facebook, influencer, tiktok, google, misc, referral, unknown) \u2192 per channel: facebook \u2192 fbCampaign \u2192 fbAdset \u2192 fbAd; google \u2192 googleCampaign; tiktok \u2192 tiktokCampaign; misc \u2192 miscSource; referral \u2192 referrer; influencer \u2192 creator. Variants split the campaign by pass instead of by session: the campaign node lists them in details.campaign.variants (empty when the campaign has no variants), and { type: "variant", variant: { campaignId, variantId } } with variantId null is the default variant; variant nodes carry only signups, pass registration and show rate, time to show, revenue and revenue per signup. A null id inside a ref is the "Unknown" bucket for sessions that could not be matched. Refs from different campaigns can be mixed in one call (max 100).
13925
-
13926
- Metrics use start/end as the session window. Units: sessions, visitors, signups, impressions, reach are counts; *Rate, thumbStopRatio, holdRate and uniqueClickthrough are percentages (0-100); spend, cpm, revenue, costPerSignup, revenuePerSignup are USD; averageTimeToShow is days from signup to first scan. A missing metric key means the metric does not apply to that node (for example spend on a Google row); null means it applies but could not be computed (no denominator, or Facebook was unreachable, see details.facebook.error). Facebook delivery metrics are read live from Meta.`,
13923
+ "description": `A campaign's metrics as a tree (channel, ad, creator, variant). Pass node refs; get back metrics and child refs to drill into. Start from { type: "campaign", campaign: { campaignId } } (id from listCampaigns). A missing metric key means it does not apply; null means it could not be computed.`,
13927
13924
  "type": "query",
13928
13925
  "path": [
13929
13926
  "api",
@@ -14330,7 +14327,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14330
14327
  {
14331
14328
  "id": "getCampaignKpis",
14332
14329
  "domain": "campaigns",
14333
- "description": "Performance metrics for an acquisition campaign. campaignId is the Feast campaign's `id` from listCampaigns (a UUID), NOT the nested Meta campaignId. Returns { id, type, value, unit } per metric. Metric ids are the same camelCase ids getCampaignBreakdown uses (signupRate, thumbStopRatio, uniqueClickthrough, revenue, ...), and units match it too: count, percent (0-100), usd (dollars). Rate metrics with a target band carry benchmark: { min, good, great } in the same unit (see getCampaignBenchmarks for the full table and grading rule). Covers ad performance (spend, impressions, hook rate (thumb stop), hold rate, CTR \u2014 sourced from synced Facebook data, so ROAS is revenue divided by spend), the funnel, automations, and results. Metrics whose value would be zero are omitted rather than returned as 0 \u2014 notably spend, so an absent spend metric means no spend OR no sync yet, never a confirmed zero.",
14330
+ "description": "Headline metrics for one acquisition campaign, one { id, type, value, unit } row per metric. campaignId is the Feast campaign id from listCampaigns, not the Meta campaignId. Metrics whose value is zero are omitted: no spend row means no spend or no Facebook sync yet, never a confirmed $0.",
14334
14331
  "type": "query",
14335
14332
  "path": [
14336
14333
  "api",
@@ -14390,7 +14387,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14390
14387
  {
14391
14388
  "id": "getCreatorConversation",
14392
14389
  "domain": "creators",
14393
- "description": "One creator's full SMS thread, newest first \u2014 what the dashboard's creator chat view renders. userId comes from listCreatorConversations, which is the queue; this is the read you make before summarizing an exchange or drafting a reply for the human to send, because the queue only carries the last message. Each row has the body, direction (from/to the creator's number), and timestamps. Returns empty when the creator has no phone number on file. Read-only: send the reply with sendText and {type:'creator', userId}; marking the thread read stays in the dashboard.",
14390
+ "description": "One creator's full SMS thread, newest first. userId from listCreatorConversations. Read this before summarizing an exchange or drafting a reply, since the queue only has the last message. Empty when the creator has no phone number. Reply with sendText and {type:'creator', userId}.",
14394
14391
  "type": "query",
14395
14392
  "path": [
14396
14393
  "api",
@@ -14439,7 +14436,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14439
14436
  {
14440
14437
  "id": "getInfluencerBoardConfig",
14441
14438
  "domain": "creators",
14442
- "description": "Read a location's creator program settings (dining credit, creator bonus, follower minimum and booking limits) plus its recruitment offers. Returns config: null when the location has no program yet. Read this before writing recruitment copy: the credit and bonus amounts you are supposed to quote live here and nowhere else. Get a locationId from queryData interface.location.",
14439
+ "description": "Read a location's creator program settings (dining credit, creator bonus, follower minimum, booking limits) and its recruitment Meta campaign, ad set and status. config is null when the location has no program. Quote credit and bonus only from here. locationId from queryData interface.location.",
14443
14440
  "type": "query",
14444
14441
  "path": [
14445
14442
  "api",
@@ -14463,7 +14460,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14463
14460
  {
14464
14461
  "id": "getJob",
14465
14462
  "domain": "core",
14466
- "description": "Poll one background job by id. Any tool that queues work returns a { jobId, jobType } pair \u2014 publishAds and the CTA path of createCreativeStrategy both do \u2014 and both values are required here, because a job id alone is not addressable. Returns { job: null } while the row hasn't landed yet, so treat null as in-flight and keep polling; status moves PENDING, RUNNING, then COMPLETED or FAILED with an errorMessage. A publish job's output reports each declared effect as done, skipped or error with a human-readable detail \u2014 including whether the program's approver was texted \u2014 and that outcome is reported nowhere else, so read it rather than assuming the effects ran. The job's input and per-step generated text are stripped by default; includeFullPayload returns both.",
14463
+ "description": "Poll one background job. Pass both jobId and jobType from the tool that queued it (a job id alone is not addressable). { job: null } means not landed yet: keep polling. status goes PENDING, RUNNING, then COMPLETED or FAILED (with errorMessage). includeFullPayload adds the input and generated text.",
14467
14464
  "type": "query",
14468
14465
  "path": [
14469
14466
  "api",
@@ -14535,7 +14532,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14535
14532
  {
14536
14533
  "id": "getMemberConversation",
14537
14534
  "domain": "membersProgram",
14538
- "description": "One member's SMS thread and activity, newest first \u2014 what the dashboard's chat page renders, and the pair to searchUsers the way getCreatorConversation pairs with listCreatorConversations. serialNumber comes from searchUsers. eventTypes: ['sentText','receivedText'] is the conversation; adding scan, order, checkout, rewardAwarded or rewardRedeemed interleaves what happened between the messages. Unfiltered it fans out to every event source and returns the member's whole history unpaginated, so pass eventTypes unless you really want it all. Read-only: reply to the member with sendText and {type:'guest', serialNumber}.",
14535
+ "description": "One member's SMS thread and activity, newest first. serialNumber from searchUsers. Pass eventTypes ['sentText','receivedText'] for the conversation; unfiltered it returns the member's whole history, unpaginated. Read-only: reply with sendText.",
14539
14536
  "type": "query",
14540
14537
  "path": [
14541
14538
  "api",
@@ -14595,7 +14592,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14595
14592
  {
14596
14593
  "id": "getOnboardingForm",
14597
14594
  "domain": "core",
14598
- "description": "Read the organization's onboarding form \u2014 the self-reported answers behind onboarding tasks the system can't observe directly. Returns null when the org has no form yet. Read this before updateOnboardingForm, which replaces nested step objects rather than merging them.",
14595
+ "description": "Read the onboarding form: the self-reported answers behind onboarding tasks the system can't observe directly. Returns null when there is no form. Read this before updateOnboardingForm, which replaces nested step objects rather than merging them.",
14599
14596
  "type": "query",
14600
14597
  "path": [
14601
14598
  "api",
@@ -14620,7 +14617,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14620
14617
  {
14621
14618
  "id": "getPassConfiguration",
14622
14619
  "domain": "passBuilder",
14623
- "description": "The organization's live wallet pass configuration \u2014 content sections, features, locations, metadata and the current version. Returns null when none has been saved. Read this before changing the pass: updatePassConfiguration is a full-document save, so the returned document is what you modify.",
14620
+ "description": "The live wallet pass configuration: content sections, features, locations, metadata and the current version. Returns null when none has been saved. Read this before changing the pass: updatePassConfiguration is a full-document save, so the returned document is what you modify.",
14624
14621
  "type": "query",
14625
14622
  "path": [
14626
14623
  "api",
@@ -14638,7 +14635,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14638
14635
  {
14639
14636
  "id": "getTaskboard",
14640
14637
  "domain": "validate",
14641
- "description": "Everything that needs fixing or finishing for the organization, as one list discriminated by `kind`. kind:'task' entries are onboarding tasks; each carries a completionUrl \u2014 a browser page where a human completes it, so include that link when you ask the user to act \u2014 and completionInstructions describing exactly what completes it. Prefer completionInstructions over guessing how a task is evaluated. kind:'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. Task statuses may lag a recompute by ~30s; this call triggers a refresh. Funnel checks cover only screens reachable from the funnel's start screen.",
14638
+ "description": "Everything that needs fixing or finishing, as one list by `kind`. 'task' entries are onboarding tasks with completionInstructions and a completionUrl to give the user for browser-only steps. 'issue' entries are live misconfigurations with a fixHint. Each call refreshes task statuses (~30s lag).",
14642
14639
  "type": "query",
14643
14640
  "path": [
14644
14641
  "api",
@@ -14811,7 +14808,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14811
14808
  {
14812
14809
  "id": "inviteUser",
14813
14810
  "domain": "core",
14814
- "description": "Invite someone to the organization by email. This sends a real email immediately \u2014 an invitation with a 14-day token, or a login reminder if they already have a Feast account, in which case they are added to the organization right away with no acceptance step. Always pass role explicitly: it defaults to OWNER, which grants full access to billing and every setting. VIEWER is read-only and SCANNER is for staff running the scanner app. Re-inviting an email cancels its pending invites and sends a fresh one. Only an OWNER can call this.",
14811
+ "description": "Invite someone to the organization by email. Sends a real email immediately; someone who already has a Feast account joins right away with no acceptance step. Always pass role: it defaults to OWNER, with full billing and settings access. VIEWER is read-only; SCANNER is for staff running the scanner app.",
14815
14812
  "type": "mutation",
14816
14813
  "path": [
14817
14814
  "api",
@@ -14845,7 +14842,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14845
14842
  {
14846
14843
  "id": "listAdTemplates",
14847
14844
  "domain": "ads",
14848
- "description": "List the Meta ad campaign templates this organization can publish, with the variables each one takes, which plan paths may be overridden, and its budget range. Call this before planAds so you know which variables to supply. Each variable that names a producedBy tool tells you where its value comes from.",
14845
+ "description": "List the Meta ad templates you can publish, with each one's variables, overridable plan paths and budget range. Call it before planAds. A variable that names a producedBy tool tells you where its value comes from.",
14849
14846
  "type": "query",
14850
14847
  "path": [
14851
14848
  "api",
@@ -14858,7 +14855,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14858
14855
  {
14859
14856
  "id": "listAutomationFlows",
14860
14857
  "domain": "automations",
14861
- "description": "Automation flows for the organization \u2014 a flow is the container grouping automations by trigger. Call this first to find the flow an automation belongs in, and only create one if none fits. Scope 'membersProgram' returns flows with no campaign.",
14858
+ "description": "Automation flows. A flow is the container grouping automations by trigger. Call this first to find the flow an automation belongs in, and only create one if none fits. Scope 'membersProgram' returns flows with no campaign.",
14862
14859
  "type": "query",
14863
14860
  "path": [
14864
14861
  "api",
@@ -14890,7 +14887,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14890
14887
  {
14891
14888
  "id": "listAutomations",
14892
14889
  "domain": "automations",
14893
- "description": "List automations for the organization, ordered by execution priority. Pass { flowId } to return only the automations in that flow \u2014 the way to read a single flow's contents before editing it. Omit the input to return every automation in the org.",
14890
+ "description": "List automations, ordered by execution priority. Pass { flowId } to return only the automations in that flow, the way to read a single flow's contents before editing it. Omit the input to return every automation.",
14894
14891
  "type": "query",
14895
14892
  "path": [
14896
14893
  "api",
@@ -14946,7 +14943,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14946
14943
  {
14947
14944
  "id": "listAvailability",
14948
14945
  "domain": "creators",
14949
- "description": "List every creator booking window for the organization. Takes no arguments and is not scoped to a location or a campaign \u2014 filter the results by locationId or campaignId yourself. All times are UTC, and the restaurant thinks in local time \u2014 convert before writing. For weekly blocks utcDaysOfWeek is the day of week IN UTC, so an evening local window that crosses midnight UTC lands on the following day: 9pm Friday New York is 02:00 Saturday UTC, and writing Friday there opens the wrong night.",
14946
+ "description": "List every creator booking window in the organization. Takes no arguments; filter by locationId or campaignId yourself. Times and utcDaysOfWeek are UTC, not the restaurant's local time.",
14950
14947
  "type": "query",
14951
14948
  "path": [
14952
14949
  "api",
@@ -14958,7 +14955,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14958
14955
  {
14959
14956
  "id": "listCampaigns",
14960
14957
  "domain": "core",
14961
- "description": "The organization's acquisition campaigns, newest first, as summaries. Start here to resolve a campaignId: the `id` field (a UUID) is what every other campaign tool takes, NOT the nested Meta campaign id. Also carries each campaign's name, shorthand (used in reservation links), publish state, and referrers. Read one campaign's full configuration \u2014 promotions, ad copy, banner and image config \u2014 with getCampaign.",
14958
+ "description": "The organization's acquisition campaigns, newest first, as summaries. Use the `id` field (a UUID) as campaignId in every other campaign tool, not the nested Meta campaign id. getCampaign returns one campaign's full configuration.",
14962
14959
  "type": "query",
14963
14960
  "path": [
14964
14961
  "api",
@@ -14971,7 +14968,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14971
14968
  {
14972
14969
  "id": "listCreatives",
14973
14970
  "domain": "creators",
14974
- "description": "The generated recruitment creatives for an organization, optionally narrowed to one offer. Each carries an imageKey resolving to the rendered variant that was picked, and selectedImageUrl for the same object as a URL \u2014 pass imageKey to planAds as a libraryAsset reference rather than choosing among the composite fields yourself. imageUrl is the base render and is not the ad asset. staleCreativeIds lists creatives generated from an older version of their offer, and is only populated when offerId is given.",
14971
+ "description": "Generated recruitment creatives, optionally for one offer. Pass a creative's imageKey to planAds as a libraryAsset; imageUrl is the base render, not the ad asset. staleCreativeIds (only with offerId) lists creatives made from an older version of their offer.",
14975
14972
  "type": "query",
14976
14973
  "path": [
14977
14974
  "api",
@@ -14999,7 +14996,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14999
14996
  {
15000
14997
  "id": "listCreatorApplications",
15001
14998
  "domain": "creators",
15002
- "description": "Creator applications waiting on an approve/deny decision, newest first, across every location. Each row carries the `eventId` for updateCreatorVisit plus who applied, where, when, and their handles and follower count. That follower count is usually the deciding factor and is not reachable through queryData, so start here rather than querying creatorVisitApplication when working the approval queue. Each row also carries the creative brief already assigned to that visit as `strategyId`/`strategyTitle` and its `campaignId`/`campaignName`, all null when no brief is assigned yet \u2014 assign one with assignVisitStrategy before approving, because the approval text links whatever brief the visit carries at that moment.",
14999
+ "description": "Creator applications awaiting approve or deny, newest first, across every location, with follower counts (not in queryData). Each row has the eventId for updateCreatorVisit and the assigned brief's strategyId; when it is null, have a brief assigned on the Creator approvals page before approving.",
15003
15000
  "type": "query",
15004
15001
  "path": [
15005
15002
  "api",
@@ -15011,7 +15008,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15011
15008
  {
15012
15009
  "id": "listCreatorConversations",
15013
15010
  "domain": "creators",
15014
- "description": "Every creator's SMS conversation with its unread state \u2014 the 'who is waiting on a reply' queue. `hasUnread` means their last message came in after ours and nobody has marked it read; those need a human. Each row carries the last message body, time and direction, the creator's handles, `visitLocationIds` (every location they have a visit at, in any status), and `visitStatus`, a derived stage that is more reliable than reading raw columns off creatorVisitApplication. Read the full thread behind a row with getCreatorConversation and its userId, and reply with sendText and {type:'creator', userId}. Read-only: marking a conversation read stays in the dashboard.",
15011
+ "description": "Every creator's SMS thread with its unread state: the 'who is waiting on a reply' queue. hasUnread rows need a human. Each row has the last message, visitLocationIds and a derived visitStatus. Read a thread with getCreatorConversation (userId); reply with sendText and {type:'creator', userId}.",
15015
15012
  "type": "query",
15016
15013
  "path": [
15017
15014
  "api",
@@ -15058,7 +15055,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15058
15055
  {
15059
15056
  "id": "listFunnelDrafts",
15060
15057
  "domain": "funnel",
15061
- "description": "List funnel drafts for the organization as summaries (id, referrer, campaign, status, edit counts), optionally filtered by referrer or status. Use getFunnelDraft for a draft's full edits.",
15058
+ "description": "List funnel drafts as summaries (id, referrer, campaign, status, edit counts), optionally filtered by referrer or status. Use getFunnelDraft for a draft's full edits.",
15062
15059
  "type": "query",
15063
15060
  "path": [
15064
15061
  "api",
@@ -15129,7 +15126,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15129
15126
  {
15130
15127
  "id": "listFunnelTemplates",
15131
15128
  "domain": "campaigns",
15132
- "description": "Lists the funnel templates that can be applied to a campaign, with per-template eligibility for this campaign and a recommended template. Each template describes the guest journey (ordered screens) and whether it collects payment. Prepay templates insert a Stripe payment screen and are only eligible when the campaign's promotion has canPrePay: true and a price; the promotion's canPrePay flag does NOT change what a template builds \u2014 pick a non-payment template (offer-basic) for offers redeemed in person. Use before applyFunnelTemplate.",
15129
+ "description": "Lists the funnel templates for a campaign: each template's guest journey (ordered screens), whether it collects payment, its eligibility for this campaign, and a recommended id. Read before applyFunnelTemplate; never guess a template id.",
15133
15130
  "type": "query",
15134
15131
  "path": [
15135
15132
  "api",
@@ -15154,7 +15151,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15154
15151
  {
15155
15152
  "id": "listIgMedia",
15156
15153
  "domain": "ads",
15157
- "description": "List recent posts from the Instagram business account linked to a Facebook Page, for use as igMedia creative references in planAds templates. Returns the instagramUserId to put on the igMedia creative ref plus up to 50 recent posts with id, caption, thumbnail, mediaType, permalink and timestamp. Takes a pageId from ads_get_user_pages; a Page with no linked Instagram business account returns instagramAccount null.",
15154
+ "description": "List up to 50 recent posts from the Instagram business account linked to a Facebook Page, for igMedia creative references in planAds, with the instagramUserId each reference needs. pageId comes from ads_get_user_pages; a Page with no linked account returns instagramAccount null.",
15158
15155
  "type": "query",
15159
15156
  "path": [
15160
15157
  "api",
@@ -15180,7 +15177,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15180
15177
  {
15181
15178
  "id": "listMedia",
15182
15179
  "domain": "core",
15183
- "description": "Uploaded media files for the organization, across one or more scopes. Each file is tagged with its scope and a canDelete flag. `cropped` lists the cropped variants under each scope's cropped/ subfolder instead of the base files.",
15180
+ "description": "Uploaded media files, across one or more scopes. Each file is tagged with its scope and a canDelete flag. `cropped` lists the cropped variants under each scope's cropped/ subfolder instead of the base files.",
15184
15181
  "type": "query",
15185
15182
  "path": [
15186
15183
  "api",
@@ -15223,7 +15220,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15223
15220
  {
15224
15221
  "id": "listMembersProgramRewards",
15225
15222
  "domain": "membersProgram",
15226
- "description": "The organization's members program rewards with their catalog item name, staff instructions, points cost and source. Items are resolved across the Feast, Toast, Square and Clover catalogs, so source tells you which one backs the reward; a null name means the item no longer exists in any of them. A reward with a pointsCost is redeemed by guests spending points; one without is granted by an automation. To find orphans \u2014 rewards nothing ever awards \u2014 cross-check listAutomations for itemIds in an awardReward action.",
15223
+ "description": "Members program rewards with their catalog item name, staff instructions, points cost and source catalog (Feast, Toast, Square or Clover). A null name means the item no longer exists in any catalog.",
15227
15224
  "type": "query",
15228
15225
  "path": [
15229
15226
  "api",
@@ -15238,6 +15235,18 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15238
15235
  "$schema": "http://json-schema.org/draft-07/schema#"
15239
15236
  }
15240
15237
  },
15238
+ {
15239
+ "id": "listReferenceScripts",
15240
+ "domain": "ads",
15241
+ "description": "List the reference ad scripts Content Studio offers as Concept presets for Bevyl videos, each distilled from an ad that performed: a preview video, its beats, lines to adapt and the concept text sent to Bevyl. Copy the structure and pacing, not the words.",
15242
+ "type": "query",
15243
+ "path": [
15244
+ "api",
15245
+ "bevyl",
15246
+ "listReferenceScripts"
15247
+ ],
15248
+ "inputJsonSchema": null
15249
+ },
15241
15250
  {
15242
15251
  "id": "listTemplateAutomations",
15243
15252
  "domain": "automations",
@@ -15269,7 +15278,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15269
15278
  {
15270
15279
  "id": "markReimbursementPaid",
15271
15280
  "domain": "creators",
15272
- "description": "Record that a creator has been paid back for a meal they bought on a reimbursing board. This moves no money \u2014 it only writes down that the client already sent it by their own means, so never call it unless the client tells you the payment has actually gone out. The submission must be approved and its reimbursement still pending; a submission with no receipt was never on a reimbursing board and is rejected. Read the receipt total off creators.creatorContentSubmission before recording anything.",
15281
+ "description": "Record that the client already paid a creator back for a meal on a reimbursing board. Moves no money: call it only after the client says the payment went out. The submission must be approved with its reimbursement pending. submissionId and receiptTotalCents from listCreatorSubmissions.",
15273
15282
  "type": "mutation",
15274
15283
  "path": [
15275
15284
  "api",
@@ -15296,7 +15305,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15296
15305
  {
15297
15306
  "id": "planAds",
15298
15307
  "domain": "ads",
15299
- "description": "Resolve an ad template and its variables into the exact tree of campaigns, ad sets and ads that would be created on Meta. Creates nothing on Meta and changes no Feastalytics data \u2014 it is a mutation only so the variables travel in a request body rather than a URL. Returns the tree, a planHash, the fully defaulted variables, and validation issues. Fix any issue with severity error and plan again; then pass the returned variables, overrides and planHash to publishAds unchanged. Never hand-assemble Meta parameters \u2014 publishAds re-derives the tree from these variables and refuses anything else.",
15308
+ "description": "Resolve an ad template and its variables into the exact campaigns, ad sets and ads publishAds would create. Creates nothing. Returns the tree, planHash, defaulted variables and issues; fix severity error issues and re-plan, then pass variables, overrides and planHash to publishAds unchanged.",
15300
15309
  "type": "mutation",
15301
15310
  "path": [
15302
15311
  "api",
@@ -15336,7 +15345,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15336
15345
  {
15337
15346
  "id": "populateCampaign",
15338
15347
  "domain": "campaigns",
15339
- "description": "Finalizes a campaign that was created with isCreating true. Optionally attaches a promotion/offer image to the campaign. Does NOT set up funnel screens \u2014 follow with applyFunnelTemplate, or the user picks a template in the funnel editor.",
15348
+ "description": 'Finalizes a campaign that was created with isCreating true. Optionally attaches a promotion/offer image to the campaign. Does NOT set up funnel screens; follow with applyFunnelTemplate, or the user picks a template in the funnel editor. contentStrategy: "tracking_only" publishes the campaign immediately.',
15340
15349
  "type": "mutation",
15341
15350
  "path": [
15342
15351
  "api",
@@ -15432,7 +15441,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15432
15441
  {
15433
15442
  "id": "publishAds",
15434
15443
  "domain": "ads",
15435
- "description": "Publish a plan produced by planAds. Declare what should be recorded once the ads exist through effects, and the worker runs them as part of the job \u2014 a recruitment publish must pass linkRecruitmentOffer with its offerId and creativeIds, which stamps the creatives, stamps the offer that the monthly sourcing cap and the dashboard's spend both read, and texts the program's approver that sourcing is live, and a directOffer publish must pass linkFeastCampaign with the campaignId it runs for, which is what puts its spend on the campaign's ads panel and KPIs; omitting either is refused rather than silently skipped. Doing it afterwards through a separate call is a step that can be missed, and missing it is silent. Pass back the variables, overrides and planHash that planAds returned, unchanged. The server re-derives the tree and refuses to publish if it no longer matches the hash, so re-plan and show the human the difference if that happens. This returns as soon as the work is queued \u2014 poll getJob with the returned jobId and jobType to follow it, and read the job's effect outcomes rather than assuming they ran. Everything is created paused; use setAdCampaignStatus to start it. Requires an explicit confirm.",
15444
+ "description": "Publish a planAds plan to Meta, everything paused; setAdCampaignStatus starts spending. Pass planAds' variables, overrides and planHash back unchanged; a stale plan is refused, so re-plan and show the human the change. recruitment requires the linkRecruitmentOffer effect (texts the program's approver); directOffer requires linkFeastCampaign. Queues a job: poll getJob and read each effect's outcome. Only with the human's explicit approval.",
15436
15445
  "type": "mutation",
15437
15446
  "path": [
15438
15447
  "api",
@@ -15539,7 +15548,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15539
15548
  {
15540
15549
  "id": "purchaseAndConfigurePhoneNumber",
15541
15550
  "domain": "core",
15542
- "description": "Buys a real number from Twilio for the organization and bills the account \u2014 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 \u2014 don't buy until you do.",
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.",
15543
15552
  "type": "mutation",
15544
15553
  "path": [
15545
15554
  "api",
@@ -15563,13 +15572,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
15563
15572
  {
15564
15573
  "id": "queryData",
15565
15574
  "domain": "data",
15566
- "description": `Run a read-only query against the organization's data. Call describeData first for object types and exact column names - do not guess columns.
15567
- Results are always scoped to the calling organization, so never filter on organizationId yourself.
15568
-
15569
- Filter leaves are one column each, written as the column name prefixed with $, combined with {"type":"and"|"or","filters":[...]}. Use {"strings":[...]} for any-of rather than a large or. Page by passing the returned nextCursor back as args.cursor.
15570
-
15571
- Example - opted-in members with more than 5 visits, newest first:
15572
- {"schemaName":"core","objectTypeName":"guest","commands":[{"type":"filter","filter":{"type":"and","filters":[{"$optIn":{"boolean":true}},{"$progress":{"number":5,"match":"GT"}}]}}],"args":{"limit":500,"order":{"field":"timeAdded","direction":"DESC"},"fields":["serialNumber","phoneNumber","progress"]}}`,
15575
+ "description": "Run a read-only query against the data catalog. Call describeData first for object types and exact column names; do not guess columns. Results are already scoped to the organization, so never filter on organizationId. Page by passing the returned nextCursor back as args.cursor.",
15573
15576
  "type": "query",
15574
15577
  "path": [
15575
15578
  "api",
@@ -15734,7 +15737,7 @@ Example - opted-in members with more than 5 visits, newest first:
15734
15737
  {
15735
15738
  "id": "saveAutomationEdits",
15736
15739
  "domain": "automations",
15737
- "description": "Promote an automation draft to production \u2014 this writes to live automations and real guests start receiving the result. Refuses if any touched automation changed since the draft was staged, in which case re-stage against the current state. Marks the draft promoted on success.",
15740
+ "description": "Promote an automation draft to production. This writes to live automations and real guests start receiving the result. Refuses if any touched automation changed since the draft was staged, in which case re-stage against the current state. Marks the draft promoted on success.",
15738
15741
  "type": "mutation",
15739
15742
  "path": [
15740
15743
  "api",
@@ -15783,7 +15786,7 @@ Example - opted-in members with more than 5 visits, newest first:
15783
15786
  {
15784
15787
  "id": "searchAvailablePhoneNumbers",
15785
15788
  "domain": "core",
15786
- "description": "Lists Twilio numbers currently purchasable for texting guests. What matters is proximity to the restaurant, not a memorable area code, so search by the restaurant's own postal code or latitude/longitude \u2014 a guessed area code lands you a number in the wrong town. This is a read-only lookup that costs nothing, so call it as often as you need while narrowing down before purchasing.",
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.",
15787
15790
  "type": "query",
15788
15791
  "path": [
15789
15792
  "api",
@@ -15839,7 +15842,7 @@ Example - opted-in members with more than 5 visits, newest first:
15839
15842
  {
15840
15843
  "id": "searchGooglePlaces",
15841
15844
  "domain": "core",
15842
- "description": "Resolve a restaurant to its Google Place ID, which gates Google review and photo scraping. type 'search' takes a query and returns ranked candidates; include the city ('Todays Pizza, Brooklyn NY') because a bare name is usually ambiguous. type 'lookup' takes a placeId and returns that one place with its name, address, website and coordinates \u2014 use it to inspect a place already stored on a config. confidentMatch is non-null only when one candidate is unambiguous, either because its website domain matches the config's or because exactly one candidate's name matches the query; when it is null, show the candidates and let the customer pick rather than guessing. Passing referrer biases the search toward that config's saved coordinates. This is read-only \u2014 write the result with updateBrandIdentity.",
15845
+ "description": "Resolve a restaurant to its Google Place ID (needed for review and photo scraping). type 'search' takes a query (include the city); type 'lookup' takes a placeId. When confidentMatch is null, show the candidates and let the customer pick. Read-only: save with updateBrandIdentity.",
15843
15846
  "type": "query",
15844
15847
  "path": [
15845
15848
  "api",
@@ -15900,7 +15903,7 @@ Example - opted-in members with more than 5 visits, newest first:
15900
15903
  {
15901
15904
  "id": "searchUsers",
15902
15905
  "domain": "membersProgram",
15903
- "description": "Search members (loyalty guests) and their activity. Returns a page of the most recent user events, one per member, each carrying the member's serialNumber plus the event type, time and related object. isUnread: true narrows the results to unread inbound texts only, overriding any broader eventTypes; progressMinBound/progressMaxBound bound the visit count. Paginate by passing the returned `cursor` back \u2014 an undefined cursor means no more pages. This finds the member; getMemberConversation with their serialNumber loads their SMS thread or full timeline.",
15906
+ "description": "Search members (loyalty guests) by name, visit count, campaign or activity. Returns a page of the most recent event per member, each with the member's serialNumber. isUnread: true lists members with unread inbound texts and overrides eventTypes. Page by passing the returned cursor back.",
15904
15907
  "type": "query",
15905
15908
  "path": [
15906
15909
  "api",
@@ -15975,7 +15978,7 @@ Example - opted-in members with more than 5 visits, newest first:
15975
15978
  {
15976
15979
  "id": "sendText",
15977
15980
  "domain": "messaging",
15978
- "description": "Send one SMS to one person from the organization's texting number \u2014 the reply you would otherwise type into the dashboard chat. `to` names who by id, never by phone number: {type:'creator', userId} for a creator, whose userId comes from listCreatorConversations or getCreatorConversation, or {type:'guest', serialNumber} for a loyalty member, whose serialNumber comes from searchUsers or getMemberConversation. The type is not cosmetic \u2014 the two are different people in different tables, and a creator who also holds a pass exists in both, so pass the type that matches the thread you are replying to. The third form, {type:'unknownSender', phoneNumber}, answers someone who texted in without being either \u2014 it is the only form that names a raw number, and it is refused unless that number has an inbound message to this organization on file. A creator must have a visit with this organization and a guest must belong to it, or the call is a 404 rather than a text to a stranger. Guest messages support the {{firstName}}-style handlebars text automations use and are rendered before sending; creator messages are sent verbatim. Sending as a creator's human handler also dismisses any reply the AI agent has staged for that creator and re-runs the agent with your message in context, so it never talks over you. High priority, sent immediately \u2014 there is no scheduling, no undo, and no bulk form: call it once per recipient.",
15981
+ "description": "Send one SMS to one person from the organization's number, immediately, with no undo or scheduling. Show the user the exact text and get a go-ahead first. `to` is an id, never a phone number: {type:'guest', serialNumber} from searchUsers, or {type:'creator', userId} from listCreatorConversations. Use the type that matches the thread; a creator with a pass exists as both. {type:'unknownSender', phoneNumber} only answers a number that texted in.",
15979
15982
  "type": "mutation",
15980
15983
  "path": [
15981
15984
  "api",
@@ -16067,7 +16070,7 @@ Example - opted-in members with more than 5 visits, newest first:
16067
16070
  {
16068
16071
  "id": "setAdCampaignStatus",
16069
16072
  "domain": "ads",
16070
- "description": "Start or pause a Meta campaign that was published from Feastalytics. ACTIVE cascades to every ad set and ad, because campaigns are published paused at all three levels and a campaign-only activate would spend nothing. Activating spends real money \u2014 confirm with the human first, and check the preflight counts in the response.",
16073
+ "description": "Start or pause a Meta campaign published from Feastalytics, including a location's creator recruitment campaign. ACTIVE cascades to every ad set and ad and spends real money: confirm with the human first, then check the preflight counts in the response. On a recruitment campaign the status is saved on the location's creator program, and a manual change cancels any pending monthly-cap reactivation.",
16071
16074
  "type": "mutation",
16072
16075
  "path": [
16073
16076
  "api",
@@ -16101,7 +16104,7 @@ Example - opted-in members with more than 5 visits, newest first:
16101
16104
  {
16102
16105
  "id": "simulateAutomations",
16103
16106
  "domain": "automations",
16104
- "description": "Dry-run automations against a synthetic event timeline and see what would fire \u2014 no real sends or side effects. Each event is `{type, at}` plus a few optional fields; `at` is an ISO 8601 timestamp and the server fills in the guest, organization and campaign. Valid types: signUp, importedCustomer, viewCampaign, addPass, visit, invalidScan, checkout, reply, buttonClick, offerRedemption, offerExpiration, formSubmission, formPropertySubmission, gotReferred, referred, subscriptionRenewal, receiveAutomation. First call: omit `events` and pass `flowId`, and the server auto-seeds a timeline from that flow's triggers (a viewCampaign event when the flow has a campaign, then the first eligible trigger event 15s later) and returns it as `eventsUsed`. To test another day or continue the journey, change `at` on those events or append more, and pass the array back as `events`. Either `events` or `flowId` is required. Pass a draft's operations as `edits` to preview unsaved changes. Returns `scheduledTexts` plus `eventsUsed`.",
16107
+ "description": "Dry-run automations against a synthetic event timeline and see which texts would go out. No real sends. Pass `flowId` alone to auto-seed a timeline from its triggers (returned as `eventsUsed`), or pass `events` to replay your own. Pass a draft's operations as `edits` to preview unsaved changes.",
16105
16108
  "type": "mutation",
16106
16109
  "path": [
16107
16110
  "api",
@@ -18425,7 +18428,7 @@ Example - opted-in members with more than 5 visits, newest first:
18425
18428
  {
18426
18429
  "id": "stageAutomationEdits",
18427
18430
  "domain": "automations",
18428
- "description": "Add changes to an open automation draft without touching live automations. Takes the same operations batchEditAutomations does, appended in order, so call it repeatedly as you build a change up. Preview by passing the draft's `operations` as `edits` to simulateAutomations, or send the returned `previewUrls` for sign-off. Nothing goes live until saveAutomationEdits runs.",
18431
+ "description": "Append operations (the batchEditAutomations format) to an open automation draft, in order. Live automations are untouched until saveAutomationEdits. Call it repeatedly to build a change up. An `update` replaces each field it sends, so send the full current `actions` array.",
18429
18432
  "type": "mutation",
18430
18433
  "path": [
18431
18434
  "api",
@@ -20287,7 +20290,7 @@ Example - opted-in members with more than 5 visits, newest first:
20287
20290
  {
20288
20291
  "id": "stageFunnelEdit",
20289
20292
  "domain": "funnel",
20290
- "description": "Stage a single renderable edit onto a funnel draft. The edit is validated against the current screen but not saved to production. Returns a summary of the draft, not its edits \u2014 use getFunnelDraft to read them back, or listFunnelScreens with the draftId to see the funnel with the edits applied.",
20293
+ "description": "Stage a single renderable edit onto a funnel draft. The edit is validated against the current screen but not saved to production. Returns a summary of the draft, not its edits: use getFunnelDraft to read them back, or listFunnelScreens with the draftId to see the funnel with the edits applied.",
20291
20294
  "type": "mutation",
20292
20295
  "path": [
20293
20296
  "api",
@@ -20858,9 +20861,6 @@ Example - opted-in members with more than 5 visits, newest first:
20858
20861
  "items": {
20859
20862
  "type": "number"
20860
20863
  }
20861
- },
20862
- "bookInFunnel": {
20863
- "type": "boolean"
20864
20864
  }
20865
20865
  },
20866
20866
  "additionalProperties": false
@@ -20872,7 +20872,7 @@ Example - opted-in members with more than 5 visits, newest first:
20872
20872
  "openTable"
20873
20873
  ],
20874
20874
  "additionalProperties": false,
20875
- "description": "OpenTable reservation widget that finds available time slots. When bookInFunnel is true, the reservation is completed in-funnel via SMS 2FA; otherwise it redirects to OpenTable."
20875
+ "description": "OpenTable reservation widget that finds available time slots and redirects to OpenTable to book."
20876
20876
  },
20877
20877
  {
20878
20878
  "type": "object",
@@ -22095,7 +22095,7 @@ Example - opted-in members with more than 5 visits, newest first:
22095
22095
  {
22096
22096
  "id": "updateAutomationFlow",
22097
22097
  "domain": "automations",
22098
- "description": "Update a flow's metadata (the group that holds automations). Pass flowId plus the new title (required), and optionally description and isCampaignCheckDisabled. Does not touch the automations inside the flow \u2014 stage those through the draft loop, or batchEditAutomations for an immediate live write.",
22098
+ "description": "Update a flow's metadata (the group that holds automations). Pass flowId plus the new title (required), and optionally description and isCampaignCheckDisabled. Does not touch the automations inside the flow. Stage those through the draft loop, or batchEditAutomations for an immediate live write.",
22099
22099
  "type": "mutation",
22100
22100
  "path": [
22101
22101
  "api",
@@ -22129,7 +22129,7 @@ Example - opted-in members with more than 5 visits, newest first:
22129
22129
  {
22130
22130
  "id": "updateAvailability",
22131
22131
  "domain": "creators",
22132
- "description": "Change an existing booking window, found by availabilityId from listAvailability. Omitted top-level fields are left alone, but block is replaced whole rather than merged \u2014 send the complete block, including the fields you are not changing. All times are UTC, and the restaurant thinks in local time \u2014 convert before writing. For weekly blocks utcDaysOfWeek is the day of week IN UTC, so an evening local window that crosses midnight UTC lands on the following day: 9pm Friday New York is 02:00 Saturday UTC, and writing Friday there opens the wrong night. Returns nothing; re-read with listAvailability to confirm.",
22132
+ "description": "Change a booking window (availabilityId from listAvailability). block is replaced whole, so send the complete block. Times and weekly days are UTC: convert the local day and time together (see the creators workflow). Returns nothing; re-read with listAvailability to confirm.",
22133
22133
  "type": "mutation",
22134
22134
  "path": [
22135
22135
  "api",
@@ -22261,7 +22261,7 @@ Example - opted-in members with more than 5 visits, newest first:
22261
22261
  {
22262
22262
  "id": "updateBrandIdentity",
22263
22263
  "domain": "core",
22264
- "description": "Update the layout config behind a branded site created by createBrandIdentity: business data, theme, tracking pixel IDs, OpenTable links, and the Google Place link. The config object is a partial patch and businessData, trackingIds and googleConfig are deep-merged, so send only the fields you are changing. Setting googleConfig.placeId to a new value enqueues a Google Maps scrape that backfills reviews and photos; changing it on a config that already has one orphans everything scraped under the old place, so confirm with the customer before replacing an existing placeId. Resolve a placeId with searchGooglePlaces first. Changing hostname enqueues a DNS update.",
22264
+ "description": "Update a branded site's config: business data, theme, tracking pixel IDs, OpenTable links, Google Place link. A partial patch; businessData, trackingIds and googleConfig are deep-merged. A new googleConfig.placeId starts a review and photo scrape, and with coordinates can also buy and bill a nearby texting number if the organization has none. Replacing an existing placeId orphans the old place's scraped data, so confirm with the customer first.",
22265
22265
  "type": "mutation",
22266
22266
  "path": [
22267
22267
  "api",
@@ -22454,6 +22454,12 @@ Example - opted-in members with more than 5 visits, newest first:
22454
22454
  "cid": {
22455
22455
  "type": "string"
22456
22456
  },
22457
+ "name": {
22458
+ "type": "string"
22459
+ },
22460
+ "address": {
22461
+ "type": "string"
22462
+ },
22457
22463
  "latitude": {
22458
22464
  "type": "number"
22459
22465
  },
@@ -22522,7 +22528,7 @@ Example - opted-in members with more than 5 visits, newest first:
22522
22528
  {
22523
22529
  "id": "updateCampaign",
22524
22530
  "domain": "campaigns",
22525
- "description": "Update an existing campaign \u2014 pass only the fields you're changing. This tool reaches further than its name suggests: `update` accepts any field on the campaign, so it is also how you publish (isPublished), set the offer image (imageUrl, which must not contain the word 'placeholder' or onboarding treats it as unset), and write a promotion's staffInstructions. Setting a price on a recurring promotion creates live Stripe products and prices in the connected account, and the Stripe account cannot be changed afterwards until those promotions are archived. Publishing is the user's call \u2014 confirm before setting isPublished.",
22531
+ "description": "Updates one campaign. Pass campaignId and only the top-level fields you change; each one replaces the stored value whole (promotions is the full list), so read with getCampaign first. It is also how you publish (isPublished, only on the user's go-ahead) and set the offer image. A price on a recurring promotion creates live Stripe products and prices.",
22526
22532
  "type": "mutation",
22527
22533
  "path": [
22528
22534
  "api",
@@ -22982,44 +22988,6 @@ Example - opted-in members with more than 5 visits, newest first:
22982
22988
  }
22983
22989
  ]
22984
22990
  },
22985
- "bannerConfig": {
22986
- "anyOf": [
22987
- {
22988
- "type": "object",
22989
- "properties": {
22990
- "type": {
22991
- "type": "string",
22992
- "const": "simple"
22993
- },
22994
- "simple": {
22995
- "type": "object",
22996
- "properties": {
22997
- "title": {
22998
- "type": "string"
22999
- },
23000
- "description": {
23001
- "type": "string"
23002
- },
23003
- "imageUrl": {
23004
- "type": "string"
23005
- }
23006
- },
23007
- "required": [
23008
- "title",
23009
- "description",
23010
- "imageUrl"
23011
- ],
23012
- "additionalProperties": false
23013
- }
23014
- },
23015
- "required": [
23016
- "type",
23017
- "simple"
23018
- ],
23019
- "additionalProperties": false
23020
- }
23021
- ]
23022
- },
23023
22991
  "promotions": {
23024
22992
  "type": "array",
23025
22993
  "items": {
@@ -23336,18 +23304,6 @@ Example - opted-in members with more than 5 visits, newest first:
23336
23304
  },
23337
23305
  "recruitmentAdCopy": {
23338
23306
  "$ref": "#/properties/update/properties/adCopy"
23339
- },
23340
- "viewers": {
23341
- "type": "array",
23342
- "items": {
23343
- "type": "string"
23344
- }
23345
- },
23346
- "editors": {
23347
- "type": "array",
23348
- "items": {
23349
- "type": "string"
23350
- }
23351
23307
  }
23352
23308
  },
23353
23309
  "additionalProperties": false
@@ -23364,7 +23320,7 @@ Example - opted-in members with more than 5 visits, newest first:
23364
23320
  {
23365
23321
  "id": "updateCreativeStrategy",
23366
23322
  "domain": "creators",
23367
- "description": "Write a creator strategy \u2014 the revision step after createCreativeStrategy, for editing concepts, hooks, scripts and deliverables. Omitting strategyId creates a new strategy instead of editing one, so always pass the id you got back from createCreativeStrategy. This replaces the fields you send rather than merging them: read the strategy first with getCreativeStrategy and send back the full concepts array with your edits applied, or you will drop the concepts you left out.",
23323
+ "description": "Save a creator brief: the revision step after createCreativeStrategy. Omitting strategyId creates a new brief instead of editing one. Sent fields replace stored ones, so read it with getCreativeStrategy and send the full concepts array with your edits.",
23368
23324
  "type": "mutation",
23369
23325
  "path": [
23370
23326
  "api",
@@ -23618,7 +23574,7 @@ Example - opted-in members with more than 5 visits, newest first:
23618
23574
  {
23619
23575
  "id": "updateCreatorVisit",
23620
23576
  "domain": "creators",
23621
- "description": "Update one creator visit: change its time or record its outcome. `status` accepts approved, denied, pending_approval, confirmed, visited, missed, issue, cancelled. status: 'approved' runs the real approval \u2014 THIS TEXTS THE CREATOR IMMEDIATELY and consumes the location's monthly creator sourcing allowance, which auto-pauses recruitment once reached; 'denied' texts a decline; approving a denied row reverses it \u2014 the creator gets a \"we changed our mind\" text and the scheduled texts are re-armed. Approving a row that is no longer actionable is a no-op and returns changed: false. Pass sideEffects: false to make any update silent \u2014 same field writes, but no creator text, no allowance spend, no post-approval automation. With sideEffects on (the default), setting startTime to a date texts the creator a confirmation and alerts the approver; setting startTime to null clears the time and texts the creator asking for a new one; of the remaining statuses only 'cancelled' texts the creator \u2014 the rest are plain record writes. Pass dryRun: true to get back the exact creator text(s) the same call would send \u2014 nothing is written or sent; use it to show the operator a preview before the real call. Get `eventId` from listCreatorApplications.",
23577
+ "description": "Update one creator visit: approve or deny an application, set or clear its time, move it, or record its outcome. Approving or denying texts the creator immediately, and approving spends the location's monthly creator allowance; setting or clearing startTime and status 'cancelled' also text them. Confirm with the user and preview with dryRun: true first. sideEffects: false writes silently. eventId from listCreatorApplications.",
23622
23578
  "type": "mutation",
23623
23579
  "path": [
23624
23580
  "api",
@@ -23645,6 +23601,9 @@ Example - opted-in members with more than 5 visits, newest first:
23645
23601
  "durationMs": {
23646
23602
  "type": "number"
23647
23603
  },
23604
+ "locationId": {
23605
+ "type": "string"
23606
+ },
23648
23607
  "status": {
23649
23608
  "type": "string",
23650
23609
  "enum": [
@@ -23658,6 +23617,17 @@ Example - opted-in members with more than 5 visits, newest first:
23658
23617
  "cancelled"
23659
23618
  ]
23660
23619
  },
23620
+ "notes": {
23621
+ "anyOf": [
23622
+ {
23623
+ "type": "string",
23624
+ "maxLength": 1e3
23625
+ },
23626
+ {
23627
+ "type": "null"
23628
+ }
23629
+ ]
23630
+ },
23661
23631
  "sideEffects": {
23662
23632
  "type": "boolean",
23663
23633
  "default": true
@@ -23677,7 +23647,7 @@ Example - opted-in members with more than 5 visits, newest first:
23677
23647
  {
23678
23648
  "id": "updateInfluencerBoardConfig",
23679
23649
  "domain": "creators",
23680
- "description": "Create or update a location's creator program. This is an upsert. There is no separate create tool, and the config is keyed by locationId, one per location, so calling it for a location with no program creates one, seeding a 5000-cent dining credit and leaving landingPageConfirmed, calendarConfigured, passConfigured and reimbursementEnabled false. Omitted fields are left alone. Every program is apply-only: creators apply, the approver reviews them, and the creator AI agent texts approved creators to book the visit. reimbursementEnabled switches the board from comping the meal to reimbursing a meal the creator paid for, and foodCreditAmountCents becomes the reimbursement cap rather than a dining credit. It changes what creators are promised on the landing page, brief and rights agreement, so never set it without the client asking for it. The Design creator program task needs a positive credit and landingPageConfirmed. launchInfluencerCampaign checks the same two. maxCreatorsPerMonth caps how many creators the location's recruitment ads source each calendar month; when the cap is reached every recruitment campaign at the location pauses automatically until the 1st of the next month, and changing or clearing the cap reconciles the campaigns immediately. agentPaused: true turns the creator AI agent off for the location: no automated creator texts (AI replies, visit reminders, content follow-ups) until it is set back to false; texts sent by people still deliver.",
23650
+ "description": "Create or update a location's creator program (an upsert, one per locationId; omitted fields are left alone). Settings change what creators are promised and texted, so never set reimbursementEnabled unless the client asks. Changing maxCreatorsPerMonth can pause or restart the location's recruitment ads immediately. agentPaused: true stops the creator AI agent's texts. locationId from queryData interface.location.",
23681
23651
  "type": "mutation",
23682
23652
  "path": [
23683
23653
  "api",
@@ -23697,9 +23667,6 @@ Example - opted-in members with more than 5 visits, newest first:
23697
23667
  "landingPageConfirmed": {
23698
23668
  "type": "boolean"
23699
23669
  },
23700
- "calendarConfigured": {
23701
- "type": "boolean"
23702
- },
23703
23670
  "approverUserId": {
23704
23671
  "type": [
23705
23672
  "string",
@@ -23719,6 +23686,10 @@ Example - opted-in members with more than 5 visits, newest first:
23719
23686
  "passConfigured": {
23720
23687
  "type": "boolean"
23721
23688
  },
23689
+ "calendarConfigured": {
23690
+ "type": "boolean",
23691
+ "description": "Ignored. Kept while older clients still send it."
23692
+ },
23722
23693
  "maxBookingDaysOut": {
23723
23694
  "anyOf": [
23724
23695
  {
@@ -23839,6 +23810,45 @@ Example - opted-in members with more than 5 visits, newest first:
23839
23810
  },
23840
23811
  "agentPaused": {
23841
23812
  "type": "boolean"
23813
+ },
23814
+ "recruitmentFacebookCampaignId": {
23815
+ "anyOf": [
23816
+ {
23817
+ "type": "string",
23818
+ "minLength": 1
23819
+ },
23820
+ {
23821
+ "type": "null"
23822
+ }
23823
+ ],
23824
+ "description": "The Meta campaign of this location's creator recruitment ad. Set it after publishing the location's ad; it is what the monthly sourcing cap, the spend panel and the ad status read. Only this location's program is written. Omit to preserve; send null to clear."
23825
+ },
23826
+ "recruitmentFacebookAdSetId": {
23827
+ "anyOf": [
23828
+ {
23829
+ "type": "string",
23830
+ "minLength": 1
23831
+ },
23832
+ {
23833
+ "type": "null"
23834
+ }
23835
+ ],
23836
+ "description": "The ad set of this location's recruitment campaign. Omit to preserve; send null to clear."
23837
+ },
23838
+ "recruitmentStatus": {
23839
+ "anyOf": [
23840
+ {
23841
+ "type": "string",
23842
+ "enum": [
23843
+ "active",
23844
+ "paused"
23845
+ ]
23846
+ },
23847
+ {
23848
+ "type": "null"
23849
+ }
23850
+ ],
23851
+ "description": "The saved status of this location's recruitment campaign. This does not start or pause anything on Meta; use setAdCampaignStatus for that. Omit to preserve; send null to clear."
23842
23852
  }
23843
23853
  },
23844
23854
  "required": [
@@ -23851,7 +23861,7 @@ Example - opted-in members with more than 5 visits, newest first:
23851
23861
  {
23852
23862
  "id": "updateMembersProgramReward",
23853
23863
  "domain": "membersProgram",
23854
- "description": "Correct a members program reward in place, instead of creating a second one. pointsCost replaces the current value; pass null to clear it, which converts a reward guests redeem with points into one an automation grants. Omitting a field leaves it alone. staffInstructions is stored on the reward's catalog item and is rejected for a POS-sourced item, where the column does not exist. Find the rewardId with listMembersProgramRewards.",
23864
+ "description": "Correct a members program reward in place instead of creating a second one. rewardId from listMembersProgramRewards. Omitted fields are left alone; pointsCost: null clears it, turning a points reward into one only an automation grants.",
23855
23865
  "type": "mutation",
23856
23866
  "path": [
23857
23867
  "api",
@@ -23890,7 +23900,7 @@ Example - opted-in members with more than 5 visits, newest first:
23890
23900
  {
23891
23901
  "id": "updateOnboardingForm",
23892
23902
  "domain": "core",
23893
- "description": "Update the onboarding form \u2014 the self-reported answers behind onboarding tasks the system can't observe directly, such as the launch date, funnel direction and the per-step isComplete markers. Top-level keys you omit are left alone, but nested step objects are REPLACED rather than merged, so read getOnboardingForm first and send back the whole step you're editing. `data` is the exception and is merged. Setting pos.details.type to \"other\" provisions a manual-entry POS location as a side effect. Task statuses recompute asynchronously, so getTaskboard can briefly lag this call.",
23903
+ "description": 'Update the onboarding form (self-reported answers behind tasks the system can\'t observe). Nested step objects are replaced, not merged: read getOnboardingForm first and send the whole step (`data` is merged). pos.details.type "other" creates a manual-entry POS location.',
23894
23904
  "type": "mutation",
23895
23905
  "path": [
23896
23906
  "api",
@@ -24491,11 +24501,7 @@ Example - opted-in members with more than 5 visits, newest first:
24491
24501
  "properties": {
24492
24502
  "type": {
24493
24503
  "type": "string",
24494
- "enum": [
24495
- "retention",
24496
- "acquisition",
24497
- "both"
24498
- ]
24504
+ "const": "both"
24499
24505
  },
24500
24506
  "retention": {
24501
24507
  "type": "object",
@@ -24520,7 +24526,7 @@ Example - opted-in members with more than 5 visits, newest first:
24520
24526
  {
24521
24527
  "id": "updateOrganization",
24522
24528
  "domain": "core",
24523
- "description": "Update the organization record. Omitted fields are left alone, but staffInstructions is replaced wholesale rather than merged \u2014 send every key you want to keep. minimumSpendValue is in dollars; timezone is an IANA zone. staffInstructions.scan completes the Members Program Visits POS setup task, and .prepaid is additionally required for Campaign POS setup when the promotion allows pre-pay. restaurantType, isArchived and isReadOnly are admin-only and rejected otherwise. periodCalendar sets how Impact and revenue plans group weeks into periods: {type: 'fiscal', fiscal: {pattern: '4-4-5' | '4-5-4' | '5-4-4' | '13x4', yearEndWeekday: 'sunday' through 'saturday', yearEndRule: 'nearestDec31' | 'lastInDecember'}}; null resets it to 13 four-week periods ending on the Sunday nearest Dec 31.",
24529
+ "description": "Update the organization record: name, timezone, minimum spend, staff instructions, reporting period calendar. Omitted fields are left alone, but staffInstructions is replaced whole, so send every key you want to keep. minimumSpendValue is in dollars.",
24524
24530
  "type": "mutation",
24525
24531
  "path": [
24526
24532
  "api",
@@ -24668,7 +24674,7 @@ Example - opted-in members with more than 5 visits, newest first:
24668
24674
  {
24669
24675
  "id": "updatePassConfiguration",
24670
24676
  "domain": "passBuilder",
24671
- "description": "Save the organization's wallet pass configuration. This is a full-document save, not a patch \u2014 anything you omit is dropped, so read with getPassConfiguration, modify, and send the whole document back. Each save appends a new version, and when sections, features or locations change from the previous version every pass already in a guest's wallet is re-pushed.",
24677
+ "description": "Save the wallet pass configuration. A full-document save, not a patch: anything omitted is dropped, so read getPassConfiguration, modify it, and send the whole document back. A change to sections, features, locations or passStyle re-pushes every pass already in a guest's wallet. Omit passStyle to keep the current style.",
24672
24678
  "type": "mutation",
24673
24679
  "path": [
24674
24680
  "api",
@@ -26102,6 +26108,22 @@ Example - opted-in members with more than 5 visits, newest first:
26102
26108
  ],
26103
26109
  "additionalProperties": false
26104
26110
  },
26111
+ "passStyle": {
26112
+ "anyOf": [
26113
+ {
26114
+ "type": "string",
26115
+ "enum": [
26116
+ "eventTicket",
26117
+ "storeCard",
26118
+ "generic",
26119
+ "coupon"
26120
+ ]
26121
+ },
26122
+ {
26123
+ "type": "null"
26124
+ }
26125
+ ]
26126
+ },
26105
26127
  "metadata": {
26106
26128
  "type": "object",
26107
26129
  "properties": {
@@ -26494,6 +26516,90 @@ function validateToolInput(tool, input) {
26494
26516
  );
26495
26517
  }
26496
26518
 
26519
+ // src/skills.ts
26520
+ import fs2 from "fs";
26521
+ import os2 from "os";
26522
+ import path2 from "path";
26523
+ var MARKER_FILE = ".feast-skill.json";
26524
+ var SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
26525
+ function defaultSkillsDir() {
26526
+ return path2.join(os2.homedir(), ".claude", "skills");
26527
+ }
26528
+ async function createClient(orgFlag, roleFlag) {
26529
+ const acting = await resolveActingContext(orgFlag, roleFlag);
26530
+ return createHttpCaller({
26531
+ accessToken: acting.accessToken,
26532
+ preferredRole: acting.preferredRole
26533
+ });
26534
+ }
26535
+ async function listSkills(orgFlag, roleFlag) {
26536
+ const client = await createClient(orgFlag, roleFlag);
26537
+ const entries = await callProcedure(client, ["api", "skills", "list"], "query", void 0);
26538
+ return Array.isArray(entries) ? entries : [];
26539
+ }
26540
+ async function fetchSkill(name, orgFlag, roleFlag) {
26541
+ if (!SKILL_NAME_PATTERN.test(name)) {
26542
+ throw new Error(`Invalid skill name "${name}". Run: feast skill list`);
26543
+ }
26544
+ const client = await createClient(orgFlag, roleFlag);
26545
+ return callProcedure(client, ["api", "skills", "get"], "query", { name });
26546
+ }
26547
+ function assertInsideDir(root, relative) {
26548
+ const target = path2.resolve(root, relative);
26549
+ if (target !== root && !target.startsWith(`${root}${path2.sep}`)) {
26550
+ throw new Error(`Refusing to write outside the skill directory: ${relative}`);
26551
+ }
26552
+ return target;
26553
+ }
26554
+ function readMarker(dir) {
26555
+ const markerPath = path2.join(dir, MARKER_FILE);
26556
+ if (!fs2.existsSync(markerPath)) {
26557
+ return void 0;
26558
+ }
26559
+ try {
26560
+ return JSON.parse(fs2.readFileSync(markerPath, "utf-8"));
26561
+ } catch {
26562
+ return {};
26563
+ }
26564
+ }
26565
+ function writeSkill(bundle, targetDir, force) {
26566
+ const dir = path2.resolve(targetDir);
26567
+ const exists = fs2.existsSync(dir);
26568
+ const marker = exists ? readMarker(dir) : void 0;
26569
+ if (exists && marker == null && !force) {
26570
+ throw new Error(
26571
+ `${dir} exists and was not installed by feast. Pass --force to replace it.`
26572
+ );
26573
+ }
26574
+ if (exists) {
26575
+ fs2.rmSync(dir, { recursive: true, force: true });
26576
+ }
26577
+ fs2.mkdirSync(dir, { recursive: true });
26578
+ for (const file of bundle.files) {
26579
+ const target = assertInsideDir(dir, file.path);
26580
+ fs2.mkdirSync(path2.dirname(target), { recursive: true });
26581
+ fs2.writeFileSync(target, file.content);
26582
+ }
26583
+ fs2.writeFileSync(
26584
+ path2.join(dir, MARKER_FILE),
26585
+ `${JSON.stringify(
26586
+ {
26587
+ name: bundle.name,
26588
+ version: bundle.version,
26589
+ publishedAt: bundle.publishedAt,
26590
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
26591
+ },
26592
+ null,
26593
+ 2
26594
+ )}
26595
+ `
26596
+ );
26597
+ return { dir, written: bundle.files.length, replaced: exists };
26598
+ }
26599
+ function shortVersion(version) {
26600
+ return version.slice(0, 7);
26601
+ }
26602
+
26497
26603
  // src/main.ts
26498
26604
  var USAGE = `Usage: feast <command> [options]
26499
26605
 
@@ -26505,6 +26611,12 @@ Commands:
26505
26611
  tools [--domain <domain>] [--json] List available tools
26506
26612
  describe <tool> Show a tool's description and input JSON schema
26507
26613
  call <tool> [options] Invoke a tool
26614
+ skill list List the skills you can install
26615
+ skill install <name> [options] Install a skill into ~/.claude/skills/<name>
26616
+
26617
+ Skill install options:
26618
+ --dir <path> Install into this directory instead of ~/.claude/skills/<name>
26619
+ --force Replace a directory that feast did not install
26508
26620
 
26509
26621
  Call options:
26510
26622
  --org <organizationId> Organization to act on (required unless you belong to exactly one)
@@ -26526,7 +26638,14 @@ Non-interactive credentials (for sandboxed agents):
26526
26638
  function parseArgs(argv) {
26527
26639
  const positional = [];
26528
26640
  const flags = {};
26529
- const valueFlags = /* @__PURE__ */ new Set(["org", "role", "input", "input-file", "domain"]);
26641
+ const valueFlags = /* @__PURE__ */ new Set([
26642
+ "org",
26643
+ "role",
26644
+ "input",
26645
+ "input-file",
26646
+ "domain",
26647
+ "dir"
26648
+ ]);
26530
26649
  for (let i = 0; i < argv.length; i++) {
26531
26650
  const arg = argv[i];
26532
26651
  if (!arg.startsWith("--")) {
@@ -26674,7 +26793,7 @@ function readCallInput(args) {
26674
26793
  }
26675
26794
  let raw = inputJson;
26676
26795
  if (inputFile != null) {
26677
- raw = fs2.readFileSync(inputFile, "utf-8");
26796
+ raw = fs3.readFileSync(inputFile, "utf-8");
26678
26797
  }
26679
26798
  if (raw == null) {
26680
26799
  return {};
@@ -26742,6 +26861,41 @@ Run: feast describe ${tool.id}`
26742
26861
  const result = await callProcedure(client, tool.path, tool.type, input);
26743
26862
  console.info(JSON.stringify(result ?? null, null, 2));
26744
26863
  }
26864
+ async function commandSkill(args) {
26865
+ const [subcommand, name] = args.positional;
26866
+ const orgFlag = args.flags["org"];
26867
+ const roleFlag = args.flags["role"];
26868
+ if (subcommand === "list") {
26869
+ const entries = await listSkills(orgFlag, roleFlag);
26870
+ if (entries.length === 0) {
26871
+ console.info("No skills are published for your organization yet.");
26872
+ return;
26873
+ }
26874
+ for (const entry of entries) {
26875
+ console.info(
26876
+ `${entry.name} ${entry.displayTitle} (version ${shortVersion(entry.version)})`
26877
+ );
26878
+ console.info(` ${entry.description.split("\n")[0]}`);
26879
+ }
26880
+ return;
26881
+ }
26882
+ if (subcommand === "install") {
26883
+ if (name == null) {
26884
+ throw new Error("Usage: feast skill install <name> [--dir <path>] [--force]");
26885
+ }
26886
+ const bundle = await fetchSkill(name, orgFlag, roleFlag);
26887
+ const targetDir = args.flags["dir"] ?? path3.join(defaultSkillsDir(), bundle.name);
26888
+ const result = writeSkill(bundle, targetDir, args.flags["force"] === true);
26889
+ console.info(
26890
+ `${result.replaced ? "Updated" : "Installed"} ${bundle.name} (version ${shortVersion(bundle.version)}, ${result.written} files) in ${result.dir}`
26891
+ );
26892
+ console.info(
26893
+ "Claude Code picks it up on the next session. For claude.ai, zip that directory and upload it under Settings > Capabilities > Skills."
26894
+ );
26895
+ return;
26896
+ }
26897
+ throw new Error("Usage: feast skill <list|install <name>>");
26898
+ }
26745
26899
  async function runCli(argv) {
26746
26900
  const [command, ...rest] = argv;
26747
26901
  const args = parseArgs(rest);
@@ -26765,6 +26919,9 @@ async function runCli(argv) {
26765
26919
  case "call":
26766
26920
  await commandCall(args);
26767
26921
  break;
26922
+ case "skill":
26923
+ await commandSkill(args);
26924
+ break;
26768
26925
  default:
26769
26926
  console.info(USAGE);
26770
26927
  if (command != null && command !== "help") {
@@ -26774,16 +26931,16 @@ async function runCli(argv) {
26774
26931
  }
26775
26932
 
26776
26933
  // src/updateNotifier.ts
26777
- import fs3 from "fs";
26934
+ import fs4 from "fs";
26778
26935
  import { createRequire as createRequire2 } from "module";
26779
- import os2 from "os";
26780
- import path2 from "path";
26936
+ import os3 from "os";
26937
+ import path4 from "path";
26781
26938
  var PACKAGE_NAME = "@feastalytics/cli";
26782
26939
  var REGISTRY_URL = "https://registry.npmjs.org";
26783
26940
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
26784
26941
  var FETCH_TIMEOUT_MS = 1500;
26785
- var CACHE_DIR = path2.join(os2.homedir(), ".config", "feast-cli");
26786
- var CACHE_PATH = path2.join(CACHE_DIR, "update-check.json");
26942
+ var CACHE_DIR = path4.join(os3.homedir(), ".config", "feast-cli");
26943
+ var CACHE_PATH = path4.join(CACHE_DIR, "update-check.json");
26787
26944
  function readPackageVersion2() {
26788
26945
  try {
26789
26946
  const require2 = createRequire2(import.meta.url);
@@ -26820,7 +26977,7 @@ function isDisabled() {
26820
26977
  function readCache() {
26821
26978
  try {
26822
26979
  const parsed = JSON.parse(
26823
- fs3.readFileSync(CACHE_PATH, "utf-8")
26980
+ fs4.readFileSync(CACHE_PATH, "utf-8")
26824
26981
  );
26825
26982
  return typeof parsed.latest === "string" && typeof parsed.checkedAt === "number" ? parsed : void 0;
26826
26983
  } catch {
@@ -26829,8 +26986,8 @@ function readCache() {
26829
26986
  }
26830
26987
  function writeCache(cache) {
26831
26988
  try {
26832
- fs3.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
26833
- fs3.writeFileSync(CACHE_PATH, JSON.stringify(cache));
26989
+ fs4.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
26990
+ fs4.writeFileSync(CACHE_PATH, JSON.stringify(cache));
26834
26991
  } catch {
26835
26992
  return;
26836
26993
  }