@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 +3 -1
- package/dist/cli.js +52 -53
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
47240
|
-
|
|
47241
|
-
|
|
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
|
|
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
|
|
47247
|
-
const found =
|
|
47246
|
+
const known = events.map((event) => {
|
|
47247
|
+
const found = endings.get(event.eventKey);
|
|
47248
47248
|
if (found === void 0) return event;
|
|
47249
|
-
|
|
47250
|
-
return { ...event, step: found.
|
|
47249
|
+
endings.delete(event.eventKey);
|
|
47250
|
+
return { ...event, step: found.ending.nodeName };
|
|
47251
47251
|
});
|
|
47252
|
-
const unfired = [...
|
|
47253
|
-
|
|
47254
|
-
|
|
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
|
-
...
|
|
47258
|
+
...ending.nodeType === null ? {} : { nodeType: ending.nodeType },
|
|
47258
47259
|
events: 0,
|
|
47259
47260
|
sessions: 0,
|
|
47260
|
-
step:
|
|
47261
|
+
step: ending.nodeName,
|
|
47261
47262
|
seen: false,
|
|
47262
|
-
|
|
47263
|
-
}
|
|
47264
|
-
);
|
|
47265
|
-
return [...
|
|
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) =>
|
|
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
|
|
47282
|
-
|
|
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: ${
|
|
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.
|
|
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;
|
|
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
|
|
47998
|
-
const
|
|
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,
|