@koda-sl/baker-cli 0.249.0-dev.e7a1a227e → 0.250.0-dev.8313698e6
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
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
ulid,
|
|
59
59
|
validateCanvasDeep,
|
|
60
60
|
ytDlpBlockSignal
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-77OP3PDC.js";
|
|
62
62
|
import {
|
|
63
63
|
csvOrJson,
|
|
64
64
|
daysAgoIso,
|
|
@@ -19266,6 +19266,14 @@ var CTA_TYPES2 = [
|
|
|
19266
19266
|
"BUY_TICKETS",
|
|
19267
19267
|
"NO_BUTTON"
|
|
19268
19268
|
];
|
|
19269
|
+
var LEAD_FORM_CTA_TYPES = [
|
|
19270
|
+
"APPLY_NOW",
|
|
19271
|
+
"DOWNLOAD",
|
|
19272
|
+
"GET_QUOTE",
|
|
19273
|
+
"LEARN_MORE",
|
|
19274
|
+
"SIGN_UP",
|
|
19275
|
+
"SUBSCRIBE"
|
|
19276
|
+
];
|
|
19269
19277
|
var AD_FORMATS2 = ["SINGLE_IMAGE", "CAROUSEL", "SINGLE_VIDEO"];
|
|
19270
19278
|
var ENROLL_STATUSES = ["OPT_IN", "OPT_OUT"];
|
|
19271
19279
|
var STAGEABLE_CREATE_STATUSES3 = ["ACTIVE", "PAUSED"];
|
|
@@ -19688,7 +19696,23 @@ var descriptionSchema = z25.string().min(1).max(META_LIMITS.creative.description
|
|
|
19688
19696
|
var callToActionSchema = z25.object({
|
|
19689
19697
|
type: z25.enum(CTA_TYPES2),
|
|
19690
19698
|
/** Overrides the base link for the CTA button; defaults to the ad's link. */
|
|
19691
|
-
link: httpsUrlSchema3.optional()
|
|
19699
|
+
link: httpsUrlSchema3.optional(),
|
|
19700
|
+
/**
|
|
19701
|
+
* The instant form the button opens — Meta's `call_to_action.value.lead_gen_form_id`.
|
|
19702
|
+
* This is the ONLY place an instant form is named: the ad set says leads open
|
|
19703
|
+
* on the ad (`destination_type: ON_AD`), and the creative says which form.
|
|
19704
|
+
* Without it Meta refuses every ad in that ad set with subcode 3390001,
|
|
19705
|
+
* "Choose or create an instant form for your leads campaign".
|
|
19706
|
+
*/
|
|
19707
|
+
lead_gen_form_id: z25.string().regex(NUMERIC_ID_REGEX3).optional()
|
|
19708
|
+
}).superRefine((p, ctx) => {
|
|
19709
|
+
if (p.lead_gen_form_id && !LEAD_FORM_CTA_TYPES.some((allowed) => allowed === p.type)) {
|
|
19710
|
+
ctx.addIssue({
|
|
19711
|
+
code: "custom",
|
|
19712
|
+
path: ["type"],
|
|
19713
|
+
message: `${p.type} cannot open an instant form \u2014 Meta refuses it (subcode 1856030). Use one of ${LEAD_FORM_CTA_TYPES.join(", ")}, or drop lead_gen_form_id if this ad should go to the website.`
|
|
19714
|
+
});
|
|
19715
|
+
}
|
|
19692
19716
|
});
|
|
19693
19717
|
var creativeEnhancementsSchema = z25.object({
|
|
19694
19718
|
standardEnhancements: z25.enum(ENROLL_STATUSES).optional(),
|
|
@@ -19786,6 +19810,19 @@ var carouselCreativeSchema2 = z25.object({
|
|
|
19786
19810
|
link: httpsUrlSchema3.optional(),
|
|
19787
19811
|
call_to_action: callToActionSchema.optional(),
|
|
19788
19812
|
cards: z25.array(carouselCardSchema).min(META_LIMITS.creative.carouselCardsMin).max(META_LIMITS.creative.carouselCardsMax)
|
|
19813
|
+
}).superRefine((p, ctx) => {
|
|
19814
|
+
const forms = new Set(
|
|
19815
|
+
[p.call_to_action?.lead_gen_form_id, ...p.cards.map((card) => card.call_to_action?.lead_gen_form_id)].filter(
|
|
19816
|
+
Boolean
|
|
19817
|
+
)
|
|
19818
|
+
);
|
|
19819
|
+
if (forms.size > 1) {
|
|
19820
|
+
ctx.addIssue({
|
|
19821
|
+
code: "custom",
|
|
19822
|
+
path: ["cards"],
|
|
19823
|
+
message: `every card of a carousel must open the SAME instant form \u2014 this one names ${[...forms].join(" and ")}`
|
|
19824
|
+
});
|
|
19825
|
+
}
|
|
19789
19826
|
});
|
|
19790
19827
|
var dynamicImageSchema = z25.object({ ...imageMediaFields }).refine((p) => countImageRefs(p) === 1, "each dynamic image needs exactly one reference");
|
|
19791
19828
|
var dynamicVideoSchema = z25.object({
|
|
@@ -20292,7 +20329,10 @@ var adSetWriteArgs = {
|
|
|
20292
20329
|
"daily-budget": { type: "string", description: "Daily budget (ABO; omit if the campaign uses CBO)" },
|
|
20293
20330
|
"lifetime-budget": { type: "string", description: "Lifetime budget (needs --end)" },
|
|
20294
20331
|
"bid-amount": { type: "string", description: "Bid amount (required for COST_CAP / bid-cap strategies)" },
|
|
20295
|
-
"bid-strategy": {
|
|
20332
|
+
"bid-strategy": {
|
|
20333
|
+
type: "string",
|
|
20334
|
+
description: "LOWEST_COST_WITHOUT_CAP (auto-bid) | LOWEST_COST_WITH_BID_CAP | COST_CAP \u2014 needs --bid-amount. Omit and Baker sets auto-bid; never leave it to Meta, whose default is a bid cap that then fails without an amount."
|
|
20335
|
+
},
|
|
20296
20336
|
currency: { type: "string", description: "3-letter currency (defaults to the account currency)" },
|
|
20297
20337
|
start: { type: "string", description: "Start time" },
|
|
20298
20338
|
end: { type: "string", description: "End time" },
|
|
@@ -20359,7 +20399,7 @@ function creativePayloadFromFlags(args) {
|
|
|
20359
20399
|
headline: args.headline,
|
|
20360
20400
|
description: args.description,
|
|
20361
20401
|
caption: args.caption,
|
|
20362
|
-
call_to_action:
|
|
20402
|
+
call_to_action: creativeCta(args),
|
|
20363
20403
|
imageHash: args["image-hash"],
|
|
20364
20404
|
imageRef: args["image-ref"],
|
|
20365
20405
|
videoId: args["video-id"],
|
|
@@ -20369,6 +20409,26 @@ function creativePayloadFromFlags(args) {
|
|
|
20369
20409
|
enhancements
|
|
20370
20410
|
};
|
|
20371
20411
|
}
|
|
20412
|
+
function ctaPatchFromFlags(args) {
|
|
20413
|
+
const patch = {
|
|
20414
|
+
...args.cta ? { type: String(upper(args.cta)) } : {},
|
|
20415
|
+
...args["lead-form"] === void 0 ? {} : { lead_gen_form_id: String(args["lead-form"]) }
|
|
20416
|
+
};
|
|
20417
|
+
return Object.keys(patch).length > 0 ? patch : void 0;
|
|
20418
|
+
}
|
|
20419
|
+
function creativeCta(args) {
|
|
20420
|
+
const patch = ctaPatchFromFlags(args);
|
|
20421
|
+
return patch ? { type: "SIGN_UP", ...patch } : void 0;
|
|
20422
|
+
}
|
|
20423
|
+
function creativeContentPatch2(file, args) {
|
|
20424
|
+
const cta = ctaPatchFromFlags(args);
|
|
20425
|
+
if (!cta) {
|
|
20426
|
+
return void 0;
|
|
20427
|
+
}
|
|
20428
|
+
const fileContent = file.content ?? {};
|
|
20429
|
+
const fileCta = fileContent.call_to_action ?? {};
|
|
20430
|
+
return { ...fileContent, call_to_action: { ...fileCta, ...cta } };
|
|
20431
|
+
}
|
|
20372
20432
|
function parseEnroll(value) {
|
|
20373
20433
|
const raw = String(value).toLowerCase();
|
|
20374
20434
|
if (raw === "on" || raw === "true" || raw === "opt_in") return "OPT_IN";
|
|
@@ -20395,6 +20455,10 @@ Example: baker ads meta creatives create --page 555 --message "Save now" --link
|
|
|
20395
20455
|
description: { type: "string", description: "Description" },
|
|
20396
20456
|
caption: { type: "string", description: "Display URL / caption" },
|
|
20397
20457
|
cta: { type: "string", description: "Call-to-action type (SHOP_NOW|LEARN_MORE|SIGN_UP|\u2026)" },
|
|
20458
|
+
"lead-form": {
|
|
20459
|
+
type: "string",
|
|
20460
|
+
description: "Instant form id the button opens \u2014 REQUIRED for an ad set that collects leads on the ad. List them with `baker ads meta lead-forms`. With a form, --cta must be APPLY_NOW|DOWNLOAD|GET_QUOTE|LEARN_MORE|SIGN_UP|SUBSCRIBE"
|
|
20461
|
+
},
|
|
20398
20462
|
"image-hash": { type: "string", description: "Meta ad-image hash (already uploaded)" },
|
|
20399
20463
|
"image-ref": { type: "string", description: "meta_temp_* ref of a staged media upload" },
|
|
20400
20464
|
"video-id": { type: "string", description: "Meta video id (already uploaded)" },
|
|
@@ -20428,6 +20492,11 @@ Amending a staged (meta_temp_*) creative merges fields into the create. Example:
|
|
|
20428
20492
|
...accountArgs2,
|
|
20429
20493
|
name: { type: "string", description: "New name" },
|
|
20430
20494
|
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED" },
|
|
20495
|
+
cta: { type: "string", description: "Call-to-action type (staged creatives only)" },
|
|
20496
|
+
"lead-form": {
|
|
20497
|
+
type: "string",
|
|
20498
|
+
description: "Instant form id the button opens (staged creatives only) \u2014 `baker ads meta lead-forms` lists them"
|
|
20499
|
+
},
|
|
20431
20500
|
file: {
|
|
20432
20501
|
type: "string",
|
|
20433
20502
|
description: "JSON file with fields to change (content only merges into a staged creative)"
|
|
@@ -20435,7 +20504,15 @@ Amending a staged (meta_temp_*) creative merges fields into the create. Example:
|
|
|
20435
20504
|
},
|
|
20436
20505
|
run: async ({ args }) => {
|
|
20437
20506
|
const accountId = bareAccountId2(args);
|
|
20438
|
-
const
|
|
20507
|
+
const file = loadJsonFileArg3(args.file);
|
|
20508
|
+
const payload = mergePayload3(file, {
|
|
20509
|
+
name: args.name,
|
|
20510
|
+
status: upper(args.status),
|
|
20511
|
+
// A creative's content is immutable on Meta, so this only ever lands on a
|
|
20512
|
+
// creative still staged in this chat — which is exactly the fix path when
|
|
20513
|
+
// staging an ad refuses it for naming no instant form.
|
|
20514
|
+
content: creativeContentPatch2(file, args)
|
|
20515
|
+
});
|
|
20439
20516
|
await stageOp2({
|
|
20440
20517
|
kind: "adCreative.update",
|
|
20441
20518
|
accountId,
|
|
@@ -23625,7 +23702,7 @@ var ANALYTICS_PRESET_INFO = [
|
|
|
23625
23702
|
},
|
|
23626
23703
|
{
|
|
23627
23704
|
name: "flow",
|
|
23628
|
-
description: "One Form in depth: conversions counted at the node that produced them, step-to-step paths for a branching Form, the per-step table, every trigger raised, and each marked conversion by name. Read convertedSessions rather than submits \u2014 a Form whose scheduling widget sits mid-flow never emits a submit and reports zero on every other report. An empty `conversions` array means nobody has marked what this Form is for, so the numbers rest on a built-in guess; mark it in the Forms builder or with `conversions: [{ triggerId }]` on the node.",
|
|
23705
|
+
description: "One Form in depth: conversions counted at the node that produced them, step-to-step paths for a branching Form, the per-step table, every trigger raised, and each marked conversion by name. Read convertedSessions rather than submits \u2014 a Form whose scheduling widget sits mid-flow never emits a submit and reports zero on every other report. An empty `conversions` array means nobody has marked what this Form is for, so the numbers rest on a built-in guess; mark it in the Forms builder or with `conversions: [{ triggerId }]` on the node. `flowDestinations` is the other half and is evidence rather than a count: which Forms demonstrably handed something over in this window. Read it against the scaffold's `flow-no-destination` warning before acting on one \u2014 the stored picture of a Form is only rewritten when it is published, so a destination added since then shows there and not in the tree.",
|
|
23629
23706
|
playbook: "flow-builder \u2014 fix the branch or the step that loses people"
|
|
23630
23707
|
},
|
|
23631
23708
|
{
|
|
@@ -24104,7 +24181,7 @@ var funnelCommand = presetCommand({
|
|
|
24104
24181
|
var flowCommand = presetCommand({
|
|
24105
24182
|
name: "flow",
|
|
24106
24183
|
preset: "flow",
|
|
24107
|
-
description: "One Form in depth: how many people it converted, where they went between steps, the per-step table, and every trigger it raised. Every count is DISTINCT VISITS, so quote shares against `funnels[].visits` \u2014 the visits that opened the Form \u2014 and never against `starts`, which is only the visits that touched it. The gap between the two is people who read the Form and left, and on most Forms it is the largest loss there is. Read `flowSummary.convertedSessions`, not `submits` \u2014 a Form that books a call on a scheduling node in the MIDDLE of the flow never emits a submit, so it reports zero submits and every one of its real bookings as conversions. `flowPaths` is the flow as edges between steps, bracketed by __flow_start__, __flow_converted__ and __flow_exit__, which is how a branching Form is read at all: a per-step table cannot say that of the people who left step two, forty went to the booking branch and ninety went nowhere.",
|
|
24184
|
+
description: "One Form in depth: how many people it converted, where they went between steps, the per-step table, and every trigger it raised. Every count is DISTINCT VISITS, so quote shares against `funnels[].visits` \u2014 the visits that opened the Form \u2014 and never against `starts`, which is only the visits that touched it. The gap between the two is people who read the Form and left, and on most Forms it is the largest loss there is. Read `flowSummary.convertedSessions`, not `submits` \u2014 a Form that books a call on a scheduling node in the MIDDLE of the flow never emits a submit, so it reports zero submits and every one of its real bookings as conversions. `flowPaths` is the flow as edges between steps, bracketed by __flow_start__, __flow_converted__ and __flow_exit__, which is how a branching Form is read at all: a per-step table cannot say that of the people who left step two, forty went to the booking branch and ninety went nowhere. `flowDestinations` is the other half and is evidence rather than a count: which Forms demonstrably handed something over in this window. Read it against the scaffold's `flow-no-destination` warning before acting on one \u2014 the stored picture of a Form is only rewritten when it is published, so a destination added since then shows there and not in the tree.",
|
|
24108
24185
|
extraArgs: { flow: { type: "string", description: "Form slug (default: every Form)", required: false } },
|
|
24109
24186
|
resolve: (args) => ({ preset: "flow", flowSlug: args.flow ? String(args.flow) : void 0 })
|
|
24110
24187
|
});
|
|
@@ -24446,8 +24523,7 @@ import { defineCommand as defineCommand94 } from "citty";
|
|
|
24446
24523
|
import { defineCommand as defineCommand89 } from "citty";
|
|
24447
24524
|
|
|
24448
24525
|
// src/commands/avatars/casting.ts
|
|
24449
|
-
var
|
|
24450
|
-
var VERBATIM_RULE = "When you do write the description yourself \u2014 a canvas node, a landing image, anywhere `--avatar` does not exist \u2014 copy `subjectDescription` VERBATIM, word for word. Re-phrasing it per generation is the other reason a face drifts across a set.";
|
|
24526
|
+
var VERBATIM_RULE = "Copy `subjectDescription` into the prompt VERBATIM, word for word. Re-phrasing it per generation is the other reason a face drifts across a set \u2014 treat it as an identifier that happens to read as prose.";
|
|
24451
24527
|
var VIDEO_ROUTING = "In video, a photoreal presenter renders on `google/veo-3.1` (or `google/veo-3.1-fast`), never `bytedance/seedance-2.0` \u2014 it refuses photoreal human faces, AI-generated ones included. Scaffolding a video creative: `baker canvas scaffold-video \u2026 --real-face`.";
|
|
24452
24528
|
function castingHints(avatar) {
|
|
24453
24529
|
if (avatar.status === "generating") {
|
|
@@ -24471,8 +24547,7 @@ function castingHints(avatar) {
|
|
|
24471
24547
|
];
|
|
24472
24548
|
}
|
|
24473
24549
|
return [
|
|
24474
|
-
`Ready to cast
|
|
24475
|
-
CAST_FLAG_RULE,
|
|
24550
|
+
`Ready to cast. Ground every render on the identity sheet, never on a source photo: \`baker images generate "<your scene>" --reference ${avatar.sheetUrl}\`.`,
|
|
24476
24551
|
VERBATIM_RULE,
|
|
24477
24552
|
VIDEO_ROUTING
|
|
24478
24553
|
];
|
|
@@ -24512,19 +24587,10 @@ function rosterHints(roster, statusFilter) {
|
|
|
24512
24587
|
}
|
|
24513
24588
|
return hints;
|
|
24514
24589
|
}
|
|
24515
|
-
function
|
|
24516
|
-
const missing = ["motion", "persona", "speech", "wardrobe", "setting"].filter(
|
|
24517
|
-
(field) => !profile?.[field]?.trim()
|
|
24518
|
-
);
|
|
24519
|
-
if (missing.length === 0) return null;
|
|
24520
|
-
return `MISSING ${missing.map((field) => `--${field}`).join(" ")}. A bare subject description holds the face still and nothing else, so clips of @${handle} come out stiff and generic \u2014 measured, filling these in nearly doubled the movement in the same brief. The user almost certainly told you how this person talks and carries themselves; put it in: \`baker avatars update ${handle} ${missing.map((field) => `--${field} "\u2026"`).join(" ")}\`.`;
|
|
24521
|
-
}
|
|
24522
|
-
function creationHints(created, profile) {
|
|
24523
|
-
const thin = thinProfileHint(profile, created.handle);
|
|
24590
|
+
function creationHints(created) {
|
|
24524
24591
|
const hints = [
|
|
24525
|
-
...thin ? [thin] : [],
|
|
24526
24592
|
...castingHints({ handle: created.handle, status: created.status }),
|
|
24527
|
-
`Once it is ready,
|
|
24593
|
+
`Once it is ready, every render grounds on its identity sheet (\`--reference <sheetUrl>\`) and reuses the subject description you just wrote, word for word. Those two things are what keep the face identical across a set.`
|
|
24528
24594
|
];
|
|
24529
24595
|
if (created.likeness === "licensed") {
|
|
24530
24596
|
hints.push(
|
|
@@ -24794,7 +24860,7 @@ var createCommand2 = defineCommand89({
|
|
|
24794
24860
|
...chatIdFromEnv() ? { chatId: chatIdFromEnv() } : {}
|
|
24795
24861
|
};
|
|
24796
24862
|
const data = await writeAvatars("/api/avatars", body);
|
|
24797
|
-
writeJson({ ok: true, data, hints: creationHints(data
|
|
24863
|
+
writeJson({ ok: true, data, hints: creationHints(data) });
|
|
24798
24864
|
} catch (err) {
|
|
24799
24865
|
failAvatarApi(err, handle);
|
|
24800
24866
|
}
|