@koda-sl/baker-cli 0.313.0-dev.ca75dc08a → 0.314.0-dev.e1de11139

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
@@ -1382,7 +1382,7 @@ baker funnel --steps '[...]' # the whole funnel, restat
1382
1382
 
1383
1383
  **Every write says what it dropped.** `--step` changes one step and leaves the rest as it stands; `--steps` replaces the whole funnel and `--preset` draws over it. Every write returns `removed[]` and a `REMOVED steps:` hint when a step went, so a restatement written from a stale read is caught in the same turn.
1384
1384
 
1385
- **A step says what triggers it, and every trigger is PICKED from `--moments`.** `triggers` is a list and any one of them firing is enough. Three kinds: `{"kind":"event","eventKey":"submit:contact"}` for anything the company's Forms, pages or systems produced (`moments.events`, `moments.sources`), `{"kind":"landing","landingId":"<id>"}` for one landing being viewed (`moments.landings`), and `{"kind":"any_landing"}` for any landing — what the default first step listens for. Nothing has to be named first: the event is used by its own key, and the step shows it by its own words. `moments.events[].origin` tells a page's event (`site`) from one the client's own systems sent (`systems`); `moments.sources` lists the client's systems by the `page:<name>` key each raises, pickable before the first send arrives. **A Form's own endings need nothing set up first** — no Source, no conversion: a Baker page raises them natively, so every Form in the workspace has its **outcome** marked in `moments.events` with `outcome: true` and the `step` it happens on — a Form nobody has filled in yet gets that row at zero, `seen: false`. It is the same ending `baker analytics conversions` recommends counting, so a step and a conversion land on the same event; `baker analytics conversions --flow <slug>` lists all of one Form's endings when a step wants another. `baker funnel` also hints, in one line, every workspace Form that reaches no step, with its outcome key. Place one with **`baker funnel --add-trigger <step-key> --event <key>`**, which adds a single way in and leaves the step's name, stage, `after[]` and other triggers untouched — `--step` takes a whole step and silently drops any trigger a restatement forgets. That is what makes a Form built minutes ago pickable. A key typed from memory saves, publishes and counts nobody forever — the hint "has a trigger and nobody reached it while N people entered" is that mistake caught after the fact. A step written with `offlineConversions`, a `conversion` trigger or an `analytics` trigger is refused before any round trip, with the shape to use.
1385
+ **A step says what triggers it, and every trigger is PICKED from `--moments`.** `triggers` is a list and any one of them firing is enough. Three kinds: `{"kind":"event","eventKey":"submit:contact"}` for anything the company's Forms, pages or systems produced (`moments.events`, `moments.sources`), `{"kind":"landing","landingId":"<id>"}` for one landing being viewed (`moments.landings`), and `{"kind":"any_landing"}` for any landing — what the default first step listens for. Nothing has to be named first: the event is used by its own key, and the step shows it by its own words. `moments.events[].origin` tells a page's event (`site`) from one the client's own systems sent (`systems`); `moments.sources` lists the client's systems by the `page:<name>` key each raises, pickable before the first send arrives. **A Form's own endings need nothing set up first** — no Source, no conversion: a Baker page raises them natively, so every Form in the workspace lists its endings in `moments.events` — each step's completion and its `submit:` — with the `step` each happens on, at zero and `seen: false` until somebody fires one, and `countedAs` when the company already counts it. **Baker never picks which ending is the Form's outcome; the agent does**, from the step names, asking the person when they do not make it obvious — and uses the same key the Form is counted on, when it is counted. `baker funnel` also hints, in one line, every workspace Form that reaches no step. Place one with **`baker funnel --add-trigger <step-key> --event <key>`**, which adds a single way in and leaves the step's name, stage, `after[]` and other triggers untouched — `--step` takes a whole step and silently drops any trigger a restatement forgets. That is what makes a Form built minutes ago pickable. A key typed from memory saves, publishes and counts nobody forever — the hint "has a trigger and nobody reached it while N people entered" is that mistake caught after the fact. A step written with `offlineConversions`, a `conversion` trigger or an `analytics` trigger is refused before any round trip, with the shape to use.
1386
1386
 
1387
1387
  **A landing being viewed can be a step** — where most funnels start — and never a conversion: `baker analytics conversions` refuses a `view:` key.
1388
1388
 
