@feastalytics/cli 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,6 +88,17 @@ npx skills add feastalytics/cli -g -a '*' -y
88
88
 
89
89
  To refresh from a local checkout instead of GitHub, run `npx skills add ./feast -g -a '*' -y` from the repo root.
90
90
 
91
+ ### Playbook skills from Feastalytics
92
+
93
+ Feastalytics also publishes playbook skills that build on this one. They are served by the API to logged-in users and installed by the CLI itself, so no GitHub access is needed:
94
+
95
+ ```bash
96
+ feast skill list # what is published, with versions
97
+ feast skill install feast-playbooks # writes ~/.claude/skills/feast-playbooks/
98
+ ```
99
+
100
+ `--dir <path>` installs somewhere else (zip that directory to upload it to claude.ai under Settings > Capabilities > Skills). A re-install replaces the previous copy; `--force` is required to replace a directory the CLI did not create. Skills belong to the organization you act on, so pass `--org` when you belong to several.
101
+
91
102
  ## Environment
92
103
 
93
104
  - `FEAST_API_URL` — override the API base URL (e.g. a local dev server)
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 = {
@@ -13903,12 +13904,25 @@ var CLI_MANIFEST = {
13903
13904
  "$schema": "http://json-schema.org/draft-07/schema#"
13904
13905
  }
13905
13906
  },
13907
+ {
13908
+ "id": "getCampaignBenchmarks",
13909
+ "domain": "campaigns",
13910
+ "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.",
13911
+ "type": "query",
13912
+ "path": [
13913
+ "api",
13914
+ "campaigns",
13915
+ "app",
13916
+ "benchmarks"
13917
+ ],
13918
+ "inputJsonSchema": null
13919
+ },
13906
13920
  {
13907
13921
  "id": "getCampaignBreakdown",
13908
13922
  "domain": "campaigns",
13909
13923
  "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.
13910
13924
 
13911
- 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. 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
+ 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).
13912
13926
 
13913
13927
  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.`,
13914
13928
  "type": "query",
@@ -14251,6 +14265,36 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14251
14265
  "creator"
14252
14266
  ],
14253
14267
  "additionalProperties": false
14268
+ },
14269
+ {
14270
+ "type": "object",
14271
+ "properties": {
14272
+ "type": {
14273
+ "type": "string",
14274
+ "const": "variant"
14275
+ },
14276
+ "variant": {
14277
+ "type": "object",
14278
+ "properties": {
14279
+ "campaignId": {
14280
+ "$ref": "#/properties/refs/items/anyOf/0/properties/campaign/properties/campaignId"
14281
+ },
14282
+ "variantId": {
14283
+ "$ref": "#/properties/refs/items/anyOf/2/properties/fbCampaign/properties/fbCampaignId"
14284
+ }
14285
+ },
14286
+ "required": [
14287
+ "campaignId",
14288
+ "variantId"
14289
+ ],
14290
+ "additionalProperties": false
14291
+ }
14292
+ },
14293
+ "required": [
14294
+ "type",
14295
+ "variant"
14296
+ ],
14297
+ "additionalProperties": false
14254
14298
  }
14255
14299
  ]
14256
14300
  },
@@ -14287,7 +14331,7 @@ Metrics use start/end as the session window. Units: sessions, visitors, signups,
14287
14331
  {
14288
14332
  "id": "getCampaignKpis",
14289
14333
  "domain": "campaigns",
14290
- "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, where unit is COUNT, PERCENTAGE (0-1) or CURRENCY (cents). 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.",
14334
+ "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.",
14291
14335
  "type": "query",
14292
14336
  "path": [
14293
14337
  "api",
@@ -26451,6 +26495,90 @@ function validateToolInput(tool, input) {
26451
26495
  );
26452
26496
  }
26453
26497
 
26498
+ // src/skills.ts
26499
+ import fs2 from "fs";
26500
+ import os2 from "os";
26501
+ import path2 from "path";
26502
+ var MARKER_FILE = ".feast-skill.json";
26503
+ var SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
26504
+ function defaultSkillsDir() {
26505
+ return path2.join(os2.homedir(), ".claude", "skills");
26506
+ }
26507
+ async function createClient(orgFlag, roleFlag) {
26508
+ const acting = await resolveActingContext(orgFlag, roleFlag);
26509
+ return createHttpCaller({
26510
+ accessToken: acting.accessToken,
26511
+ preferredRole: acting.preferredRole
26512
+ });
26513
+ }
26514
+ async function listSkills(orgFlag, roleFlag) {
26515
+ const client = await createClient(orgFlag, roleFlag);
26516
+ const entries = await callProcedure(client, ["api", "skills", "list"], "query", void 0);
26517
+ return Array.isArray(entries) ? entries : [];
26518
+ }
26519
+ async function fetchSkill(name, orgFlag, roleFlag) {
26520
+ if (!SKILL_NAME_PATTERN.test(name)) {
26521
+ throw new Error(`Invalid skill name "${name}". Run: feast skill list`);
26522
+ }
26523
+ const client = await createClient(orgFlag, roleFlag);
26524
+ return callProcedure(client, ["api", "skills", "get"], "query", { name });
26525
+ }
26526
+ function assertInsideDir(root, relative) {
26527
+ const target = path2.resolve(root, relative);
26528
+ if (target !== root && !target.startsWith(`${root}${path2.sep}`)) {
26529
+ throw new Error(`Refusing to write outside the skill directory: ${relative}`);
26530
+ }
26531
+ return target;
26532
+ }
26533
+ function readMarker(dir) {
26534
+ const markerPath = path2.join(dir, MARKER_FILE);
26535
+ if (!fs2.existsSync(markerPath)) {
26536
+ return void 0;
26537
+ }
26538
+ try {
26539
+ return JSON.parse(fs2.readFileSync(markerPath, "utf-8"));
26540
+ } catch {
26541
+ return {};
26542
+ }
26543
+ }
26544
+ function writeSkill(bundle, targetDir, force) {
26545
+ const dir = path2.resolve(targetDir);
26546
+ const exists = fs2.existsSync(dir);
26547
+ const marker = exists ? readMarker(dir) : void 0;
26548
+ if (exists && marker == null && !force) {
26549
+ throw new Error(
26550
+ `${dir} exists and was not installed by feast. Pass --force to replace it.`
26551
+ );
26552
+ }
26553
+ if (exists) {
26554
+ fs2.rmSync(dir, { recursive: true, force: true });
26555
+ }
26556
+ fs2.mkdirSync(dir, { recursive: true });
26557
+ for (const file of bundle.files) {
26558
+ const target = assertInsideDir(dir, file.path);
26559
+ fs2.mkdirSync(path2.dirname(target), { recursive: true });
26560
+ fs2.writeFileSync(target, file.content);
26561
+ }
26562
+ fs2.writeFileSync(
26563
+ path2.join(dir, MARKER_FILE),
26564
+ `${JSON.stringify(
26565
+ {
26566
+ name: bundle.name,
26567
+ version: bundle.version,
26568
+ publishedAt: bundle.publishedAt,
26569
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
26570
+ },
26571
+ null,
26572
+ 2
26573
+ )}
26574
+ `
26575
+ );
26576
+ return { dir, written: bundle.files.length, replaced: exists };
26577
+ }
26578
+ function shortVersion(version) {
26579
+ return version.slice(0, 7);
26580
+ }
26581
+
26454
26582
  // src/main.ts
26455
26583
  var USAGE = `Usage: feast <command> [options]
