@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 +11 -0
- package/dist/cli.js +249 -70
- package/feast/references/setup.md +11 -0
- package/package.json +1 -1
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(
|
|
3225
|
-
let input =
|
|
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 [
|
|
3478
|
-
wsComponent.path =
|
|
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,
|
|
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
|
-
...
|
|
6846
|
+
...path5,
|
|
6847
6847
|
key
|
|
6848
6848
|
]);
|
|
6849
6849
|
},
|
|
6850
6850
|
apply(_1, _2, args) {
|
|
6851
|
-
const isApply =
|
|
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 ?
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
7445
|
+
query(path5, input, opts) {
|
|
7446
7446
|
return this.requestAsPromise({
|
|
7447
7447
|
type: "query",
|
|
7448
|
-
path:
|
|
7448
|
+
path: path5,
|
|
7449
7449
|
input,
|
|
7450
7450
|
context: opts?.context,
|
|
7451
7451
|
signal: opts?.signal
|
|
7452
7452
|
});
|
|
7453
7453
|
}
|
|
7454
|
-
mutation(
|
|
7454
|
+
mutation(path5, input, opts) {
|
|
7455
7455
|
return this.requestAsPromise({
|
|
7456
7456
|
type: "mutation",
|
|
7457
|
-
path:
|
|
7457
|
+
path: path5,
|
|
7458
7458
|
input,
|
|
7459
7459
|
context: opts?.context,
|
|
7460
7460
|
signal: opts?.signal
|
|
7461
7461
|
});
|
|
7462
7462
|
}
|
|
7463
|
-
subscription(
|
|
7463
|
+
subscription(path5, input, opts) {
|
|
7464
7464
|
const observable$ = this.$request({
|
|
7465
7465
|
type: "subscription",
|
|
7466
|
-
path:
|
|
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:
|
|
7538
|
+
return createRecursiveProxy(({ path: path5, args }) => {
|
|
7539
7539
|
const pathCopy = [
|
|
7540
7540
|
key,
|
|
7541
|
-
...
|
|
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
|
|
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:
|
|
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 = (
|
|
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(
|
|
8117
|
-
if (includes(
|
|
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(
|
|
8120
|
+
if (includes(path5, "prototype")) {
|
|
8121
8121
|
throw new Error("prototype is not allowed as a property");
|
|
8122
8122
|
}
|
|
8123
|
-
if (includes(
|
|
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,
|
|
8128
|
-
validatePath(
|
|
8129
|
-
for (let i = 0; i <
|
|
8130
|
-
const key =
|
|
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 = +
|
|
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,
|
|
8152
|
-
validatePath(
|
|
8153
|
-
if (
|
|
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 <
|
|
8158
|
-
const key =
|
|
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 ===
|
|
8168
|
+
const isEnd = i === path5.length - 2;
|
|
8169
8169
|
if (isEnd) {
|
|
8170
8170
|
break;
|
|
8171
8171
|
}
|
|
8172
8172
|
const row = +key;
|
|
8173
|
-
const type = +
|
|
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 =
|
|
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 = +
|
|
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,
|
|
8248
|
-
plain = setDeep(plain,
|
|
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,
|
|
8255
|
-
const object = getDeep(plain, parsePath(
|
|
8256
|
-
identicalPaths.map((
|
|
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,
|
|
8274
|
+
function addIdentity(object, path5, identities) {
|
|
8275
8275
|
const existingSet = identities.get(object);
|
|
8276
8276
|
if (existingSet) {
|
|
8277
|
-
existingSet.push(
|
|
8277
|
+
existingSet.push(path5);
|
|
8278
8278
|
} else {
|
|
8279
|
-
identities.set(object, [
|
|
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((
|
|
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,
|
|
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,
|
|
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, [...
|
|
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
|
|
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,
|
|
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([
|
|
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 =
|
|
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
|
|
26913
|
+
import fs4 from "fs";
|
|
26735
26914
|
import { createRequire as createRequire2 } from "module";
|
|
26736
|
-
import
|
|
26737
|
-
import
|
|
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 =
|
|
26743
|
-
var CACHE_PATH =
|
|
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
|
-
|
|
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
|
-
|
|
26790
|
-
|
|
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.
|
|
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": {
|