@@ -2215,6 +2215,8 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
2215
2215
  | `baker images download <targets>` | Remote URLs and/or library ids → local files, so the local transforms can read them | n/a (writes to disk, no upload) |
2216
2216
  | `baker images tags` | List available image tag names (defaults + company custom tags) | n/a |
2217
2217
 
2218
+ **Every image record carries `width` and `height`, without `--full`.** They are there to be passed straight to the component that places the image: a remote image rendered with no declared size makes the site build download the file purely to measure it, which is the only reason building a site needs the network at all — and a dropped connection there fails the entire quality-check run for a reason nothing on the page caused.
2219
+
2218
2220
  **Auto-ingest** runs the full `processImage` pipeline (Gemini describe + Voyage multimodal embed + OpenRouter text embed) on every hit. Override with `--auto-ingest N` (turn on) or `--no-auto-ingest` (turn off where default is on). When auto-ingest succeeds, the matching returned hit uses the Baker-owned URL and keeps the original provider URL as `sourceUrl`. After auto-ingest the next `baker images library` query for the same concept hits the local row.
2219
2221
 
2220
2222
  **Provider context biases the description.** When a hit carries human-readable text (Giphy `alt_text` / `title`, Pexels `alt`, Pixabay `tags`, Brandfetch type+theme, etc.) it's surfaced on the hit as `descriptionContext` and forwarded to Gemini during auto-ingest as a hint — Gemini still trusts the pixels (e.g. it'll correct a wrong botanical name) but uses the hint for cultural/brand/scene awareness it can't infer from bytes alone.
package/dist/cli.js CHANGED
@@ -222,6 +222,15 @@ function compactImage(image) {
222
222
  imageUrl: String(image.imageUrl ?? ""),
223
223
  description: String(image.description ?? ""),
224
224
  tags: Array.isArray(image.tags) ? image.tags : [],
225
+ // Two numbers that are not about choosing the image but about placing it.
226
+ // A remote <BakerImage> with no width/height makes Astro DOWNLOAD the file
227
+ // at build time just to measure it — one request per image per build, on a
228
+ // build that is otherwise offline, and a single dropped connection there
229
+ // fails a client's whole verify. They are cheap enough to always carry, and
230
+ // burying them behind --full meant the agent placing the image never had
231
+ // them.
232
+ width: typeof image.width === "number" ? image.width : null,
233
+ height: typeof image.height === "number" ? image.height : null,
225
234
  // The facts that decide a pick, so choosing does not need a second call:
226
235
  // whose brand it is, who is in it, and what text is burned in.
227
236
  subject: typeof analysis.subject === "string" ? analysis.subject : null,
@@ -235,8 +244,6 @@ function fullImage(image) {
235
244
  const compact3 = compactImage(image);
236
245
  return {
237
246
  ...compact3,
238
- width: image.width ?? null,
239
- height: image.height ?? null,
240
247
  aspectRatio: formatAspectRatio(image.aspectRatio),
241
248
  dominantColor: image.dominantColor ?? null,
242
249
  imagePalette: Array.isArray(image.imagePalette) ? image.imagePalette : [],
@@ -28864,10 +28871,6 @@ function flowEventKeys(tree, slug) {
28864
28871
  }
28865
28872
  ];
28866
28873
  }
28867
- function suggestedEnding(endings) {
28868
- const deepestFirst = [...endings].reverse();
28869
- return deepestFirst.find((row) => row.isCompletion && row.nodeType !== "text") ?? deepestFirst.find((row) => row.isCompletion) ?? endings[endings.length - 1] ?? null;
28870
- }
28871
28874
 
28872
28875
  // ../api/src/event-ingest/arrivals.ts
28873
28876
  import { z as z33 } from "zod";
@@ -30086,12 +30089,8 @@ function flowConversionHints(slug, rows, noConversionReason) {
30086
30089
  `Nothing here is counted, and that is on purpose: this Form declares no outcome \u2014 "${noConversionReason}". Leave it alone unless that has stopped being true.`
30087
30090
  ];
30088
30091
  }