26456
26584
 
@@ -26462,6 +26590,12 @@ Commands:
26462
26590
  tools [--domain <domain>] [--json] List available tools
26463
26591
  describe <tool> Show a tool's description and input JSON schema
26464
26592
  call <tool> [options] Invoke a tool
26593
+ skill list List the skills you can install
26594
+ skill install <name> [options] Install a skill into ~/.claude/skills/<name>
26595
+
26596
+ Skill install options:
26597
+ --dir <path> Install into this directory instead of ~/.claude/skills/<name>
26598
+ --force Replace a directory that feast did not install
26465
26599
 
26466
26600
  Call options:
26467
26601
  --org <organizationId> Organization to act on (required unless you belong to exactly one)
@@ -26483,7 +26617,14 @@ Non-interactive credentials (for sandboxed agents):
26483
26617
  function parseArgs(argv) {
26484
26618
  const positional = [];
26485
26619
  const flags = {};
26486
- const valueFlags = /* @__PURE__ */ new Set(["org", "role", "input", "input-file", "domain"]);
26620
+ const valueFlags = /* @__PURE__ */ new Set([
26621
+ "org",
26622
+ "role",
26623
+ "input",
26624
+ "input-file",
26625
+ "domain",
26626
+ "dir"
26627
+ ]);
26487
26628
  for (let i = 0; i < argv.length; i++) {
26488
26629
  const arg = argv[i];
26489
26630
  if (!arg.startsWith("--")) {
@@ -26631,7 +26772,7 @@ function readCallInput(args) {
26631
26772
  }
26632
26773
  let raw = inputJson;
26633
26774
  if (inputFile != null) {
26634
- raw = fs2.readFileSync(inputFile, "utf-8");
26775
+ raw = fs3.readFileSync(inputFile, "utf-8");
26635
26776
  }
26636
26777
  if (raw == null) {
26637
26778
  return {};
@@ -26699,6 +26840,41 @@ Run: feast describe ${tool.id}`
26699
26840
  const result = await callProcedure(client, tool.path, tool.type, input);
26700
26841
  console.info(JSON.stringify(result ?? null, null, 2));
26701
26842
  }
26843
+ async function commandSkill(args) {
26844
+ const [subcommand, name] = args.positional;
26845
+ const orgFlag = args.flags["org"];
26846
+ const roleFlag = args.flags["role"];
26847
+ if (subcommand === "list") {
26848
+ const entries = await listSkills(orgFlag, roleFlag);
26849
+ if (entries.length === 0) {
26850
+ console.info("No skills are published for your organization yet.");
26851
+ return;
26852
+ }
26853
+ for (const entry of entries) {
26854
+ console.info(
26855
+ `${entry.name} ${entry.displayTitle} (version ${shortVersion(entry.version)})`
26856
+ );
26857
+ console.info(` ${entry.description.split("\n")[0]}`);
26858
+ }
26859
+ return;
26860
+ }
26861
+ if (subcommand === "install") {
26862
+ if (name == null) {
26863
+ throw new Error("Usage: feast skill install <name> [--dir <path>] [--force]");
26864
+ }
26865
+ const bundle = await fetchSkill(name, orgFlag, roleFlag);
26866
+ const targetDir = args.flags["dir"] ?? path3.join(defaultSkillsDir(), bundle.name);
26867
+ const result = writeSkill(bundle, targetDir, args.flags["force"] === true);
26868
+ console.info(
26869
+ `${result.replaced ? "Updated" : "Installed"} ${bundle.name} (version ${shortVersion(bundle.version)}, ${result.written} files) in ${result.dir}`
26870
+ );
26871
+ console.info(
26872
+ "Claude Code picks it up on the next session. For claude.ai, zip that directory and upload it under Settings > Capabilities > Skills."
26873
+ );
26874
+ return;
26875
+ }
26876
+ throw new Error("Usage: feast skill <list|install <name>>");
26877
+ }
26702
26878
  async function runCli(argv) {
26703
26879
  const [command, ...rest] = argv;
26704
26880
  const args = parseArgs(rest);
@@ -26722,6 +26898,9 @@ async function runCli(argv) {
26722
26898
  case "call":
26723
26899
  await commandCall(args);
26724
26900
  break;
26901
+ case "skill":
26902
+ await commandSkill(args);
26903
+ break;
26725
26904
  default:
26726
26905
  console.info(USAGE);
26727
26906
  if (command != null && command !== "help") {
@@ -26731,16 +26910,16 @@ async function runCli(argv) {
26731
26910
  }
26732
26911
 
26733
26912
  // src/updateNotifier.ts
26734
- import fs3 from "fs";
26913
+ import fs4 from "fs";
26735
26914
  import { createRequire as createRequire2 } from "module";
26736
- import os2 from "os";
26737
- import path2 from "path";
26915
+ import os3 from "os";
26916
+ import path4 from "path";
26738
26917
  var PACKAGE_NAME = "@feastalytics/cli";
26739
26918
  var REGISTRY_URL = "https://registry.npmjs.org";
26740
26919
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
26741
26920
  var FETCH_TIMEOUT_MS = 1500;
26742
- var CACHE_DIR = path2.join(os2.homedir(), ".config", "feast-cli");
26743
- var CACHE_PATH = path2.join(CACHE_DIR, "update-check.json");
26921
+ var CACHE_DIR = path4.join(os3.homedir(), ".config", "feast-cli");
26922
+ var CACHE_PATH = path4.join(CACHE_DIR, "update-check.json");
26744
26923
  function readPackageVersion2() {
26745
26924
  try {
26746
26925
  const require2 = createRequire2(import.meta.url);
@@ -26777,7 +26956,7 @@ function isDisabled() {
26777
26956
  function readCache() {
26778
26957
  try {
26779
26958
  const parsed = JSON.parse(
26780
- fs3.readFileSync(CACHE_PATH, "utf-8")
26959
+ fs4.readFileSync(CACHE_PATH, "utf-8")
26781
26960
  );
26782
26961
  return typeof parsed.latest === "string" && typeof parsed.checkedAt === "number" ? parsed : void 0;
26783
26962
  } catch {
@@ -26786,8 +26965,8 @@ function readCache() {
26786
26965
  }
26787
26966
  function writeCache(cache) {
26788
26967
  try {
26789
- fs3.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
26790
- fs3.writeFileSync(CACHE_PATH, JSON.stringify(cache));
26968
+ fs4.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
26969
+ fs4.writeFileSync(CACHE_PATH, JSON.stringify(cache));
26791
26970
  } catch {
26792
26971
  return;
26793
26972
  }
@@ -44,6 +44,17 @@ If you've been invoking the CLI through `npx` rather than a global install, skip
44
44
 
45
45
  Update the skill whenever you update the CLI: the two ship from the same repo but on different triggers, so a new CLI version usually means this skill's guidance has moved too.
46
46
 
47
+ ## Playbook skills
48
+
49
+ Feastalytics publishes further skills that build on this one (campaign diagnosis and other playbooks). They come from the API, not from GitHub:
50
+
51
+ ```bash
52
+ feast skill list # what is published for your organization
53
+ feast skill install feast-playbooks # install or refresh one into ~/.claude/skills/
54
+ ```
55
+
56
+ Install what `feast skill list` offers when the user asks for a playbook this skill does not cover, and re-run the install when the CLI prints an update notice.
57
+
47
58
  ## Which organization
48
59
 
49
60
  Most tools act on one organization. A user often belongs to several, so which one you target matters and must be explicit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feastalytics/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Command-line client for the Feastalytics platform — list, create, and update campaigns, automations, offers, and members-program rewards from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {