@koda-sl/baker-cli 0.119.0 → 0.120.0-dev.3bcc79f9c

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
@@ -1,19 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ BackendClient,
3
4
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
4
5
  IMAGE_GENERATE_MODELS,
5
6
  LayerExecutionError,
6
7
  MODEL_REGISTRY,
7
8
  SEEDANCE_DURATIONS,
8
9
  ValidationError,
10
+ collectAssetRefLikes,
9
11
  createEngineFromEnv,
10
12
  defaultRegistry,
11
13
  describeFailureReason,
12
- elementMentionKeywords,
13
14
  generateCatalog,
15
+ isPersistedAssetRef,
16
+ requireCredentialsFromEnv,
14
17
  resolveConcurrency,
18
+ sha256Hex,
19
+ ulid,
15
20
  validateCanvasDeep
16
- } from "./chunk-MF34WJ7M.js";
21
+ } from "./chunk-OCMOQOIJ.js";
17
22
 
18
23
  // src/cli.ts
19
24
  import { defineCommand as defineCommand157, runMain } from "citty";
@@ -152,9 +157,9 @@ async function handleResponse(response) {
152
157
  throw new ApiError("INTERNAL_ERROR", "Failed to parse API response as JSON");
153
158
  }
154
159
  }
155
- async function apiGet(path12, params) {
160
+ async function apiGet(path14, params) {
156
161
  const env = getEnv();
157
- const url = new URL(path12, env.BAKER_API_URL);
162
+ const url = new URL(path14, env.BAKER_API_URL);
158
163
  if (params) {
159
164
  const clean = sanitizeParams(params);
160
165
  for (const [key, value] of Object.entries(clean)) {
@@ -179,12 +184,12 @@ async function apiGet(path12, params) {
179
184
  }
180
185
  return handleResponse(response);
181
186
  }
182
- async function apiPost(path12, body, opts) {
187
+ async function apiPost(path14, body, opts) {
183
188
  const env = getEnv();
184
189
  const timeoutMs = opts?.timeoutMs ?? 6e4;
185
190
  let response;
186
191
  try {
187
- response = await fetchWithRateLimitRetry(new URL(path12, env.BAKER_API_URL).toString(), {
192
+ response = await fetchWithRateLimitRetry(new URL(path14, env.BAKER_API_URL).toString(), {
188
193
  method: "POST",
189
194
  headers: {
190
195
  Authorization: `Bearer ${env.BAKER_API_KEY}`,
@@ -989,58 +994,9 @@ var LINKEDIN_LIMITS = {
989
994
  questionsSoftMax: 4,
990
995
  // playbook: >4 fields tanks completion rate
991
996
  customQuestionsMax: 3,
992
- customQuestionTextMax: 100,
993
- choiceOptionsMin: 2,
994
- choiceOptionsMax: 30,
995
- choiceOptionTextMax: 100,
996
- thankYouMessageMax: 300,
997
- privacyPolicyTextMax: 2e3,
998
- legalDisclaimerMax: 2e3,
999
- consentsMax: 5,
1000
- // Campaign Manager caps disclosure checkboxes at 5
1001
- consentTextMax: 500,
1002
- hiddenFieldsMax: 20,
1003
- hiddenFieldNameMax: 100,
1004
- hiddenFieldValueMax: 2e3
997
+ thankYouMessageMax: 300
1005
998
  }
1006
999
  };
1007
- var LEAD_FORM_PREDEFINED_FIELDS = [
1008
- // Contact
1009
- "FIRST_NAME",
1010
- "LAST_NAME",
1011
- "EMAIL",
1012
- "PHONE_NUMBER",
1013
- "CITY",
1014
- "STATE",
1015
- "COUNTRY",
1016
- "ZIP_CODE",
1017
- "WORK_EMAIL",
1018
- // Work
1019
- "JOB_TITLE",
1020
- "JOB_FUNCTION",
1021
- "SENIORITY",
1022
- "YEARS_OF_EXPERIENCE",
1023
- // Company
1024
- "COMPANY_NAME",
1025
- "COMPANY_SIZE",
1026
- "INDUSTRY",
1027
- // Education
1028
- "DEGREE",
1029
- "FIELD_OF_STUDY",
1030
- "SCHOOL_NAME",
1031
- "START_DATE",
1032
- "GRADUATION_DATE",
1033
- // Demographic
1034
- "GENDER"
1035
- ];
1036
- var LEAD_FORM_QUESTION_TYPES = ["SINGLE_LINE_TEXT", "MULTIPLE_CHOICE"];
1037
- var LEAD_FORM_CONFIRMATION_CTAS = [
1038
- "VIEW_COMPANY_WEBSITE",
1039
- "DOWNLOAD",
1040
- "LEARN_MORE",
1041
- "VIEW_NOW",
1042
- "TRY_NOW"
1043
- ];
1044
1000
  var CAMPAIGN_TYPES = ["SPONSORED_UPDATES", "TEXT_AD", "SPONSORED_INMAILS", "DYNAMIC"];
1045
1001
  var COST_TYPES = ["CPC", "CPM", "CPV"];
1046
1002
  var OBJECTIVE_TYPES = [
@@ -1093,7 +1049,6 @@ var CURRENCY_MINIMUMS = {
1093
1049
  GBP: { dailyBudgetMin: 8, unitCostMin: 2 }
1094
1050
  };
1095
1051
  var DEFAULT_CURRENCY_MINIMUM = { dailyBudgetMin: 10, unitCostMin: 2 };
1096
- var TEMP_REF_PREFIX = "li_temp_";
1097
1052
  var TEMP_REF_REGEX = /^li_temp_[A-Za-z0-9_-]{4,}$/;
1098
1053
  var NUMERIC_ID_REGEX = /^\d+$/;
1099
1054
  var URN_REGEX = /^urn:li:[a-zA-Z]+:.+$/;
@@ -1229,15 +1184,7 @@ var campaignUpdateSchema = z3.object({
1229
1184
  offsiteDeliveryEnabled: z3.boolean().optional(),
1230
1185
  connectedTelevisionOnly: z3.boolean().optional(),
1231
1186
  creativeSelection: z3.enum(CREATIVE_SELECTIONS).optional(),
1232
- status: entityStatusSchema.optional(),
1233
- // Create-only on LinkedIn — a live campaign can't change these in place.
1234
- // Accepted here only so `draft amend` can carry them into a staged CREATE
1235
- // (li_temp_* target, re-validated as `campaignCreateSchema`); stage-time
1236
- // rejects them outright when the update targets a live campaign.
1237
- campaignGroup: campaignFields.campaignGroup.optional(),
1238
- type: campaignFields.type.optional(),
1239
- locale: campaignFields.locale.optional(),
1240
- associatedEntity: campaignFields.associatedEntity
1187
+ status: entityStatusSchema.optional()
1241
1188
  }).superRefine((p, ctx) => {
1242
1189
  if (!Object.values(p).some((val) => val !== void 0)) {
1243
1190
  ctx.addIssue({ code: "custom", message: "update needs at least one field" });
@@ -1452,12 +1399,7 @@ var creativeUpdateSchema = z3.object({
1452
1399
  * live ad's snapshot content (real targets, direct-content formats) and
1453
1400
  * re-validated against the full creative schema. Post-based ads reject it.
1454
1401
  */
1455
- content: z3.record(z3.string(), z3.unknown()).optional(),
1456
- // Re-parenting a creative — LinkedIn has no in-place move-to-campaign API.
1457
- // Accepted here only so `draft amend` can carry it into a staged CREATE
1458
- // (li_temp_* target, re-validated as `creativeCreateSchema`); stage-time
1459
- // rejects it outright when the update targets a live creative.
1460
- campaign: parentRefSchema.optional()
1402
+ content: z3.record(z3.string(), z3.unknown()).optional()
1461
1403
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
1462
1404
  var listRowSchema = z3.record(z3.string(), z3.string());
1463
1405
  var audienceCreateSchema = z3.object({
@@ -1521,101 +1463,21 @@ var conversionUpdateSchema = z3.object({
1521
1463
  attributionType: z3.enum(ATTRIBUTION_TYPES).optional(),
1522
1464
  enabled: z3.boolean().optional()
1523
1465
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
1524
- var LEAD = LINKEDIN_LIMITS.leadForm;
1525
- var leadFormQuestionSchema = z3.object({
1526
- name: z3.string().min(1).max(LEAD.customQuestionTextMax),
1527
- predefinedField: z3.enum(LEAD_FORM_PREDEFINED_FIELDS).optional(),
1528
- questionType: z3.enum(LEAD_FORM_QUESTION_TYPES).optional(),
1529
- options: z3.array(z3.string().min(1).max(LEAD.choiceOptionTextMax)).optional()
1530
- }).superRefine((q, ctx) => {
1531
- if (q.predefinedField && q.questionType) {
1532
- ctx.addIssue({
1533
- code: "custom",
1534
- path: ["questionType"],
1535
- message: "a question is either a predefinedField or a custom questionType, not both"
1536
- });
1537
- }
1538
- if (q.questionType === "MULTIPLE_CHOICE") {
1539
- const count = q.options?.length ?? 0;
1540
- if (count < LEAD.choiceOptionsMin || count > LEAD.choiceOptionsMax) {
1541
- ctx.addIssue({
1542
- code: "custom",
1543
- path: ["options"],
1544
- message: `MULTIPLE_CHOICE needs ${LEAD.choiceOptionsMin}-${LEAD.choiceOptionsMax} options`
1545
- });
1546
- }
1547
- }
1548
- if (q.questionType !== "MULTIPLE_CHOICE" && q.options?.length) {
1549
- ctx.addIssue({
1550
- code: "custom",
1551
- path: ["options"],
1552
- message: "options are only valid on MULTIPLE_CHOICE questions"
1553
- });
1554
- }
1555
- });
1556
- var leadFormConsentSchema = z3.object({
1557
- text: z3.string().min(1).max(LEAD.consentTextMax),
1558
- required: z3.boolean().default(false)
1559
- });
1560
- var leadFormHiddenFieldSchema = z3.object({
1561
- name: z3.string().min(1).max(LEAD.hiddenFieldNameMax),
1562
- value: z3.string().max(LEAD.hiddenFieldValueMax)
1563
- });
1564
- var leadFormThankYouSchema = z3.object({
1565
- message: z3.string().max(LEAD.thankYouMessageMax).optional(),
1566
- cta: z3.enum(LEAD_FORM_CONFIRMATION_CTAS).optional(),
1567
- landingUrl: httpsUrlSchema.optional(),
1568
- /** "Book an appointment" — a Calendly / Chili Piper scheduling link, in place of a landing page. */
1569
- appointmentUrl: httpsUrlSchema.optional()
1570
- }).superRefine((t, ctx) => {
1571
- if (t.landingUrl && t.appointmentUrl) {
1572
- ctx.addIssue({
1573
- code: "custom",
1574
- path: ["appointmentUrl"],
1575
- message: "link the confirmation to a landingUrl OR an appointmentUrl, not both"
1576
- });
1577
- }
1578
- if (t.cta && !t.landingUrl) {
1579
- ctx.addIssue({ code: "custom", path: ["cta"], message: "a confirmation cta needs a landingUrl" });
1580
- }
1581
- });
1582
- var leadFormFields = {
1583
- name: z3.string().min(1).max(LEAD.nameMax),
1584
- headline: z3.string().min(1).max(LEAD.headlineMax),
1585
- description: z3.string().max(LEAD.descriptionMax).optional(),
1586
- /** Form banner. Baker library id (uploaded at publish) or an image URN already on LinkedIn. */
1587
- formImageId: bakerMediaIdSchema.optional(),
1588
- formImageUrn: z3.string().regex(IMAGE_URN_REGEX).optional(),
1589
- /** Form language, e.g. { country: "US", language: "en" }. Defaults to the account locale on LinkedIn. */
1590
- locale: z3.object({ country: z3.string().length(2), language: z3.string().length(2) }).optional(),
1466
+ var leadFormCreateSchema = z3.object({
1467
+ name: z3.string().min(1).max(LINKEDIN_LIMITS.leadForm.nameMax),
1468
+ headline: z3.string().min(1).max(LINKEDIN_LIMITS.leadForm.headlineMax),
1469
+ description: z3.string().max(LINKEDIN_LIMITS.leadForm.descriptionMax).optional(),
1591
1470
  privacyPolicyUrl: httpsUrlSchema,
1592
- privacyPolicyText: z3.string().max(LEAD.privacyPolicyTextMax).optional(),
1593
- questions: z3.array(leadFormQuestionSchema).min(1).max(LEAD.questionsMax),
1594
- consents: z3.array(leadFormConsentSchema).max(LEAD.consentsMax).optional(),
1595
- hiddenFields: z3.array(leadFormHiddenFieldSchema).max(LEAD.hiddenFieldsMax).optional(),
1596
- thankYou: leadFormThankYouSchema.optional(),
1597
- legalDisclaimer: z3.string().max(LEAD.legalDisclaimerMax).optional(),
1471
+ questions: z3.array(z3.object({ name: z3.string().min(1), predefinedField: z3.string().optional() })).min(1).max(LINKEDIN_LIMITS.leadForm.questionsMax),
1472
+ thankYou: z3.object({
1473
+ message: z3.string().max(LINKEDIN_LIMITS.leadForm.thankYouMessageMax).optional(),
1474
+ landingUrl: httpsUrlSchema.optional()
1475
+ }).optional(),
1476
+ legalDisclaimer: z3.string().optional(),
1598
1477
  raw: z3.record(z3.string(), z3.unknown()).optional()
1599
- };
1600
- var leadFormCreateSchema = z3.object(leadFormFields).superRefine((p, ctx) => {
1601
- if (p.formImageId && p.formImageUrn) {
1602
- ctx.addIssue({
1603
- code: "custom",
1604
- path: ["formImageId"],
1605
- message: "provide at most one of formImageId (Baker library) or formImageUrn (already on LinkedIn)"
1606
- });
1607
- }
1608
- const customCount = p.questions.filter((q) => q.questionType).length;
1609
- if (customCount > LEAD.customQuestionsMax) {
1610
- ctx.addIssue({
1611
- code: "custom",
1612
- path: ["questions"],
1613
- message: `at most ${LEAD.customQuestionsMax} custom questions (LinkedIn limit); the rest must be predefined profile fields`
1614
- });
1615
- }
1616
1478
  });
1617
1479
  var leadFormUpdateSchema = z3.object({
1618
- name: leadFormFields.name.optional(),
1480
+ name: leadFormCreateSchema.shape.name.optional(),
1619
1481
  raw: z3.record(z3.string(), z3.unknown()).optional()
1620
1482
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
1621
1483
  var LINKEDIN_DRAFT_OP_KINDS = [
@@ -1681,12 +1543,6 @@ var linkedinDraftStageResponseSchema = z4.object({
1681
1543
  /** True when the op amended an already-staged op in place instead of appending a new one. */
1682
1544
  amended: z4.boolean().optional()
1683
1545
  });
1684
- var linkedinDraftAmendRequestSchema = z4.object({
1685
- chatId: z4.string(),
1686
- /** The staged op's ref, or (for staged updates) the target id/URN — matched like `findAmendTarget`. */
1687
- ref: z4.string(),
1688
- patch: z4.record(z4.string(), z4.unknown())
1689
- });
1690
1546
  var linkedinDraftOpViewSchema = z4.object({
1691
1547
  ref: z4.string(),
1692
1548
  kind: linkedinDraftOpKindSchema,
@@ -1697,45 +1553,14 @@ var linkedinDraftOpViewSchema = z4.object({
1697
1553
  stagedAt: z4.number(),
1698
1554
  result: linkedinDraftOpResultSchema.optional()
1699
1555
  });
1700
- var linkedinDraftShowRequestSchema = z4.object({
1701
- chatId: z4.string(),
1702
- ref: z4.string()
1703
- });
1704
- var linkedinDraftShowResponseSchema = z4.object({
1705
- op: linkedinDraftOpViewSchema.extend({
1706
- payload: z4.unknown().optional(),
1707
- warnings: z4.array(z4.string()).optional(),
1708
- annotations: z4.unknown().optional()
1709
- })
1710
- });
1711
1556
  var linkedinDraftListRequestSchema = z4.object({
1712
1557
  chatId: z4.string()
1713
1558
  });
1714
- var linkedinDraftStatusCollectionSchema = z4.object({
1715
- label: z4.string(),
1716
- added: z4.number(),
1717
- removed: z4.number(),
1718
- existing: z4.number()
1719
- });
1720
- var linkedinDraftChangeOperationSchema = z4.enum(["create", "update", "pause", "resume", "archive"]);
1721
- var linkedinDraftStatusNodeSchema = z4.lazy(
1722
- () => z4.object({
1723
- entity: z4.string(),
1724
- name: z4.string(),
1725
- operation: linkedinDraftChangeOperationSchema.optional(),
1726
- existing: z4.boolean(),
1727
- collections: z4.array(linkedinDraftStatusCollectionSchema),
1728
- children: z4.array(linkedinDraftStatusNodeSchema),
1729
- warnings: z4.array(z4.string()).optional()
1730
- })
1731
- );
1732
1559
  var linkedinDraftListResponseSchema = z4.object({
1733
1560
  status: z4.enum(["active", "publishing", "applied", "discarded", "none"]),
1734
1561
  mode: linkedinWriteModeSchema,
1735
1562
  count: z4.number(),
1736
- ops: z4.array(linkedinDraftOpViewSchema),
1737
- /** Grouped Campaign group ▸ Campaign ▸ Creative tree for the readable CLI status view. */
1738
- tree: z4.array(linkedinDraftStatusNodeSchema).optional()
1563
+ ops: z4.array(linkedinDraftOpViewSchema)
1739
1564
  });
1740
1565
  var linkedinDraftRemoveRequestSchema = z4.object({
1741
1566
  chatId: z4.string(),
@@ -3056,31 +2881,31 @@ function cachePath(category, key) {
3056
2881
  return join2(dir, `${hashKey(key)}.json`);
3057
2882
  }
3058
2883
  function cacheGet(category, key) {
3059
- const path12 = cachePath(category, key);
3060
- if (!existsSync2(path12)) {
2884
+ const path14 = cachePath(category, key);
2885
+ if (!existsSync2(path14)) {
3061
2886
  return null;
3062
2887
  }
3063
2888
  try {
3064
- const raw = readFileSync2(path12, "utf-8");
2889
+ const raw = readFileSync2(path14, "utf-8");
3065
2890
  const entry = JSON.parse(raw);
3066
2891
  if (entry.expiresAt < Date.now()) {
3067
- rmSync(path12, { force: true });
2892
+ rmSync(path14, { force: true });
3068
2893
  return null;
3069
2894
  }
3070
2895
  return entry;
3071
2896
  } catch {
3072
- rmSync(path12, { force: true });
2897
+ rmSync(path14, { force: true });
3073
2898
  return null;
3074
2899
  }
3075
2900
  }
3076
2901
  function cacheSet(category, key, data, ttlMs, fields) {
3077
- const path12 = cachePath(category, key);
2902
+ const path14 = cachePath(category, key);
3078
2903
  const entry = {
3079
2904
  expiresAt: Date.now() + ttlMs,
3080
2905
  data,
3081
2906
  fields
3082
2907
  };
3083
- writeFileSync(path12, JSON.stringify(entry), "utf-8");
2908
+ writeFileSync(path14, JSON.stringify(entry), "utf-8");
3084
2909
  }
3085
2910
  var HOUR = 60 * 60 * 1e3;
3086
2911
  var MINUTE = 60 * 1e3;
@@ -4116,19 +3941,6 @@ var GOOGLE_ADS_LIMITS = {
4116
3941
  },
4117
3942
  label: {
4118
3943
  nameMax: 255
4119
- },
4120
- assetGroup: {
4121
- nameMax: 255,
4122
- headlinesMin: 3,
4123
- headlinesMax: 15,
4124
- headlineTextMax: 30,
4125
- longHeadlinesMin: 1,
4126
- longHeadlinesMax: 5,
4127
- longHeadlineTextMax: 90,
4128
- descriptionsMin: 2,
4129
- descriptionsMax: 5,
4130
- descriptionTextMax: 90,
4131
- businessNameMax: 25
4132
3944
  }
4133
3945
  };
4134
3946
  var KEYWORD_MATCH_TYPES = ["EXACT", "PHRASE", "BROAD"];
@@ -4428,6 +4240,15 @@ var responsiveDisplayAdSchema = z8.object({
4428
4240
  logoImageAssets: z8.array(refSchema).optional(),
4429
4241
  finalUrls: z8.array(httpsUrlSchema2).min(1)
4430
4242
  });
4243
+ var performanceMaxAssetGroupSchema = z8.object({
4244
+ format: z8.literal("performanceMaxAssetGroup"),
4245
+ name: z8.string().min(1).max(255),
4246
+ headlines: z8.array(z8.object({ text: z8.string().min(1).max(30) })).min(3).max(15),
4247
+ descriptions: z8.array(z8.object({ text: z8.string().min(1).max(90) })).min(2).max(5),
4248
+ finalUrls: z8.array(httpsUrlSchema2).min(1),
4249
+ imageAssets: z8.array(refSchema).optional(),
4250
+ logoAssets: z8.array(refSchema).optional()
4251
+ });
4431
4252
  var callAdSchema = z8.object({
4432
4253
  format: z8.literal("call"),
4433
4254
  countryCode: z8.string().length(2),
@@ -4446,9 +4267,7 @@ var appAdSchema = z8.object({
4446
4267
  });
4447
4268
  var videoAdSchema = z8.object({
4448
4269
  format: z8.literal("video"),
4449
- // A raw YouTube id is not a publishable Google Ads reference — the video must be staged as
4450
- // its own `google.asset.create` (type: youtubeVideo) first, then referenced here by asset ref.
4451
- videoAssets: z8.array(refSchema).min(1),
4270
+ youtubeVideoId: z8.string().min(1),
4452
4271
  finalUrls: z8.array(httpsUrlSchema2).min(1)
4453
4272
  });
4454
4273
  var demandGenAdSchema = z8.object({
@@ -4457,13 +4276,12 @@ var demandGenAdSchema = z8.object({
4457
4276
  descriptions: z8.array(z8.object({ text: z8.string().min(1).max(90) })).min(1).max(5),
4458
4277
  businessName: z8.string().min(1).max(25),
4459
4278
  finalUrls: z8.array(httpsUrlSchema2).min(1),
4460
- imageAssets: z8.array(refSchema).optional(),
4461
- squareImageAssets: z8.array(refSchema).optional(),
4462
- logoImageAssets: z8.array(refSchema).optional()
4279
+ imageAssets: z8.array(refSchema).optional()
4463
4280
  });
4464
4281
  var adContentSchema = z8.discriminatedUnion("format", [
4465
4282
  responsiveSearchAdSchema,
4466
4283
  responsiveDisplayAdSchema,
4284
+ performanceMaxAssetGroupSchema,
4467
4285
  callAdSchema,
4468
4286
  appAdSchema,
4469
4287
  videoAdSchema,
@@ -4516,18 +4334,6 @@ var assetCreateSchema = z8.discriminatedUnion("type", [
4516
4334
  structuredSnippetAssetSchema,
4517
4335
  callToActionAssetSchema
4518
4336
  ]);
4519
- var assetUpdateSchema = z8.object({
4520
- name: z8.string().min(1).optional(),
4521
- linkText: z8.string().min(1).max(GOOGLE_ADS_LIMITS.asset.sitelinkLinkTextMax).optional(),
4522
- description1: z8.string().max(GOOGLE_ADS_LIMITS.asset.sitelinkDescriptionMax).optional(),
4523
- description2: z8.string().max(GOOGLE_ADS_LIMITS.asset.sitelinkDescriptionMax).optional(),
4524
- finalUrls: z8.array(httpsUrlSchema2).min(1).optional(),
4525
- calloutText: z8.string().min(1).max(GOOGLE_ADS_LIMITS.asset.calloutTextMax).optional(),
4526
- header: z8.string().min(1).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetHeaderMax).optional(),
4527
- values: z8.array(z8.string().min(1)).min(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMin).max(GOOGLE_ADS_LIMITS.asset.structuredSnippetValuesMax).optional(),
4528
- callToAction: z8.string().min(1).optional(),
4529
- text: z8.string().min(1).optional()
4530
- }).refine((p) => Object.values(p).some((v) => v !== void 0), "update needs at least one field");
4531
4337
  var assetLinkAttachSchema = z8.object({
4532
4338
  level: z8.enum(["campaign", "adGroup", "customer"]),
4533
4339
  parent: refSchema.optional(),
@@ -4542,24 +4348,6 @@ var assetLinkAttachSchema = z8.object({
4542
4348
  });
4543
4349
  }
4544
4350
  });
4545
- var assetGroupCreateSchema = z8.object({
4546
- campaign: refSchema,
4547
- name: z8.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.nameMax),
4548
- finalUrls: z8.array(httpsUrlSchema2).min(1),
4549
- headlines: z8.array(z8.object({ text: z8.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.headlineTextMax) })).min(GOOGLE_ADS_LIMITS.assetGroup.headlinesMin).max(GOOGLE_ADS_LIMITS.assetGroup.headlinesMax),
4550
- longHeadlines: z8.array(z8.object({ text: z8.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.longHeadlineTextMax) })).min(GOOGLE_ADS_LIMITS.assetGroup.longHeadlinesMin).max(GOOGLE_ADS_LIMITS.assetGroup.longHeadlinesMax),
4551
- descriptions: z8.array(z8.object({ text: z8.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.descriptionTextMax) })).min(GOOGLE_ADS_LIMITS.assetGroup.descriptionsMin).max(GOOGLE_ADS_LIMITS.assetGroup.descriptionsMax),
4552
- businessName: z8.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.businessNameMax),
4553
- imageAssets: z8.array(refSchema).optional(),
4554
- squareImageAssets: z8.array(refSchema).optional(),
4555
- logoAssets: z8.array(refSchema).optional(),
4556
- status: z8.enum(["ENABLED", "PAUSED"]).default("PAUSED")
4557
- });
4558
- var assetGroupUpdateSchema = z8.object({
4559
- name: z8.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.nameMax).optional(),
4560
- finalUrls: z8.array(httpsUrlSchema2).min(1).optional(),
4561
- status: z8.enum(["ENABLED", "PAUSED", "REMOVED"]).optional()
4562
- }).refine((p) => Object.values(p).some((v) => v !== void 0), "update needs at least one field");
4563
4351
  var audienceCreateSchema2 = z8.object({
4564
4352
  name: z8.string().min(1).max(GOOGLE_ADS_LIMITS.audience.nameMax),
4565
4353
  type: z8.enum(USER_LIST_TYPES).default("BASIC"),
@@ -4590,9 +4378,6 @@ var conversionActionUpdateSchema = z8.object({
4590
4378
  category: z8.enum(CONVERSION_ACTION_CATEGORIES).optional(),
4591
4379
  countingType: z8.enum(CONVERSION_COUNTING_TYPES).optional(),
4592
4380
  defaultValueMicros: microsSchema.optional(),
4593
- defaultCurrencyCode: z8.string().length(3).optional(),
4594
- clickThroughLookbackWindowDays: z8.number().int().positive().optional(),
4595
- viewThroughLookbackWindowDays: z8.number().int().positive().optional(),
4596
4381
  status: z8.enum(["ENABLED", "REMOVED", "HIDDEN"]).optional()
4597
4382
  }).refine((p) => Object.values(p).some((v) => v !== void 0), "update needs at least one field");
4598
4383
  var biddingStrategyCreateSchema = z8.object({
@@ -4690,11 +4475,8 @@ var GOOGLE_DRAFT_OP_KINDS = [
4690
4475
  "google.ad.resume",
4691
4476
  "google.ad.remove",
4692
4477
  "google.asset.create",
4693
- "google.asset.update",
4694
4478
  "google.assetLink.attach",
4695
4479
  "google.assetLink.detach",
4696
- "google.assetGroup.create",
4697
- "google.assetGroup.update",
4698
4480
  "google.audience.create",
4699
4481
  "google.audienceCriterion.attach",
4700
4482
  "google.audienceCriterion.detach",
@@ -4746,11 +4528,8 @@ var googleDraftOpInputSchema = z8.discriminatedUnion("kind", [
4746
4528
  targetOp("google.ad.resume"),
4747
4529
  targetOp("google.ad.remove"),
4748
4530
  createOp2("google.asset.create", assetCreateSchema),
4749
- updateOp2("google.asset.update", assetUpdateSchema),
4750
4531
  createOp2("google.assetLink.attach", assetLinkAttachSchema),
4751
4532
  targetOp("google.assetLink.detach"),
4752
- createOp2("google.assetGroup.create", assetGroupCreateSchema),
4753
- updateOp2("google.assetGroup.update", assetGroupUpdateSchema),
4754
4533
  createOp2("google.audience.create", audienceCreateSchema2),
4755
4534
  createOp2("google.audienceCriterion.attach", audienceCriterionAttachSchema),
4756
4535
  targetOp("google.audienceCriterion.detach"),
@@ -4789,15 +4568,6 @@ var googleDraftStageResponseSchema = z9.object({
4789
4568
  /** True when the op amended an already-staged op in place instead of appending a new one. */
4790
4569
  amended: z9.boolean().optional()
4791
4570
  });
4792
- var googleDraftAmendRequestSchema = z9.object({
4793
- chatId: z9.string(),
4794
- ref: z9.string(),
4795
- patch: z9.record(z9.string(), z9.unknown())
4796
- });
4797
- var googleDraftShowRequestSchema = z9.object({
4798
- chatId: z9.string(),
4799
- ref: z9.string()
4800
- });
4801
4571
  var GOOGLE_DRAFT_BATCH_MAX = 500;
4802
4572
  var googleDraftStageBatchRequestSchema = z9.object({
4803
4573
  chatId: z9.string(),
@@ -4827,13 +4597,6 @@ var googleDraftOpViewSchema = z9.object({
4827
4597
  stagedAt: z9.number(),
4828
4598
  result: googleDraftOpResultSchema.optional()
4829
4599
  });
4830
- var googleDraftShowResponseSchema = z9.object({
4831
- op: googleDraftOpViewSchema.extend({
4832
- payload: z9.unknown().optional(),
4833
- warnings: z9.array(z9.string()).optional(),
4834
- annotations: z9.unknown().optional()
4835
- })
4836
- });
4837
4600
  var googleDraftListRequestSchema = z9.object({
4838
4601
  chatId: z9.string()
4839
4602
  });
@@ -5055,11 +4818,11 @@ function rawTextEntries(value) {
5055
4818
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
5056
4819
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
5057
4820
  }
5058
- function rawFileEntries(path12) {
5059
- if (typeof path12 !== "string" || path12.length === 0) {
4821
+ function rawFileEntries(path14) {
4822
+ if (typeof path14 !== "string" || path14.length === 0) {
5060
4823
  return [];
5061
4824
  }
5062
- return readFileSync3(path12, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4825
+ return readFileSync3(path14, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
5063
4826
  }
5064
4827
  function keywordEntries(args) {
5065
4828
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -5082,19 +4845,19 @@ function keywordEntries(args) {
5082
4845
  }
5083
4846
  return entries;
5084
4847
  }
5085
- function loadJsonFileArg(path12) {
5086
- if (typeof path12 !== "string" || path12.length === 0) {
4848
+ function loadJsonFileArg(path14) {
4849
+ if (typeof path14 !== "string" || path14.length === 0) {
5087
4850
  return {};
5088
4851
  }
5089
4852
  try {
5090
- const parsed = JSON.parse(readFileSync3(path12, "utf8"));
4853
+ const parsed = JSON.parse(readFileSync3(path14, "utf8"));
5091
4854
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5092
- failWriteValidation(`${path12} must contain a JSON object`);
4855
+ failWriteValidation(`${path14} must contain a JSON object`);
5093
4856
  }
5094
4857
  return parsed;
5095
4858
  } catch (err) {
5096
4859
  if (err instanceof SyntaxError) {
5097
- failWriteValidation(`${path12} is not valid JSON: ${err.message}`);
4860
+ failWriteValidation(`${path14} is not valid JSON: ${err.message}`);
5098
4861
  }
5099
4862
  throw err;
5100
4863
  }
@@ -5108,28 +4871,6 @@ function mergePayload(file, flags) {
5108
4871
  }
5109
4872
  return merged;
5110
4873
  }
5111
- function loadPatchArg(args) {
5112
- const file = typeof args.file === "string" && args.file.length > 0 ? args.file : void 0;
5113
- const inline = typeof args.patch === "string" && args.patch.length > 0 ? args.patch : void 0;
5114
- if (Boolean(file) === Boolean(inline)) {
5115
- failWriteValidation("pass exactly one of --file <patch.json> or --patch '<json>'");
5116
- }
5117
- if (file) {
5118
- return loadJsonFileArg(file);
5119
- }
5120
- try {
5121
- const parsed = JSON.parse(inline);
5122
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5123
- failWriteValidation("--patch must be a JSON object");
5124
- }
5125
- return parsed;
5126
- } catch (err) {
5127
- if (err instanceof SyntaxError) {
5128
- failWriteValidation(`--patch is not valid JSON: ${err.message}`);
5129
- }
5130
- throw err;
5131
- }
5132
- }
5133
4874
  function handleGoogleError(err) {
5134
4875
  if (err instanceof ApiError) {
5135
4876
  writeJsonEnvelope({ ok: false, error: { code: err.code ?? "API_ERROR", message: err.message } });
@@ -5205,10 +4946,10 @@ async function stageUpdate(kind, customerId, target, payload) {
5205
4946
  async function stageTarget(kind, customerId, target) {
5206
4947
  await stageGoogleOp({ kind, customerId, target });
5207
4948
  }
5208
- async function draftAction(path12, body) {
4949
+ async function draftAction(path14, body) {
5209
4950
  try {
5210
4951
  const chatId = requireChatId();
5211
- const response = await apiPost(path12, { chatId, ...body });
4952
+ const response = await apiPost(path14, { chatId, ...body });
5212
4953
  writeJsonEnvelope(response);
5213
4954
  } catch (err) {
5214
4955
  handleGoogleError(err);
@@ -5231,7 +4972,7 @@ async function draftListAction(json) {
5231
4972
 
5232
4973
  // src/commands/ads/google/draft.ts
5233
4974
  var draftCommand2 = defineCommand22({
5234
- meta: { name: "draft", description: "List, remove, clear, amend, or show staged Google Ads write ops for this chat" },
4975
+ meta: { name: "draft", description: "List, remove, or clear staged Google Ads write ops for this chat" },
5235
4976
  subCommands: {
5236
4977
  list: defineCommand22({
5237
4978
  meta: {
@@ -5255,42 +4996,6 @@ var draftCommand2 = defineCommand22({
5255
4996
  run: async () => {
5256
4997
  await draftAction("/api/ads/google/draft/clear", {});
5257
4998
  }
5258
- }),
5259
- amend: defineCommand22({
5260
- meta: {
5261
- name: "amend",
5262
- description: "Update a staged op in place \u2014 merges a JSON patch into its payload (RFC-7386: objects deep-merge, null deletes a key, arrays/scalars replace) and re-validates. Use this instead of remove + re-create."
5263
- },
5264
- args: {
5265
- ref: {
5266
- type: "positional",
5267
- description: "Staged op ref (g_temp_*) or target id/resource name",
5268
- required: false
5269
- },
5270
- file: { type: "string", description: "JSON file with the patch object" },
5271
- patch: { type: "string", description: "Inline JSON patch object" }
5272
- },
5273
- run: async ({ args }) => {
5274
- const ref = requireTarget(args, "op");
5275
- const patch = loadPatchArg(args);
5276
- await draftAction("/api/ads/google/draft/amend", { ref, patch });
5277
- }
5278
- }),
5279
- show: defineCommand22({
5280
- meta: {
5281
- name: "show",
5282
- description: "Print the full staged payload for one op \u2014 the receipt to verify a change looks right before publish (never truncated)."
5283
- },
5284
- args: {
5285
- ref: {
5286
- type: "positional",
5287
- description: "Staged op ref (g_temp_*) or target id/resource name",
5288
- required: false
5289
- }
5290
- },
5291
- run: async ({ args }) => {
5292
- await draftAction("/api/ads/google/draft/show", { ref: requireTarget(args, "op") });
5293
- }
5294
4999
  })
5295
5000
  }
5296
5001
  });
@@ -7014,55 +6719,6 @@ function rdaContentFromFlags(args, base) {
7014
6719
  }
7015
6720
  return content;
7016
6721
  }
7017
- function videoContentFromFlags(args, base) {
7018
- const content = {
7019
- format: "video",
7020
- ...base !== null && typeof base === "object" && !Array.isArray(base) ? base : {}
7021
- };
7022
- const videoAssets = listFlag(args["video-assets"]);
7023
- if (videoAssets) {
7024
- content.videoAssets = videoAssets;
7025
- }
7026
- const finalUrls = listFlag(args["final-url"]);
7027
- if (finalUrls) {
7028
- content.finalUrls = finalUrls;
7029
- }
7030
- return content;
7031
- }
7032
- function demandGenContentFromFlags(args, base) {
7033
- const content = {
7034
- format: "demandGen",
7035
- ...base !== null && typeof base === "object" && !Array.isArray(base) ? base : {}
7036
- };
7037
- const headlines = listFlag(args.headlines);
7038
- if (headlines) {
7039
- content.headlines = headlines.map((text) => ({ text }));
7040
- }
7041
- const descriptions = listFlag(args.descriptions);
7042
- if (descriptions) {
7043
- content.descriptions = descriptions.map((text) => ({ text }));
7044
- }
7045
- if (typeof args["business-name"] === "string") {
7046
- content.businessName = args["business-name"];
7047
- }
7048
- const finalUrls = listFlag(args["final-url"]);
7049
- if (finalUrls) {
7050
- content.finalUrls = finalUrls;
7051
- }
7052
- const images = listFlag(args["image-assets"]);
7053
- if (images) {
7054
- content.imageAssets = images;
7055
- }
7056
- const squareImages = listFlag(args["square-image-assets"]);
7057
- if (squareImages) {
7058
- content.squareImageAssets = squareImages;
7059
- }
7060
- const logoImages = listFlag(args["logo-image-assets"]);
7061
- if (logoImages) {
7062
- content.logoImageAssets = logoImages;
7063
- }
7064
- return content;
7065
- }
7066
6722
  function adContentFromFlags(args, format, fileContent) {
7067
6723
  if (format === "responsiveSearch") {
7068
6724
  return fileContent ?? rsaContentFromFlags(args);
@@ -7070,12 +6726,6 @@ function adContentFromFlags(args, format, fileContent) {
7070
6726
  if (format === "responsiveDisplay") {
7071
6727
  return rdaContentFromFlags(args, fileContent);
7072
6728
  }
7073
- if (format === "video") {
7074
- return videoContentFromFlags(args, fileContent);
7075
- }
7076
- if (format === "demandGen") {
7077
- return demandGenContentFromFlags(args, fileContent);
7078
- }
7079
6729
  return fileContent ?? failWriteValidation(`--format ${format} needs --file with the ad content`);
7080
6730
  }
7081
6731
  var adsCommand = defineCommand30({
@@ -7084,7 +6734,7 @@ var adsCommand = defineCommand30({
7084
6734
  create: defineCommand30({
7085
6735
  meta: {
7086
6736
  name: "create",
7087
- description: "Stage an ad (responsive search via flags; other formats via --file). RSA needs 3\u201315 headlines and 2\u20134 descriptions \u2014 give Google 8\u201312 varied headlines to test, and stage 2\u20134 ads per ad group. Performance Max has no ad format of its own \u2014 stage its copy with `asset-groups create`."
6737
+ description: "Stage an ad (responsive search via flags; other formats via --file). RSA needs 3\u201315 headlines and 2\u20134 descriptions \u2014 give Google 8\u201312 varied headlines to test, and stage 2\u20134 ads per ad group."
7088
6738
  },
7089
6739
  args: {
7090
6740
  ...customerIdArg,
@@ -7092,14 +6742,14 @@ var adsCommand = defineCommand30({
7092
6742
  "ad-group-ref": { type: "string", description: "Ad group ref" },
7093
6743
  format: {
7094
6744
  type: "string",
7095
- description: "responsiveSearch (default) | responsiveDisplay | call | app | video | demandGen"
6745
+ description: "responsiveSearch (default) | responsiveDisplay | performanceMaxAssetGroup | call | app | video | demandGen"
7096
6746
  },
7097
- headlines: { type: "string", description: "Comma-separated headlines (RSA/RDA/demandGen)" },
7098
- descriptions: { type: "string", description: "Comma-separated descriptions (RSA/RDA/demandGen)" },
6747
+ headlines: { type: "string", description: "Comma-separated headlines (RSA/RDA)" },
6748
+ descriptions: { type: "string", description: "Comma-separated descriptions (RSA/RDA)" },
7099
6749
  path1: { type: "string" },
7100
6750
  path2: { type: "string" },
7101
6751
  "long-headline": { type: "string", description: "Long headline (responsiveDisplay)" },
7102
- "business-name": { type: "string", description: "Business name (responsiveDisplay/demandGen)" },
6752
+ "business-name": { type: "string", description: "Business name (responsiveDisplay)" },
7103
6753
  "marketing-images": {
7104
6754
  type: "string",
7105
6755
  description: "Comma-separated marketing image asset refs (responsiveDisplay)"
@@ -7109,13 +6759,6 @@ var adsCommand = defineCommand30({
7109
6759
  description: "Comma-separated square marketing image asset refs (responsiveDisplay)"
7110
6760
  },
7111
6761
  "logo-images": { type: "string", description: "Comma-separated logo image asset refs (responsiveDisplay)" },
7112
- "video-assets": {
7113
- type: "string",
7114
- description: `Comma-separated video asset refs (video format) \u2014 stage the YouTube video as an asset first (assets create --file '{"type":"youtubeVideo",\u2026}')`
7115
- },
7116
- "image-assets": { type: "string", description: "Comma-separated image asset refs (demandGen)" },
7117
- "square-image-assets": { type: "string", description: "Comma-separated square image asset refs (demandGen)" },
7118
- "logo-image-assets": { type: "string", description: "Comma-separated logo image asset refs (demandGen)" },
7119
6762
  "final-url": { type: "string", description: "Comma-separated final URLs" },
7120
6763
  status: { type: "string" }
7121
6764
  },
@@ -7150,78 +6793,6 @@ var adsCommand = defineCommand30({
7150
6793
  remove: statusCommand2("google.ad.remove", "ad")
7151
6794
  }
7152
6795
  });
7153
- var assetGroupsCommand = defineCommand30({
7154
- meta: {
7155
- name: "asset-groups",
7156
- description: "Stage Performance Max asset group create/update \u2014 the PMax campaign's copy and images."
7157
- },
7158
- subCommands: {
7159
- create: defineCommand30({
7160
- meta: {
7161
- name: "create",
7162
- description: "Stage a Performance Max asset group: 3\u201315 headlines (\u226430ch), 1\u20135 long headlines (\u226490ch), 2\u20135 descriptions (\u226490ch), a business name, and image refs."
7163
- },
7164
- args: {
7165
- ...customerIdArg,
7166
- ...fileArg,
7167
- "campaign-ref": {
7168
- type: "string",
7169
- description: "Performance Max campaign ref (g_temp_*, id, or resource name)"
7170
- },
7171
- name: { type: "string", description: "Asset group name" },
7172
- "final-urls": { type: "string", description: "Comma-separated final URLs" },
7173
- headlines: { type: "string", description: "Comma-separated headlines (3\u201315, \u226430 chars)" },
7174
- "long-headlines": { type: "string", description: "Comma-separated long headlines (1\u20135, \u226490 chars)" },
7175
- descriptions: { type: "string", description: "Comma-separated descriptions (2\u20135, \u226490 chars)" },
7176
- "business-name": { type: "string" },
7177
- "image-assets": { type: "string", description: "Comma-separated marketing image asset refs" },
7178
- "square-image-assets": { type: "string", description: "Comma-separated square marketing image asset refs" },
7179
- "logo-assets": { type: "string", description: "Comma-separated logo image asset refs" },
7180
- status: { type: "string", description: "ENABLED | PAUSED (default PAUSED)" }
7181
- },
7182
- run: async ({ args }) => {
7183
- const customerId = requireCustomerId(args);
7184
- const headlines = listFlag(args.headlines);
7185
- const longHeadlines = listFlag(args["long-headlines"]);
7186
- const descriptions = listFlag(args.descriptions);
7187
- const payload = mergePayload(loadJsonFileArg(args.file), {
7188
- campaign: args["campaign-ref"],
7189
- name: args.name,
7190
- finalUrls: listFlag(args["final-urls"]),
7191
- headlines: headlines?.map((text) => ({ text })),
7192
- longHeadlines: longHeadlines?.map((text) => ({ text })),
7193
- descriptions: descriptions?.map((text) => ({ text })),
7194
- businessName: args["business-name"],
7195
- imageAssets: listFlag(args["image-assets"]),
7196
- squareImageAssets: listFlag(args["square-image-assets"]),
7197
- logoAssets: listFlag(args["logo-assets"]),
7198
- status: args.status
7199
- });
7200
- await stageCreate("google.assetGroup.create", customerId, payload);
7201
- }
7202
- }),
7203
- update: defineCommand30({
7204
- meta: { name: "update", description: "Stage a Performance Max asset group update" },
7205
- args: {
7206
- ...customerIdArg,
7207
- ...fileArg,
7208
- name: { type: "string" },
7209
- "final-urls": { type: "string", description: "Comma-separated final URLs" },
7210
- status: { type: "string", description: "ENABLED | PAUSED | REMOVED" }
7211
- },
7212
- run: async ({ args }) => {
7213
- const customerId = requireCustomerId(args);
7214
- const target = requireTarget(args, "asset group");
7215
- const payload = mergePayload(loadJsonFileArg(args.file), {
7216
- name: args.name,
7217
- finalUrls: listFlag(args["final-urls"]),
7218
- status: args.status
7219
- });
7220
- await stageUpdate("google.assetGroup.update", customerId, target, payload);
7221
- }
7222
- })
7223
- }
7224
- });
7225
6796
  function fileCreateCommand(name, kind, description) {
7226
6797
  return defineCommand30({
7227
6798
  meta: { name, description },
@@ -7236,47 +6807,10 @@ function fileCreateCommand(name, kind, description) {
7236
6807
  }
7237
6808
  });
7238
6809
  }
7239
- var assetsUpdateCommand = defineCommand30({
7240
- meta: {
7241
- name: "update",
7242
- description: "Update a staged or live asset in place (flat partial \u2014 set only the fields that changed). Works across asset types: sitelink (--link-text/--description1/--description2/--final-urls), callout (--callout-text), structured snippet (--header/--values), call-to-action (--cta), text (--text), or --name."
7243
- },
7244
- args: {
7245
- ...customerIdArg,
7246
- ...fileArg,
7247
- name: { type: "string", description: "Asset display name" },
7248
- "link-text": { type: "string", description: "Sitelink link text" },
7249
- description1: { type: "string", description: "Sitelink description line 1" },
7250
- description2: { type: "string", description: "Sitelink description line 2" },
7251
- "final-urls": { type: "string", description: "Comma-separated sitelink final URLs" },
7252
- "callout-text": { type: "string", description: "Callout text" },
7253
- header: { type: "string", description: "Structured snippet header" },
7254
- values: { type: "string", description: "Comma-separated structured snippet values" },
7255
- cta: { type: "string", description: "Call-to-action text" },
7256
- text: { type: "string", description: "Text asset content" }
7257
- },
7258
- run: async ({ args }) => {
7259
- const customerId = requireCustomerId(args);
7260
- const target = requireTarget(args, "asset");
7261
- const payload = mergePayload(loadJsonFileArg(args.file), {
7262
- name: args.name,
7263
- linkText: args["link-text"],
7264
- description1: args.description1,
7265
- description2: args.description2,
7266
- finalUrls: listFlag(args["final-urls"]),
7267
- calloutText: args["callout-text"],
7268
- header: args.header,
7269
- values: listFlag(args.values),
7270
- callToAction: args.cta,
7271
- text: args.text
7272
- });
7273
- await stageUpdate("google.asset.update", customerId, target, payload);
7274
- }
7275
- });
7276
6810
  var assetsCommand = defineCommand30({
7277
6811
  meta: {
7278
6812
  name: "assets",
7279
- description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts, and \u22651 structured snippet per campaign."
6813
+ description: "Stage asset create + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts, and \u22651 structured snippet per campaign."
7280
6814
  },
7281
6815
  subCommands: {
7282
6816
  create: fileCreateCommand(
@@ -7284,7 +6818,6 @@ var assetsCommand = defineCommand30({
7284
6818
  "google.asset.create",
7285
6819
  "Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)"
7286
6820
  ),
7287
- update: assetsUpdateCommand,
7288
6821
  attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer"),
7289
6822
  detach: statusCommand2("google.assetLink.detach", "asset link")
7290
6823
  }
@@ -7380,7 +6913,6 @@ var googleWriteCommands = {
7380
6913
  "keyword-lists": keywordListsCommand,
7381
6914
  ads: adsCommand,
7382
6915
  assets: assetsCommand,
7383
- "asset-groups": assetGroupsCommand,
7384
6916
  audiences: audiencesCommand,
7385
6917
  conversions: conversionsCommand,
7386
6918
  "bidding-strategies": biddingStrategiesCommand,
@@ -7966,11 +7498,6 @@ registerSchema({
7966
7498
  ...writeAccountArgs,
7967
7499
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
7968
7500
  name: { type: "string", description: "New creative name", required: false },
7969
- campaign: {
7970
- type: "string",
7971
- description: "Re-parent to a different ad set (id/URN or li_temp_*) \u2014 staged creatives only",
7972
- required: false
7973
- },
7974
7501
  headline: { type: "string", description: "New headline", required: false },
7975
7502
  intro: { type: "string", description: "Commentary / intro text", required: false },
7976
7503
  description: { type: "string", description: "Description (text/spotlight)", required: false },
@@ -8963,19 +8490,19 @@ function failWriteValidation2(message) {
8963
8490
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
8964
8491
  process.exit(1);
8965
8492
  }
8966
- function loadJsonFileArg2(path12) {
8967
- if (typeof path12 !== "string" || path12.length === 0) {
8493
+ function loadJsonFileArg2(path14) {
8494
+ if (typeof path14 !== "string" || path14.length === 0) {
8968
8495
  return {};
8969
8496
  }
8970
8497
  try {
8971
- const parsed = JSON.parse(readFileSync7(path12, "utf8"));
8498
+ const parsed = JSON.parse(readFileSync7(path14, "utf8"));
8972
8499
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8973
- failWriteValidation2(`${path12} must contain a JSON object`);
8500
+ failWriteValidation2(`${path14} must contain a JSON object`);
8974
8501
  }
8975
8502
  return parsed;
8976
8503
  } catch (err) {
8977
8504
  if (err instanceof SyntaxError) {
8978
- failWriteValidation2(`${path12} is not valid JSON: ${err.message}`);
8505
+ failWriteValidation2(`${path14} is not valid JSON: ${err.message}`);
8979
8506
  }
8980
8507
  throw err;
8981
8508
  }
@@ -8989,28 +8516,6 @@ function mergePayload2(file, flags) {
8989
8516
  }
8990
8517
  return merged;
8991
8518
  }
8992
- function loadPatchArg2(args) {
8993
- const file = typeof args.file === "string" && args.file.length > 0 ? args.file : void 0;
8994
- const inline = typeof args.patch === "string" && args.patch.length > 0 ? args.patch : void 0;
8995
- if (Boolean(file) === Boolean(inline)) {
8996
- failWriteValidation2("pass exactly one of --file <patch.json> or --patch '<json>'");
8997
- }
8998
- if (file) {
8999
- return loadJsonFileArg2(file);
9000
- }
9001
- try {
9002
- const parsed = JSON.parse(inline);
9003
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9004
- failWriteValidation2("--patch must be a JSON object");
9005
- }
9006
- return parsed;
9007
- } catch (err) {
9008
- if (err instanceof SyntaxError) {
9009
- failWriteValidation2(`--patch is not valid JSON: ${err.message}`);
9010
- }
9011
- throw err;
9012
- }
9013
- }
9014
8519
  function parseMoneyFlag(amount, currency, flag) {
9015
8520
  if (amount === void 0 || amount === null || amount === "") {
9016
8521
  return void 0;
@@ -9060,15 +8565,15 @@ function parseLocaleFlag(value) {
9060
8565
  }
9061
8566
  return { language: match[1], country: match[2].toUpperCase() };
9062
8567
  }
9063
- function loadTargetingFileArg(path12) {
9064
- if (typeof path12 !== "string" || path12.length === 0) {
8568
+ function loadTargetingFileArg(path14) {
8569
+ if (typeof path14 !== "string" || path14.length === 0) {
9065
8570
  return void 0;
9066
8571
  }
9067
- const parsed = loadJsonFileArg2(path12);
8572
+ const parsed = loadJsonFileArg2(path14);
9068
8573
  const criteria = parsed.targetingCriteria ?? parsed;
9069
8574
  if (!criteria.include) {
9070
8575
  failWriteValidation2(
9071
- `${path12} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8576
+ `${path14} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
9072
8577
  );
9073
8578
  }
9074
8579
  return criteria;
@@ -9103,14 +8608,14 @@ function parseCsvLine(line) {
9103
8608
  cells.push(current);
9104
8609
  return cells.map((cell) => cell.trim());
9105
8610
  }
9106
- function parseListFileArg(path12, maxRows) {
9107
- if (typeof path12 !== "string" || path12.length === 0) {
8611
+ function parseListFileArg(path14, maxRows) {
8612
+ if (typeof path14 !== "string" || path14.length === 0) {
9108
8613
  return void 0;
9109
8614
  }
9110
- const raw = readFileSync7(path12, "utf8");
8615
+ const raw = readFileSync7(path14, "utf8");
9111
8616
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
9112
8617
  if (lines.length < 2) {
9113
- failWriteValidation2(`${path12} needs a header row and at least one data row`);
8618
+ failWriteValidation2(`${path14} needs a header row and at least one data row`);
9114
8619
  }
9115
8620
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
9116
8621
  const rows = [];
@@ -9129,7 +8634,7 @@ function parseListFileArg(path12, maxRows) {
9129
8634
  }
9130
8635
  }
9131
8636
  if (rows.length > maxRows) {
9132
- failWriteValidation2(`${path12} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8637
+ failWriteValidation2(`${path14} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
9133
8638
  }
9134
8639
  return { columns, rows };
9135
8640
  }
@@ -9309,21 +8814,11 @@ Example: baker ads linkedin campaigns create --name "ABM Tier-1" --group li_temp
9309
8814
  await stageOp({ kind: "campaign.create", accountId, payload });
9310
8815
  }
9311
8816
  });
9312
- function campaignUpdatePayloadFromFlags(args, target, file) {
9313
- const flags = campaignPayloadFromFlags(args);
9314
- if (!target.startsWith(TEMP_REF_PREFIX)) {
9315
- flags.campaignGroup = void 0;
9316
- flags.type = void 0;
9317
- flags.locale = void 0;
9318
- flags.associatedEntity = void 0;
9319
- }
9320
- return mergePayload2(file, flags);
9321
- }
9322
8817
  var campaignsUpdateCommand = defineCommand38({
9323
8818
  meta: {
9324
8819
  name: "update",
9325
8820
  description: `Stage changes to an existing campaign (budget, bid, targeting, flags, schedule, status). ${STAGED_NOTE}
9326
- Pass a li_temp_* ref to amend a campaign staged in this chat \u2014 fields merge into the staged create, and --group/--type/--locale/--associated-entity carry through too (they're create-only on a LIVE campaign, but fair game while it's still staged).
8821
+ Pass a li_temp_* ref to amend a campaign staged in this chat \u2014 fields merge into the staged create.
9327
8822
  Example: baker ads linkedin campaigns update 123456 --daily-budget 100 --currency EUR --audience-expansion off`
9328
8823
  },
9329
8824
  args: {
@@ -9332,9 +8827,18 @@ Example: baker ads linkedin campaigns update 123456 --daily-budget 100 --currenc
9332
8827
  },
9333
8828
  run: async ({ args }) => {
9334
8829
  const accountId = bareAccountId(args);
9335
- const target = requireTarget2(args, "campaign");
9336
- const payload = campaignUpdatePayloadFromFlags(args, target, loadJsonFileArg2(args.file));
9337
- await stageOp({ kind: "campaign.update", accountId, target, payload });
8830
+ const flags = campaignPayloadFromFlags(args);
8831
+ flags.campaignGroup = void 0;
8832
+ flags.type = void 0;
8833
+ flags.locale = void 0;
8834
+ flags.associatedEntity = void 0;
8835
+ const payload = mergePayload2(loadJsonFileArg2(args.file), flags);
8836
+ await stageOp({
8837
+ kind: "campaign.update",
8838
+ accountId,
8839
+ target: requireTarget2(args, "campaign"),
8840
+ payload
8841
+ });
9338
8842
  }
9339
8843
  });
9340
8844
  function statusSugarCommand(entity, kind, name) {
@@ -9585,21 +9089,15 @@ var creativesUpdateCommand = defineCommand38({
9585
9089
  name: "update",
9586
9090
  description: `Stage changes to an existing creative, or amend a creative staged in this chat by passing its li_temp_* ref. ${STAGED_NOTE}
9587
9091
  Direct-content ads (text/spotlight/follower/jobs) update copy/media/URL IN PLACE \u2014 changed fields merge into the ad's current content. Post-based ads (image/video/carousel/\u2026) can't edit content; use \`creatives duplicate <id> --headline "\u2026" --replace\` for those. Staged creatives (li_temp_*) accept all content flags and re-validate.
9588
- --campaign re-parents the ad to a different ad set \u2014 LinkedIn has no in-place move API, so this only works while the creative is still staged (li_temp_*); passing it against a live creative id/URN is rejected at stage time.
9589
9092
  Examples:
9590
9093
  baker ads linkedin creatives update 1458423674 --headline "Sharper headline" --description "New copy."
9591
- baker ads linkedin creatives update urn:li:sponsoredCreative:789 --intended-status PAUSED
9592
- baker ads linkedin creatives update li_temp_x3 --campaign li_temp_x2`
9094
+ baker ads linkedin creatives update urn:li:sponsoredCreative:789 --intended-status PAUSED`
9593
9095
  },
9594
9096
  args: {
9595
9097
  id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
9596
9098
  ...accountArgs,
9597
9099
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT" },
9598
9100
  name: { type: "string", description: "New creative name" },
9599
- campaign: {
9600
- type: "string",
9601
- description: "Re-parent to a different ad set (id/URN or li_temp_*) \u2014 staged creatives only"
9602
- },
9603
9101
  intro: { type: "string", description: "Commentary / intro text" },
9604
9102
  headline: { type: "string", description: "New headline" },
9605
9103
  description: { type: "string", description: "Description (text/spotlight ads)" },
@@ -9627,7 +9125,6 @@ Examples:
9627
9125
  const payload = mergePayload2(file, {
9628
9126
  intendedStatus,
9629
9127
  name: args.name,
9630
- campaign: args.campaign,
9631
9128
  content: Object.keys(content).length > 0 ? content : void 0
9632
9129
  });
9633
9130
  await stageOp({ kind: "creative.update", accountId, target, payload });
@@ -9771,10 +9268,7 @@ var leadFormsCreateCommand = defineCommand38({
9771
9268
  meta: {
9772
9269
  name: "create",
9773
9270
  description: `Stage a new Lead Gen Form from a JSON file. ${STAGED_NOTE}
9774
- Required: name, headline (\u226460), privacyPolicyUrl, questions[] (\u226412; playbook: \u22644 for completion).
9775
- Each question is a predefined profile field ({ name, predefinedField: "EMAIL" }) or a custom question ({ name, questionType: "MULTIPLE_CHOICE", options: [...] }; \u22643 custom).
9776
- Best-practice fields the preview will nudge for if missing: 1-3 qualifying questions, consents[] (disclosure checkboxes), thankYou.message + thankYou.landingUrl|appointmentUrl.
9777
- Also supported: locale, formImageId|formImageUrn, privacyPolicyText, hiddenFields[], legalDisclaimer, thankYou.cta. Example: baker ads linkedin lead-forms create --file form.json`
9271
+ The file needs: name, headline (\u226460), privacyPolicyUrl, questions[] (\u226412; playbook: \u22644 for completion). Example: baker ads linkedin lead-forms create --file form.json`
9778
9272
  },
9779
9273
  args: {
9780
9274
  ...accountArgs,
@@ -10272,108 +9766,23 @@ function resolveRange(args) {
10272
9766
 
10273
9767
  // src/commands/ads/linkedin/draft.ts
10274
9768
  import { defineCommand as defineCommand45 } from "citty";
10275
-
10276
- // src/commands/ads/linkedin/draft-status.ts
10277
- var OPERATION_LABELS2 = {
10278
- create: "Creating",
10279
- update: "Updating",
10280
- pause: "Pausing",
10281
- resume: "Resuming",
10282
- archive: "Archiving"
10283
- };
10284
- function badge2(node) {
10285
- const label = node.operation ? OPERATION_LABELS2[node.operation] : void 0;
10286
- if (label) {
10287
- return label;
10288
- }
10289
- return node.existing ? "on LinkedIn" : "staged";
10290
- }
10291
- function collectionLine2(collection) {
10292
- const parts = [];
10293
- if (collection.added > 0) {
10294
- parts.push(`+${collection.added}`);
10295
- }
10296
- if (collection.existing > 0) {
10297
- parts.push(`${collection.existing} existing`);
10298
- }
10299
- if (collection.removed > 0) {
10300
- parts.push(`-${collection.removed}`);
10301
- }
10302
- return parts.length > 0 ? `${collection.label}: ${parts.join(", ")}` : void 0;
10303
- }
10304
- function renderNode2(node, depth, lines) {
10305
- const indent = " ".repeat(depth);
10306
- lines.push(`${indent}\u2022 ${node.name} \xB7 ${badge2(node)}`);
10307
- for (const warning of node.warnings ?? []) {
10308
- lines.push(`${indent} \u26A0 ${warning}`);
10309
- }
10310
- for (const collection of node.collections) {
10311
- const line = collectionLine2(collection);
10312
- if (line) {
10313
- lines.push(`${indent} ${line}`);
10314
- }
10315
- }
10316
- for (const child of node.children) {
10317
- renderNode2(child, depth + 1, lines);
10318
- }
10319
- }
10320
- function renderSection2(title, nodes, lines) {
10321
- if (nodes.length === 0) {
10322
- return;
10323
- }
10324
- lines.push("");
10325
- lines.push(title);
10326
- for (const node of nodes) {
10327
- renderNode2(node, 1, lines);
10328
- }
10329
- }
10330
- var CAMPAIGN_TREE_ENTITIES = /* @__PURE__ */ new Set(["campaignGroup", "campaign", "creative"]);
10331
- function renderDraftStatus2(data) {
10332
- if (data.status === "none" || data.count === 0) {
10333
- return "No staged LinkedIn changes on this chat.";
10334
- }
10335
- const modeNote = data.mode === "live" ? "Live \u2014 publishing writes directly to LinkedIn." : "Simulated \u2014 publishing completes the whole draft without calling LinkedIn.";
10336
- const lines = [
10337
- `LinkedIn Ads \u2014 ${data.count} staged change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`,
10338
- modeNote
10339
- ];
10340
- const tree = data.tree ?? [];
10341
- renderSection2(
10342
- "Campaigns",
10343
- tree.filter((node) => CAMPAIGN_TREE_ENTITIES.has(node.entity)),
10344
- lines
10345
- );
10346
- const rest = tree.filter((node) => !CAMPAIGN_TREE_ENTITIES.has(node.entity));
10347
- renderSection2("Other", rest, lines);
10348
- return lines.join("\n");
10349
- }
10350
-
10351
- // src/commands/ads/linkedin/draft.ts
10352
- async function listDraft(json) {
10353
- try {
10354
- const chatId = requireChatId();
10355
- const response = await apiPost("/api/ads/linkedin/draft", {
10356
- chatId
10357
- });
10358
- if (json || !response.ok || !response.data) {
10359
- writeJsonEnvelope(response);
10360
- return;
10361
- }
10362
- process.stdout.write(`${renderDraftStatus2(response.data)}
10363
- `);
10364
- } catch (err) {
10365
- handleLinkedinError(err);
9769
+ async function listDraft() {
9770
+ try {
9771
+ const chatId = requireChatId();
9772
+ const response = await apiPost("/api/ads/linkedin/draft", {
9773
+ chatId
9774
+ });
9775
+ writeJsonEnvelope(response);
9776
+ } catch (err) {
9777
+ handleLinkedinError(err);
10366
9778
  }
10367
9779
  }
10368
9780
  var listCommand3 = defineCommand45({
10369
9781
  meta: {
10370
9782
  name: "list",
10371
- description: "Review everything staged as a Campaign group \u25B8 Campaign \u25B8 Creative tree (--json for the raw envelope). Example: baker ads linkedin draft"
9783
+ description: "Show every LinkedIn write op staged in this chat, its mode (simulated = publish will NOT hit LinkedIn), dependencies, and post-publish results. Example: baker ads linkedin draft"
10372
9784
  },
10373
- args: { json: { type: "boolean", description: "Print the raw JSON envelope instead of the readable tree" } },
10374
- run: async ({ args }) => {
10375
- await listDraft(args.json === true);
10376
- }
9785
+ run: listDraft
10377
9786
  });
10378
9787
  var removeCommand2 = defineCommand45({
10379
9788
  meta: {
@@ -10417,70 +9826,17 @@ var clearCommand2 = defineCommand45({
10417
9826
  }
10418
9827
  }
10419
9828
  });
10420
- var amendCommand = defineCommand45({
10421
- meta: {
10422
- name: "amend",
10423
- description: "Update a staged op in place \u2014 merges a JSON patch into its payload (RFC-7386: objects deep-merge, null deletes a key, arrays/scalars replace) and re-validates. Use this instead of remove + re-create."
10424
- },
10425
- args: {
10426
- ref: { type: "positional", description: "Staged op ref (li_temp_*) or target id/URN", required: true },
10427
- file: { type: "string", description: "JSON file with the patch object" },
10428
- patch: { type: "string", description: "Inline JSON patch object" }
10429
- },
10430
- run: async ({ args }) => {
10431
- const ref = requireTarget2(args, "op");
10432
- const patch = loadPatchArg2(args);
10433
- try {
10434
- const chatId = requireChatId();
10435
- const body = { chatId, ref, patch };
10436
- const response = await apiPost(
10437
- "/api/ads/linkedin/draft/amend",
10438
- body
10439
- );
10440
- writeJsonEnvelope(response);
10441
- } catch (err) {
10442
- handleLinkedinError(err);
10443
- }
10444
- }
10445
- });
10446
- var showCommand = defineCommand45({
10447
- meta: {
10448
- name: "show",
10449
- description: "Print the full staged payload for one op \u2014 the receipt to verify a change looks right before publish (never truncated)."
10450
- },
10451
- args: {
10452
- ref: { type: "positional", description: "Staged op ref (li_temp_*) or target id/URN", required: true }
10453
- },
10454
- run: async ({ args }) => {
10455
- const ref = requireTarget2(args, "op");
10456
- try {
10457
- const chatId = requireChatId();
10458
- const body = { chatId, ref };
10459
- const response = await apiPost(
10460
- "/api/ads/linkedin/draft/show",
10461
- body
10462
- );
10463
- writeJsonEnvelope(response);
10464
- } catch (err) {
10465
- handleLinkedinError(err);
10466
- }
10467
- }
10468
- });
10469
9829
  var linkedinDraftCommand = defineCommand45({
10470
9830
  meta: {
10471
9831
  name: "draft",
10472
- description: "Review and edit the LinkedIn write ops staged in this chat BEFORE publish. Subcommands: list (default), remove, clear, amend, show. Ops never hit LinkedIn until the chat is published."
9832
+ description: "Review and edit the LinkedIn write ops staged in this chat BEFORE publish. Subcommands: list (default), remove, clear. Ops never hit LinkedIn until the chat is published."
10473
9833
  },
10474
9834
  subCommands: {
10475
9835
  list: listCommand3,
10476
9836
  remove: removeCommand2,
10477
- clear: clearCommand2,
10478
- amend: amendCommand,
10479
- show: showCommand
9837
+ clear: clearCommand2
10480
9838
  },
10481
- run: async () => {
10482
- await listDraft(false);
10483
- }
9839
+ run: listDraft
10484
9840
  });
10485
9841
 
10486
9842
  // src/commands/ads/linkedin/facets.ts
@@ -13481,7 +12837,7 @@ async function probeDuration(filePath) {
13481
12837
 
13482
12838
  // src/commands/canvas/run.ts
13483
12839
  import { readFile as readFile2 } from "fs/promises";
13484
- import path4 from "path";
12840
+ import path5 from "path";
13485
12841
  import { defineCommand as defineCommand85 } from "citty";
13486
12842
 
13487
12843
  // src/commands/canvas/placeholders.ts
@@ -13525,9 +12881,102 @@ function isResolvableRelative(value) {
13525
12881
  return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !path2.isAbsolute(value);
13526
12882
  }
13527
12883
 
12884
+ // src/commands/canvas/run-record.ts
12885
+ import path3 from "path";
12886
+ var MAX_RUN_NODES = 200;
12887
+ var MAX_OUTPUTS_PER_NODE = 10;
12888
+ var MAX_FINAL_OUTPUTS = 10;
12889
+ var MAX_CREATIVE_SLUG_LENGTH = 100;
12890
+ function creativeSlugFromCanvasPath(filePath) {
12891
+ const normalized = filePath.split(path3.sep).join("/");
12892
+ const match = normalized.match(/(?:^|\/)src\/creatives\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\//);
12893
+ const slug = match?.[1] ?? null;
12894
+ return slug && slug.length <= MAX_CREATIVE_SLUG_LENGTH ? slug : null;
12895
+ }
12896
+ var OUTPUT_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
12897
+ function toRecordOutput(slot, value) {
12898
+ const refs = collectAssetRefLikes(value);
12899
+ const ref = refs.length === 1 ? refs[0] : null;
12900
+ if (!ref || !isPersistedAssetRef(ref)) return null;
12901
+ const kind = typeof ref.kind === "string" && OUTPUT_KINDS.has(ref.kind) ? ref.kind : null;
12902
+ if (!kind) return null;
12903
+ return {
12904
+ slot,
12905
+ kind,
12906
+ sha256: ref.sha256,
12907
+ url: ref.url,
12908
+ mime: ref.mime,
12909
+ width: typeof ref.width === "number" ? ref.width : void 0,
12910
+ height: typeof ref.height === "number" ? ref.height : void 0,
12911
+ durationMs: typeof ref.duration_ms === "number" ? ref.duration_ms : void 0
12912
+ };
12913
+ }
12914
+ function nodeOutputsToRecord(nodeOutputs) {
12915
+ const out = [];
12916
+ for (const [slot, value] of Object.entries(nodeOutputs)) {
12917
+ if (Array.isArray(value)) {
12918
+ value.forEach((item, i) => {
12919
+ const rec = toRecordOutput(`${slot}#${i}`, item);
12920
+ if (rec) out.push(rec);
12921
+ });
12922
+ } else {
12923
+ const rec = toRecordOutput(slot, value);
12924
+ if (rec) out.push(rec);
12925
+ }
12926
+ }
12927
+ return out.slice(0, MAX_OUTPUTS_PER_NODE);
12928
+ }
12929
+ function finalOutputsToRecord(output) {
12930
+ if (Array.isArray(output)) {
12931
+ return output.map((item, i) => toRecordOutput(`final#${i}`, item)).filter((rec2) => rec2 !== null).slice(0, MAX_FINAL_OUTPUTS);
12932
+ }
12933
+ const rec = toRecordOutput("final", output);
12934
+ return rec ? [rec] : [];
12935
+ }
12936
+ function buildRunRecord(result, meta) {
12937
+ const nodes = result.node_runs.slice(0, MAX_RUN_NODES).map((run) => ({
12938
+ nodeId: run.node_id,
12939
+ nodeType: run.node_type,
12940
+ cached: run.cached,
12941
+ credits: run.credits,
12942
+ durationMs: run.duration_ms,
12943
+ outputs: nodeOutputsToRecord(result.outputs_by_node[run.node_id] ?? {})
12944
+ }));
12945
+ const finalOutputs = finalOutputsToRecord(result.output);
12946
+ return {
12947
+ runId: result.run_id,
12948
+ creativeSlug: meta.creativeSlug,
12949
+ canvasPath: meta.canvasPath,
12950
+ canvasSha: meta.canvasSha,
12951
+ chatId: meta.chatId,
12952
+ status: "completed",
12953
+ stats: {
12954
+ totalNodes: result.stats.total_nodes,
12955
+ cachedNodes: result.stats.cached_nodes,
12956
+ totalCredits: result.stats.total_credits,
12957
+ durationMs: result.stats.duration_ms
12958
+ },
12959
+ nodes,
12960
+ finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0
12961
+ };
12962
+ }
12963
+ function buildFailedRunRecord(runId, errorMessage, meta) {
12964
+ return {
12965
+ runId,
12966
+ creativeSlug: meta.creativeSlug,
12967
+ canvasPath: meta.canvasPath,
12968
+ canvasSha: meta.canvasSha,
12969
+ chatId: meta.chatId,
12970
+ status: "failed",
12971
+ errorMessage: errorMessage.slice(0, 2e3),
12972
+ stats: { totalNodes: 0, cachedNodes: 0, totalCredits: 0, durationMs: 0 },
12973
+ nodes: []
12974
+ };
12975
+ }
12976
+
13528
12977
  // src/commands/canvas/run-retention.ts
13529
12978
  import { rm } from "fs/promises";
13530
- import path3 from "path";
12979
+ import path4 from "path";
13531
12980
  function runDirsToPrune(entries, keep, currentRunId) {
13532
12981
  const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
13533
12982
  if (keep <= 0) return runs;
@@ -13544,7 +12993,7 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
13544
12993
  const toPrune = runDirsToPrune(entries, keep, currentRunId);
13545
12994
  if (toPrune.length === 0) return;
13546
12995
  for (const dir of toPrune) {
13547
- await rm(path3.join(outputsDir, dir), { recursive: true, force: true }).catch(
12996
+ await rm(path4.join(outputsDir, dir), { recursive: true, force: true }).catch(
13548
12997
  (e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
13549
12998
  );
13550
12999
  }
@@ -13571,10 +13020,22 @@ var runCommand = defineCommand85({
13571
13020
  "keep-runs": {
13572
13021
  type: "string",
13573
13022
  description: "After the run, prune old r_* run dirs, keeping the N newest (off by default)"
13023
+ },
13024
+ "remote-cache": {
13025
+ type: "string",
13026
+ description: "on | off \u2014 company-scoped remote cache + durable asset persistence (default on; env BAKER_CANVAS_REMOTE_CACHE)"
13027
+ },
13028
+ // citty consumes any `--no-<flag>` as a negation of `<flag>`, so the
13029
+ // opt-out spelling `--no-record` requires the flag to be named `record`
13030
+ // (a literal "no-record" arg would never receive a value).
13031
+ record: {
13032
+ type: "boolean",
13033
+ default: true,
13034
+ description: "Post the durable run-history record to Baker (disable with --no-record)"
13574
13035
  }
13575
13036
  },
13576
13037
  async run({ args }) {
13577
- const filePath = path4.resolve(String(args.file));
13038
+ const filePath = path5.resolve(String(args.file));
13578
13039
  const raw = await readFile2(filePath, "utf8");
13579
13040
  let parsed;
13580
13041
  try {
@@ -13585,7 +13046,7 @@ var runCommand = defineCommand85({
13585
13046
  `);
13586
13047
  process.exit(2);
13587
13048
  }
13588
- parsed = resolveRelativeCanvasPaths(parsed, path4.dirname(filePath));
13049
+ parsed = resolveRelativeCanvasPaths(parsed, path5.dirname(filePath));
13589
13050
  const pending = unsuppliedPlaceholderAssets(parsed);
13590
13051
  if (pending.length > 0) {
13591
13052
  process.stderr.write(
@@ -13605,16 +13066,26 @@ var runCommand = defineCommand85({
13605
13066
  );
13606
13067
  process.exit(2);
13607
13068
  }
13069
+ const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
13608
13070
  const engine = createEngineFromEnv({
13609
13071
  cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
13610
13072
  outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
13611
13073
  log: (line) => process.stdout.write(`${line}
13612
- `)
13074
+ `),
13075
+ remoteCache
13613
13076
  });
13077
+ const runId = args["run-id"] ? String(args["run-id"]) : `r_${ulid()}`;
13078
+ const recordMeta = {
13079
+ creativeSlug: creativeSlugFromCanvasPath(filePath) ?? void 0,
13080
+ canvasPath: path5.relative(process.cwd(), filePath) || void 0,
13081
+ canvasSha: sha256Hex(Buffer.from(raw)),
13082
+ chatId: getEnv().BAKER_CHAT_ID || void 0
13083
+ };
13084
+ const record = args.record === false ? null : buildRecorder();
13614
13085
  try {
13615
13086
  const policy = args["cache-policy"] ?? "read_write";
13616
13087
  const result = await engine.run(parsed, {
13617
- run_id: args["run-id"] ? String(args["run-id"]) : void 0,
13088
+ run_id: runId,
13618
13089
  cache_policy: policy,
13619
13090
  concurrency: resolveConcurrency(
13620
13091
  // --concurrency wins; --parallel is the discoverable alias for the same bound.
@@ -13622,9 +13093,10 @@ var runCommand = defineCommand85({
13622
13093
  process.env.BAKER_CANVAS_CONCURRENCY
13623
13094
  )
13624
13095
  });
13096
+ if (record) await record(buildRunRecord(result, recordMeta));
13625
13097
  const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
13626
13098
  if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
13627
- const outputsDir = args["outputs-dir"] ? path4.resolve(String(args["outputs-dir"])) : path4.resolve("canvas");
13099
+ const outputsDir = args["outputs-dir"] ? path5.resolve(String(args["outputs-dir"])) : path5.resolve("canvas");
13628
13100
  await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
13629
13101
  `));
13630
13102
  }
@@ -13652,6 +13124,7 @@ var runCommand = defineCommand85({
13652
13124
  }
13653
13125
  if (e instanceof LayerExecutionError) {
13654
13126
  const failures = e.failures.map((f) => ({ node_id: f.nodeId, message: describeFailureReason(f.reason) }));
13127
+ if (record) await record(buildFailedRunRecord(runId, e.message, recordMeta));
13655
13128
  process.stderr.write(
13656
13129
  `${JSON.stringify({ ok: false, error: { code: "runtime", message: e.message, failures } }, null, 2)}
13657
13130
  `
@@ -13659,16 +13132,30 @@ var runCommand = defineCommand85({
13659
13132
  process.exit(1);
13660
13133
  }
13661
13134
  const msg = e instanceof Error ? e.message : String(e);
13135
+ if (record) await record(buildFailedRunRecord(runId, msg, recordMeta));
13662
13136
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "runtime", message: msg } }, null, 2)}
13663
13137
  `);
13664
13138
  process.exit(1);
13665
13139
  }
13666
13140
  }
13667
13141
  });
13142
+ function buildRecorder() {
13143
+ return async (payload) => {
13144
+ try {
13145
+ const creds = requireCredentialsFromEnv();
13146
+ const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
13147
+ await client.recordRun(payload);
13148
+ } catch (e) {
13149
+ const msg = e instanceof Error ? e.message : String(e);
13150
+ process.stderr.write(`[warn] run record not persisted (${msg})
13151
+ `);
13152
+ }
13153
+ };
13154
+ }
13668
13155
 
13669
13156
  // src/commands/canvas/scaffold-static-ad.ts
13670
- import { readFile as readFile3, writeFile } from "fs/promises";
13671
- import path6 from "path";
13157
+ import { access, cp, mkdir, readFile as readFile3, writeFile } from "fs/promises";
13158
+ import path8 from "path";
13672
13159
  import { defineCommand as defineCommand86 } from "citty";
13673
13160
 
13674
13161
  // src/engine/scaffold/staticAd.ts
@@ -13854,18 +13341,106 @@ function staticAdReport(input, elementsInput, opts) {
13854
13341
  };
13855
13342
  }
13856
13343
 
13344
+ // src/commands/canvas/creative-definition.ts
13345
+ import path6 from "path";
13346
+ var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
13347
+ var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
13348
+ function titleFromSlug(slug) {
13349
+ const title = slug.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
13350
+ return title || slug;
13351
+ }
13352
+ function resolvePlatform(platform) {
13353
+ const value = platform?.trim();
13354
+ return value && PLATFORM_VALUES.includes(value) ? value : "meta";
13355
+ }
13356
+ function resolveFormats(aspect) {
13357
+ const value = aspect?.trim();
13358
+ return value && FORMAT_VALUES.includes(value) ? [value] : ["4:5"];
13359
+ }
13360
+ function referenceRelativePath(kind, ext) {
13361
+ const name = kind === "video" ? "source" : "original";
13362
+ return `references/${name}${ext}`;
13363
+ }
13364
+ function sourceExtension(source, isUrl, kind) {
13365
+ const raw = isUrl ? urlPathname(source) : source;
13366
+ const ext = path6.extname(raw).toLowerCase();
13367
+ if (/^\.[a-z0-9]{1,5}$/.test(ext)) return ext;
13368
+ return kind === "video" ? ".mp4" : ".jpg";
13369
+ }
13370
+ function urlPathname(source) {
13371
+ try {
13372
+ return new URL(source).pathname;
13373
+ } catch {
13374
+ return source;
13375
+ }
13376
+ }
13377
+ function describeBlueprintIntent(blueprint) {
13378
+ const intent = blueprint?.ad_intent;
13379
+ if (typeof intent === "string" && intent.trim()) return intent.trim();
13380
+ if (intent && typeof intent === "object") {
13381
+ const summary = intent.summary ?? intent.feeling;
13382
+ if (typeof summary === "string" && summary.trim()) return summary.trim();
13383
+ }
13384
+ return void 0;
13385
+ }
13386
+ function yamlScalar(value) {
13387
+ return JSON.stringify(value);
13388
+ }
13389
+ function buildCreativeDefinition(input) {
13390
+ const lines = ["---", `title: ${yamlScalar(input.title)}`, `kind: ${input.kind}`, `platform: ${input.platform}`];
13391
+ lines.push(`formats: [${input.formats.map(yamlScalar).join(", ")}]`);
13392
+ lines.push(`status: ${input.status ?? "draft"}`);
13393
+ if (input.sourceReferenceUrl) lines.push(`sourceReferenceUrl: ${yamlScalar(input.sourceReferenceUrl)}`);
13394
+ if (input.sourceAdvertiser) lines.push(`sourceAdvertiser: ${yamlScalar(input.sourceAdvertiser)}`);
13395
+ if (input.sourceKind) lines.push(`sourceKind: ${input.sourceKind}`);
13396
+ if (input.sourcePath) lines.push(`sourcePath: ${yamlScalar(input.sourcePath)}`);
13397
+ lines.push("---", "");
13398
+ lines.push(input.description?.trim() || `${input.title} \u2014 canvas-built ${input.kind} ad for ${input.platform}.`);
13399
+ lines.push("");
13400
+ return lines.join("\n");
13401
+ }
13402
+
13857
13403
  // src/commands/canvas/scaffold-static-ad-paths.ts
13858
- import path5 from "path";
13859
- function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd()) {
13404
+ import path7 from "path";
13405
+ function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
13860
13406
  const file = rawFile.trim();
13861
13407
  const imageIsUrl = /^https?:\/\//i.test(file);
13862
- const imageSource = imageIsUrl ? file : path5.resolve(cwd, file);
13863
- const outPath = out ? path5.resolve(cwd, out) : imageIsUrl ? path5.join(cwd, "static-ad.canvas.json") : path5.join(path5.dirname(imageSource), "static-ad.canvas.json");
13864
- const blueprintPath = path5.join(path5.dirname(outPath), "prompt.json");
13865
- return { imageIsUrl, imageSource, outPath, blueprintPath };
13408
+ const imageSource = imageIsUrl ? file : path7.resolve(cwd, file);
13409
+ const outPath = out ? path7.resolve(cwd, out) : slug ? path7.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path7.join(cwd, "static-ad.canvas.json") : path7.join(path7.dirname(imageSource), "static-ad.canvas.json");
13410
+ const blueprintPath = path7.join(path7.dirname(outPath), "prompt.json");
13411
+ const creativeDir = slug ? path7.dirname(outPath) : null;
13412
+ const definitionPath = creativeDir ? path7.join(creativeDir, "_definition.md") : null;
13413
+ const referencesDir = creativeDir ? path7.join(creativeDir, "references") : null;
13414
+ return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
13415
+ }
13416
+ var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
13417
+ var SCAFFOLD_SLUG_MAX_LENGTH = 100;
13418
+ function isValidScaffoldSlug(slug) {
13419
+ return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
13866
13420
  }
13867
13421
 
13868
13422
  // src/commands/canvas/scaffold-static-ad.ts
13423
+ async function fileExists(target) {
13424
+ try {
13425
+ await access(target);
13426
+ return true;
13427
+ } catch {
13428
+ return false;
13429
+ }
13430
+ }
13431
+ async function copySourceIntoReferences(source, isUrl, referencesDir) {
13432
+ await mkdir(referencesDir, { recursive: true });
13433
+ const relPath = referenceRelativePath("image", sourceExtension(source, isUrl, "image"));
13434
+ const dest = path8.join(referencesDir, path8.basename(relPath));
13435
+ if (isUrl) {
13436
+ const res = await fetch(source);
13437
+ if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
13438
+ await writeFile(dest, Buffer.from(await res.arrayBuffer()));
13439
+ } else {
13440
+ await cp(source, dest);
13441
+ }
13442
+ return relPath;
13443
+ }
13869
13444
  function resolveModel(kind, preferred) {
13870
13445
  const ids = Object.keys(MODEL_REGISTRY[kind]);
13871
13446
  return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
@@ -14025,6 +13600,16 @@ var scaffoldStaticAdCommand = defineCommand86({
14025
13600
  file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
14026
13601
  context: { type: "string", description: "Known provenance (advertiser, category, market) to ground the describe" },
14027
13602
  out: { type: "string", description: "Output canvas path (default <image-dir>/static-ad.canvas.json)" },
13603
+ slug: {
13604
+ type: "string",
13605
+ description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
13606
+ },
13607
+ title: { type: "string", description: "Creative title for _definition.md (default: title-cased slug)" },
13608
+ platform: {
13609
+ type: "string",
13610
+ description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
13611
+ },
13612
+ advertiser: { type: "string", description: "Source advertiser recorded in _definition.md" },
14028
13613
  "describe-model": { type: "string", description: "Override the image_describe model id" },
14029
13614
  "select-model": { type: "string", description: "Override the text_generate model id for element selection" },
14030
13615
  "layout-model": { type: "string", description: "Override the text_generate model id for the layout pass" },
@@ -14033,10 +13618,21 @@ var scaffoldStaticAdCommand = defineCommand86({
14033
13618
  "skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" }
14034
13619
  },
14035
13620
  async run({ args }) {
14036
- const { imageIsUrl, imageSource, outPath, blueprintPath } = resolveScaffoldStaticAdPaths(
13621
+ const slug = args.slug ? String(args.slug) : void 0;
13622
+ if (slug && !isValidScaffoldSlug(slug)) {
13623
+ process.stderr.write(
13624
+ `${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
13625
+ `
13626
+ );
13627
+ process.exit(2);
13628
+ }
13629
+ const { imageIsUrl, imageSource, outPath, blueprintPath, definitionPath, referencesDir } = resolveScaffoldStaticAdPaths(
14037
13630
  String(args.file),
14038
- args.out ? String(args.out) : void 0
13631
+ args.out ? String(args.out) : void 0,
13632
+ process.cwd(),
13633
+ slug
14039
13634
  );
13635
+ await mkdir(path8.dirname(outPath), { recursive: true });
14040
13636
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
14041
13637
  const describeCanvas = buildDescribeCanvas(
14042
13638
  imageSource,
@@ -14053,11 +13649,21 @@ var scaffoldStaticAdCommand = defineCommand86({
14053
13649
  }
14054
13650
  await writeFile(blueprintPath, `${JSON.stringify(annotated, null, 2)}
14055
13651
  `, "utf8");
13652
+ let canvasImagePath = imageSource;
13653
+ let canvasImageIsUrl = imageIsUrl;
13654
+ let canvasBlueprintPath = blueprintPath;
13655
+ let sourceRelPath;
13656
+ if (referencesDir) {
13657
+ sourceRelPath = await copySourceIntoReferences(imageSource, imageIsUrl, referencesDir);
13658
+ canvasImagePath = sourceRelPath;
13659
+ canvasImageIsUrl = false;
13660
+ canvasBlueprintPath = "./prompt.json";
13661
+ }
14056
13662
  const opts = {
14057
13663
  genModel,
14058
- imagePath: imageSource,
14059
- imageIsUrl,
14060
- blueprintPath,
13664
+ imagePath: canvasImagePath,
13665
+ imageIsUrl: canvasImageIsUrl,
13666
+ blueprintPath: canvasBlueprintPath,
14061
13667
  aspectRatio: args.aspect ? String(args.aspect) : void 0,
14062
13668
  includeFont: !args["skip-font"]
14063
13669
  };
@@ -14079,12 +13685,31 @@ var scaffoldStaticAdCommand = defineCommand86({
14079
13685
  }
14080
13686
  await writeFile(outPath, `${JSON.stringify(canvas, null, 2)}
14081
13687
  `, "utf8");
13688
+ if (definitionPath && !await fileExists(definitionPath)) {
13689
+ await writeFile(
13690
+ definitionPath,
13691
+ buildCreativeDefinition({
13692
+ title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
13693
+ kind: "static",
13694
+ platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
13695
+ formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
13696
+ sourceReferenceUrl: imageIsUrl ? imageSource : void 0,
13697
+ sourceAdvertiser: args.advertiser ? String(args.advertiser) : args.context ? String(args.context) : void 0,
13698
+ sourceKind: "image",
13699
+ sourcePath: sourceRelPath,
13700
+ description: describeBlueprintIntent(blueprint)
13701
+ }),
13702
+ "utf8"
13703
+ );
13704
+ }
14082
13705
  process.stdout.write(
14083
13706
  `${JSON.stringify(
14084
13707
  {
14085
13708
  ok: true,
14086
13709
  canvas_path: outPath,
14087
13710
  prompt_path: blueprintPath,
13711
+ definition_path: definitionPath ?? void 0,
13712
+ source_reference: sourceRelPath ?? void 0,
14088
13713
  output: canvas.output,
14089
13714
  models: { describe: describeModel, select: selectModel, layout: layoutModel, gen: opts.genModel },
14090
13715
  aspect_ratio: report.aspect_ratio,
@@ -14095,7 +13720,7 @@ var scaffoldStaticAdCommand = defineCommand86({
14095
13720
  run_estimated_credits: validation.estimatedCredits
14096
13721
  },
14097
13722
  checklist: {
14098
- edit_prompt: `Edit ${path6.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
13723
+ edit_prompt: `Edit ${path8.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
14099
13724
  assets_to_supply: report.elements,
14100
13725
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path, or delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
14101
13726
  note: "Replace every [TODO] ingest path with a real file, then `baker canvas validate` and `baker canvas run`. Running generates a billed image \u2014 it is not free."
@@ -14110,8 +13735,8 @@ var scaffoldStaticAdCommand = defineCommand86({
14110
13735
  });
14111
13736
 
14112
13737
  // src/commands/canvas/scaffold-video.ts
14113
- import { cp, mkdir, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
14114
- import path9 from "path";
13738
+ import { cp as cp2, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
13739
+ import path11 from "path";
14115
13740
  import { defineCommand as defineCommand87 } from "citty";
14116
13741
 
14117
13742
  // src/engine/nodes/local/lib/sceneDetect.ts
@@ -14141,16 +13766,6 @@ function isLikelyOverSegmented(cuts, opts = {}) {
14141
13766
  const median = gaps.length % 2 ? gaps[mid] : (gaps[mid - 1] + gaps[mid]) / 2;
14142
13767
  return median < maxMedianGap;
14143
13768
  }
14144
- function mergeRecheckCuts(firstPass, recheck, isolationS = 0.75) {
14145
- const kept = [...new Set(recheck)].sort((a, b) => a - b);
14146
- const extras = [...new Set(firstPass)].filter((c) => kept.every((k) => Math.abs(k - c) >= isolationS)).sort((a, b) => a - b);
14147
- const isolated = extras.filter((c, i) => {
14148
- const prev = extras[i - 1];
14149
- const next = extras[i + 1];
14150
- return (prev === void 0 || c - prev >= isolationS) && (next === void 0 || next - c >= isolationS);
14151
- });
14152
- return [.../* @__PURE__ */ new Set([...kept, ...isolated])].sort((a, b) => a - b);
14153
- }
14154
13769
  function timecodeToSeconds(tc) {
14155
13770
  const m = tc.trim().match(/^(\d+):(\d{1,2}):(\d{1,2}(?:\.\d+)?)$/);
14156
13771
  if (!m) return null;
@@ -14204,13 +13819,12 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
14204
13819
  const timeoutMs = opts.timeout_ms ?? 12e4;
14205
13820
  const cuts = await runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs);
14206
13821
  if (!pinned && isLikelyOverSegmented(cuts)) {
14207
- const rechecked = await runSceneDetectOnce(
13822
+ return await runSceneDetectOnce(
14208
13823
  filePath,
14209
13824
  PYSCENEDETECT_RECHECK_THRESHOLD,
14210
13825
  PYSCENEDETECT_RECHECK_MIN_SCENE_LEN_S,
14211
13826
  timeoutMs
14212
13827
  );
14213
- return mergeRecheckCuts(cuts, rechecked);
14214
13828
  }
14215
13829
  return cuts;
14216
13830
  }
@@ -14348,22 +13962,9 @@ function videoResolutionParam(videoModel, resolution) {
14348
13962
  return { resolution: resolution ?? DEFAULT_VIDEO_RESOLUTION };
14349
13963
  }
14350
13964
  var WORDS_PER_SECOND = 2.5;
14351
- function wordCount(text) {
14352
- return text.trim().split(/\s+/).filter(Boolean).length;
14353
- }
14354
13965
  function estSpeechS(text) {
14355
- return wordCount(text) / WORDS_PER_SECOND;
14356
- }
14357
- var OBSERVED_WPS_MIN = 1;
14358
- var OBSERVED_WPS_MAX = 6;
14359
- function estSpeechWindowS(text, startS, endS) {
14360
- const words = wordCount(text);
14361
- const window = (endS ?? 0) - (startS ?? 0);
14362
- if (words > 0 && window > 0.3) {
14363
- const wps = words / window;
14364
- if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window;
14365
- }
14366
- return estSpeechS(text);
13966
+ const words = text.trim().split(/\s+/).filter(Boolean).length;
13967
+ return words / WORDS_PER_SECOND;
14367
13968
  }
14368
13969
  var NARRATOR_SPEAKERS = /* @__PURE__ */ new Set([
14369
13970
  "voiceover",
@@ -14549,13 +14150,7 @@ var DialogueLine = z11.object({
14549
14150
  start_s: z11.number().optional(),
14550
14151
  end_s: z11.number().optional(),
14551
14152
  delivery: z11.string().optional(),
14552
- voice_description: z11.string().optional(),
14553
- // DECON-supplied: is this speaker's FACE visibly speaking in THIS scene? Element
14554
- // presence alone can't answer that — a founder pictured in a polaroid close-up is
14555
- // "present" yet the line is voiceover, and treating it as on-camera produced a
14556
- // native Seedance lip-sync clip of a still photograph. `false` pins the line to
14557
- // the VO path; absent keeps the presence-based decision (old blueprints).
14558
- on_camera: z11.boolean().optional()
14153
+ voice_description: z11.string().optional()
14559
14154
  }).loose();
14560
14155
  var Sfx = z11.object({
14561
14156
  at_s: z11.number().optional(),
@@ -14571,22 +14166,6 @@ var CompositionRegion = z11.object({
14571
14166
  is_presenter: z11.boolean().optional(),
14572
14167
  // The cast id shown/speaking in this region (routes lip-sync + element refs).
14573
14168
  cast_ref: z11.string().optional(),
14574
- // What the region's content IS: camera | screen_capture | static_graphic |
14575
- // generated. Authoritative for routing when present (regex-over-prose fallback
14576
- // otherwise): screen_capture/static_graphic are rebuilt from REAL surfaces on the
14577
- // overlay layer, never AI-generated.
14578
- kind: z11.string().optional(),
14579
- // Opaque id naming the SPECIFIC on-screen document/note/app-state this
14580
- // screen_capture region shows. Two scenes share it only when they show the SAME
14581
- // recording continuing (scrolling/typing/waiting within it) — a genuinely
14582
- // DIFFERENT document/note/recording (a source video splicing two screen captures)
14583
- // gets a different id. Breaks a persistent-layout run into separate surface stubs
14584
- // instead of asking the operator for one screenshot that can't cover both.
14585
- surface_id: z11.string().optional(),
14586
- // Camera bubble(s)/inset(s) embedded INSIDE this region's surface (a Loom-style
14587
- // presenter bubble inside a screen recording) — video-in-video the reproduction
14588
- // must re-composite, not paint into the surface.
14589
- nested: z11.array(z11.object({}).loose()).optional(),
14590
14169
  summary: z11.string().optional(),
14591
14170
  frame_prompt: z11.string().optional(),
14592
14171
  motion_prompt: z11.string().optional()
@@ -14640,11 +14219,6 @@ var Scene = z11.object({
14640
14219
  sfx: z11.array(Sfx).optional(),
14641
14220
  overlays: z11.array(z11.unknown()).optional(),
14642
14221
  floating_elements: z11.array(z11.unknown()).optional(),
14643
- // DECON-supplied: how much the picture itself moves within the shot. Gates the
14644
- // flash-hold optimization — a sub-2s b-roll flash with REAL subject motion
14645
- // (pouring, spreading, hands working) must stay a real clip; freezing it turns
14646
- // a montage into a slideshow. Absent (old blueprints) keeps the cheap still.
14647
- motion_level: z11.enum(["static", "subtle", "dynamic"]).optional(),
14648
14222
  transcript_slice: z11.array(TranscriptWord).optional(),
14649
14223
  start_frame_asset: FrameAsset,
14650
14224
  end_frame_asset: FrameAsset,
@@ -14884,33 +14458,6 @@ function buildElementSlots(elements) {
14884
14458
  function slotsForFrame(slots, sceneIndex, edge) {
14885
14459
  return slots.filter((s) => s.presence.get(sceneIndex)?.has(edge));
14886
14460
  }
14887
- var MENTION_WIRED_TYPES = /* @__PURE__ */ new Set(["logo", "badge"]);
14888
- function promptMentionsSlot(prompt, slot) {
14889
- return elementMentionKeywords(slot).some(
14890
- (w) => new RegExp(`\\b${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(prompt)
14891
- );
14892
- }
14893
- function edgePromptText(scene, edge) {
14894
- const regionPrompts = (compositeRegionsOf(scene) ?? []).map((r) => r.frame_prompt ?? "").filter(Boolean).join("\n");
14895
- const own = (edge === "start" ? scene.start_frame_prompt : scene.end_frame_prompt) ?? "";
14896
- return [own, regionPrompts].join("\n").trim();
14897
- }
14898
- function extendPresenceByPromptMentions(slots, blueprint) {
14899
- const extendable = slots.filter((s) => MENTION_WIRED_TYPES.has(s.type.toLowerCase()));
14900
- if (extendable.length === 0) return;
14901
- blueprint.scenes.forEach((scene, i) => {
14902
- for (const edge of EDGES) {
14903
- const text = edgePromptText(scene, edge);
14904
- if (!text) continue;
14905
- for (const slot of extendable) {
14906
- if (!promptMentionsSlot(text, slot)) continue;
14907
- const set = slot.presence.get(i) ?? /* @__PURE__ */ new Set();
14908
- set.add(edge);
14909
- slot.presence.set(i, set);
14910
- }
14911
- }
14912
- });
14913
- }
14914
14461
  var ACTOR_SHEET_MODEL = "google/gemini-3-pro-image-preview";
14915
14462
  var SHEET_SUBJECT_TYPE = {
14916
14463
  person: "person",
@@ -15093,94 +14640,21 @@ function seedanceAudioLine(scene, mode, audio, nativeLine) {
15093
14640
  }
15094
14641
  return null;
15095
14642
  }
15096
- var FLOAT_STOP_WORDS = /* @__PURE__ */ new Set([
15097
- "with",
15098
- "across",
15099
- "around",
15100
- "above",
15101
- "below",
15102
- "over",
15103
- "under",
15104
- "screen",
15105
- "frame",
15106
- "scene",
15107
- "video",
15108
- "element",
15109
- "elements",
15110
- "graphic",
15111
- "overlay",
15112
- "animated",
15113
- "floating",
15114
- "small",
15115
- "large",
15116
- "white",
15117
- "black",
15118
- "color",
15119
- "colored"
15120
- ]);
15121
- function floatTokens(desc) {
15122
- return desc.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4 && !FLOAT_STOP_WORDS.has(w));
15123
- }
15124
- function scrubFloatSentences(text, floatDescs) {
15125
- if (floatDescs.length === 0 || !text) return text;
15126
- const tokenSets = floatDescs.map((d) => new Set(floatTokens(d)));
15127
- const kept = text.split(/(?<=[.!?])\s+/).filter((sentence) => {
15128
- const words = new Set(floatTokens(sentence));
15129
- return !tokenSets.some((ts) => {
15130
- let hits = 0;
15131
- for (const w of words) if (ts.has(w)) hits++;
15132
- return hits >= 2;
15133
- });
15134
- }).join(" ").trim();
15135
- return kept;
15136
- }
15137
- function sceneFloatDescs(scene) {
15138
- const floats = z11.array(FloatingElement).safeParse(scene.floating_elements ?? []);
15139
- if (!floats.success) return [];
15140
- return floats.data.map((f) => f.description?.trim() ?? "").filter(Boolean);
15141
- }
15142
- function visualBriefParts(scene, sceneIndex, plate) {
14643
+ function buildSeedancePrompt(scene, sceneIndex, present, mode, audio, nativeLine, nativeLang) {
14644
+ const loc = (s) => nativeLine ? localizeNumeralsForNative(s, nativeLang) : s;
15143
14645
  const parts = [];
15144
- const floats = sceneFloatDescs(scene);
15145
- const scrub = (s) => {
15146
- if (!s) return s;
15147
- const out = scrubFloatSentences(s, floats);
15148
- return out || void 0;
15149
- };
15150
- const summary = scrub((plate ? plate.summary : scene.summary)?.trim());
15151
- parts.push(summary ? `Scene ${sceneIndex + 1}: ${summary}` : `Scene ${sceneIndex + 1}`);
15152
- const action = !plate ? scrub(scene.action_detail) : void 0;
15153
- if (action) parts.push(`Action: ${action}`);
14646
+ const summary = scene.summary?.trim();
14647
+ parts.push(summary ? `Scene ${sceneIndex + 1}: ${loc(summary)}` : `Scene ${sceneIndex + 1}`);
14648
+ if (scene.action_detail) parts.push(`Action: ${loc(scene.action_detail)}`);
15154
14649
  const cm = scene.camera_motion;
15155
14650
  if (cm) {
15156
14651
  const camera = [cm.movement, cm.detail].filter(Boolean).join(" \u2014 ");
15157
14652
  if (camera) parts.push(`Camera: ${camera}`);
15158
14653
  }
15159
- const motion = scrub(plate ? plate.motion_prompt : scene.motion_prompt);
15160
- if (motion) parts.push(`Motion: ${motion}`);
15161
- const uiFloats = floats.filter((d) => UI_SURFACE_RE.test(d));
15162
- const fxFloats = floats.filter((d) => !UI_SURFACE_RE.test(d));
15163
- if (plate || uiFloats.length > 0) {
14654
+ if (scene.motion_prompt) parts.push(`Motion: ${loc(scene.motion_prompt)}`);
14655
+ if (present.length > 0) {
15164
14656
  parts.push(
15165
- "NEVER render any screen, phone, app, website, or UI element \u2014 this clip is the clean background plate; the screen surface is composited on top later as a separate real layer."
15166
- );
15167
- }
15168
- if (fxFloats.length > 0) {
15169
- parts.push(
15170
- "NEVER render floating decorative elements \u2014 no hearts, sparkles, confetti, badges, stickers, icons, or particle effects. They are composited later as a separate real overlay layer; the picture stays clean of them."
15171
- );
15172
- }
15173
- return parts;
15174
- }
15175
- function buildSeedancePrompt(scene, sceneIndex, present, mode, audio, nativeLine, nativeLang, uiRouted) {
15176
- const loc = (s) => nativeLine ? localizeNumeralsForNative(s, nativeLang) : s;
15177
- const routed = uiRouted ?? (compositeRegionsOf(scene) !== null && isUiOnlyComposite(compositeRegionsOf(scene) ?? []));
15178
- const plate = routed ? compositePlateRegion(scene) ?? {} : null;
15179
- const parts = visualBriefParts(scene, sceneIndex, plate);
15180
- const refs = plate ? present.filter((s) => !UI_SURFACE_RE.test(s.description ?? "")) : present;
15181
- if (refs.length > 0) {
15182
- parts.push(
15183
- `Keep these consistent with their references: ${refs.map((s) => `${s.label} (${s.description ?? s.type})`).join("; ")}`
14657
+ `Keep these consistent with their references: ${present.map((s) => `${s.label} (${s.description ?? s.type})`).join("; ")}`
15184
14658
  );
15185
14659
  }
15186
14660
  if (nativeLine) {
@@ -15311,8 +14785,7 @@ function emitSceneClip(i, scene, present, mode, nativeTurn, ambientBroll, frames
15311
14785
  mode,
15312
14786
  Boolean(nativeTurn) || ambientBroll,
15313
14787
  nativeTurn?.text,
15314
- opts.nativeLang,
15315
- opts.uiRouted
14788
+ opts.nativeLang
15316
14789
  ),
15317
14790
  duration: lengths.genDur,
15318
14791
  ...videoResolutionParam(opts.videoModel, opts.resolution),
@@ -15341,59 +14814,12 @@ function emitSceneClip(i, scene, present, mode, nativeTurn, ambientBroll, frames
15341
14814
  var COMPOSITE_LAYOUTS = /* @__PURE__ */ new Set(["split_screen", "pip", "keyed_overlay"]);
15342
14815
  var UI_SURFACE_RE = /\b(?:app|ui|web ?site|web ?page|website|browser|chat|interface|mock-?up|in[- ]?app|dashboard|app screen|phone screen|screen[- ]?(?:recording|capture|grab|share))\b/i;
15343
14816
  function regionIsUiSurface(r) {
15344
- const kind = (r.kind ?? "").trim().toLowerCase();
15345
- if (kind === "screen_capture" || kind === "static_graphic") return true;
15346
- if (kind === "camera") return false;
15347
- return UI_SURFACE_RE.test(`${r.panel ?? ""} ${r.summary ?? ""}`);
14817
+ return UI_SURFACE_RE.test(`${r.panel ?? ""} ${r.summary ?? ""} ${r.frame_prompt ?? ""}`);
15348
14818
  }
15349
14819
  function isUiOnlyComposite(regions) {
15350
14820
  const ui = regions.filter(regionIsUiSurface).length;
15351
14821
  return ui >= 1 && regions.length - ui <= 1;
15352
14822
  }
15353
- function compositeRegionsOf(scene) {
15354
- const comp = scene.composition;
15355
- const layout = (comp?.layout ?? "").toLowerCase();
15356
- if (!COMPOSITE_LAYOUTS.has(layout)) return null;
15357
- const regions = (comp?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
15358
- return regions.length >= 2 ? regions : null;
15359
- }
15360
- function compositePlateRegion(scene) {
15361
- const regions = compositeRegionsOf(scene);
15362
- if (!regions) return null;
15363
- return regions.find((r) => r.is_presenter) ?? regions.find((r) => !regionIsUiSurface(r)) ?? null;
15364
- }
15365
- function uiRoutedSceneSet(input) {
15366
- return new Set(uiRoutedRuns(input).flat());
15367
- }
15368
- function uiRoutedRuns(input) {
15369
- const blueprint = VideoBlueprint.parse(input);
15370
- const info = blueprint.scenes.map((scene) => {
15371
- const regions = compositeRegionsOf(scene);
15372
- if (!regions) return null;
15373
- const comp = scene.composition;
15374
- const panels = regions.map((r) => (r.panel ?? "").toLowerCase()).sort().join(",");
15375
- const surfaceIds = regions.filter(regionIsUiSurface).map((r) => (r.surface_id ?? "").trim()).filter(Boolean).sort().join(",");
15376
- const sig = surfaceIds ? `sid:${surfaceIds}` : `${(comp?.layout ?? "").toLowerCase()}|${(comp?.split_axis ?? "").toLowerCase()}|${panels}`;
15377
- return { sig, ui: isUiOnlyComposite(regions) };
15378
- });
15379
- const runs = [];
15380
- let run = [];
15381
- const flushRun = () => {
15382
- if (run.some((i) => info[i]?.ui)) runs.push(run);
15383
- run = [];
15384
- };
15385
- info.forEach((inf, i) => {
15386
- if (!inf) {
15387
- flushRun();
15388
- return;
15389
- }
15390
- const prev = i > 0 ? info[i - 1] : null;
15391
- if (!prev || prev.sig !== inf.sig) flushRun();
15392
- run.push(i);
15393
- });
15394
- flushRun();
15395
- return runs;
15396
- }
15397
14823
  function sceneIsFullScreenUi(scene, present) {
15398
14824
  if (scene.narrative_role?.trim() === "cta") return false;
15399
14825
  const hasCast = present.some((s) => {
@@ -15427,11 +14853,14 @@ function screenStillArgs(durationS, dims) {
15427
14853
  "{{out.video}}"
15428
14854
  ];
15429
14855
  }
15430
- function layeredComposition(scene, uiRouted) {
15431
- const regions = compositeRegionsOf(scene);
15432
- if (!regions) return null;
15433
- if (uiRouted) return null;
15434
- return { layout: (scene.composition?.layout ?? "").toLowerCase(), regions, comp: scene.composition ?? {} };
14856
+ function layeredComposition(scene) {
14857
+ const comp = scene.composition;
14858
+ const layout = (comp?.layout ?? "").toLowerCase();
14859
+ if (!COMPOSITE_LAYOUTS.has(layout)) return null;
14860
+ const regions = (comp?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
14861
+ if (regions.length < 2) return null;
14862
+ if (isUiOnlyComposite(regions)) return null;
14863
+ return { layout, regions, comp: comp ?? {} };
15435
14864
  }
15436
14865
  function splitAxisOf(comp, regions) {
15437
14866
  const panels = regions.map((r) => (r.panel ?? "").toLowerCase());
@@ -15522,7 +14951,7 @@ function buildCompositeScene(layout, regions, comp, scene, i, present, mode, nat
15522
14951
  const bgIdx = regions.findIndex((_, k) => k !== presIdx);
15523
14952
  const bgRef = regionRefs[bgIdx >= 0 ? bgIdx : 0];
15524
14953
  let presRef = regionRefs[presIdx >= 0 ? presIdx : 1];
15525
- if (layout === "keyed_overlay") {
14954
+ if (layout === "keyed_overlay" && presIdx >= 0) {
15526
14955
  const keyId = `s${i}_key`;
15527
14956
  nodes.push({ id: keyId, type: "video_background_remove", inputs: { video: presRef }, params: {} });
15528
14957
  presRef = `$ref:${keyId}.video`;
@@ -15623,67 +15052,6 @@ function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
15623
15052
  });
15624
15053
  clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
15625
15054
  }
15626
- function sceneIsAllGraphic(scene) {
15627
- const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
15628
- if (regions.length === 0 || regions.some((r) => (r.kind ?? "").toLowerCase() === "camera")) return false;
15629
- return regions.some((r) => (r.kind ?? "").toLowerCase() === "static_graphic");
15630
- }
15631
- function sceneIsFullFrameGraphic(scene) {
15632
- return sceneIsAllGraphic(scene);
15633
- }
15634
- function fullFrameGraphicScenes(blueprint) {
15635
- const scenes = blueprint.scenes;
15636
- const graphic = new Set(scenes.flatMap((s, i) => sceneIsFullFrameGraphic(s) ? [i] : []));
15637
- const blocked = (s) => {
15638
- const regions = (s.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
15639
- if (regions.some((r) => (r.kind ?? "").toLowerCase() === "camera")) return true;
15640
- return (s.dialogue ?? []).some((d) => d.on_camera === true);
15641
- };
15642
- let changed = true;
15643
- while (changed) {
15644
- changed = false;
15645
- scenes.forEach((s, i) => {
15646
- if (graphic.has(i)) return;
15647
- const chainsToPrev = s.continues_previous && graphic.has(i - 1);
15648
- const next = scenes[i + 1];
15649
- const chainsToNext = Boolean(next?.continues_previous) && graphic.has(i + 1);
15650
- if ((chainsToPrev || chainsToNext) && !blocked(s)) {
15651
- graphic.add(i);
15652
- changed = true;
15653
- }
15654
- });
15655
- }
15656
- return graphic;
15657
- }
15658
- function emitGraphicScene(i, scene, lengths, out, outAr, surfaceIngests, nodes, clips) {
15659
- const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
15660
- const surfaceId = regions.find((r) => r.surface_id)?.surface_id;
15661
- let refId = surfaceId ? surfaceIngests.get(surfaceId) : void 0;
15662
- if (!refId) {
15663
- const label = commentSafe((scene.summary || scene.start_frame_prompt || "the designed panel").slice(0, 120));
15664
- refId = `s${i}_graphic_ref`;
15665
- nodes.push({
15666
- id: refId,
15667
- type: "ingest",
15668
- params: {
15669
- source: "path",
15670
- path: `[TODO: supply the REAL designed graphic for "${label}" \u2014 NEVER AI-generate a designed panel (its typography/wordmarks garble). Rebuild it as brand HTML in video-overlay-composition (the scene's text/logos are already seeded there), export the design from the brand kit, or source stock art; text rides the overlay layer, not this plate]`,
15671
- expect: "image"
15672
- }
15673
- });
15674
- if (surfaceId) surfaceIngests.set(surfaceId, refId);
15675
- }
15676
- nodes.push({
15677
- id: `s${i}_clip`,
15678
- type: "ffmpeg",
15679
- inputs: { frame: `$ref:${refId}.asset` },
15680
- params: {
15681
- args: screenStillArgs(lengths.trimTarget, canvasDims(outAr)),
15682
- outputs: { video: { kind: "video", ext: "mp4" } }
15683
- }
15684
- });
15685
- clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
15686
- }
15687
15055
  var BRAND_CARD_RE = /\b(?:solid|plain|flat|brand|logo|wordmark|end[- ]?card|cta card|title card|colou?r background|background colou?r)\b/i;
15688
15056
  function sceneIsBrandCard(scene, present, isCta) {
15689
15057
  if (!isCta) return false;
@@ -15924,12 +15292,8 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
15924
15292
  speaker: sp,
15925
15293
  // Shown = a cast member speaking AND their element is actually on screen
15926
15294
  // here (not a cutaway). A b-roll cutaway mid-phrase fails this and gets
15927
- // its own clip while the phrase voice plays under it. An explicit
15928
- // deconstruct voiceover stamp (`on_camera: false`) wins over element
15929
- // presence — a speaker pictured in a photo is "present" but not talking.
15930
- // An all-graphic composition (no camera region) is voiceover by
15931
- // definition: nobody is on screen to lip-sync.
15932
- shown: l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, casts, cameraOn) && !multiSpeaker.has(sceneIndex) && presenterPresent(sp, sceneIndex),
15295
+ // its own clip while the phrase voice plays under it.
15296
+ shown: isOnCameraSpeaker(raw, casts, cameraOn) && !multiSpeaker.has(sceneIndex) && presenterPresent(sp, sceneIndex),
15933
15297
  start,
15934
15298
  // Real speech end. When the deconstruct gives no end_s, estimate it from
15935
15299
  // the words — NOT the scene end (which would fabricate continuity across
@@ -16027,8 +15391,7 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
16027
15391
  shootMode: mode,
16028
15392
  ingestCache: env.ingestCache
16029
15393
  };
16030
- const chained = anchorScene.continues_previous && out.phraseEnd?.scene === anchor - 1 ? out.phraseEnd.ref : void 0;
16031
- const first = chained ?? buildFrameRef(
15394
+ const first = buildFrameRef(
16032
15395
  "start",
16033
15396
  anchorScene.start_frame_asset?.url,
16034
15397
  anchorScene.start_frame_prompt,
@@ -16055,16 +15418,7 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
16055
15418
  const genDur = ceilToSeedance(phraseLen);
16056
15419
  const clipParams = {
16057
15420
  model: env.opts.videoModel,
16058
- prompt: buildSeedancePrompt(
16059
- anchorScene,
16060
- anchor,
16061
- present,
16062
- mode,
16063
- true,
16064
- phrase.text,
16065
- env.ttsLanguageCode,
16066
- env.uiRouted.has(anchor)
16067
- ),
15421
+ prompt: buildSeedancePrompt(anchorScene, anchor, present, mode, true, phrase.text, env.ttsLanguageCode),
16068
15422
  duration: genDur,
16069
15423
  ...videoResolutionParam(env.opts.videoModel, env.opts.resolution),
16070
15424
  generate_audio: true
@@ -16076,7 +15430,6 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
16076
15430
  inputs: { first_frame: first, last_frame: last },
16077
15431
  params: clipParams
16078
15432
  });
16079
- out.phraseEnd = { ref: last, scene: lastShown };
16080
15433
  const clipRef = `$ref:s${anchor}_clip.video`;
16081
15434
  const speechOffset = Math.max(0, phrase.start_s - clipStart);
16082
15435
  const extractLen = Math.min(Math.max(0.5, phrase.end_s - phrase.start_s), Math.max(0.5, genDur - speechOffset));
@@ -16111,8 +15464,7 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
16111
15464
  scene: anchor,
16112
15465
  voice_convert_node: convId,
16113
15466
  scene_s: Math.round(phraseLen * 100) / 100,
16114
- est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
16115
- speech_words: wordCount(phrase.text)
15467
+ est_speech_s: Math.round(estSpeechS(phrase.text) * 100) / 100
16116
15468
  });
16117
15469
  for (const s of phrase.shownScenes) {
16118
15470
  const sc = env.blueprint.scenes[s];
@@ -16170,8 +15522,7 @@ function emitCompositeInTimeline(composite, scene, i, isLast, env, canonical, en
16170
15522
  scene: i,
16171
15523
  voice_convert_node: `${voiceNode}_conv`,
16172
15524
  scene_s: Math.round(sceneDurationS(scene) * 100) / 100,
16173
- est_speech_s: Math.round(estSpeechWindowS(text, start, end) * 100) / 100,
16174
- speech_words: wordCount(text)
15525
+ est_speech_s: Math.round(estSpeechS(text) * 100) / 100
16175
15526
  });
16176
15527
  }
16177
15528
  const mode = sceneShootMode(scene, present, nativeTurn, env.cameraOn, env.casts);
@@ -16263,10 +15614,6 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
16263
15614
  shootMode: mode,
16264
15615
  ingestCache: env.ingestCache
16265
15616
  };
16266
- if (!env.reuse && env.graphicScenes.has(i)) {
16267
- emitGraphicScene(i, scene, lengths, lengths.out, env.outAr, env.surfaceIngests, nodes, out.clips);
16268
- return void 0;
16269
- }
16270
15617
  if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
16271
15618
  emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
16272
15619
  return void 0;
@@ -16276,7 +15623,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
16276
15623
  emitBrandCardScene(i, lengths, lengths.out, env.outAr, brandPlateColor(env.blueprint), nodes, out.clips);
16277
15624
  return void 0;
16278
15625
  }
16279
- if (!ambientBroll && lengths.dur <= FLASH_HOLD_MAX_S && scene.motion_level !== "dynamic") {
15626
+ if (!ambientBroll && lengths.dur <= FLASH_HOLD_MAX_S) {
16280
15627
  emitFlashHold(i, scene, env.slots, ctx, lengths, lengths.out, env.outAr, nodes, out.clips);
16281
15628
  return void 0;
16282
15629
  }
@@ -16311,8 +15658,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
16311
15658
  genAr: env.genAr,
16312
15659
  videoModel: env.opts.videoModel,
16313
15660
  resolution: env.opts.resolution,
16314
- nativeLang: env.ttsLanguageCode,
16315
- uiRouted: env.uiRouted.has(i)
15661
+ nativeLang: env.ttsLanguageCode
16316
15662
  },
16317
15663
  nodes
16318
15664
  );
@@ -16333,11 +15679,10 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
16333
15679
  }
16334
15680
  function buildTimeline(blueprint, slots, opts, nodes) {
16335
15681
  const reuse = opts.frames === "reuse";
16336
- const uiRouted = uiRoutedSceneSet(blueprint);
16337
15682
  const compositeScenes = /* @__PURE__ */ new Set();
16338
15683
  if (!reuse) {
16339
15684
  blueprint.scenes.forEach((s, i) => {
16340
- if (layeredComposition(s, uiRouted.has(i))) compositeScenes.add(i);
15685
+ if (layeredComposition(s)) compositeScenes.add(i);
16341
15686
  });
16342
15687
  }
16343
15688
  const canonical = collapseVoiceover(blueprint);
@@ -16353,9 +15698,6 @@ function buildTimeline(blueprint, slots, opts, nodes) {
16353
15698
  cameraOn: onCameraDialogue(blueprint),
16354
15699
  casts: castIdSet(blueprint),
16355
15700
  ingestCache: /* @__PURE__ */ new Map(),
16356
- uiRouted,
16357
- graphicScenes: fullFrameGraphicScenes(blueprint),
16358
- surfaceIngests: /* @__PURE__ */ new Map(),
16359
15701
  ttsLanguageCode: deriveTtsLanguageCode(blueprint)
16360
15702
  };
16361
15703
  const out = {
@@ -16384,7 +15726,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
16384
15726
  const lastIndex = blueprint.scenes.length - 1;
16385
15727
  let prevEndFrame;
16386
15728
  blueprint.scenes.forEach((scene, i) => {
16387
- const composite = compositeScenes.has(i) ? layeredComposition(scene, uiRouted.has(i)) : null;
15729
+ const composite = compositeScenes.has(i) ? layeredComposition(scene) : null;
16388
15730
  if (composite) {
16389
15731
  emitCompositeInTimeline(
16390
15732
  composite,
@@ -16581,42 +15923,9 @@ function sourceHint(fe) {
16581
15923
  return `baker images icon "${desc}"`;
16582
15924
  }
16583
15925
  }
16584
- function floatKey(fe) {
16585
- return [
16586
- (fe.kind ?? "element").toLowerCase(),
16587
- (fe.brand_name || fe.what_it_represents || fe.description || "").toLowerCase().trim(),
16588
- positionClass(fe.position)
16589
- ].join("|");
16590
- }
16591
- function floatIsRoutedSurface(fe, sceneIsUiRouted) {
16592
- return sceneIsUiRouted && (fe.kind ?? "").toLowerCase() === "ui_element" && UI_SURFACE_RE.test(fe.description ?? "");
16593
- }
16594
- function addFloatDetection(windows, fe, at, end) {
16595
- const wins = windows.get(floatKey(fe)) ?? [];
16596
- const last = wins[wins.length - 1];
16597
- if (last && at <= last.end + OVERLAY_REDETECT_GAP_S) {
16598
- last.end = Math.max(last.end, end);
16599
- return;
16600
- }
16601
- wins.push({ fe, at, end });
16602
- windows.set(floatKey(fe), wins);
16603
- }
16604
- function collectFloatWindows(blueprint, uiRouted) {
16605
- const windows = /* @__PURE__ */ new Map();
16606
- blueprint.scenes.forEach((scene, i) => {
16607
- const sceneStart = scene.start_s ?? 0;
16608
- const floats = z11.array(FloatingElement).safeParse(scene.floating_elements ?? []);
16609
- if (!floats.success) return;
16610
- for (const fe of floats.data) {
16611
- const at = fe.appears_at_s ?? sceneStart;
16612
- const dur = fe.duration_s ?? 2.5;
16613
- if (floatIsRoutedSurface(fe, uiRouted.has(i)) || dur < OVERLAY_MIN_DUR_S) continue;
16614
- addFloatDetection(windows, fe, at, at + dur);
16615
- }
16616
- });
16617
- return [...windows.values()].flat().sort((a, b) => a.at - b.at);
16618
- }
16619
- function floatingStub(fe, at, dur) {
15926
+ function floatingStub(fe, sceneStart) {
15927
+ const at = fe.appears_at_s ?? sceneStart;
15928
+ const dur = fe.duration_s ?? 2.5;
16620
15929
  const kind = commentSafe(fe.kind ?? "element");
16621
15930
  const label = commentSafe(fe.brand_name || fe.what_it_represents || fe.description || fe.kind || "element");
16622
15931
  const hint = commentSafe(sourceHint(fe));
@@ -16626,49 +15935,27 @@ function floatingStub(fe, at, dur) {
16626
15935
  `<img class="ov clip ${positionClass(fe.position)}" src="your-${slug}.png" data-start="${at}" data-dur="${dur}" alt="" /> -->`
16627
15936
  ].join("\n");
16628
15937
  }
16629
- function uiSurfaceStub(blueprint, run) {
16630
- const scenes = run.map((i) => blueprint.scenes[i]).filter((s) => Boolean(s));
16631
- if (scenes.length === 0) return "";
16632
- const first = scenes[0];
16633
- const last = scenes[scenes.length - 1];
16634
- const surfaceOf = (scene) => {
16635
- const regions = compositeRegionsOf(scene) ?? [];
16636
- return regions.find(regionIsUiSurface) ?? regions.find((r) => !r.is_presenter);
16637
- };
16638
- const ui = scenes.map(surfaceOf).find(Boolean);
16639
- const at = first.start_s ?? 0;
16640
- const end = last.end_s ?? at + 2.5;
16641
- const dur = Math.max(0.5, Math.round((end - at) * 100) / 100);
15938
+ function uiPipStub(scene) {
15939
+ const comp = scene.composition;
15940
+ const layout = (comp?.layout ?? "").toLowerCase();
15941
+ if (!COMPOSITE_LAYOUTS.has(layout)) return "";
15942
+ const regions = (comp?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
15943
+ if (regions.length < 2 || !isUiOnlyComposite(regions)) return "";
15944
+ const ui = regions.find(regionIsUiSurface);
15945
+ const at = scene.start_s ?? 0;
15946
+ const dur = Math.max(0.5, Math.round(((scene.end_s ?? at + 2.5) - at) * 100) / 100);
16642
15947
  const label = commentSafe(ui?.summary || ui?.frame_prompt || ui?.panel || "the app screen");
16643
- const states = scenes.length > 1 ? scenes.map((s) => {
16644
- const beat = surfaceOf(s)?.summary || s.summary || "\u2026";
16645
- return ` @ ${s.start_s ?? 0}s \u2014 ${commentSafe(beat)}`;
16646
- }) : [];
16647
- const isGraphic = scenes.some((s) => (compositeRegionsOf(s) ?? []).some((r) => r.kind === "static_graphic"));
16648
- const sourcing = isGraphic ? [
16649
- " Build it as a REAL designed panel, NEVER AI-generate its text/typography:",
16650
- " hand-build a brand-accurate HTML block in this composition (restyle the seeded",
16651
- " overlay text, or `npx hyperframes add <block>` for a ready-made card),",
16652
- " \u2014 OR drop the design asset (export it from the brand kit) as panel.png here;",
16653
- " then nest it as a timed clip:"
16654
- ] : [
15948
+ return [
15949
+ `<!-- PHONE UI @ ${at}s for ${dur}s \u2014 the app/site screen this scene shows: ${label}.`,
16655
15950
  " Build it as a REAL surface, NEVER AI: capture the live page \u2014",
16656
15951
  " baker images screenshot https://<brand-domain>/<path> (image-library skill)",
16657
15952
  " \u2014 OR hand-build a brand-accurate HTML screen; then frame it in a phone mockup:",
16658
15953
  " npx hyperframes add phone-scroll (writes compositions/phone-scroll.html)",
16659
- " drop the screenshot as screenshot.png in this dir and nest it as a PIP clip:"
16660
- ];
16661
- const kindLabel = isGraphic ? "GRAPHIC PANEL" : "PHONE UI";
16662
- const noun = isGraphic ? "the designed graphic panel this window shows" : "the app/site screen this window shows";
16663
- return [
16664
- `<!-- ${kindLabel} @ ${at}s for ${dur}s \u2014 ${noun}: ${label}.`,
16665
- ...states.length > 0 ? [" ONE continuous surface; its states over the window:", ...states] : [],
16666
- ...sourcing,
15954
+ " drop the screenshot as screenshot.png in this dir and nest it as a PIP clip:",
16667
15955
  ` <div class="clip" data-composition-src="compositions/phone-scroll.html" data-start="${at}" data-duration="${dur}" data-track-index="2" data-width="1080" data-height="1920"></div> -->`
16668
15956
  ].join("\n");
16669
15957
  }
16670
- var CAPTION_ROLE_RE = /^(?:caption|captions|subtitle|subtitles|karaoke)$/i;
16671
- function buildOverlayHtml(input, opts = {}) {
15958
+ function buildOverlayHtml(input) {
16672
15959
  const blueprint = VideoBlueprint.parse(input);
16673
15960
  const blocks = [
16674
15961
  [
@@ -16687,15 +15974,33 @@ function buildOverlayHtml(input, opts = {}) {
16687
15974
  " Positions: edit the .pos-* classes or add your own. -->"
16688
15975
  ].join("\n")
16689
15976
  ];
16690
- const ovParts = mergeCaptions(blueprint).filter((e) => !(opts.captionsActive && CAPTION_ROLE_RE.test(e.ov.role ?? ""))).map((e) => overlayElement(e.ov, e.at, Math.round((e.end - e.at) * 1e3) / 1e3)).filter(Boolean);
15977
+ const ovParts = mergeCaptions(blueprint).map((e) => overlayElement(e.ov, e.at, Math.round((e.end - e.at) * 1e3) / 1e3)).filter(Boolean);
16691
15978
  if (ovParts.length > 0) blocks.push(ovParts.join("\n"));
16692
- const runs = uiRoutedRuns(blueprint);
16693
- const uiRouted = new Set(runs.flat());
16694
- const floatParts = collectFloatWindows(blueprint, uiRouted).map((w) => floatingStub(w.fe, w.at, Math.round((w.end - w.at) * 100) / 100)).filter(Boolean);
16695
- if (floatParts.length > 0) blocks.push(floatParts.join("\n"));
16696
- for (const run of runs) {
16697
- const stub = uiSurfaceStub(blueprint, run);
16698
- if (stub) blocks.push(stub);
15979
+ const seenFloats = /* @__PURE__ */ new Map();
15980
+ const keepFloat = (fe, sceneStart) => {
15981
+ const at = fe.appears_at_s ?? sceneStart;
15982
+ const dur = fe.duration_s ?? 2.5;
15983
+ if (dur < OVERLAY_MIN_DUR_S) return false;
15984
+ const key = [
15985
+ (fe.kind ?? "element").toLowerCase(),
15986
+ (fe.brand_name || fe.what_it_represents || fe.description || "").toLowerCase().trim(),
15987
+ positionClass(fe.position)
15988
+ ].join("|");
15989
+ const lastEnd = seenFloats.get(key);
15990
+ if (lastEnd !== void 0 && at <= lastEnd + OVERLAY_REDETECT_GAP_S) {
15991
+ seenFloats.set(key, Math.max(lastEnd, at + dur));
15992
+ return false;
15993
+ }
15994
+ seenFloats.set(key, at + dur);
15995
+ return true;
15996
+ };
15997
+ for (const scene of blueprint.scenes) {
15998
+ const sceneStart = scene.start_s ?? 0;
15999
+ const floats = z11.array(FloatingElement).safeParse(scene.floating_elements ?? []);
16000
+ const parts = (floats.success ? floats.data.filter((fe) => keepFloat(fe, sceneStart)).map((fe) => floatingStub(fe, sceneStart)) : []).filter(Boolean);
16001
+ const pip = uiPipStub(scene);
16002
+ if (pip) parts.push(pip);
16003
+ if (parts.length > 0) blocks.push(parts.join("\n"));
16699
16004
  }
16700
16005
  return blocks.join("\n\n");
16701
16006
  }
@@ -16770,7 +16075,6 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
16770
16075
  params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
16771
16076
  });
16772
16077
  const slots = buildElementSlots(elements);
16773
- extendPresenceByPromptMentions(slots, blueprint);
16774
16078
  slots.forEach((slot, i) => {
16775
16079
  nodes.push({
16776
16080
  id: slot.id,
@@ -16784,7 +16088,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
16784
16088
  let videoNode = "spine";
16785
16089
  const overlays = blueprint.scenes.flatMap((s) => s.overlays ?? []);
16786
16090
  const floating = blueprint.scenes.flatMap((s) => s.floating_elements ?? []);
16787
- const hasUiPip = uiRoutedRuns(blueprint).length > 0;
16091
+ const hasUiPip = blueprint.scenes.some((s) => uiPipStub(s) !== "");
16788
16092
  if (overlays.length > 0 || floating.length > 0 || hasUiPip) {
16789
16093
  nodes.push({
16790
16094
  id: "overlaid",
@@ -17167,10 +16471,7 @@ function collectClipAdvisories(scene, i, out) {
17167
16471
  const window = sceneDurationS(scene);
17168
16472
  if (window > SEEDANCE_SAFE_MAX_S)
17169
16473
  out.oversize.push({ scene: i, scene_s: round22(window), clip_s: ceilToSeedance(window) });
17170
- const speech = (scene.dialogue ?? []).reduce(
17171
- (s, line) => s + (line.line ? estSpeechWindowS(line.line, line.start_s, line.end_s) : 0),
17172
- 0
17173
- );
16474
+ const speech = (scene.dialogue ?? []).reduce((s, line) => s + (line.line ? estSpeechS(line.line) : 0), 0);
17174
16475
  if (speech > window * OVERSTUFF_RATIO)
17175
16476
  out.overstuffed.push({ scene: i, scene_s: round22(window), est_speech_s: round22(speech) });
17176
16477
  }
@@ -17219,23 +16520,23 @@ function videoReport(input, elementsInput) {
17219
16520
 
17220
16521
  // src/commands/canvas/composition-path.ts
17221
16522
  import { existsSync as existsSync4 } from "fs";
17222
- import path7 from "path";
16523
+ import path9 from "path";
17223
16524
  function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth = 8) {
17224
- const rel = path7.join("canvas", name);
16525
+ const rel = path9.join("canvas", name);
17225
16526
  let dir = startDir;
17226
16527
  for (let i = 0; i < maxDepth; i++) {
17227
- const candidate = path7.join(dir, rel);
17228
- if (exists(path7.join(candidate, "meta.json"))) return candidate;
17229
- const parent = path7.dirname(dir);
16528
+ const candidate = path9.join(dir, rel);
16529
+ if (exists(path9.join(candidate, "meta.json"))) return candidate;
16530
+ const parent = path9.dirname(dir);
17230
16531
  if (parent === dir) break;
17231
16532
  dir = parent;
17232
16533
  }
17233
- return path7.resolve(startDir, "../../../", rel);
16534
+ return path9.resolve(startDir, "../../../", rel);
17234
16535
  }
17235
16536
 
17236
16537
  // src/commands/canvas/gitignore.ts
17237
16538
  import { appendFile, readFile as readFile5 } from "fs/promises";
17238
- import path8 from "path";
16539
+ import path10 from "path";
17239
16540
  function missingGitignoreEntries(existing, entries) {
17240
16541
  const present = new Set(
17241
16542
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -17243,7 +16544,7 @@ function missingGitignoreEntries(existing, entries) {
17243
16544
  return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
17244
16545
  }
17245
16546
  async function ensureGitignore(dir, entries) {
17246
- const file = path8.join(dir, ".gitignore");
16547
+ const file = path10.join(dir, ".gitignore");
17247
16548
  let existing;
17248
16549
  try {
17249
16550
  existing = await readFile5(file, "utf8");
@@ -17304,8 +16605,8 @@ async function loadTranscriptBestEffort(ref) {
17304
16605
  async function stageCaptions(outDir, transcript) {
17305
16606
  const text = transcript?.trim();
17306
16607
  if (!text || text === "[]") return {};
17307
- const compositionPath = path9.join(outDir, "tiktok-captions-composition");
17308
- await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
16608
+ const compositionPath = path11.join(outDir, "tiktok-captions-composition");
16609
+ await cp2(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
17309
16610
  return { compositionPath };
17310
16611
  }
17311
16612
  function patchCompositionMeta(metaJson, dims) {
@@ -17322,10 +16623,10 @@ function patchCompositionHtml(html, dims) {
17322
16623
  return html.replace(/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+(")/i, `$1${dims.w}$2${dims.h}$3`).replace(/(width:\s*)\d+(px;\s*height:\s*)\d+(px;)/i, `$1${dims.w}$2${dims.h}$3`).replace(/(data-width=")\d+(")/i, `$1${dims.w}$2`).replace(/(data-height=")\d+(")/i, `$1${dims.h}$2`);
17323
16624
  }
17324
16625
  async function stampCompositionDims(compositionDir, dims) {
17325
- const metaPath = path9.join(compositionDir, "meta.json");
16626
+ const metaPath = path11.join(compositionDir, "meta.json");
17326
16627
  const rawMeta = await readFile6(metaPath, "utf8");
17327
16628
  await writeFile2(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
17328
- const htmlPath = path9.join(compositionDir, "index.html");
16629
+ const htmlPath = path11.join(compositionDir, "index.html");
17329
16630
  const rawHtml = await readFile6(htmlPath, "utf8");
17330
16631
  await writeFile2(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
17331
16632
  }
@@ -17359,7 +16660,7 @@ async function detectShotCutsBestEffort(videoPath, threshold) {
17359
16660
  process.stderr.write(`Shot-cut detection skipped (${msg}); using LLM boundaries.
17360
16661
  `);
17361
16662
  }
17362
- return void 0;
16663
+ return [];
17363
16664
  }
17364
16665
  }
17365
16666
  function fail2(code, message) {
@@ -17388,7 +16689,7 @@ function buildDeconstructCanvas(videoPath, deconstructModel, opts) {
17388
16689
  if (typeof opts.maxScenes === "number") deconstructParams.max_scenes = opts.maxScenes;
17389
16690
  if (opts.language) deconstructParams.language = opts.language;
17390
16691
  if (opts.focus) deconstructParams.focus = opts.focus;
17391
- if (opts.shotCuts) deconstructParams.shot_cuts = opts.shotCuts;
16692
+ if (opts.shotCuts && opts.shotCuts.length > 0) deconstructParams.shot_cuts = opts.shotCuts;
17392
16693
  deconstructParams.max_clip_s = SEEDANCE_DURATIONS[SEEDANCE_DURATIONS.length - 1];
17393
16694
  return {
17394
16695
  schema: "baker-canvas/1",
@@ -17465,6 +16766,10 @@ var scaffoldVideoCommand = defineCommand87({
17465
16766
  args: {
17466
16767
  file: { type: "positional", required: true, description: "Path to the reference video" },
17467
16768
  out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
16769
+ slug: {
16770
+ type: "string",
16771
+ description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
16772
+ },
17468
16773
  frames: { type: "string", description: '"generate" (default, anchored regen) or "reuse" (wire real frames in)' },
17469
16774
  ambient: {
17470
16775
  type: "boolean",
@@ -17491,11 +16796,19 @@ var scaffoldVideoCommand = defineCommand87({
17491
16796
  }
17492
16797
  },
17493
16798
  async run({ args }) {
17494
- const videoPath = path9.resolve(String(args.file));
17495
- const base = path9.basename(videoPath, path9.extname(videoPath));
17496
- const outPath = args.out ? path9.resolve(String(args.out)) : path9.join(path9.dirname(videoPath), `${base}.video.canvas.json`);
17497
- const outDir = path9.dirname(outPath);
17498
- const blueprintPath = path9.join(outDir, "prompt.json");
16799
+ const videoPath = path11.resolve(String(args.file));
16800
+ const base = path11.basename(videoPath, path11.extname(videoPath));
16801
+ const slug = args.slug ? String(args.slug) : void 0;
16802
+ if (slug && !isValidScaffoldSlug(slug)) {
16803
+ process.stderr.write(
16804
+ `${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
16805
+ `
16806
+ );
16807
+ process.exit(2);
16808
+ }
16809
+ const outPath = args.out ? path11.resolve(String(args.out)) : slug ? path11.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path11.join(path11.dirname(videoPath), `${base}.video.canvas.json`);
16810
+ const outDir = path11.dirname(outPath);
16811
+ const blueprintPath = path11.join(outDir, "prompt.json");
17499
16812
  const frames = args.frames === "reuse" ? "reuse" : "generate";
17500
16813
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
17501
16814
  if (Number.isFinite(maxScenes)) {
@@ -17514,7 +16827,7 @@ var scaffoldVideoCommand = defineCommand87({
17514
16827
  shotCuts
17515
16828
  });
17516
16829
  const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
17517
- await mkdir(outDir, { recursive: true });
16830
+ await mkdir2(outDir, { recursive: true });
17518
16831
  const annotated = annotateBlueprintWithElements(blueprint, elements);
17519
16832
  await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
17520
16833
  `, "utf8");
@@ -17535,11 +16848,11 @@ var scaffoldVideoCommand = defineCommand87({
17535
16848
  `
17536
16849
  );
17537
16850
  }
17538
- const compositionDest = path9.join(outDir, "video-overlay-composition");
17539
- await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
16851
+ const compositionDest = path11.join(outDir, "video-overlay-composition");
16852
+ await cp2(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
17540
16853
  await stampCompositionDims(compositionDest, outDims);
17541
- const indexPath = path9.join(compositionDest, "index.html");
17542
- const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
16854
+ const indexPath = path11.join(compositionDest, "index.html");
16855
+ const overlayHtml = buildOverlayHtml(blueprint);
17543
16856
  const indexHtml = await readFile6(indexPath, "utf8");
17544
16857
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
17545
16858
  if (injected === indexHtml && overlayHtml.trim()) {
@@ -17554,9 +16867,9 @@ var scaffoldVideoCommand = defineCommand87({
17554
16867
  const opts = {
17555
16868
  imageModel,
17556
16869
  videoModel,
17557
- overlayCompositionPath: path9.relative(outDir, compositionDest),
17558
- captionsCompositionPath: captions.compositionPath ? path9.relative(outDir, captions.compositionPath) : void 0,
17559
- blueprintPath: path9.relative(outDir, blueprintPath),
16870
+ overlayCompositionPath: path11.relative(outDir, compositionDest),
16871
+ captionsCompositionPath: captions.compositionPath ? path11.relative(outDir, captions.compositionPath) : void 0,
16872
+ blueprintPath: path11.relative(outDir, blueprintPath),
17560
16873
  frames,
17561
16874
  ambient: Boolean(args.ambient),
17562
16875
  ...args.aspect ? { aspect: String(args.aspect) } : {},
@@ -17571,31 +16884,15 @@ var scaffoldVideoCommand = defineCommand87({
17571
16884
  return fail2("scaffold", e instanceof Error ? e.message : String(e));
17572
16885
  }
17573
16886
  const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(canvas, outDir), defaultRegistry());
17574
- if (!validation.ok) {
17575
- const meta = canvas.metadata;
17576
- const todo = meta.todo ?? {};
17577
- todo.blocking_validation_issues = validation.issues;
17578
- meta.todo = todo;
17579
- }
17580
- await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
17581
- `, "utf8");
17582
16887
  if (!validation.ok) {
17583
16888
  process.stderr.write(
17584
- `${JSON.stringify(
17585
- {
17586
- ok: false,
17587
- error: { code: "validation", issues: validation.issues },
17588
- canvas_path: outPath,
17589
- prompt_path: blueprintPath,
17590
- note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running."
17591
- },
17592
- null,
17593
- 2
17594
- )}
16889
+ `${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
17595
16890
  `
17596
16891
  );
17597
16892
  process.exit(2);
17598
16893
  }
16894
+ await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
16895
+ `, "utf8");
17599
16896
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
17600
16897
  process.stdout.write(
17601
16898
  `${JSON.stringify(
@@ -17614,7 +16911,7 @@ var scaffoldVideoCommand = defineCommand87({
17614
16911
  run_estimated_credits: validation.estimatedCredits
17615
16912
  },
17616
16913
  checklist: {
17617
- edit_prompt: `Edit ${path9.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
16914
+ edit_prompt: `Edit ${path11.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
17618
16915
  recurring_elements_to_supply: report.elements,
17619
16916
  voices_to_confirm: report.dialogue.map((d) => ({
17620
16917
  scene: d.scene,
@@ -17641,7 +16938,7 @@ var scaffoldVideoCommand = defineCommand87({
17641
16938
 
17642
16939
  // src/commands/canvas/set-prompt.ts
17643
16940
  import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
17644
- import path10 from "path";
16941
+ import path12 from "path";
17645
16942
  import { defineCommand as defineCommand88 } from "citty";
17646
16943
  function setNodePrompt(canvas, nodeId, text) {
17647
16944
  const nodes = canvas?.nodes;
@@ -17669,7 +16966,7 @@ var setPromptCommand = defineCommand88({
17669
16966
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
17670
16967
  },
17671
16968
  async run({ args }) {
17672
- const filePath = path10.resolve(String(args.file));
16969
+ const filePath = path12.resolve(String(args.file));
17673
16970
  let canvas;
17674
16971
  try {
17675
16972
  canvas = JSON.parse(await readFile7(filePath, "utf8"));
@@ -17679,7 +16976,7 @@ var setPromptCommand = defineCommand88({
17679
16976
  process.exit(2);
17680
16977
  }
17681
16978
  let text;
17682
- if (args["text-file"]) text = await readFile7(path10.resolve(String(args["text-file"])), "utf8");
16979
+ if (args["text-file"]) text = await readFile7(path12.resolve(String(args["text-file"])), "utf8");
17683
16980
  else if (args.text !== void 0) text = String(args.text);
17684
16981
  else {
17685
16982
  process.stderr.write(
@@ -17700,7 +16997,7 @@ var setPromptCommand = defineCommand88({
17700
16997
  process.exit(2);
17701
16998
  return;
17702
16999
  }
17703
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path10.dirname(filePath)), defaultRegistry());
17000
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path12.dirname(filePath)), defaultRegistry());
17704
17001
  if (!validation.ok) {
17705
17002
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
17706
17003
  `);
@@ -17716,7 +17013,7 @@ var setPromptCommand = defineCommand88({
17716
17013
 
17717
17014
  // src/commands/canvas/validate.ts
17718
17015
  import { readFile as readFile8 } from "fs/promises";
17719
- import path11 from "path";
17016
+ import path13 from "path";
17720
17017
  import { defineCommand as defineCommand89 } from "citty";
17721
17018
  var validateCommand = defineCommand89({
17722
17019
  meta: {
@@ -17725,7 +17022,7 @@ var validateCommand = defineCommand89({
17725
17022
  },
17726
17023
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
17727
17024
  async run({ args }) {
17728
- const filePath = path11.resolve(String(args.file));
17025
+ const filePath = path13.resolve(String(args.file));
17729
17026
  const raw = await readFile8(filePath, "utf8");
17730
17027
  let parsed;
17731
17028
  try {
@@ -17736,7 +17033,7 @@ var validateCommand = defineCommand89({
17736
17033
  `);
17737
17034
  process.exit(2);
17738
17035
  }
17739
- parsed = resolveRelativeCanvasPaths(parsed, path11.dirname(filePath));
17036
+ parsed = resolveRelativeCanvasPaths(parsed, path13.dirname(filePath));
17740
17037
  const result = await validateCanvasDeep(parsed, defaultRegistry());
17741
17038
  if (!result.ok) {
17742
17039
  process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
@@ -17873,6 +17170,16 @@ registerSchema({
17873
17170
  type: "string",
17874
17171
  description: "Optional URL of the original reference ad",
17875
17172
  required: false
17173
+ },
17174
+ slug: {
17175
+ type: "string",
17176
+ description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
17177
+ required: false
17178
+ },
17179
+ runId: {
17180
+ type: "string",
17181
+ description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
17182
+ required: false
17876
17183
  }
17877
17184
  }
17878
17185
  });
@@ -17882,6 +17189,13 @@ function detectCreativeContentType(filePath) {
17882
17189
  unsupportedMessage: "Unsupported creative image extension. Use PNG, JPG, or WebP."
17883
17190
  });
17884
17191
  }
17192
+ function chatIdFromEnv() {
17193
+ try {
17194
+ return getEnv().BAKER_CHAT_ID || void 0;
17195
+ } catch {
17196
+ return void 0;
17197
+ }
17198
+ }
17885
17199
  function parseOptionalUrl(value) {
17886
17200
  if (value === void 0 || value.trim() === "") {
17887
17201
  return void 0;
@@ -17912,7 +17226,11 @@ async function publishCreative(args, deps = defaultImageApiDeps) {
17912
17226
  return publishImageAsCreative(deps, {
17913
17227
  imageId: upload.imageId,
17914
17228
  title,
17915
- sourceReferenceUrl
17229
+ sourceReferenceUrl,
17230
+ slug: args.slug,
17231
+ runId: args.runId,
17232
+ // Attribute the publish to the driving chat (injected by the bridge).
17233
+ chatId: chatIdFromEnv()
17916
17234
  });
17917
17235
  }
17918
17236
  var publishCommand = defineCommand91({
@@ -17928,6 +17246,16 @@ var publishCommand = defineCommand91({
17928
17246
  type: "string",
17929
17247
  description: "Optional URL of the original reference ad",
17930
17248
  required: false
17249
+ },
17250
+ slug: {
17251
+ type: "string",
17252
+ description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
17253
+ required: false
17254
+ },
17255
+ runId: {
17256
+ type: "string",
17257
+ description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
17258
+ required: false
17931
17259
  }
17932
17260
  },
17933
17261
  run: async ({ args }) => {
@@ -17946,7 +17274,9 @@ var publishCommand = defineCommand91({
17946
17274
  file,
17947
17275
  title,
17948
17276
  context: args.context,
17949
- sourceReferenceUrl: args.sourceReferenceUrl
17277
+ sourceReferenceUrl: args.sourceReferenceUrl,
17278
+ slug: args.slug,
17279
+ runId: args.runId
17950
17280
  });
17951
17281
  writeJson({ ok: true, data });
17952
17282
  } catch (err) {
@@ -18844,9 +18174,9 @@ async function readImageBuffer(pathOrUrl) {
18844
18174
  }
18845
18175
  return readFile10(pathOrUrl);
18846
18176
  }
18847
- async function isDirectory(path12) {
18177
+ async function isDirectory(path14) {
18848
18178
  try {
18849
- const s = await stat2(path12);
18179
+ const s = await stat2(path14);
18850
18180
  return s.isDirectory();
18851
18181
  } catch {
18852
18182
  return false;