30089
- const suggestion = suggestedEnding(rows);
30090
30092
  return [
30091
- `Nothing on "${slug}" is counted, so it converts nobody on every report however many people finish it \u2014 Baker never guesses which ending is the point of a Form.`,
30092
- ...suggestion ? [
30093
- `Name its outcome: \`baker analytics conversions --event "${suggestion.eventKey}" --name "<what a person calls it>"\` (that key is "${suggestion.nodeName}"). It applies at once and rescores the whole history; nothing is staged and there is nothing to publish.`
30094
- ] : [],
30093
+ `Nothing on "${slug}" is counted, so it converts nobody on every report however many people finish it. Which ending is its outcome is your call, never Baker's: read the endings above by the step each happens on (\`nodeName\`), and when the step names do not make it obvious \u2014 a Form that branches into a rejection and a confirmation \u2014 ask the person which one means a real result. Then \`baker analytics conversions --event "<key>" --name "<what a person calls it>"\`. If this Form is already on a funnel step, count the key that step uses, so the two numbers agree.`,
30095
30094
  `If this Form has no outcome of its own \u2014 it hands the visitor to a shop, a portal or a funnel that converts them there \u2014 say so instead, with \`noConversion: { "reason": "\u2026" }\` on its first step.`
30096
30095
  ];
30097
30096
  }
@@ -47236,56 +47235,58 @@ import { readFileSync as readFileSync15 } from "fs";
47236
47235
  import { defineCommand as defineCommand129 } from "citty";
47237
47236
 
47238
47237
  // src/commands/funnel/formMoments.ts
