@koda-sl/baker-cli 0.307.0-dev.5c40193fb → 0.307.0-dev.ce3b0e7c2

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
@@ -4816,6 +4816,8 @@ var chatChangeTypeSchema = z13.enum([
4816
4816
  "conversion-sends",
4817
4817
  // The company's sources: a HubSpot event made or changed. Staged, applied at Publish.
4818
4818
  "event-sources",
4819
+ // What the company counts as a conversion, when an agent changed it. Staged, applied at Publish.
4820
+ "conversions",
4819
4821
  // An A/B test between a landing and its alternative. Staged, not immediate.
4820
4822
  "experiment",
4821
4823
  "briefs"
@@ -14950,7 +14952,9 @@ var analyticsConversionRowSchema = z30.object({
14950
14952
  * The doors its events came in by in the window, in
14951
14953
  * {@link ANALYTICS_EVENT_DOORS} order. Empty when it has not fired.
14952
14954
  */
14953
- doors: z30.array(z30.enum(ANALYTICS_EVENT_DOORS)).default([])
14955
+ doors: z30.array(z30.enum(ANALYTICS_EVENT_DOORS)).default([]),
14956
+ /** Proposed in this chat: counted from when it is published, so every report reads it as nothing until then. */
14957
+ staged: z30.boolean().optional()
14954
14958
  });
14955
14959
  var analyticsEventCatalogRowSchema = z30.object({
14956
14960
  /** `form:…` | `submit:…` | `page:…` | `exit:…` — parse with `parseEventKey`. */
@@ -15558,14 +15562,24 @@ var analyticsConversionsRequestSchema = z30.object({
15558
15562
  var analyticsConversionsResponseSchema = z30.object({
15559
15563
  ok: z30.literal(true),
15560
15564
  data: z30.object({
15565
+ /**
15566
+ * What the company counts — inside a chat, with this chat's proposal on
15567
+ * top: `staged` marks a definition that applies when the chat is published.
15568
+ */
15561
15569
  definitions: z30.array(
15562
15570
  z30.object({
15563
15571
  eventKey: z30.string(),
15564
15572
  name: z30.string(),
15565
15573
  countMode: z30.string(),
15566
- updatedAt: z30.number()
15574
+ updatedAt: z30.number(),
15575
+ staged: z30.boolean().optional()
15567
15576
  })
15568
- )
15577
+ ),
15578
+ /** This chat's proposal, when it has one: counted from Publish, and gone if the chat is discarded. */
15579
+ staged: z30.object({
15580
+ set: z30.array(z30.object({ eventKey: z30.string(), name: z30.string() })),
15581
+ remove: z30.array(z30.string())
15582
+ }).optional()
15569
15583
  }),
15570
15584
  hints: z30.array(z30.string()).optional()
15571
15585
  });
@@ -23163,6 +23177,20 @@ var lookalikeSpecSchema = z31.object({
23163
23177
  ratio: z31.number().min(0.01).max(0.2).optional(),
23164
23178
  country: z31.string().length(2).optional()
23165
23179
  });
23180
+ function valuesOfKey(node, key) {
23181
+ if (Array.isArray(node)) return node.flatMap((entry) => valuesOfKey(entry, key));
23182
+ if (!node || typeof node !== "object") return [];
23183
+ const record = node;
23184
+ return [
23185
+ ...key in record ? [record[key]] : [],
23186
+ ...Object.values(record).flatMap((value) => valuesOfKey(value, key))
23187
+ ];
23188
+ }
23189
+ function mixedEventSourceTypes(rule) {
23190
+ const types = valuesOfKey(rule, "event_sources").flatMap((sources) => Array.isArray(sources) ? sources : []).map((source) => source?.type).filter((type) => typeof type === "string");
23191
+ const distinct = [...new Set(types)].sort();
23192
+ return distinct.length > 1 ? distinct : [];
23193
+ }
23166
23194
  var customAudienceCreateSchema = z31.object({
23167
23195
  name: z31.string().min(1).max(META_LIMITS.audience.nameMax),
23168
23196
  subtype: z31.enum(CUSTOM_AUDIENCE_SUBTYPES),
@@ -23179,6 +23207,21 @@ var customAudienceCreateSchema = z31.object({
23179
23207
  if ((p.subtype === "WEBSITE" || p.subtype === "ENGAGEMENT") && !p.rule) {
23180
23208
  ctx.addIssue({ code: "custom", path: ["rule"], message: `${p.subtype} audiences need a rule (use --file)` });
23181
23209
  }
23210
+ if (p.rule && p.retention_days !== void 0) {
23211
+ ctx.addIssue({
23212
+ code: "custom",
23213
+ path: ["retention_days"],
23214
+ message: "a rule carries its own window in retention_seconds, so drop retention_days \u2014 Meta takes it only on a customer-file audience, and sending both leaves the review showing a window the audience will not have"
23215
+ });
23216
+ }
23217
+ const mixed = mixedEventSourceTypes(p.rule);
23218
+ if (mixed.length > 0) {
23219
+ ctx.addIssue({
23220
+ code: "custom",
23221
+ path: ["rule"],
23222
+ message: `one audience covers one kind of source, but this rule mixes ${mixed.join(" and ")} \u2014 split it into one audience per source`
23223
+ });
23224
+ }
23182
23225
  });
23183
23226
  var customAudienceUpdateSchema = z31.object({
23184
23227
  name: z31.string().min(1).max(META_LIMITS.audience.nameMax).optional(),
@@ -23919,6 +23962,7 @@ var audiencesCreateCommand2 = defineCommand57({
23919
23962
  name: "create",
23920
23963
  description: `Stage a new custom audience shell. ${STAGED_NOTE2}
23921
23964
  LOOKALIKE/WEBSITE/ENGAGEMENT specifics go via --file (lookalike_spec / rule). Member-list upload is not staged in v1.
23965
+ A WEBSITE/ENGAGEMENT rule carries its own window in retention_seconds, so --retention-days is refused next to one, and one audience covers one kind of event source (a Page and an Instagram profile are two audiences).
23922
23966
  Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WEBSITE --file rule.json`
23923
23967
  },
23924
23968
  args: {
@@ -23926,7 +23970,10 @@ Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WE
23926
23970
  name: { type: "string", description: "Audience name" },
23927
23971
  subtype: { type: "string", description: "CUSTOM|WEBSITE|ENGAGEMENT|LOOKALIKE" },
23928
23972
  description: { type: "string", description: "Description" },
23929
- "retention-days": { type: "string", description: "Retention window in days" },
23973
+ "retention-days": {
23974
+ type: "string",
23975
+ description: "Retention window in days \u2014 customer-file audiences only; a rule carries its own retention_seconds"
23976
+ },
23930
23977
  file: { type: "string", description: "JSON file with lookalike_spec / rule / full payload" }
23931
23978
  },
23932
23979
  run: async ({ args: args4 }) => {
@@ -29245,6 +29292,14 @@ function namingHints(definitions) {
29245
29292
  `Names in use here: ${names.join(", ")}. Reuse one EXACTLY when another Form produces the same outcome \u2014 that is what makes them one number \u2014 and use \`--rename "<old>" --to "<existing>"\` to merge two that should always have been one.`
29246
29293
  ];
29247
29294
  }
29295
+ function stagedHints(definitions) {
29296
+ const staged = definitions.filter((row) => row.staged === true);
29297
+ if (staged.length === 0) return [];
29298
+ return [
29299
+ `Staged on this chat, not counted yet: ${staged.map((row) => `"${row.name}"`).join(", ")}. The person sees ${staged.length === 1 ? "it" : "them"} under Conversions in the chat's changes, and ${staged.length === 1 ? "it counts" : "they count"} \u2014 history included \u2014 from when they publish the chat; discarding the chat drops ${staged.length === 1 ? "it" : "them"}. Tell them that, not that it is already counted.`,
29300
+ 'When you talk to the person, call each event by its name ("Appointment scheduled"), never its key (`page:Appointment scheduled`): the key is how Baker spells it internally, and the `page:` part means nothing to them.'
29301
+ ];
29302
+ }
29248
29303
 
29249
29304
  // src/commands/analytics/conversionList.ts
29250
29305
  function mergeStoredWithCounts(stored, reported) {
@@ -29270,7 +29325,8 @@ function mergeStoredWithCounts(stored, reported) {
29270
29325
  conversions: counted?.conversions ?? 0,
29271
29326
  convertingVisits: counted?.convertingVisits ?? 0,
29272
29327
  lastSeen: counted?.lastSeen ?? null,
29273
- doors: counted?.doors ?? []
29328
+ doors: counted?.doors ?? [],
29329
+ ...rows.some((row) => row.staged === true) ? { staged: true } : {}
29274
29330
  };
29275
29331
  }).sort((a, b) => a.name.localeCompare(b.name));
29276
29332
  }
@@ -31397,6 +31453,7 @@ function conversionsEnvelope(args4, definitions, candidates) {
31397
31453
  const hints2 = [
31398
31454
  ...flow ? flowConversionHints(flow.slug, flow.endings, flow.noConversionReason) : [],
31399
31455
  ...flowCoverageHints(coverage),
31456
+ ...stagedHints(definitions),
31400
31457
  ...page.hints,
31401
31458
  ...namingHints(definitions)
31402
31459
  ];
@@ -31415,7 +31472,7 @@ var conversionsCommand3 = (() => {
31415
31472
  const args4 = {
31416
31473
  event: {
31417
31474
  type: "string",
31418
- description: "The event to start counting, from the `candidates` or `--flow` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. REPEATABLE \u2014 pass it several times with one --name to count all of them as that single outcome, which is the whole point of the name. Applies immediately and scores the WHOLE history, not just from now on, so counting the right event today also corrects last month's reports. An event a client's system sends (the API, HubSpot) counts here too: it is credited to the LAST LANDING its person viewed, up to 90 days before, so a deal won weeks later is that landing's conversion. A person who never saw a landing is not counted at all. Such an event is usually also a funnel step (`baker funnel`) and a send (`baker analytics sends`)",
31475
+ description: "The event to start counting, from the `candidates` or `--flow` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. REPEATABLE \u2014 pass it several times with one --name to count all of them as that single outcome, which is the whole point of the name. Inside a chat it is STAGED: the chat lists it under Conversions and it counts from when the chat is published \u2014 then it scores the WHOLE history, not just from then on, so counting the right event also corrects last month's reports. An event a client's system sends (the API, HubSpot) counts here too: it is credited to the LAST LANDING its person viewed, up to 90 days before, so a deal won weeks later is that landing's conversion. A person who never saw a landing is not counted at all. Such an event is usually also a funnel step (`baker funnel`) and a send (`baker analytics sends`)",
31419
31476
  required: false
31420
31477
  },
31421
31478
  name: {
@@ -31472,7 +31529,7 @@ var conversionsCommand3 = (() => {
31472
31529
  return defineCommand97({
31473
31530
  meta: {
31474
31531
  name: "conversions",
31475
- description: "What this company counts as a conversion ON ITS PAGES \u2014 a browser event, once per visit: the numerator of every conversion rate on Measure, and nothing more. An event one of the client's systems sends (a deal won in HubSpot, an order the server posts) is not a page conversion: it is a step on the funnel (`baker funnel`) and can be sent to an ad platform (`baker analytics sends`). Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. It applies IMMEDIATELY and retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers the moment you name the right event. Nothing here is staged and publishing is not involved. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
31532
+ description: "What this company counts as a conversion \u2014 the numerator of every conversion rate on Measure. A page event (a Form ending, a data-baker-* event) and an event one of the client's systems sends (a HubSpot event made with `baker analytics sources --create-hubspot`, an order the server posts) both count; a system's event is credited to the last landing its person viewed. A HubSpot event is `page:<its name>` here \u2014 `--event 'page:Deal won' --name 'Deal won'`. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. Inside a chat a change is STAGED: the person sees it under Conversions in the chat's changes and it applies when they publish, retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers. A read shows a proposal marked `staged`. Call an event by its name when you talk to the person, never by its key. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
31476
31533
  },
31477
31534
  args: args4,
31478
31535
  run: async ({ args: raw }) => {
@@ -31503,12 +31560,8 @@ var conversionsCommand3 = (() => {
31503
31560
  }
31504
31561
  });
31505
31562
  })();
31506
- var SYSTEM_EVENTS_HINT = "Events your systems send are not listed: a system's event is a funnel step (`baker funnel`) and is sent to an ad platform with `baker analytics sends`, never counted as a page conversion.";
31507
31563
  function pageCandidates(definitions, catalogue, listing) {
31508
- const candidates = catalogue.filter((row) => row.origin !== "systems");
31509
- const hints2 = conversionHints(definitions, candidates, listing);
31510
- if (listing && candidates.length < catalogue.length) hints2.push(SYSTEM_EVENTS_HINT);
31511
- return { candidates, hints: hints2 };
31564
+ return { candidates: [...catalogue], hints: conversionHints(definitions, catalogue, listing) };
31512
31565
  }
31513
31566
  function parsePairs(raw) {
31514
31567
  if (raw === void 0) return void 0;
@@ -45255,12 +45308,37 @@ function splitParts(raw) {
45255
45308
  }
45256
45309
  return parts.map((part) => part.trim()).filter((part) => part.length > 0);
45257
45310
  }
45311
+ var TRANSFORM_REFUSAL = "A value cannot run code. Map the field as it is (firstname=full_name), or split it in the step with a codeExecution that exposes first_name and last_name and map code:first_name.";
45312
+ var METHOD_CALL = /\.\s*[A-Za-z_$][\w$]*\s*\(/;
45313
+ var TRAILING_INDEX = /\[[^\]]*\]$/;
45314
+ function mentionPart(inner) {
45315
+ if (inner.startsWith("tracking.")) return { kind: "tracking", name: inner.slice("tracking.".length) };
45316
+ if (inner.startsWith("global.")) return { kind: "global", name: inner.slice("global.".length) };
45317
+ return referencePart(inner);
45318
+ }
45319
+ function referencePart(ref) {
45320
+ return /\.codeExecution\.[^.]+$/.test(ref) ? { kind: "code", ref } : { kind: "field", ref };
45321
+ }
45258
45322
  function parsePart(part) {
45259
45323
  if (part.startsWith('"') && part.endsWith('"') && part.length >= 2) {
45260
45324
  return { kind: "text", text: part.slice(1, -1) };
45261
45325
  }
45326
+ if (METHOD_CALL.test(part) || TRAILING_INDEX.test(part)) {
45327
+ throw new MappingError(TRANSFORM_REFUSAL);
45328
+ }
45329
+ if (part.startsWith("{{")) {
45330
+ const close = part.indexOf("}}");
45331
+ if (close < 0) {
45332
+ throw new MappingError(
45333
+ `Unclosed \`{{\` in value \`${part}\`.`,
45334
+ "A mention is written {{<stepId>.customForm.<field>}}."
45335
+ );
45336
+ }
45337
+ if (part.slice(close + 2).trim()) throw new MappingError(TRANSFORM_REFUSAL);
45338
+ return mentionPart(part.slice(2, close).trim());
45339
+ }
45262
45340
  const colon = part.indexOf(":");
45263
- if (colon <= 0) return { kind: "field", ref: part };
45341
+ if (colon <= 0) return referencePart(part);
45264
45342
  const prefix = part.slice(0, colon);
45265
45343
  const rest = part.slice(colon + 1).trim();
45266
45344
  if (!PREFIXES.includes(prefix)) {
@@ -45318,7 +45396,12 @@ function fieldOwners(tree) {
45318
45396
  ];
45319
45397
  });
45320
45398
  }
45321
- function splitRef(ref) {
45399
+ function splitRef(ref, segment) {
45400
+ const marker = `.${segment}.`;
45401
+ const at = ref.lastIndexOf(marker);
45402
+ if (at > 0 && !ref.slice(at + marker.length).includes(".")) {
45403
+ return { step: ref.slice(0, at), name: ref.slice(at + marker.length) };
45404
+ }
45322
45405
  const dot = ref.lastIndexOf(".");
45323
45406
  if (dot <= 0) return { name: ref };
45324
45407
  return { step: ref.slice(0, dot), name: ref.slice(dot + 1) };
@@ -45330,7 +45413,7 @@ function titleize(value) {
45330
45413
  return value.split(/[_-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
45331
45414
  }
45332
45415
  function resolveField(tree, ref) {
45333
- const { step, name } = splitRef(ref);
45416
+ const { step, name } = splitRef(ref, "customForm");
45334
45417
  const owners = fieldOwners(tree);
45335
45418
  const inventory = owners.map((owner2) => ` ${owner2.name} (${owner2.id}): ${owner2.fields.join(", ") || "(no fields)"}`).join("\n");
45336
45419
  const candidates = owners.filter(
@@ -45373,7 +45456,7 @@ function resolveGlobal(tree, name) {
45373
45456
  return mention(id, id);
45374
45457
  }
45375
45458
  function resolveCode(tree, ref) {
45376
- const { step, name } = splitRef(ref);
45459
+ const { step, name } = splitRef(ref, "codeExecution");
45377
45460
  const owners = walkNodes(tree).flatMap((node) => {
45378
45461
  const exposed = externalOf(node).codeExecution?.exposedValues ?? [];
45379
45462
  const names = exposed.map((value) => value?.name).filter((value) => typeof value === "string");
@@ -45917,7 +46000,7 @@ function describeDestination(chosen) {
45917
46000
  // two different questions with two different answers, and the type is the
45918
46001
  // one an agent does not know it is being asked until a mapping is refused.
45919
46002
  ...slots.flatMap((slot) => slot.fieldTypes ? [`${slot.slot} field types \u2014 ${slot.fieldTypes}`] : []),
45920
- 'Values (right of the `=`) are the same for every slot: a form field, tracking:<var>, global:<name>, code:<name>, a "literal", or those joined with +.'
46003
+ 'Values (right of the `=`) are the same for every slot: a form field, tracking:<var>, global:<name>, code:<name>, a "literal", or those joined with +. A field may also be written as the file stores it, {{<stepId>.customForm.<field>}}. A value cannot run code (no .split(), .slice(), [0]).'
45921
46004
  ]
45922
46005
  });
45923
46006
  }
@@ -59508,7 +59591,7 @@ registerSchema({
59508
59591
  },
59509
59592
  sort: {
59510
59593
  type: "string",
59511
- description: "Sort: relevance, search_volume, competition, cpc (default: search_volume)",
59594
+ description: "Sort: search_volume, competition, cpc (default: search_volume)",
59512
59595
  required: false
59513
59596
  },
59514
59597
  type: { type: "string", description: "Filter: paid, organic, all (default: all)", required: false },
@@ -59537,7 +59620,7 @@ Examples:
59537
59620
  target: { type: "positional", description: "Domain or URL to analyze", required: true },
59538
59621
  location: { type: "string", description: "Country code or location code (default: us)", required: false },
59539
59622
  language: { type: "string", description: "Language code or name (default: en)", required: false },
59540
- sort: { type: "string", description: "Sort: relevance, search_volume, competition, cpc", required: false },
59623
+ sort: { type: "string", description: "Sort: search_volume, competition, cpc", required: false },
59541
59624
  type: { type: "string", description: "Filter: paid, organic, all (default: all)", required: false },
59542
59625
  limit: { type: "string", description: "Max results (default: 50, max: 1000)", required: false },
59543
59626
  output: { type: "string", description: "Format: json|csv|md|jsonl", required: false, default: "json" },