47239
- var MAX_NAMED_FORMS = 8;
47240
- function formMoments(events, forms) {
47241
- const outcomes = /* @__PURE__ */ new Map();
47238
+ function offeredEndings(form) {
47239
+ return flowEventKeys(form.tree, form.slug).filter((ending) => ending.isCompletion || ending.nodeId === null);
47240
+ }
47241
+ function formMoments(events, forms, counted) {
47242
+ const endings = /* @__PURE__ */ new Map();
47242
47243
  for (const form of forms) {
47243
- const outcome = suggestedEnding(flowEventKeys(form.tree, form.slug));
47244
- if (outcome !== null) outcomes.set(outcome.eventKey, { slug: form.slug, outcome });
47244
+ for (const ending of offeredEndings(form)) endings.set(ending.eventKey, { slug: form.slug, ending });
47245
47245
  }
47246
- const marked = events.map((event) => {
47247
- const found = outcomes.get(event.eventKey);
47246
+ const known = events.map((event) => {
47247
+ const found = endings.get(event.eventKey);
47248
47248
  if (found === void 0) return event;
47249
- outcomes.delete(event.eventKey);
47250
- return { ...event, step: found.outcome.nodeName, outcome: true };
47249
+ endings.delete(event.eventKey);
47250
+ return { ...event, step: found.ending.nodeName };
47251
47251
  });
47252
- const unfired = [...outcomes.values()].map(
47253
- ({ slug, outcome }) => ({
47254
- eventKey: outcome.eventKey,
47252
+ const unfired = [...endings.values()].map(({ slug, ending }) => {
47253
+ const countedAs = counted.get(ending.eventKey);
47254
+ return {
47255
+ eventKey: ending.eventKey,
47255
47256
  origin: "site",
47256
47257
  flowSlug: slug,
47257
- ...outcome.nodeType === null ? {} : { nodeType: outcome.nodeType },
47258
+ ...ending.nodeType === null ? {} : { nodeType: ending.nodeType },
47258
47259
  events: 0,
47259
47260
  sessions: 0,
47260
- step: outcome.nodeName,
47261
+ step: ending.nodeName,
47261
47262
  seen: false,
47262
- outcome: true
47263
- })
47264
- );
47265
- return [...marked, ...unfired];
47263
+ ...countedAs === void 0 ? {} : { countedAs }
47264
+ };
47265
+ });
47266
+ return [...known, ...unfired];
47266
47267
  }
47267
- function withFormMoments(moments, forms) {
47268
- return { ...moments, events: formMoments(moments.events, forms) };
47268
+ function withFormMoments(moments, forms, counted) {
47269
+ return { ...moments, events: formMoments(moments.events, forms, counted) };
47269
47270
  }
47270
47271
  function formsNamedHere(forms) {
47271
- return forms.filter((form) => suggestedEnding(flowEventKeys(form.tree, form.slug)) !== null).map((form) => form.slug);
47272
+ return forms.filter((form) => flowEventKeys(form.tree, form.slug).length > 0).map((form) => form.slug);
47272
47273
  }
47273
- function unplacedFormHints(steps, forms) {
47274
+ function unplacedFormHints(steps, forms, counted) {
47274
47275
  const onAStep = new Set(
47275
47276
  steps.flatMap((step) => (step.triggers ?? []).flatMap((trigger) => trigger.eventKey ? [trigger.eventKey] : []))
47276
47277
  );
47277
47278
  const unplaced = [];
47278
47279
  for (const form of forms) {
47279
47280
  const endings = flowEventKeys(form.tree, form.slug);
47280
- if (endings.some((ending) => onAStep.has(ending.eventKey))) continue;
47281
- const outcome = suggestedEnding(endings);
47282
- if (outcome !== null) unplaced.push(`${form.slug} \u2192 \`${outcome.eventKey}\` ("${outcome.nodeName}")`);
47281
+ if (endings.length === 0 || endings.some((ending) => onAStep.has(ending.eventKey))) continue;
47282
+ const countedOn = endings.filter((ending) => counted.has(ending.eventKey));
47283
+ unplaced.push(
47284
+ countedOn.length === 0 ? form.slug : `${form.slug} (counted as ${countedOn.map((ending) => `"${counted.get(ending.eventKey)}" on \`${ending.eventKey}\``).join(", ")})`
47285
+ );
47283
47286
  }
47284
47287
  if (unplaced.length === 0) return [];
47285
- const shown = unplaced.slice(0, MAX_NAMED_FORMS);
47286
- const rest = unplaced.length - shown.length;
47287
47288
  return [
47288
- `${unplaced.length} Form(s) in this workspace reach no funnel step, so they move nobody along it however many people finish them: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. Place one with \`baker funnel --add-trigger <step> --event <key>\`, which adds that one way in and leaves the rest of the step alone (\`baker funnel\` with no flags lists the step keys). If you also count a Form as a page conversion, count THAT SAME key, or the two numbers disagree for ever. A Form's other endings \u2014 a booking widget mid-flow, an embed's own signals \u2014 are listed by \`baker analytics conversions --flow <slug>\`.`
47289
+ `${unplaced.length} Form(s) in this workspace reach no funnel step, so they move nobody along it however many people finish them: ${unplaced.join("; ")}. Which ending moves somebody to which step is your call: \`baker funnel --moments\` lists every Form's endings by the step each happens on, and when the step names do not make it obvious \u2014 a Form that branches into a rejection and a confirmation \u2014 ask the person which one means a lead or a customer. Place it with \`baker funnel --add-trigger <step> --event <key>\`. A Form already counted as a page conversion goes on the funnel by THAT SAME key, or the two numbers disagree for ever.`
47289
47290
  ];
47290
47291
  }
47291
47292
 
@@ -47664,6 +47665,13 @@ var funnelResponseSchema = z44.object({
47664
47665
  * one", given in the same turn.
47665
47666
  */
47666
47667
  removed: z44.array(z44.string()).optional(),
47668
+ /**
47669
+ * What this company counts as page conversions, this chat's staged counts
47670
+ * included, by event key and name. Here so a Form's ending can say it is
47671
+ * already counted — the agent choosing a step's way in should use that
47672
+ * same key — and outside `moments` because the plain read's hints say it too.
47673
+ */
47674
+ counted: z44.array(z44.object({ eventKey: z44.string(), name: z44.string() })).optional(),
47667
47675
  /** Everything a trigger can name, when asked for with `moments`. */
47668
47676
  moments: z44.object({
47669
47677
  events: z44.array(
@@ -47684,16 +47692,6 @@ var funnelResponseSchema = z44.object({
47684
47692
  * itself, which is the only place the name is known.
47685
47693
  */
47686
47694
  step: z44.string().optional(),
47687
- /**
47688
- * `true` on the ONE ending of a Form that is its outcome — the
47689
- * booking on a Form that books, the submit on a Form that submits.
47690
- * A Form raises several events and only one of them is the point;
47691
- * without this the agent is handed five equal-looking keys and the
47692
- * lifecycle noise a widget raises about itself sits among them.
47693
- * Same rule as `baker analytics conversions` recommends, so the
47694
- * conversion and the funnel step land on the same event.
47695
- */
47696
- outcome: z44.boolean().optional(),
47697
47695
  /**
47698
47696
  * `false` on an ending a Form raises natively but nobody has fired
47699
47697
  * yet — derived from the Form, offered at zero, exactly as a
@@ -47944,7 +47942,7 @@ var args3 = {
47944
47942
  },
47945
47943
  event: {
47946
47944
  type: "string",
47947
- description: "The event key `--add-trigger` or `--remove-trigger` acts on \u2014 an `events[].eventKey` from `--moments`, never invented. A Form's own outcome is listed there at zero, marked `outcome: true`, before anybody has filled the Form in",
47945
+ description: "The event key `--add-trigger` or `--remove-trigger` acts on \u2014 an `events[].eventKey` from `--moments`, never invented. Every Form's endings are listed there by the step each happens on, at zero before anybody has filled the Form in; which one means the step is your decision",
47948
47946
  required: false
47949
47947
  },
47950
47948
  "remove-step": {
@@ -47959,7 +47957,7 @@ var args3 = {
47959
47957
  },
47960
47958
  moments: {
47961
47959
  type: "boolean",
47962
- description: "Also return everything a trigger can name, so a trigger is PICKED rather than invented: `events` (every event this company's Forms, pages and systems produced in the window, with volume, the Form it belongs to, `origin` \u2014 `site` for a page or Form, `systems` for the client's own server, `both` \u2014 and `countedAs` when the company also counts it as a page conversion, which a trigger does not need; with **each Form's OUTCOME in this workspace marked** `outcome: true` and the `step` it happens on \u2014 a Form nobody has filled in yet gets that row at zero, `seen: false`, so a Form built minutes ago is pickable with nothing set up first. It is the booking on a Form that books, the submit on a Form that submits, and it is the same ending `baker analytics conversions` recommends counting, so the step and the conversion land on the same event. One marked row and not all seven a Form raises: the rest are rarely what a step wants, and `baker analytics conversions --flow <slug>` lists every ending of one Form, fired or not), `sources` (the client's own systems that post events to Baker, by the `page:<name>` key each raises, pickable before its first send arrives) and `landings` (its pages, by the id a `landing` trigger takes). Use this before writing any `triggers`: a key typed from memory saves, publishes and counts nobody forever. A Form's events need NOTHING set up first \u2014 no Source, no conversion: a Baker page raises them by itself",
47960
+ description: "Also return everything a trigger can name, so a trigger is PICKED rather than invented: `events` (every event this company's Forms, pages and systems produced in the window, with volume, the Form it belongs to, `origin` \u2014 `site` for a page or Form, `systems` for the client's own server, `both` \u2014 and `countedAs` when the company also counts it as a page conversion, which a trigger does not need; PLUS **every workspace Form's endings** \u2014 each step's completion and its `submit:` \u2014 with the `step` each happens on, at zero and `seen: false` until somebody fires one, so a Form built minutes ago is pickable with nothing set up first, and `countedAs` when the company already counts it. Nothing is marked as the outcome: which ending means a step is YOUR decision \u2014 read the step names, and ask the person when they do not make it obvious (a Form that branches into \u201CNot a fit\u201D and \u201CQuote confirmed\u201D). If the Form is already counted, use that same key, so the step and the conversion agree. `baker analytics conversions --flow <slug>` also lists a widget's housekeeping), `sources` (the client's own systems that post events to Baker, by the `page:<name>` key each raises, pickable before its first send arrives) and `landings` (its pages, by the id a `landing` trigger takes). Use this before writing any `triggers`: a key typed from memory saves, publishes and counts nobody forever. A Form's events need NOTHING set up first \u2014 no Source, no conversion: a Baker page raises them by itself",
47963
47961
  required: false
47964
47962
  },
47965
47963
  days: {
@@ -47994,8 +47992,9 @@ var funnelCommand2 = defineCommand129({
47994
47992
  ...namedByCaller.length === 0 ? {} : { namedByCaller }
47995
47993
  };
47996
47994
  const response = await apiPost("/api/funnel", request);
47997
- const data = response.data.moments === void 0 ? response.data : { ...response.data, moments: withFormMoments(response.data.moments, forms) };
47998
- const hints2 = [...response.hints ?? [], ...unplacedFormHints(response.data.steps ?? [], forms)];
47995
+ const counted = new Map((response.data.counted ?? []).map((row) => [row.eventKey, row.name]));
47996
+ const data = response.data.moments === void 0 ? response.data : { ...response.data, moments: withFormMoments(response.data.moments, forms, counted) };
47997
+ const hints2 = [...response.hints ?? [], ...unplacedFormHints(response.data.steps ?? [], forms, counted)];
47999
47998
  writeJsonEnvelope({
48000
47999
  ok: true,
48001
48000
  data,