@productmaker/mcp 2.1.0 → 2.3.1

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/stdio.js CHANGED
@@ -150,7 +150,7 @@ var ApiClient = class {
150
150
  };
151
151
 
152
152
  // src/index.ts
153
- var MCP_VERSION = "2.1.0";
153
+ var MCP_VERSION = "2.3.1";
154
154
 
155
155
  // src/tools/tasks.ts
156
156
  import { z as z40 } from "zod";
@@ -932,7 +932,7 @@ var Product = z4.object({
932
932
  });
933
933
 
934
934
  // ../../packages/types/src/ingest.ts
935
- import { z as z13 } from "zod";
935
+ import { z as z14 } from "zod";
936
936
 
937
937
  // ../../packages/types/src/video-tier.ts
938
938
  import { z as z5 } from "zod";
@@ -944,18 +944,204 @@ var VideoTier = z5.preprocess((v) => v === "grok" ? "single_call" : v, z5.enum([
944
944
  ]));
945
945
  var DEFAULT_VIDEO_TIER = "single_call";
946
946
 
947
+ // ../../packages/types/src/angle.ts
948
+ import { z as z6 } from "zod";
949
+ var ImageWinnerSignal = z6.enum([
950
+ "winner",
951
+ "opportunity",
952
+ "saturated_twist"
953
+ ]);
954
+ var WinnerVisualMode = z6.enum([
955
+ "auto",
956
+ "inspired",
957
+ "original"
958
+ ]);
959
+ var AnchoredIntel = z6.object({
960
+ painPointIndices: z6.array(z6.number().int().nonnegative()).max(10),
961
+ hookIndices: z6.array(z6.number().int().nonnegative()).max(10),
962
+ claimIndices: z6.array(z6.number().int().nonnegative()).max(10),
963
+ sourceAngleType: z6.enum([
964
+ "gap",
965
+ "validated_local",
966
+ "saturated_differentiated",
967
+ "novel"
968
+ ]),
969
+ // SNAPSHOT of the resolved intel strings at angle-generation time.
970
+ // Indices reference into Task.competitorIntel which we treat as immutable
971
+ // post-scan, but defensive: a future re-scan path that rewrites
972
+ // Task.competitorIntel would invalidate the indices silently. Persisting
973
+ // the resolved strings ALONGSIDE the indices means downstream consumers
974
+ // (script writer, landing copy) can safely use the strings directly
975
+ // without re-resolving — staleness-immune. The indices remain useful for
976
+ // analytics ("which intel items did angles cite?") and as a sanity-check.
977
+ resolvedPainPoints: z6.array(z6.string()).max(10).optional(),
978
+ resolvedHooks: z6.array(z6.string()).max(10).optional(),
979
+ resolvedClaims: z6.array(z6.string()).max(10).optional(),
980
+ /**
981
+ * Free-form pillar name describing the AXIS of the product this angle
982
+ * attacks (e.g. "ciencia/mecanismo", "tiempo/ritual", "comparación-con-
983
+ * alternativas", "social-proof", "dolor-emocional", "identidad/persona").
984
+ * The LLM picks the label — we do NOT lock an enum because the right
985
+ * pillar taxonomy varies per product (a supplement's "mecanismo" pillar
986
+ * is different from a gadget's "fricción de uso"). Constraint enforced
987
+ * at generation time: the 3 angles emitted for one task MUST use 3
988
+ * DISTINCT pillars (anti-convergence). Lives inside AnchoredIntel because
989
+ * it ships as analytics metadata alongside the anchoredIntel sourceType +
990
+ * indices, and persists as JSON without a column migration. Optional
991
+ * because angles persisted before this change have no pillar.
992
+ */
993
+ pillar: z6.string().min(1).max(60).optional(),
994
+ // Telemetry: why the angle did NOT import a gapAngle. The legacy single value
995
+ // 'none_relevant' was OVERLOADED (it meant "no gaps existed" AND "gaps existed
996
+ // but none fit" AND was the sanitizer default) which made the gap feature
997
+ // unmeasurable. Split into distinct, one-meaning-each states:
998
+ // - persona_mismatch / product_mismatch: the LLM saw gaps and judged them a
999
+ // wrong-persona / wrong-subcategory fit (trusted from the LLM).
1000
+ // - no_gaps_in_block: there were no gapAngles in the block (or no block).
1001
+ // - gaps_present_not_imported: gaps existed and the angle went novel anyway
1002
+ // (the sanitizer's honest default when the LLM didn't pick persona/product).
1003
+ // - gaps_all_filtered_noise: the reference HAD gaps but every one was dropped
1004
+ // as detection noise (retailer-meta/cross-category) so the LLM saw none.
1005
+ // - n_a: the angle imported a gap (sourceAngleType="gap"), so "why skipped"
1006
+ // is moot.
1007
+ // The deterministic gap-presence states (no_gaps_in_block / gaps_present_not_
1008
+ // imported / gaps_all_filtered_noise) are set by sanitizeAnchoredIntel from
1009
+ // facts it computes (intel.gapAngles.length, intel.gapAnglesFilteredCount), NOT
1010
+ // trusted from the lazy LLM. z.preprocess aliases historical 'none_relevant'
1011
+ // rows → 'no_gaps_in_block' so the JSON column parses with no migration.
1012
+ // Analytics-only: NO runtime path branches on this value.
1013
+ skippedGapReason: z6.preprocess((v) => v === "none_relevant" ? "no_gaps_in_block" : v, z6.enum([
1014
+ "persona_mismatch",
1015
+ "product_mismatch",
1016
+ "no_gaps_in_block",
1017
+ "gaps_present_not_imported",
1018
+ "gaps_all_filtered_noise",
1019
+ "n_a"
1020
+ ])).optional(),
1021
+ // Telemetry: captures the prod-observed case where a "novel" angle actually
1022
+ // reflects a GAP's theme/promise without formally anchoring indices (gapAngles
1023
+ // are a separate array, not index-cited, so a real gap import is reclassified
1024
+ // 'novel' by the empty-indices coherence guard). Self-reported by the LLM:
1025
+ // - gap_imported: the angle's theme imports a gap's logic.
1026
+ // - product_derived: derived purely from the product.
1027
+ // - hybrid: a mix.
1028
+ // SOFT analytics-only signal (the LLM may confabulate) — validated against
1029
+ // known prod cases before trusting aggregates; never a control-flow input.
1030
+ // Optional in storage; on the wire it ships as a required enum with a 'none'
1031
+ // sentinel (strict structured output forbids optional properties) that the
1032
+ // sanitizer maps to undefined.
1033
+ themeSourceType: z6.enum([
1034
+ "gap_imported",
1035
+ "product_derived",
1036
+ "hybrid"
1037
+ ]).optional()
1038
+ });
1039
+ var Angle = z6.object({
1040
+ angle: z6.string().min(1).max(480),
1041
+ buyerPersona: z6.string().min(1).max(360),
1042
+ /** Character description for video (role + appearance + attitude). Drives voice selection and Frame0Spec. */
1043
+ actor: z6.string().min(1).max(360).optional(),
1044
+ // Binary only: voice catalog has no non-binary voices.
1045
+ // See packages/angles/src/index.ts + getAngles coercion in angle.repository.ts.
1046
+ gender: z6.enum([
1047
+ "female",
1048
+ "male"
1049
+ ]).optional(),
1050
+ /** Per-angle user-supplied script/visual notes. Hydrated from the
1051
+ * `Angle.scriptNotes` DB column or the `angleVideoOptions[i].scriptNotes`
1052
+ * passed from the chat. Per-angle overrides global `videoScriptNotes`. */
1053
+ scriptNotes: z6.string().max(2e3).optional(),
1054
+ /** AI-chosen per-angle narrative (set by the batch selector in angles.worker).
1055
+ * Pre-fills the review dropdown; user can override per angle. */
1056
+ narrativeStyle: NarrativeStyle.optional(),
1057
+ /** See AnchoredIntel docs. Optional because angles persisted before this
1058
+ * change have no anchoring captured; new angles ALWAYS emit it (with empty
1059
+ * arrays + 'novel' when intel was unavailable). */
1060
+ anchoredIntel: AnchoredIntel.optional(),
1061
+ /** Server-computed: true when this angle is eligible for the gap_replication
1062
+ * narrative (isReplicationAngle(anchoredIntel, intel) && intel.gapBlueprint
1063
+ * present). Computed server-side (not derived client-side) because the web
1064
+ * only sees this angle's anchoredIntel, not the task's
1065
+ * competitorIntel.gapBlueprint. Gates the review dropdown's gap_replication
1066
+ * option — absent/false = hidden. */
1067
+ gapReplicationEligible: z6.boolean().optional(),
1068
+ /** Server-computed: true when this angle's effective narrative is
1069
+ * gap_replication, which FORCES the multi-shot STANDARD ("Multiescena")
1070
+ * render pipeline regardless of the task's chosen tier (single_call can't
1071
+ * produce the multi-shot replication — see task-results.service.ts and the
1072
+ * render-time tier override). Surfaces "renders in Multiescena" on the
1073
+ * review card so a single_call user isn't surprised by the STANDARD-tier
1074
+ * charge. */
1075
+ rendersMultiShot: z6.boolean().optional(),
1076
+ /** Server-computed product-level origin bucket for the review card's badge
1077
+ * (NOT the internal sourceAngleType taxonomy — see resolveAngleOrigin in
1078
+ * task-results.service.ts): 'gap' = market opportunity competitors aren't
1079
+ * using; 'saturated_twist' = competitors' most-used approach with our own
1080
+ * twist; 'ai_recommended' = AI-authored for this product (default). */
1081
+ angleOrigin: z6.enum([
1082
+ "gap",
1083
+ "saturated_twist",
1084
+ "ai_recommended"
1085
+ ]).optional(),
1086
+ /** Server-computed badge TIER for the replication angle (two-tier UX,
1087
+ * v1.32.0 — see resolveReplicationTier). 'winner' = a validated angle
1088
+ * opportunity backed by a proven ad (anchor gap/saturated → golden badge);
1089
+ * 'opportunity' = a product-coherent replicable winner with no validated
1090
+ * angle gap (anchor 'dominant' → opportunity/twist badge, no overclaim).
1091
+ * Present only on the angle that carries gap_replication; absent otherwise. */
1092
+ replicationTier: z6.enum([
1093
+ "winner",
1094
+ "opportunity"
1095
+ ]).optional(),
1096
+ /** Server-confirmed availability of a coherent, captured IMAGE winner for
1097
+ * this task. The review UI must not claim "anuncio ganador" from the user's
1098
+ * mode choice alone: fail-open scans may legitimately have no winner. */
1099
+ imageWinnerAvailable: z6.boolean().optional(),
1100
+ /** True when `winnerVisualMode=auto` will actually attach the image reference.
1101
+ * A single-image request requires `imageWinnerSignal="winner"`; a multi-image
1102
+ * portfolio may spend one experimental slot on opportunity/twist. Explicit
1103
+ * `inspired` may also apply it; `original` always suppresses it. */
1104
+ imageWinnerAutoApplied: z6.boolean().optional(),
1105
+ /** Server-authored evidence tier for the image reference. The UI renders
1106
+ * this on the inspired control and the selected strategy badge. */
1107
+ imageWinnerSignal: ImageWinnerSignal.optional()
1108
+ });
1109
+ var AdVideo = z6.object({
1110
+ source: z6.enum([
1111
+ "tiktok_shop",
1112
+ "meta_ads"
1113
+ ]),
1114
+ url: z6.string().url(),
1115
+ title: z6.string().optional(),
1116
+ description: z6.string().optional(),
1117
+ metrics: z6.object({
1118
+ impressions: z6.number().int().nonnegative().optional(),
1119
+ views: z6.number().int().nonnegative().optional(),
1120
+ likes: z6.number().int().nonnegative().optional(),
1121
+ ctaClicks: z6.number().int().nonnegative().optional(),
1122
+ diggCount: z6.number().int().nonnegative().optional(),
1123
+ shareCount: z6.number().int().nonnegative().optional(),
1124
+ commentCount: z6.number().int().nonnegative().optional(),
1125
+ playCount: z6.number().int().nonnegative().optional(),
1126
+ collectCount: z6.number().int().nonnegative().optional(),
1127
+ durationSeconds: z6.number().int().nonnegative().optional()
1128
+ }).partial().optional(),
1129
+ country: z6.string().optional(),
1130
+ lastSeen: z6.string().datetime().optional()
1131
+ });
1132
+
947
1133
  // ../../packages/types/src/creative.ts
948
- import { z as z9 } from "zod";
1134
+ import { z as z10 } from "zod";
949
1135
 
950
1136
  // ../../packages/types/src/landing.ts
951
- import { z as z8 } from "zod";
1137
+ import { z as z9 } from "zod";
952
1138
 
953
1139
  // ../../packages/types/src/landing-strategy.ts
954
- import { z as z7 } from "zod";
1140
+ import { z as z8 } from "zod";
955
1141
 
956
1142
  // ../../packages/types/src/landing-role.ts
957
- import { z as z6 } from "zod";
958
- var LandingRole = z6.enum([
1143
+ import { z as z7 } from "zod";
1144
+ var LandingRole = z7.enum([
959
1145
  "hero",
960
1146
  "benefits1",
961
1147
  "socialproof",
@@ -967,7 +1153,7 @@ var LandingRole = z6.enum([
967
1153
  "authority",
968
1154
  "logistics"
969
1155
  ]);
970
- var LandingRoleLegacy = z6.enum([
1156
+ var LandingRoleLegacy = z7.enum([
971
1157
  "hero",
972
1158
  "benefits1",
973
1159
  "benefits2",
@@ -993,7 +1179,7 @@ var LANDING_STRATEGIES = [
993
1179
  "offer_value",
994
1180
  "lifestyle_identity"
995
1181
  ];
996
- var LandingStrategySchema = z7.enum(LANDING_STRATEGIES);
1182
+ var LandingStrategySchema = z8.enum(LANDING_STRATEGIES);
997
1183
  var EffectiveLandingStrategySchema = LandingStrategySchema.exclude([
998
1184
  "auto"
999
1185
  ]);
@@ -1004,8 +1190,8 @@ var SEMANTIC_LANDING_STRATEGIES = [
1004
1190
  "offer_value",
1005
1191
  "lifestyle_identity"
1006
1192
  ];
1007
- var SemanticLandingStrategySchema = z7.enum(SEMANTIC_LANDING_STRATEGIES);
1008
- var LandingVisualStyleFamilySchema = z7.enum([
1193
+ var SemanticLandingStrategySchema = z8.enum(SEMANTIC_LANDING_STRATEGIES);
1194
+ var LandingVisualStyleFamilySchema = z8.enum([
1009
1195
  "editorial_dtc",
1010
1196
  "ugc_scrapbook",
1011
1197
  "technical_diagram",
@@ -1013,39 +1199,39 @@ var LandingVisualStyleFamilySchema = z7.enum([
1013
1199
  "warm_organic",
1014
1200
  "cinematic_problem_solution"
1015
1201
  ]);
1016
- var LandingVisualTypographySchema = z7.enum([
1202
+ var LandingVisualTypographySchema = z8.enum([
1017
1203
  "serif_editorial",
1018
1204
  "sans_modern",
1019
1205
  "display_bold",
1020
1206
  "handwritten_warm"
1021
1207
  ]);
1022
- var LandingVisualCompositionSchema = z7.enum([
1208
+ var LandingVisualCompositionSchema = z8.enum([
1023
1209
  "full_bleed",
1024
1210
  "editorial_panels",
1025
1211
  "cards_collage",
1026
1212
  "diagrammatic",
1027
1213
  "commerce_modules"
1028
1214
  ]);
1029
- var LandingVisualImageTreatmentSchema = z7.enum([
1215
+ var LandingVisualImageTreatmentSchema = z8.enum([
1030
1216
  "studio_editorial",
1031
1217
  "phone_ugc",
1032
1218
  "documentary_lifestyle",
1033
1219
  "technical_macro",
1034
1220
  "high_contrast_direct_response"
1035
1221
  ]);
1036
- var LandingVisualDensitySchema = z7.enum([
1222
+ var LandingVisualDensitySchema = z8.enum([
1037
1223
  "minimal",
1038
1224
  "balanced",
1039
1225
  "rich"
1040
1226
  ]);
1041
- var LandingVisualSignatureSchema = z7.object({
1227
+ var LandingVisualSignatureSchema = z8.object({
1042
1228
  styleFamily: LandingVisualStyleFamilySchema,
1043
1229
  typography: LandingVisualTypographySchema,
1044
1230
  composition: LandingVisualCompositionSchema,
1045
1231
  imageTreatment: LandingVisualImageTreatmentSchema,
1046
1232
  density: LandingVisualDensitySchema,
1047
- paletteIntent: z7.string().trim().min(1).max(160),
1048
- recurringMotif: z7.string().trim().min(1).max(160)
1233
+ paletteIntent: z8.string().trim().min(1).max(160),
1234
+ recurringMotif: z8.string().trim().min(1).max(160)
1049
1235
  });
1050
1236
  var LANDING_SECTION_KEYS = [
1051
1237
  "problem_hook",
@@ -1087,18 +1273,18 @@ var LANDING_SECTION_KEYS = [
1087
1273
  "lifestyle_fit",
1088
1274
  "join_lifestyle"
1089
1275
  ];
1090
- var LandingSectionKeySchema = z7.enum(LANDING_SECTION_KEYS);
1091
- var LandingSectionPlanEntrySchema = z7.object({
1276
+ var LandingSectionKeySchema = z8.enum(LANDING_SECTION_KEYS);
1277
+ var LandingSectionPlanEntrySchema = z8.object({
1092
1278
  /** Stable persistence/regeneration identifier. Never shown to the user. */
1093
1279
  role: LandingRole,
1094
1280
  /** Strategy-specific section identity shown in UI and used by prompts. */
1095
1281
  sectionKey: LandingSectionKeySchema,
1096
- index: z7.number().int().min(1).max(10)
1282
+ index: z8.number().int().min(1).max(10)
1097
1283
  });
1098
- var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefine((plan, ctx) => {
1284
+ var LandingSectionPlanSchema = z8.array(LandingSectionPlanEntrySchema).superRefine((plan, ctx) => {
1099
1285
  if (plan.length !== 7 && plan.length !== 10) {
1100
1286
  ctx.addIssue({
1101
- code: z7.ZodIssueCode.custom,
1287
+ code: z8.ZodIssueCode.custom,
1102
1288
  message: "sectionPlan must contain exactly 7 or 10 entries"
1103
1289
  });
1104
1290
  }
@@ -1107,13 +1293,13 @@ var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefi
1107
1293
  const positions = new Set(plan.map((entry) => entry.index));
1108
1294
  if (roles.size !== plan.length) {
1109
1295
  ctx.addIssue({
1110
- code: z7.ZodIssueCode.custom,
1296
+ code: z8.ZodIssueCode.custom,
1111
1297
  message: "sectionPlan roles must be unique"
1112
1298
  });
1113
1299
  }
1114
1300
  if (keys.size !== plan.length) {
1115
1301
  ctx.addIssue({
1116
- code: z7.ZodIssueCode.custom,
1302
+ code: z8.ZodIssueCode.custom,
1117
1303
  message: "sectionPlan sectionKeys must be unique"
1118
1304
  });
1119
1305
  }
@@ -1122,48 +1308,50 @@ var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefi
1122
1308
  }, (_, index) => index + 1);
1123
1309
  if (positions.size !== plan.length || !requiredPositions.every((position) => positions.has(position))) {
1124
1310
  ctx.addIssue({
1125
- code: z7.ZodIssueCode.custom,
1311
+ code: z8.ZodIssueCode.custom,
1126
1312
  message: "sectionPlan indexes must be contiguous from 1 through plan length"
1127
1313
  });
1128
1314
  }
1129
1315
  });
1130
1316
 
1131
1317
  // ../../packages/types/src/landing.ts
1132
- var LandingImageReadable = z8.object({
1133
- index: z8.number().int().min(1).max(20),
1318
+ var LandingImageReadable = z9.object({
1319
+ index: z9.number().int().min(1).max(20),
1134
1320
  /** Child task that owns this image when variants are rendered alongside the root task. */
1135
- variantTaskId: z8.string().uuid().optional(),
1321
+ variantTaskId: z9.string().uuid().optional(),
1136
1322
  role: LandingRoleLegacy,
1137
1323
  /** Strategy-specific display identity; absent on legacy tasks. */
1138
1324
  sectionKey: LandingSectionKeySchema.optional(),
1139
- url: z8.string().min(1),
1140
- width: z8.number().int(),
1141
- height: z8.number().int(),
1142
- notes: z8.string().optional()
1325
+ url: z9.string().min(1),
1326
+ width: z9.number().int(),
1327
+ height: z9.number().int(),
1328
+ notes: z9.string().optional()
1143
1329
  });
1144
- var LandingImage = z8.object({
1145
- index: z8.number().int().min(1).max(10),
1330
+ var LandingImage = z9.object({
1331
+ index: z9.number().int().min(1).max(10),
1146
1332
  role: LandingRole,
1147
1333
  sectionKey: LandingSectionKeySchema.optional(),
1148
- url: z8.string().min(1),
1149
- width: z8.number().int(),
1150
- height: z8.number().int(),
1151
- notes: z8.string().optional()
1334
+ url: z9.string().min(1),
1335
+ width: z9.number().int(),
1336
+ height: z9.number().int(),
1337
+ notes: z9.string().optional()
1152
1338
  });
1153
1339
 
1154
1340
  // ../../packages/types/src/creative.ts
1155
- var VideoCreativeAspectRatio = z9.enum([
1341
+ var VideoCreativeAspectRatio = z10.enum([
1156
1342
  "16:9",
1157
1343
  "9:16",
1158
1344
  "Auto"
1159
1345
  ]);
1160
- var ImageCreativeStyle = z9.enum([
1346
+ var PINNED_HEADLINE_MAX_CHARS = 120;
1347
+ var PINNED_SUBHEADLINE_MAX_CHARS = 160;
1348
+ var ImageCreativeStyle = z10.enum([
1161
1349
  "studio",
1162
1350
  "floating",
1163
1351
  "ingredients",
1164
1352
  "in_use"
1165
1353
  ]);
1166
- var ImageCreativeArchetype = z9.enum([
1354
+ var ImageCreativeArchetype = z10.enum([
1167
1355
  "direct_response_product_ad",
1168
1356
  "hero_outcome",
1169
1357
  "offer_value_prop",
@@ -1172,215 +1360,228 @@ var ImageCreativeArchetype = z9.enum([
1172
1360
  "problem_solution",
1173
1361
  "lifestyle_identity"
1174
1362
  ]);
1175
- var ImageCreativeVisualTreatment = z9.enum([
1363
+ var ImageCreativeVisualTreatment = z10.enum([
1176
1364
  "performance_product_ad",
1177
1365
  "premium_dtc_carousel"
1178
1366
  ]);
1179
- var ImageCreativeAspectRatio = z9.enum([
1367
+ var ImageCreativeAspectRatio = z10.enum([
1180
1368
  "1:1",
1181
1369
  "4:5",
1182
1370
  "9:16",
1183
1371
  "16:9"
1184
1372
  ]);
1185
- var VideoCreativeVariant = z9.object({
1186
- id: z9.string(),
1187
- assetKey: z9.string(),
1188
- url: z9.string().url().optional(),
1189
- thumbnailUrl: z9.string().url().optional(),
1190
- label: z9.string().optional(),
1191
- format: z9.string().optional()
1192
- });
1193
- var VideoCreativeVideo = z9.object({
1194
- provider: z9.string(),
1195
- model: z9.string(),
1196
- durationSeconds: z9.number().int().positive().optional(),
1197
- variants: z9.array(VideoCreativeVariant).default([]),
1198
- clipUrls: z9.array(z9.string().url()).optional(),
1199
- adCopy: z9.string().optional()
1200
- });
1201
- var VideoCreativeContext = z9.object({
1202
- productTitle: z9.string(),
1203
- primaryAngle: z9.string(),
1204
- buyerPersona: z9.string().optional(),
1205
- brandId: z9.string().uuid().optional(),
1206
- features: z9.array(z9.string()).default([]),
1207
- scriptNotes: z9.string().optional(),
1208
- cta: z9.string().optional(),
1209
- language: z9.string(),
1373
+ var VideoCreativeVariant = z10.object({
1374
+ id: z10.string(),
1375
+ assetKey: z10.string(),
1376
+ url: z10.string().url().optional(),
1377
+ thumbnailUrl: z10.string().url().optional(),
1378
+ label: z10.string().optional(),
1379
+ format: z10.string().optional()
1380
+ });
1381
+ var VideoCreativeVideo = z10.object({
1382
+ provider: z10.string(),
1383
+ model: z10.string(),
1384
+ durationSeconds: z10.number().int().positive().optional(),
1385
+ variants: z10.array(VideoCreativeVariant).default([]),
1386
+ clipUrls: z10.array(z10.string().url()).optional(),
1387
+ adCopy: z10.string().optional()
1388
+ });
1389
+ var VideoCreativeContext = z10.object({
1390
+ productTitle: z10.string(),
1391
+ primaryAngle: z10.string(),
1392
+ buyerPersona: z10.string().optional(),
1393
+ brandId: z10.string().uuid().optional(),
1394
+ features: z10.array(z10.string()).default([]),
1395
+ scriptNotes: z10.string().optional(),
1396
+ cta: z10.string().optional(),
1397
+ language: z10.string(),
1210
1398
  aspectRatio: VideoCreativeAspectRatio,
1211
- durationSeconds: z9.number().int().positive()
1399
+ durationSeconds: z10.number().int().positive()
1212
1400
  });
1213
- var VideoCreativeJobResult = z9.object({
1401
+ var VideoCreativeJobResult = z10.object({
1214
1402
  video: VideoCreativeVideo,
1215
1403
  context: VideoCreativeContext,
1216
- createdAt: z9.string().datetime().optional()
1404
+ createdAt: z10.string().datetime().optional()
1217
1405
  });
1218
- var VideoCreativeJobStatusEnum = z9.enum([
1406
+ var VideoCreativeJobStatusEnum = z10.enum([
1219
1407
  "pending",
1220
1408
  "processing",
1221
1409
  "completed",
1222
1410
  "error"
1223
1411
  ]);
1224
- var VideoCreativeJobStatus = z9.object({
1225
- jobId: z9.string(),
1412
+ var VideoCreativeJobStatus = z10.object({
1413
+ jobId: z10.string(),
1226
1414
  status: VideoCreativeJobStatusEnum,
1227
- progress: z9.number().min(0).max(100).optional(),
1228
- etaSeconds: z9.number().int().nonnegative().optional(),
1229
- message: z9.string().optional(),
1230
- error: z9.string().optional(),
1415
+ progress: z10.number().min(0).max(100).optional(),
1416
+ etaSeconds: z10.number().int().nonnegative().optional(),
1417
+ message: z10.string().optional(),
1418
+ error: z10.string().optional(),
1231
1419
  /**
1232
1420
  * Primary angle that the backend will use for generation.
1233
1421
  * Can be user-provided (request.primaryAngle) or generated (angles[0]).
1234
1422
  */
1235
- primaryAngle: z9.string().optional(),
1236
- primaryAngleSource: z9.enum([
1423
+ primaryAngle: z10.string().optional(),
1424
+ primaryAngleSource: z10.enum([
1237
1425
  "user",
1238
1426
  "generated"
1239
1427
  ]).optional(),
1240
- buyerPersona: z9.string().optional(),
1428
+ buyerPersona: z10.string().optional(),
1241
1429
  result: VideoCreativeJobResult.nullable().optional()
1242
1430
  });
1243
- var VideoCreativeJobPayload = z9.object({
1244
- jobId: z9.string().optional(),
1431
+ var VideoCreativeJobPayload = z10.object({
1432
+ jobId: z10.string().optional(),
1245
1433
  status: VideoCreativeJobStatusEnum.optional(),
1246
- progress: z9.number().min(0).max(100).optional(),
1247
- etaSeconds: z9.number().int().nonnegative().optional(),
1248
- message: z9.string().optional(),
1249
- error: z9.string().optional(),
1250
- primaryAngle: z9.string().optional(),
1251
- primaryAngleSource: z9.enum([
1434
+ progress: z10.number().min(0).max(100).optional(),
1435
+ etaSeconds: z10.number().int().nonnegative().optional(),
1436
+ message: z10.string().optional(),
1437
+ error: z10.string().optional(),
1438
+ primaryAngle: z10.string().optional(),
1439
+ primaryAngleSource: z10.enum([
1252
1440
  "user",
1253
1441
  "generated"
1254
1442
  ]).optional(),
1255
- buyerPersona: z9.string().optional(),
1443
+ buyerPersona: z10.string().optional(),
1256
1444
  result: VideoCreativeJobResult.nullable().optional()
1257
1445
  });
1258
- var VideoCreativeHistoryEntry = z9.object({
1259
- id: z9.string(),
1260
- createdAt: z9.string().datetime(),
1446
+ var VideoCreativeHistoryEntry = z10.object({
1447
+ id: z10.string(),
1448
+ createdAt: z10.string().datetime(),
1261
1449
  status: VideoCreativeJobStatusEnum,
1262
1450
  result: VideoCreativeJobResult.nullable(),
1263
- error: z9.string().optional()
1451
+ error: z10.string().optional()
1452
+ });
1453
+ var AdCreative = z10.object({
1454
+ provider: z10.string(),
1455
+ model: z10.string(),
1456
+ prompt: z10.string().min(1),
1457
+ url: z10.string().min(1),
1458
+ width: z10.number().int().positive(),
1459
+ height: z10.number().int().positive(),
1460
+ error: z10.string().optional(),
1461
+ variants: z10.array(z10.string().url()).optional(),
1462
+ /** One-to-one with `variants`: null for ProductMaker-original images and the
1463
+ * evidence tier for the competitor-reference variant. */
1464
+ variantSignals: z10.array(ImageWinnerSignal.nullable()).optional(),
1465
+ createdAt: z10.string().datetime().optional(),
1466
+ adCopy: z10.string().optional()
1467
+ }).superRefine((creative, ctx) => {
1468
+ if (creative.variantSignals !== void 0 && (creative.variants === void 0 || creative.variantSignals.length !== creative.variants.length)) {
1469
+ ctx.addIssue({
1470
+ code: z10.ZodIssueCode.custom,
1471
+ path: [
1472
+ "variantSignals"
1473
+ ],
1474
+ message: "variantSignals must align one-to-one with variants"
1475
+ });
1476
+ }
1264
1477
  });
1265
- var AdCreative = z9.object({
1266
- provider: z9.string(),
1267
- model: z9.string(),
1268
- prompt: z9.string().min(1),
1269
- url: z9.string().min(1),
1270
- width: z9.number().int().positive(),
1271
- height: z9.number().int().positive(),
1272
- error: z9.string().optional(),
1273
- variants: z9.array(z9.string().url()).optional(),
1274
- createdAt: z9.string().datetime().optional(),
1275
- adCopy: z9.string().optional()
1276
- });
1277
- var ImageCreativeCopy = z9.object({
1278
- title: z9.string().optional(),
1279
- subtitle: z9.string().optional(),
1280
- headline: z9.string().optional(),
1281
- bullets: z9.array(z9.string()).optional(),
1282
- discount: z9.string().optional(),
1283
- promoSticker: z9.string().optional(),
1284
- colors: z9.array(z9.string()).optional()
1478
+ var ImageCreativeCopy = z10.object({
1479
+ title: z10.string().optional(),
1480
+ subtitle: z10.string().optional(),
1481
+ headline: z10.string().optional(),
1482
+ bullets: z10.array(z10.string()).optional(),
1483
+ discount: z10.string().optional(),
1484
+ promoSticker: z10.string().optional(),
1485
+ colors: z10.array(z10.string()).optional()
1285
1486
  }).partial();
1286
- var ImageCreativeVariant = z9.object({
1287
- id: z9.string(),
1288
- url: z9.string().url(),
1289
- downloadUrl: z9.string().url().optional()
1290
- });
1291
- var ImageCreativeResult = z9.object({
1292
- provider: z9.string(),
1293
- model: z9.string(),
1294
- prompt: z9.string(),
1295
- width: z9.number().int().positive(),
1296
- height: z9.number().int().positive(),
1297
- variants: z9.array(ImageCreativeVariant).min(1),
1487
+ var ImageCreativeVariant = z10.object({
1488
+ id: z10.string(),
1489
+ url: z10.string().url(),
1490
+ downloadUrl: z10.string().url().optional()
1491
+ });
1492
+ var ImageCreativeResult = z10.object({
1493
+ provider: z10.string(),
1494
+ model: z10.string(),
1495
+ prompt: z10.string(),
1496
+ width: z10.number().int().positive(),
1497
+ height: z10.number().int().positive(),
1498
+ variants: z10.array(ImageCreativeVariant).min(1),
1298
1499
  copy: ImageCreativeCopy.optional(),
1299
- createdAt: z9.string().datetime().optional(),
1300
- adCopy: z9.string().optional()
1301
- });
1302
- var ImageCreativeEditableRequest = z9.object({
1303
- price: z9.string(),
1304
- offer: z9.string(),
1305
- compareAtPrice: z9.string(),
1306
- country: z9.string()
1500
+ createdAt: z10.string().datetime().optional(),
1501
+ adCopy: z10.string().optional()
1502
+ });
1503
+ var ImageCreativeEditableRequest = z10.object({
1504
+ price: z10.string(),
1505
+ offer: z10.string(),
1506
+ compareAtPrice: z10.string(),
1507
+ country: z10.string()
1307
1508
  }).partial();
1308
- var ImageCreativeJobStatusEnum = z9.enum([
1509
+ var ImageCreativeJobStatusEnum = z10.enum([
1309
1510
  "pending",
1310
1511
  "processing",
1311
1512
  "completed",
1312
1513
  "error"
1313
1514
  ]);
1314
- var ImageCreativeJobStatus = z9.object({
1315
- jobId: z9.string().uuid(),
1515
+ var ImageCreativeJobStatus = z10.object({
1516
+ jobId: z10.string().uuid(),
1316
1517
  status: ImageCreativeJobStatusEnum,
1317
- error: z9.string().optional(),
1518
+ error: z10.string().optional(),
1318
1519
  result: ImageCreativeResult.nullable().optional(),
1319
1520
  request: ImageCreativeEditableRequest.optional()
1320
1521
  });
1321
- var ImageCreativeJobPayload = z9.object({
1322
- jobId: z9.string().uuid(),
1522
+ var ImageCreativeJobPayload = z10.object({
1523
+ jobId: z10.string().uuid(),
1323
1524
  status: ImageCreativeJobStatusEnum
1324
1525
  });
1325
- var ImageCreativeHistoryEntry = z9.object({
1326
- id: z9.string().uuid(),
1327
- createdAt: z9.string().datetime(),
1526
+ var ImageCreativeHistoryEntry = z10.object({
1527
+ id: z10.string().uuid(),
1528
+ createdAt: z10.string().datetime(),
1328
1529
  status: ImageCreativeJobStatusEnum,
1329
1530
  result: ImageCreativeResult.nullable(),
1330
- error: z9.string().optional()
1531
+ error: z10.string().optional()
1331
1532
  });
1332
- var LandingCreativeJobStatusEnum = z9.enum([
1533
+ var LandingCreativeJobStatusEnum = z10.enum([
1333
1534
  "pending",
1334
1535
  "processing",
1335
1536
  "completed",
1336
1537
  "error"
1337
1538
  ]);
1338
- var LandingCreativeResult = z9.object({
1339
- images: z9.array(LandingImageReadable).max(20),
1340
- createdAt: z9.string().datetime().optional()
1539
+ var LandingCreativeResult = z10.object({
1540
+ images: z10.array(LandingImageReadable).max(20),
1541
+ createdAt: z10.string().datetime().optional()
1341
1542
  });
1342
- var LandingCreativeJobStatus = z9.object({
1343
- jobId: z9.string().uuid(),
1543
+ var LandingCreativeJobStatus = z10.object({
1544
+ jobId: z10.string().uuid(),
1344
1545
  status: LandingCreativeJobStatusEnum,
1345
- error: z9.string().optional(),
1546
+ error: z10.string().optional(),
1346
1547
  result: LandingCreativeResult.nullable().optional()
1347
1548
  });
1348
- var LandingCreativeJobPayload = z9.object({
1349
- jobId: z9.string().uuid(),
1549
+ var LandingCreativeJobPayload = z10.object({
1550
+ jobId: z10.string().uuid(),
1350
1551
  status: LandingCreativeJobStatusEnum
1351
1552
  });
1352
- var LandingCreativeHistoryEntry = z9.object({
1353
- id: z9.string().uuid(),
1354
- createdAt: z9.string().datetime(),
1553
+ var LandingCreativeHistoryEntry = z10.object({
1554
+ id: z10.string().uuid(),
1555
+ createdAt: z10.string().datetime(),
1355
1556
  status: LandingCreativeJobStatusEnum,
1356
1557
  result: LandingCreativeResult.nullable(),
1357
- error: z9.string().optional()
1558
+ error: z10.string().optional()
1358
1559
  });
1359
- var CreativeGeneratorJob = z9.object({
1360
- taskId: z9.string().uuid(),
1361
- cleanImagesWaitAttempt: z9.number().int().nonnegative().optional()
1560
+ var CreativeGeneratorJob = z10.object({
1561
+ taskId: z10.string().uuid(),
1562
+ cleanImagesWaitAttempt: z10.number().int().nonnegative().optional()
1362
1563
  });
1363
1564
 
1364
1565
  // ../../packages/types/src/product-label.ts
1365
- import { z as z10 } from "zod";
1566
+ import { z as z11 } from "zod";
1366
1567
  var PRODUCT_LABEL_MAX_CHARS = 80;
1367
- var ProductLabelSchema = z10.string().transform((value) => value.trim()).pipe(z10.string().max(PRODUCT_LABEL_MAX_CHARS)).optional();
1568
+ var ProductLabelSchema = z11.string().transform((value) => value.trim()).pipe(z11.string().max(PRODUCT_LABEL_MAX_CHARS)).optional();
1368
1569
 
1369
1570
  // ../../packages/types/src/landing-mode.ts
1370
- import { z as z11 } from "zod";
1371
- var LandingModeSchema = z11.enum([
1571
+ import { z as z12 } from "zod";
1572
+ var LandingModeSchema = z12.enum([
1372
1573
  "classic",
1373
1574
  "code"
1374
1575
  ]);
1375
1576
 
1376
1577
  // ../../packages/types/src/landing-image-format.ts
1377
- import { z as z12 } from "zod";
1578
+ import { z as z13 } from "zod";
1378
1579
  var LANDING_IMAGE_FORMATS = [
1379
1580
  "9:16",
1380
1581
  "4:5",
1381
1582
  "1:1"
1382
1583
  ];
1383
- var LandingImageFormatSchema = z12.enum(LANDING_IMAGE_FORMATS);
1584
+ var LandingImageFormatSchema = z13.enum(LANDING_IMAGE_FORMATS);
1384
1585
 
1385
1586
  // ../../packages/types/src/ingest.ts
1386
1587
  var INGEST_MAX_REFERENCE_IMAGES = 5;
@@ -1399,9 +1600,9 @@ var INGEST_ALLOWED_IMAGE_MIME_TYPES = [
1399
1600
  ];
1400
1601
  var PRODUCT_CONTEXT_MAX_CHARS = 3e3;
1401
1602
  var INGEST_ALLOWED_IMAGE_MIME_TYPE_SET = new Set(INGEST_ALLOWED_IMAGE_MIME_TYPES);
1402
- var IngestOptions = z13.object({
1603
+ var IngestOptions = z14.object({
1403
1604
  /** Optional owner-scoped guided journey context. Never bypasses ingest gates. */
1404
- dropshippingProjectId: z13.string().uuid().optional(),
1605
+ dropshippingProjectId: z14.string().uuid().optional(),
1405
1606
  languageCode: LanguageCodeSchema.default("es-CO"),
1406
1607
  ugcFormat: VideoFormat.default("single_15s"),
1407
1608
  country: CountrySchema,
@@ -1414,26 +1615,33 @@ var IngestOptions = z13.object({
1414
1615
  videoTier: VideoTier.default(DEFAULT_VIDEO_TIER),
1415
1616
  narrativeStyle: NarrativeStyle.default("review"),
1416
1617
  videoPreset: VideoPreset.optional(),
1417
- multiAngleVideos: z13.boolean().default(false),
1418
- videoAngleCount: z13.number().int().min(1).max(3).optional(),
1618
+ multiAngleVideos: z14.boolean().default(false),
1619
+ videoAngleCount: z14.number().int().min(1).max(3).optional(),
1419
1620
  /** Per-angle narrative override (positional). angleNarratives[i] forces the
1420
1621
  * narrative of angle position i, overriding the batch auto-selector; a list
1421
1622
  * shorter than the generated angle count leaves the remaining positions
1422
1623
  * auto-selected. Empty/omitted → all angles auto-select as before. */
1423
- angleNarratives: z13.array(NarrativeStyle).max(3).optional(),
1424
- // 720p is the recommended default for every tier (flipped 2026-06-22 — grok provider
1425
- // unified per-second pricing, so 720p is only a small premium and worth the sharpness).
1624
+ angleNarratives: z14.array(NarrativeStyle).max(3).optional(),
1625
+ // 480p is the recommended default for every tier (flipped back 2026-07-30 — the
1626
+ // 720p premium is real but small, and users on the entry plan were defaulting
1627
+ // into it while burning half a month of credits on one video).
1426
1628
  // This Zod default is what callers that omit the field (MCP/API) actually get,
1427
- // so it must agree with resolveSingleCallAwareResolution's unset fallback (720p).
1428
- videoResolution: VideoResolution.default("720p"),
1429
- generateAdCopy: z13.boolean().default(true),
1430
- generateAiMusic: z13.boolean().optional(),
1629
+ // so it must agree with resolveSingleCallAwareResolution's unset fallback (480p).
1630
+ videoResolution: VideoResolution.default("480p"),
1631
+ generateAdCopy: z14.boolean().default(true),
1632
+ generateAiMusic: z14.boolean().optional(),
1431
1633
  voiceId: VoiceId.optional(),
1432
- isCombo: z13.boolean().default(false),
1433
- productContext: z13.string().transform((value) => value.trim()).pipe(z13.string().max(PRODUCT_CONTEXT_MAX_CHARS)).optional(),
1434
- creativeDirection: z13.string().transform((value) => value.trim()).pipe(z13.string().max(500)).optional(),
1634
+ isCombo: z14.boolean().default(false),
1635
+ productContext: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PRODUCT_CONTEXT_MAX_CHARS)).optional(),
1636
+ creativeDirection: z14.string().transform((value) => value.trim()).pipe(z14.string().max(500)).optional(),
1637
+ /** Exact copy the user pinned. Rendered VERBATIM on the image creatives this task
1638
+ * produces and on the landing hero — never on video, which steers via scriptNotes.
1639
+ * `.pipe(...max)` REJECTS over-cap instead of truncating: silently shortening a pin
1640
+ * would ship copy the user never wrote, which is the bug the pin exists to prevent. */
1641
+ headline: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PINNED_HEADLINE_MAX_CHARS)).optional(),
1642
+ subheadline: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PINNED_SUBHEADLINE_MAX_CHARS)).optional(),
1435
1643
  productLabel: ProductLabelSchema,
1436
- selectedOutputs: z13.array(z13.enum([
1644
+ selectedOutputs: z14.array(z14.enum([
1437
1645
  "landing",
1438
1646
  "video",
1439
1647
  "image"
@@ -1445,26 +1653,26 @@ var IngestOptions = z13.object({
1445
1653
  landingStrategy: LandingStrategySchema.optional(),
1446
1654
  /** Aspect ratio for classic image landing blocks. Ignored by code landing. Absent ⇒ '9:16'. */
1447
1655
  landingImageFormat: LandingImageFormatSchema.optional(),
1448
- imageStyle: z13.string().optional(),
1449
- creativeArchetype: ImageCreativeArchetype.or(z13.literal("ai_decides")).optional(),
1450
- visualTreatment: ImageCreativeVisualTreatment.or(z13.literal("ai_decides")).optional(),
1451
- aspectRatio: z13.string().optional(),
1452
- brandColors: z13.string().optional(),
1453
- nVariants: z13.number().int().min(1).max(6).optional(),
1454
- imageModel: z13.string().optional(),
1455
- styleMix: z13.string().optional(),
1456
- aiDecidesAll: z13.boolean().optional(),
1656
+ imageStyle: z14.string().optional(),
1657
+ creativeArchetype: ImageCreativeArchetype.or(z14.literal("ai_decides")).optional(),
1658
+ visualTreatment: ImageCreativeVisualTreatment.or(z14.literal("ai_decides")).optional(),
1659
+ aspectRatio: z14.string().optional(),
1660
+ brandColors: z14.string().optional(),
1661
+ nVariants: z14.number().int().min(1).max(6).optional(),
1662
+ imageModel: z14.string().optional(),
1663
+ styleMix: z14.string().optional(),
1664
+ aiDecidesAll: z14.boolean().optional(),
1457
1665
  /** Opt out of the competitor-spy analysis. When false the worker skips
1458
1666
  * enqueueCompetitorScan entirely, saving ~$0.25-0.35 user-visible per
1459
1667
  * scan. Default true preserves the existing flow; users with their own
1460
1668
  * competitor intel or in privacy-sensitive niches can disable. */
1461
- competitorScanEnabled: z13.boolean().default(true),
1462
- brandId: z13.string().uuid().optional()
1669
+ competitorScanEnabled: z14.boolean().default(true),
1670
+ brandId: z14.string().uuid().optional()
1463
1671
  }).partial().superRefine((data, ctx) => {
1464
1672
  const message = validatePriceInversion(data.price, data.compareAtPrice);
1465
1673
  if (message) {
1466
1674
  ctx.addIssue({
1467
- code: z13.ZodIssueCode.custom,
1675
+ code: z14.ZodIssueCode.custom,
1468
1676
  path: [
1469
1677
  "compareAtPrice"
1470
1678
  ],
@@ -1475,11 +1683,11 @@ var IngestOptions = z13.object({
1475
1683
  });
1476
1684
  }
1477
1685
  });
1478
- var IngestRequest = z13.object({
1479
- url: z13.string().url(),
1686
+ var IngestRequest = z14.object({
1687
+ url: z14.string().url(),
1480
1688
  options: IngestOptions.optional()
1481
1689
  });
1482
- var VideoOptionsOverride = z13.object({
1690
+ var VideoOptionsOverride = z14.object({
1483
1691
  videoModel: VideoModel.optional(),
1484
1692
  videoTier: VideoTier.optional(),
1485
1693
  ugcFormat: VideoFormat.optional(),
@@ -1488,38 +1696,38 @@ var VideoOptionsOverride = z13.object({
1488
1696
  videoResolution: VideoResolution.optional(),
1489
1697
  voiceId: VoiceId.optional(),
1490
1698
  /** Actor/character description (role + appearance + attitude). Drives voice and video character. */
1491
- actor: z13.string().max(360).optional()
1699
+ actor: z14.string().max(360).optional()
1492
1700
  });
1493
1701
  var AngleVideoOptions = VideoOptionsOverride.extend({
1494
- angleIndex: z13.number().int().nonnegative(),
1702
+ angleIndex: z14.number().int().nonnegative(),
1495
1703
  /** Per-angle angle/hook text override. When provided, replaces the auto-generated angle. */
1496
- angle: z13.string().max(480).optional(),
1704
+ angle: z14.string().max(480).optional(),
1497
1705
  /** Per-angle creative direction (camera/action/setting/tone). NOT spoken dialogue — the script is authored/edited at script review. */
1498
- scriptNotes: z13.string().max(2e3).optional(),
1706
+ scriptNotes: z14.string().max(2e3).optional(),
1499
1707
  /** Per-angle buyer persona override (describes the target audience) */
1500
- buyerPersona: z13.string().max(360).optional(),
1708
+ buyerPersona: z14.string().max(360).optional(),
1501
1709
  /** Per-angle actor/character description (role + appearance + attitude). Drives voice and video character. */
1502
- actor: z13.string().max(360).optional()
1503
- });
1504
- var RapidScriptBlueprintShotForApproval = z13.object({
1505
- id: z13.string().min(1),
1506
- beatSource: z13.string().min(1),
1507
- durationSec: z13.number().positive(),
1508
- role: z13.string().min(1).max(120),
1509
- text: z13.string().min(1).max(320),
1510
- visualIntent: z13.string().min(1).max(500).optional(),
1511
- variantKey: z13.string().min(1).max(120).optional(),
1512
- targetWords: z13.number().int().positive().optional(),
1513
- minWords: z13.number().int().positive(),
1514
- maxWords: z13.number().int().positive()
1710
+ actor: z14.string().max(360).optional()
1711
+ });
1712
+ var RapidScriptBlueprintShotForApproval = z14.object({
1713
+ id: z14.string().min(1),
1714
+ beatSource: z14.string().min(1),
1715
+ durationSec: z14.number().positive(),
1716
+ role: z14.string().min(1).max(120),
1717
+ text: z14.string().min(1).max(320),
1718
+ visualIntent: z14.string().min(1).max(500).optional(),
1719
+ variantKey: z14.string().min(1).max(120).optional(),
1720
+ targetWords: z14.number().int().positive().optional(),
1721
+ minWords: z14.number().int().positive(),
1722
+ maxWords: z14.number().int().positive()
1515
1723
  }).refine((shot) => shot.maxWords >= shot.minWords, {
1516
1724
  message: "maxWords must be greater than or equal to minWords",
1517
1725
  path: [
1518
1726
  "maxWords"
1519
1727
  ]
1520
1728
  });
1521
- var RapidScriptBlueprintForApproval = z13.object({
1522
- shots: z13.array(RapidScriptBlueprintShotForApproval).min(1).max(10)
1729
+ var RapidScriptBlueprintForApproval = z14.object({
1730
+ shots: z14.array(RapidScriptBlueprintShotForApproval).min(1).max(10)
1523
1731
  }).refine((blueprint) => {
1524
1732
  const ids = blueprint.shots.map((shot) => shot.id);
1525
1733
  return new Set(ids).size === ids.length;
@@ -1532,27 +1740,27 @@ var RapidScriptBlueprintForApproval = z13.object({
1532
1740
  var AngleVideoOptionsWithBlueprint = AngleVideoOptions.extend({
1533
1741
  rapidScriptBlueprint: RapidScriptBlueprintForApproval.optional()
1534
1742
  });
1535
- var ProductImageSelection = z13.string().min(1).max(2048).refine((value) => !value.includes("\0"), "Invalid image reference");
1536
- var IngestResumePayload = z13.object({
1537
- selectedAngleIndices: z13.array(z13.number().int().nonnegative()).min(1).max(3),
1743
+ var ProductImageSelection = z14.string().min(1).max(2048).refine((value) => !value.includes("\0"), "Invalid image reference");
1744
+ var IngestResumePayload = z14.object({
1745
+ selectedAngleIndices: z14.array(z14.number().int().nonnegative()).min(1).max(3),
1538
1746
  ugcFormat: VideoFormat.default("single_15s"),
1539
1747
  videoStyle: VideoStyle.default("ugc"),
1540
1748
  videoModel: VideoModel.default("veo"),
1541
1749
  videoTier: VideoTier.optional(),
1542
1750
  narrativeStyle: NarrativeStyle.default("review"),
1543
- // 720p is the recommended default for every tier (flipped 2026-06-22) — keep in sync
1544
- // with IngestOptions.videoResolution and resolveSingleCallAwareResolution.
1545
- videoResolution: VideoResolution.default("720p"),
1546
- pauseForVideoScriptReview: z13.boolean().optional(),
1547
- generateAiMusic: z13.boolean().optional(),
1548
- scriptNotes: z13.string().max(2e3).optional(),
1751
+ // 480p is the recommended default for every tier (flipped back 2026-07-30) — keep in
1752
+ // sync with IngestOptions.videoResolution and resolveSingleCallAwareResolution.
1753
+ videoResolution: VideoResolution.default("480p"),
1754
+ pauseForVideoScriptReview: z14.boolean().optional(),
1755
+ generateAiMusic: z14.boolean().optional(),
1756
+ scriptNotes: z14.string().max(2e3).optional(),
1549
1757
  languageCode: LanguageCodeSchema.optional(),
1550
1758
  country: CountrySchema.optional(),
1551
1759
  price: PriceSchema.optional(),
1552
1760
  offer: OfferSchema.optional(),
1553
1761
  compareAtPrice: CompareAtPriceSchema.optional(),
1554
1762
  paymentMethods: PaymentMethodsSchema.optional(),
1555
- angleVideoOptions: z13.array(AngleVideoOptionsWithBlueprint).max(3).refine((opts) => {
1763
+ angleVideoOptions: z14.array(AngleVideoOptionsWithBlueprint).max(3).refine((opts) => {
1556
1764
  if (!opts?.length) return true;
1557
1765
  const indices = opts.map((o) => o.angleIndex);
1558
1766
  return new Set(indices).size === indices.length;
@@ -1561,191 +1769,29 @@ var IngestResumePayload = z13.object({
1561
1769
  }).optional(),
1562
1770
  voiceId: VoiceId.optional(),
1563
1771
  /** Global actor/character description (role + appearance + attitude). Default for all angles. */
1564
- actor: z13.string().max(360).optional(),
1772
+ actor: z14.string().max(360).optional(),
1565
1773
  /** Custom angle override for landing/creative (uses this instead of first angle) */
1566
- customAngleOverride: z13.string().min(1).max(480).optional(),
1774
+ customAngleOverride: z14.string().min(1).max(480).optional(),
1567
1775
  /** Pain points to emphasize in landings, creatives, and videos */
1568
- painPoints: z13.string().max(1500).optional(),
1776
+ painPoints: z14.string().max(1500).optional(),
1569
1777
  /** Benefits to highlight in landings, creatives, and videos */
1570
- benefits: z13.string().max(1500).optional(),
1778
+ benefits: z14.string().max(1500).optional(),
1571
1779
  /** Target gender for landing/creative images */
1572
- creativeGender: z13.enum([
1780
+ creativeGender: z14.enum([
1573
1781
  "female",
1574
1782
  "male",
1575
1783
  "mixed"
1576
1784
  ]).optional(),
1785
+ /** Non-blocking static winner preference. Omitted defaults to original. */
1786
+ winnerVisualMode: WinnerVisualMode.optional(),
1577
1787
  /** Which product image URLs to use for this generation (ephemeral selection) */
1578
- selectedImageUrls: z13.array(ProductImageSelection).max(INGEST_MAX_REFERENCE_IMAGES).optional(),
1788
+ selectedImageUrls: z14.array(ProductImageSelection).max(INGEST_MAX_REFERENCE_IMAGES).optional(),
1579
1789
  /** User-confirmed intended product target for the selected reference image set */
1580
1790
  productTargetConfirmation: ProductTargetConfirmationSchema.optional(),
1581
1791
  /** User-confirmed grouping for the selected reference image set */
1582
1792
  productReferenceConfirmation: ProductReferenceConfirmationSchema.optional()
1583
1793
  });
1584
1794
 
1585
- // ../../packages/types/src/angle.ts
1586
- import { z as z14 } from "zod";
1587
- var AnchoredIntel = z14.object({
1588
- painPointIndices: z14.array(z14.number().int().nonnegative()).max(10),
1589
- hookIndices: z14.array(z14.number().int().nonnegative()).max(10),
1590
- claimIndices: z14.array(z14.number().int().nonnegative()).max(10),
1591
- sourceAngleType: z14.enum([
1592
- "gap",
1593
- "validated_local",
1594
- "saturated_differentiated",
1595
- "novel"
1596
- ]),
1597
- // SNAPSHOT of the resolved intel strings at angle-generation time.
1598
- // Indices reference into Task.competitorIntel which we treat as immutable
1599
- // post-scan, but defensive: a future re-scan path that rewrites
1600
- // Task.competitorIntel would invalidate the indices silently. Persisting
1601
- // the resolved strings ALONGSIDE the indices means downstream consumers
1602
- // (script writer, landing copy) can safely use the strings directly
1603
- // without re-resolving — staleness-immune. The indices remain useful for
1604
- // analytics ("which intel items did angles cite?") and as a sanity-check.
1605
- resolvedPainPoints: z14.array(z14.string()).max(10).optional(),
1606
- resolvedHooks: z14.array(z14.string()).max(10).optional(),
1607
- resolvedClaims: z14.array(z14.string()).max(10).optional(),
1608
- /**
1609
- * Free-form pillar name describing the AXIS of the product this angle
1610
- * attacks (e.g. "ciencia/mecanismo", "tiempo/ritual", "comparación-con-
1611
- * alternativas", "social-proof", "dolor-emocional", "identidad/persona").
1612
- * The LLM picks the label — we do NOT lock an enum because the right
1613
- * pillar taxonomy varies per product (a supplement's "mecanismo" pillar
1614
- * is different from a gadget's "fricción de uso"). Constraint enforced
1615
- * at generation time: the 3 angles emitted for one task MUST use 3
1616
- * DISTINCT pillars (anti-convergence). Lives inside AnchoredIntel because
1617
- * it ships as analytics metadata alongside the anchoredIntel sourceType +
1618
- * indices, and persists as JSON without a column migration. Optional
1619
- * because angles persisted before this change have no pillar.
1620
- */
1621
- pillar: z14.string().min(1).max(60).optional(),
1622
- // Telemetry: why the angle did NOT import a gapAngle. The legacy single value
1623
- // 'none_relevant' was OVERLOADED (it meant "no gaps existed" AND "gaps existed
1624
- // but none fit" AND was the sanitizer default) which made the gap feature
1625
- // unmeasurable. Split into distinct, one-meaning-each states:
1626
- // - persona_mismatch / product_mismatch: the LLM saw gaps and judged them a
1627
- // wrong-persona / wrong-subcategory fit (trusted from the LLM).
1628
- // - no_gaps_in_block: there were no gapAngles in the block (or no block).
1629
- // - gaps_present_not_imported: gaps existed and the angle went novel anyway
1630
- // (the sanitizer's honest default when the LLM didn't pick persona/product).
1631
- // - gaps_all_filtered_noise: the reference HAD gaps but every one was dropped
1632
- // as detection noise (retailer-meta/cross-category) so the LLM saw none.
1633
- // - n_a: the angle imported a gap (sourceAngleType="gap"), so "why skipped"
1634
- // is moot.
1635
- // The deterministic gap-presence states (no_gaps_in_block / gaps_present_not_
1636
- // imported / gaps_all_filtered_noise) are set by sanitizeAnchoredIntel from
1637
- // facts it computes (intel.gapAngles.length, intel.gapAnglesFilteredCount), NOT
1638
- // trusted from the lazy LLM. z.preprocess aliases historical 'none_relevant'
1639
- // rows → 'no_gaps_in_block' so the JSON column parses with no migration.
1640
- // Analytics-only: NO runtime path branches on this value.
1641
- skippedGapReason: z14.preprocess((v) => v === "none_relevant" ? "no_gaps_in_block" : v, z14.enum([
1642
- "persona_mismatch",
1643
- "product_mismatch",
1644
- "no_gaps_in_block",
1645
- "gaps_present_not_imported",
1646
- "gaps_all_filtered_noise",
1647
- "n_a"
1648
- ])).optional(),
1649
- // Telemetry: captures the prod-observed case where a "novel" angle actually
1650
- // reflects a GAP's theme/promise without formally anchoring indices (gapAngles
1651
- // are a separate array, not index-cited, so a real gap import is reclassified
1652
- // 'novel' by the empty-indices coherence guard). Self-reported by the LLM:
1653
- // - gap_imported: the angle's theme imports a gap's logic.
1654
- // - product_derived: derived purely from the product.
1655
- // - hybrid: a mix.
1656
- // SOFT analytics-only signal (the LLM may confabulate) — validated against
1657
- // known prod cases before trusting aggregates; never a control-flow input.
1658
- // Optional in storage; on the wire it ships as a required enum with a 'none'
1659
- // sentinel (strict structured output forbids optional properties) that the
1660
- // sanitizer maps to undefined.
1661
- themeSourceType: z14.enum([
1662
- "gap_imported",
1663
- "product_derived",
1664
- "hybrid"
1665
- ]).optional()
1666
- });
1667
- var Angle = z14.object({
1668
- angle: z14.string().min(1).max(480),
1669
- buyerPersona: z14.string().min(1).max(360),
1670
- /** Character description for video (role + appearance + attitude). Drives voice selection and Frame0Spec. */
1671
- actor: z14.string().min(1).max(360).optional(),
1672
- // Binary only: voice catalog has no non-binary voices.
1673
- // See packages/angles/src/index.ts + getAngles coercion in angle.repository.ts.
1674
- gender: z14.enum([
1675
- "female",
1676
- "male"
1677
- ]).optional(),
1678
- /** Per-angle user-supplied script/visual notes. Hydrated from the
1679
- * `Angle.scriptNotes` DB column or the `angleVideoOptions[i].scriptNotes`
1680
- * passed from the chat. Per-angle overrides global `videoScriptNotes`. */
1681
- scriptNotes: z14.string().max(2e3).optional(),
1682
- /** AI-chosen per-angle narrative (set by the batch selector in angles.worker).
1683
- * Pre-fills the review dropdown; user can override per angle. */
1684
- narrativeStyle: NarrativeStyle.optional(),
1685
- /** See AnchoredIntel docs. Optional because angles persisted before this
1686
- * change have no anchoring captured; new angles ALWAYS emit it (with empty
1687
- * arrays + 'novel' when intel was unavailable). */
1688
- anchoredIntel: AnchoredIntel.optional(),
1689
- /** Server-computed: true when this angle is eligible for the gap_replication
1690
- * narrative (isReplicationAngle(anchoredIntel, intel) && intel.gapBlueprint
1691
- * present). Computed server-side (not derived client-side) because the web
1692
- * only sees this angle's anchoredIntel, not the task's
1693
- * competitorIntel.gapBlueprint. Gates the review dropdown's gap_replication
1694
- * option — absent/false = hidden. */
1695
- gapReplicationEligible: z14.boolean().optional(),
1696
- /** Server-computed: true when this angle's effective narrative is
1697
- * gap_replication, which FORCES the multi-shot STANDARD ("Multiescena")
1698
- * render pipeline regardless of the task's chosen tier (single_call can't
1699
- * produce the multi-shot replication — see task-results.service.ts and the
1700
- * render-time tier override). Surfaces "renders in Multiescena" on the
1701
- * review card so a single_call user isn't surprised by the STANDARD-tier
1702
- * charge. */
1703
- rendersMultiShot: z14.boolean().optional(),
1704
- /** Server-computed product-level origin bucket for the review card's badge
1705
- * (NOT the internal sourceAngleType taxonomy — see resolveAngleOrigin in
1706
- * task-results.service.ts): 'gap' = market opportunity competitors aren't
1707
- * using; 'saturated_twist' = competitors' most-used approach with our own
1708
- * twist; 'ai_recommended' = AI-authored for this product (default). */
1709
- angleOrigin: z14.enum([
1710
- "gap",
1711
- "saturated_twist",
1712
- "ai_recommended"
1713
- ]).optional(),
1714
- /** Server-computed badge TIER for the replication angle (two-tier UX,
1715
- * v1.32.0 — see resolveReplicationTier). 'winner' = a validated angle
1716
- * opportunity backed by a proven ad (anchor gap/saturated → golden badge);
1717
- * 'opportunity' = a product-coherent replicable winner with no validated
1718
- * angle gap (anchor 'dominant' → opportunity/twist badge, no overclaim).
1719
- * Present only on the angle that carries gap_replication; absent otherwise. */
1720
- replicationTier: z14.enum([
1721
- "winner",
1722
- "opportunity"
1723
- ]).optional()
1724
- });
1725
- var AdVideo = z14.object({
1726
- source: z14.enum([
1727
- "tiktok_shop",
1728
- "meta_ads"
1729
- ]),
1730
- url: z14.string().url(),
1731
- title: z14.string().optional(),
1732
- description: z14.string().optional(),
1733
- metrics: z14.object({
1734
- impressions: z14.number().int().nonnegative().optional(),
1735
- views: z14.number().int().nonnegative().optional(),
1736
- likes: z14.number().int().nonnegative().optional(),
1737
- ctaClicks: z14.number().int().nonnegative().optional(),
1738
- diggCount: z14.number().int().nonnegative().optional(),
1739
- shareCount: z14.number().int().nonnegative().optional(),
1740
- commentCount: z14.number().int().nonnegative().optional(),
1741
- playCount: z14.number().int().nonnegative().optional(),
1742
- collectCount: z14.number().int().nonnegative().optional(),
1743
- durationSeconds: z14.number().int().nonnegative().optional()
1744
- }).partial().optional(),
1745
- country: z14.string().optional(),
1746
- lastSeen: z14.string().datetime().optional()
1747
- });
1748
-
1749
1795
  // ../../packages/types/src/frame0.ts
1750
1796
  import { z as z15 } from "zod";
1751
1797
  var Frame0SpecVersion = z15.literal("frame0_spec_v1");
@@ -1924,7 +1970,15 @@ var VideoGenerator = z16.object({
1924
1970
  angleKey: z16.string().min(1),
1925
1971
  angleIndex: z16.number().int().nonnegative(),
1926
1972
  angle: z16.string().min(1).max(480).optional(),
1927
- buyerPersona: z16.string().min(1).max(360).optional(),
1973
+ // READ contract: the cap must fit what the generators actually persist, not
1974
+ // what the write surfaces accept. Angle-derived personas are LLM-written and
1975
+ // routinely exceed the 360-char write cap (prod alert 88badc8: 419 chars), and
1976
+ // this schema serializes existing DB rows — a too-tight cap turns a valid row
1977
+ // into a ZodError that 500s the whole `GET /v1/results/:taskId` payload and
1978
+ // hides the entire task. Same reasoning as the landing-images cap in
1979
+ // `result.ts`. Write-side caps (angle.ts, wizard.ts, task-creative-variants.ts)
1980
+ // intentionally stay at 360.
1981
+ buyerPersona: z16.string().min(1).max(1200).optional(),
1928
1982
  script: z16.string().min(1),
1929
1983
  prompt: z16.string().min(1),
1930
1984
  // Empty string allowed: Pro pipeline pauses for script review BEFORE the grid
@@ -2353,6 +2407,8 @@ var ApproveImageAnglesRequest = z19.object({
2353
2407
  painPoints: z19.string().max(1500).optional(),
2354
2408
  /** Features to include in the image */
2355
2409
  features: z19.array(z19.string().min(1).max(240)).max(20).optional(),
2410
+ /** Non-blocking static winner preference. Omitted defaults to original. */
2411
+ winnerVisualMode: WinnerVisualMode.optional(),
2356
2412
  /** Which product image URLs to use for this generation (ephemeral selection) */
2357
2413
  selectedImageUrls: z19.array(ProductImageSelection).max(5).optional(),
2358
2414
  productTargetConfirmation: ProductTargetConfirmationSchema.optional(),
@@ -2377,12 +2433,13 @@ var ImageSuggestionsResponse = z19.object({
2377
2433
  buyerPersona: z19.string().optional()
2378
2434
  });
2379
2435
  var RefineAngleRequest = z19.object({
2380
- /** Refine mode: 'all' refines angle + pains + benefits, 'pain_points'/'benefits' regenerate just that section */
2436
+ /** Refine mode: 'reference' adapts a competitor image winner's selling structure to the user's product. */
2381
2437
  mode: z19.enum([
2382
2438
  "all",
2383
2439
  "pain_points",
2384
2440
  "benefits",
2385
- "video"
2441
+ "video",
2442
+ "reference"
2386
2443
  ]),
2387
2444
  /** Current angle text (used as context for aligned generation) */
2388
2445
  currentAngle: z19.string().max(480).optional(),
@@ -2475,7 +2532,6 @@ var TASK_CREATIVE_VARIANT_TEXT_LIMITS = {
2475
2532
  recommendationRationale: 600
2476
2533
  };
2477
2534
  var TaskCreativeVariantVideoPreset = VideoPreset.exclude([
2478
- "problem_solution",
2479
2535
  "ugc_unboxing_chaos",
2480
2536
  "ugc_curiosity_reveal",
2481
2537
  "ugc_listicle_3",
@@ -2832,7 +2888,10 @@ var StatusResponse = z22.object({
2832
2888
  /** Derived from landingEnabled/videoGeneratorEnabled/adCreativeEnabled flags;
2833
2889
  * canonical [landing, video, image] order regardless of input order.
2834
2890
  * Always present (server derivation has a "legacy = enabled" fallback). */
2835
- selectedOutputs: SelectedOutputsSchema
2891
+ selectedOutputs: SelectedOutputsSchema,
2892
+ /** Persisted image portfolio size so a refreshed review reconstructs the
2893
+ * same visual plan even when browser session state is unavailable. */
2894
+ imageVariantCount: z22.number().int().min(1).max(6).optional()
2836
2895
  });
2837
2896
  var TaskSummary = z22.object({
2838
2897
  taskId: z22.string().uuid(),
@@ -4117,6 +4176,8 @@ var TikTokCreateCampaignResponseSchema = z32.object({
4117
4176
  // ../../packages/types/src/pricing.ts
4118
4177
  var CREDITS_PER_USD_CENT = 10;
4119
4178
  var CREDITS_PER_DOLLAR = 100 * CREDITS_PER_USD_CENT;
4179
+ var KALODATA_USD_CENTS_PER_PROVIDER_CREDIT = 9;
4180
+ var KALODATA_PROVIDER_CREDITS_PER_SOURCE_CEILING = 7.5;
4120
4181
  var DEFAULT_PRICING = {
4121
4182
  // IMPORTANT: despite the field name, these values are in *USD cents* (not USD dollars).
4122
4183
  // We store cents (and sometimes fractional cents) everywhere so we can convert to credits:
@@ -4162,16 +4223,16 @@ var DEFAULT_PRICING = {
4162
4223
  userPriceCents: 0.8
4163
4224
  },
4164
4225
  "openrouter:chat-parse": {
4165
- ourCostCents: 0.05,
4166
- userPriceCents: 0.1
4226
+ ourCostCents: 0.2,
4227
+ userPriceCents: 0.4
4167
4228
  },
4168
4229
  "openrouter:audio-tag-injection": {
4169
- ourCostCents: 0.4,
4170
- userPriceCents: 0.8
4230
+ ourCostCents: 0.1,
4231
+ userPriceCents: 0.2
4171
4232
  },
4172
4233
  "openrouter:product-consolidation": {
4173
- ourCostCents: 0.4,
4174
- userPriceCents: 0.8
4234
+ ourCostCents: 0.07,
4235
+ userPriceCents: 0.14
4175
4236
  },
4176
4237
  "openrouter:video-final-simplify-rewrite": {
4177
4238
  ourCostCents: 0.4,
@@ -4190,12 +4251,12 @@ var DEFAULT_PRICING = {
4190
4251
  userPriceCents: 0.3
4191
4252
  },
4192
4253
  "openrouter:vision-extract-combo": {
4193
- ourCostCents: 0.3,
4194
- userPriceCents: 0.6
4254
+ ourCostCents: 0.6,
4255
+ userPriceCents: 1.2
4195
4256
  },
4196
4257
  "openrouter:rich-scene-analyzer": {
4197
- ourCostCents: 0.15,
4198
- userPriceCents: 0.3
4258
+ ourCostCents: 13.42,
4259
+ userPriceCents: 26.84
4199
4260
  },
4200
4261
  "openrouter:product-info-extractor": {
4201
4262
  ourCostCents: 0.15,
@@ -4535,6 +4596,10 @@ var DEFAULT_PRICING = {
4535
4596
  ourCostCents: 2,
4536
4597
  userPriceCents: 4
4537
4598
  },
4599
+ "kie:image-generation:nano-banana-2-lite": {
4600
+ ourCostCents: 2,
4601
+ userPriceCents: 4
4602
+ },
4538
4603
  "kie:image-generation:nano-banana-2": {
4539
4604
  ourCostCents: 4,
4540
4605
  userPriceCents: 8
@@ -4571,6 +4636,10 @@ var DEFAULT_PRICING = {
4571
4636
  ourCostCents: 3,
4572
4637
  userPriceCents: 6
4573
4638
  },
4639
+ "fal:image-generation:gpt-image-2:1k": {
4640
+ ourCostCents: 1.5,
4641
+ userPriceCents: 6
4642
+ },
4574
4643
  // Video reference pricing varies by model:
4575
4644
  // - gpt-image-2 @ 2K (primary since 2026-04-24): $0.05 / $0.10
4576
4645
  // - nano-banana-2 (fallback for 5xx/timeout): $0.04 / $0.08
@@ -4767,15 +4836,16 @@ var DEFAULT_PRICING = {
4767
4836
  ourCostCents: 2.5,
4768
4837
  userPriceCents: 5
4769
4838
  },
4770
- // Usage spec extraction (Gemini 3 Flash Preview:online)
4839
+ // Usage spec extraction (gemini-3-flash-preview:online). Measured 1.5807¢ — the :online
4840
+ // web-search surcharge is what makes it ~4x the plain-Flash rate it used to be priced at.
4771
4841
  "openrouter:product-usage-extract": {
4772
- ourCostCents: 0.4,
4773
- userPriceCents: 0.8
4842
+ ourCostCents: 1.58,
4843
+ userPriceCents: 3.16
4774
4844
  },
4775
- // Functional demonstration extraction (Gemini 3 Flash Preview:online with web search)
4845
+ // Functional demonstration extraction (gemini-3-flash-preview:online). Measured 1.0643¢.
4776
4846
  "openrouter:functional-demo-extract": {
4777
- ourCostCents: 0.4,
4778
- userPriceCents: 0.8
4847
+ ourCostCents: 1.06,
4848
+ userPriceCents: 2.12
4779
4849
  },
4780
4850
  // Fusion of angles and usage into visual context (Gemini 3 Flash Preview)
4781
4851
  "openrouter:visual-context-fusion": {
@@ -4792,6 +4862,38 @@ var DEFAULT_PRICING = {
4792
4862
  ourCostCents: 2,
4793
4863
  userPriceCents: 4
4794
4864
  },
4865
+ // Free-form (v2) code landings — estimator inputs + usage.cost fallbacks. Edit-chat
4866
+ // rows are the FLAT per-message price (100/300 credits).
4867
+ "openrouter:landing-art-direction": {
4868
+ ourCostCents: 5,
4869
+ userPriceCents: 10
4870
+ },
4871
+ // v2.1: ONE coherent call authors every section (replaced the per-section
4872
+ // 'landing-section-code' fan-out — kept below for legacy BillingEntry rows).
4873
+ "openrouter:landing-section-authoring": {
4874
+ ourCostCents: 20,
4875
+ userPriceCents: 40
4876
+ },
4877
+ "openrouter:landing-section-code": {
4878
+ ourCostCents: 5,
4879
+ userPriceCents: 10
4880
+ },
4881
+ "openrouter:landing-visual-judge": {
4882
+ ourCostCents: 3,
4883
+ userPriceCents: 6
4884
+ },
4885
+ "system:landing-qa": {
4886
+ ourCostCents: 1,
4887
+ userPriceCents: 0
4888
+ },
4889
+ "openrouter:landing-edit-chat-section": {
4890
+ ourCostCents: 5,
4891
+ userPriceCents: 10
4892
+ },
4893
+ "openrouter:landing-edit-chat-full": {
4894
+ ourCostCents: 15,
4895
+ userPriceCents: 30
4896
+ },
4795
4897
  // Product mix helper calls (Gemini 3 Flash Preview)
4796
4898
  "openrouter:product-mix-script-generation": {
4797
4899
  ourCostCents: 0.4,
@@ -4822,15 +4924,18 @@ var DEFAULT_PRICING = {
4822
4924
  ourCostCents: 0.2,
4823
4925
  userPriceCents: 0.4
4824
4926
  },
4825
- // Landing image QA (Gemini 3 Flash Preview, 1 image). Checks anatomy/text/background cutoff.
4927
+ // Landing image QA (gemini-3.6-flash since 2026-08-01, 1-2 images). Anatomy/text/background cutoff.
4928
+ // 1.88¢ = the post-model-swap steady state at prod averages (11,109 prompt / 928 completion
4929
+ // tokens) WITH the prompt-cache breakpoint; without caching it would be ~2.36¢.
4826
4930
  "openrouter:landing-image-qa": {
4827
- ourCostCents: 0.15,
4828
- userPriceCents: 0.3
4931
+ ourCostCents: 1.88,
4932
+ userPriceCents: 3.76
4829
4933
  },
4830
- // Creative image QA (Gemini 3 Flash Preview, 1 image). Checks product fidelity and severe render defects.
4934
+ // Creative image QA (gemini-3-flash-preview — deliberately NOT on 3.6; see ai_models.md).
4935
+ // Measured 0.8196¢ over 30d of prod.
4831
4936
  "openrouter:creative-image-qa": {
4832
- ourCostCents: 0.15,
4833
- userPriceCents: 0.3
4937
+ ourCostCents: 0.82,
4938
+ userPriceCents: 1.64
4834
4939
  },
4835
4940
  // Anatomy check (Gemini 3 Flash Preview, 1 image). Standalone anatomy verification.
4836
4941
  "openrouter:anatomy-check": {
@@ -4869,10 +4974,10 @@ var DEFAULT_PRICING = {
4869
4974
  ourCostCents: 0.15,
4870
4975
  userPriceCents: 0.3
4871
4976
  },
4872
- // Policy rewrite helper (Gemini 3 Flash Preview)
4977
+ // Policy rewrite helper (gemini-3-flash-preview). Measured 0.0416¢ — a short text-only call.
4873
4978
  "openrouter:policy-prompt-rewrite": {
4874
- ourCostCents: 0.4,
4875
- userPriceCents: 0.8
4979
+ ourCostCents: 0.04,
4980
+ userPriceCents: 0.08
4876
4981
  },
4877
4982
  // TTS dialogue rewrite helper (OpenRouter model: google/gemini-3-flash-preview)
4878
4983
  // Our cost: ~$0.0005 USD per request => 0.05 USD cents; keep 2x markup.
@@ -4943,14 +5048,25 @@ var DEFAULT_PRICING = {
4943
5048
  userPriceCents: 0.8
4944
5049
  },
4945
5050
  // single_call: flattens the approved plan into the one Grok render prompt
4946
- // (gemini-3.6-flash, medium reasoning, 8k maxTokens). Sized from 30d of prod entries on
4947
- // the SAME model — rapid-grok-shot-action averaged 2.11¢ our / 4.23¢ user. Only
4948
- // attempt 1 is charged; the flatten's internal retries record COGS only, so this
4949
- // quotes once per render. Real calls bill the actual usage.cost x2; this value is
4950
- // the estimator basis + the fallback when usage.cost is missing.
5051
+ // (gemini-3.6-flash, medium reasoning, 8k maxTokens).
5052
+ //
5053
+ // RE-DERIVED 2026-08-01 from 860 prod rows (July 2026, still-paper-34632546):
5054
+ // mean 4.573¢ our, p50 4.674¢, p95 6.205¢. The previous 2¢ was inferred by
5055
+ // ANALOGY from `rapid-grok-shot-action` (2.11¢) rather than measured on this
5056
+ // label, and was 2.3x too low — real calls bill the actual usage.cost x2, so
5057
+ // the money was always collected correctly, but every QUOTE understated this
5058
+ // line by that factor. 5¢ sits above the mean and the median with headroom,
5059
+ // below the p95.
5060
+ //
5061
+ // ⚠️ This is a PER-CALL rate and the flatten is NOT called once per render:
5062
+ // measured 4.34 calls per (task, angle) on average, because a structurally
5063
+ // valid prompt that exceeds Grok's 4090-BYTE ceiling is regenerated shorter
5064
+ // (up to MAX_FLATTEN_ATTEMPTS). The estimator multiplies this rate by that cap.
5065
+ // Reducing the CALL COUNT is the real cost lever here, not this rate — see the
5066
+ // note on the flatten line in cost-estimator.service.ts.
4951
5067
  "openrouter:single-call-flatten": {
4952
- ourCostCents: 2,
4953
- userPriceCents: 4
5068
+ ourCostCents: 5,
5069
+ userPriceCents: 10
4954
5070
  },
4955
5071
  "openrouter:hand-detector": {
4956
5072
  ourCostCents: 0.15,
@@ -4969,8 +5085,8 @@ var DEFAULT_PRICING = {
4969
5085
  userPriceCents: 0.8
4970
5086
  },
4971
5087
  "openrouter:wearable-product-semantics": {
4972
- ourCostCents: 0.4,
4973
- userPriceCents: 0.8
5088
+ ourCostCents: 0.11,
5089
+ userPriceCents: 0.22
4974
5090
  },
4975
5091
  // Clip Judge — text-only comparison of QA results between original and retry clips
4976
5092
  // (google/gemini-3-flash-preview, text-only). Our cost: ~$0.0006 => 0.06 cents; 2x markup.
@@ -4987,30 +5103,60 @@ var DEFAULT_PRICING = {
4987
5103
  // Image Judge — text-only comparison of QA results between image candidates
4988
5104
  // (google/gemini-3-flash-preview, text-only). Our cost: ~$0.0006 => 0.06 cents; 2x markup.
4989
5105
  "openrouter:image-judge-text": {
4990
- ourCostCents: 0.06,
4991
- userPriceCents: 0.12
5106
+ ourCostCents: 0.39,
5107
+ userPriceCents: 0.78
4992
5108
  },
4993
5109
  // Image Judge — vision comparison of two images + product reference (google/gemini-3-flash-preview)
4994
- // Our cost: ~$0.0015 => 0.15 cents; 2x markup (images are cheaper than video).
5110
+ // Measured 0.8863¢ over 30d of prod (the old ~0.15¢ predated the multi-image comparison).
4995
5111
  "openrouter:image-judge-vision": {
4996
- ourCostCents: 0.15,
4997
- userPriceCents: 0.3
5112
+ ourCostCents: 0.89,
5113
+ userPriceCents: 1.78
4998
5114
  },
4999
5115
  // Video QA — product integrity scanner (Gemini 3 Flash Preview, 5 frames + reference).
5000
5116
  "openrouter:video-product-integrity": {
5001
5117
  ourCostCents: 0.15,
5002
5118
  userPriceCents: 0.3
5003
5119
  },
5004
- // Video QA — independent anatomy arm-scanner (Gemini 3 Flash Preview, one call per
5005
- // sampled frame, up to ARM_SCAN_MAX_FRAMES per clip). Billed PER FRAME (units: 1 per
5006
- // call), so this rate is per frame, not per clip. Sized above the 30d prod average
5007
- // (10.303 entries at $6.81 our => 0.066¢/frame) so the fallback path — used when
5008
- // OpenRouter omits usage.cost — cannot under-charge. Before this key existed those
5009
- // fallbacks hit the `No pricing rule found; skipping` branch and went UNBILLED.
5120
+ // Video QA — anatomy. THREE keys, and they do NOT share a unit. Getting this wrong
5121
+ // over-charges by ~42x, which is exactly what a first pass at the 2026-07-30 change did.
5122
+ //
5123
+ // (a) The arm-scan's per-FRAME counting sweep. RETIRED 2026-07-30 — it has NO producer left.
5124
+ // Its last call site was the whole-video concat gate, removed as redundant with the
5125
+ // per-scene frame judge (b). It was ALSO duration-blind on `double_30s` — the sweep
5126
+ // samples t=0,1,2… capped at 16, so a 30s assembled video was only partly read while all
5127
+ // 16 frames still billed — but NOT on `single_15s` (~15s, covered completely), which is
5128
+ // the dominant format. Redundancy is what carries the removal; the blindness is secondary.
5129
+ // The estimator no longer quotes it either.
5130
+ // The KEY STAYS DEFINED ON PURPOSE. `BillingService.record` treats an unknown key as
5131
+ // "No pricing rule found; skipping" and bills NOTHING — that silent hole already cost us
5132
+ // 10,303 unbilled calls before 2026-07-18. `scanClipArms` is still exported, so if
5133
+ // anything ever calls it again it must land on a rate, not on the skip branch.
5134
+ // Rate stays PER FRAME, sized above the
5135
+ // 30d prod average (10303 rows => 0.132c user/frame).
5010
5136
  "openrouter:video-anatomy-arm-scan": {
5011
5137
  ourCostCents: 0.1,
5012
5138
  userPriceCents: 0.2
5013
5139
  },
5140
+ // (b) The frame JUDGE that replaced the sweep on the `analyzeVideoClipDJ` path: ALL frames
5141
+ // in ONE multi-image call, so a unit is one call. MEASURED, not estimated —
5142
+ // usage.cost on a real 16-frame judgement at 768px is $0.0417 => 4.17c our. The table is
5143
+ // the FALLBACK used when OpenRouter omits usage.cost, so it must sit at or above that.
5144
+ // Billed PER FRAME (units = frames in the call), like the sweep and for the same reason:
5145
+ // the cost scales with IMAGES and QA_FRAME_JUDGE_FRAMES is env-overridable, so a per-CALL
5146
+ // unit would freeze the fallback charge and the credit-gate quote at the cap the rate
5147
+ // happened to be measured at. Measured $0.0417 over 16 frames => 0.26c/frame; priced at
5148
+ // 0.32c for headroom (n=1 on one clip, and the 1.9k reasoning-token share moves with
5149
+ // QA_DETECTOR_EFFORT). Re-derive from 30d of BillingEntry once this key has volume.
5150
+ "openrouter:video-qa-anatomy-frame-judge": {
5151
+ ourCostCents: 0.32,
5152
+ userPriceCents: 0.64
5153
+ },
5154
+ // (c) Tracing confirmation of a frames-only finding: 3 stills + a 3s 480p slice, one call,
5155
+ // no 16-image prompt — materially cheaper than the judgement it verifies.
5156
+ "openrouter:video-qa-anatomy-tracing-confirm": {
5157
+ ourCostCents: 1.5,
5158
+ userPriceCents: 3
5159
+ },
5014
5160
  // Physics confirmation — skeptical second opinion on D+J floating-object flags
5015
5161
  // (Gemini 3 Flash Preview, ~3 frames per flagged moment, 1 call). Aligned with other vision-QA keys.
5016
5162
  "openrouter:video-physics-confirmation": {
@@ -5052,10 +5198,11 @@ var DEFAULT_PRICING = {
5052
5198
  ourCostCents: 1,
5053
5199
  userPriceCents: 2
5054
5200
  },
5055
- // LLM image ranking (Gemini 3 Flash, ~10 images multi-modal)
5201
+ // LLM image ranking (gemini-3-flash-preview, ~10 images multi-modal). Unit = one CANDIDATE
5202
+ // image, not one call, so the per-unit rate is small: measured 0.0924¢/unit.
5056
5203
  "openrouter:image-ranking": {
5057
- ourCostCents: 0.3,
5058
- userPriceCents: 0.6
5204
+ ourCostCents: 0.09,
5205
+ userPriceCents: 0.18
5059
5206
  },
5060
5207
  // Visual context pass — frame0 description (Gemini 3 Flash Preview, 1 image)
5061
5208
  "openrouter:visual-context-frame0-describe": {
@@ -5144,10 +5291,50 @@ var DEFAULT_PRICING = {
5144
5291
  ourCostCents: 1,
5145
5292
  userPriceCents: 2
5146
5293
  },
5147
- // Product research acquires KaloPilot credits at $0.10 per provider credit;
5148
- // the source worker overrides ourCostCents with the exact reported usage.
5294
+ // ─── Product research (winning products) ─────────────────────────────────
5295
+ // BUNDLE, billed as two rows against the same `productResearchRunId`:
5296
+ // revenue → `system:product-research-report` = 200¢ / 2,000 credits, charged
5297
+ // UP FRONT by purchaseProductResearchRun (which also debits
5298
+ // User.creditCents in the same transaction), and reversed by a
5299
+ // `system:product-research-failure-refund` row when the run fails.
5300
+ // cost → `kalodata:product-research-source` (+ `openrouter:*` analysis),
5301
+ // recorded AFTER the provider answers, with creditOperation
5302
+ // 'ledger_only' — the user was already debited.
5303
+ //
5304
+ // ⚠️ `userPriceCents: 0` on the kalodata row is DELIBERATE, not a missing
5305
+ // charge. Revenue cannot live here: the cost row is written after the
5306
+ // provider bills us, while the price is committed at purchase time, and a
5307
+ // run that fails post-source is refunded in full while its kalodata cost
5308
+ // stands. Any per-provider markup report that divides price÷cost will
5309
+ // therefore show kalodata (and the openrouter product-research labels) at
5310
+ // 0.00×. The cost row carries `bundledRevenue*` metadata so the pairing is
5311
+ // discoverable from the row itself.
5312
+ //
5313
+ // ⚠️ Two traps in aggregating this bundle, both measured on the 27 runs to
5314
+ // 2026-08-01 — read before writing a P&L query:
5315
+ // 1. Refunds are stored as a POSITIVE `userPriceCents` on
5316
+ // `system:product-research-failure-refund` (metadata.target 'refund',
5317
+ // not 'product_research'). Grouping on target='product_research' counts
5318
+ // the purchase and drops its reversal; summing userPriceCents across
5319
+ // the run double-counts instead of netting. Retained revenue is
5320
+ // Σ(label='product-research-report') − Σ(label='product-research-failure-refund').
5321
+ // For those 27 runs that is $54 charged − $30 refunded = $24 kept.
5322
+ // 2. A cost row is per PROVIDER CALL, not per run: `getFreshProductResearchSource`
5323
+ // serves repeat queries from the source cache, so a charged run can have
5324
+ // no kalodata row at all (12 of the 27 did). `bundledRevenueCents` is
5325
+ // therefore context for THIS row — never a revenue total to sum.
5326
+ // Total COGS is Σ ourCostCents over every row with a non-null
5327
+ // productResearchRunId, which needs no per-provider grouping at all.
5328
+ //
5329
+ // The source worker overrides ourCostCents with the exact reported usage
5330
+ // (providerCredits × KALODATA_USD_CENTS_PER_PROVIDER_CREDIT). Nothing reads
5331
+ // this entry today — no cost-estimator key and no credit gate resolves it,
5332
+ // since the gate is a flat `creditCents >= 200` in
5333
+ // purchaseProductResearchRun. It is kept as the audited reference rate so
5334
+ // the estimate stops contradicting the ledger the moment something does
5335
+ // quote from it.
5149
5336
  "kalodata:product-research-source": {
5150
- ourCostCents: 24.9,
5337
+ ourCostCents: KALODATA_PROVIDER_CREDITS_PER_SOURCE_CEILING * KALODATA_USD_CENTS_PER_PROVIDER_CREDIT,
5151
5338
  userPriceCents: 0
5152
5339
  },
5153
5340
  "system:product-research-report": {
@@ -5255,9 +5442,9 @@ var DEFAULT_PRICING = {
5255
5442
  // here (0.0653¢ our/frame) is LOWER, so the prod-derived rate stands per the
5256
5443
  // quote-at-the-ceiling rule. Do not re-add a second key: TS1117 duplicate.
5257
5444
  // Unit = one winner-selection pass over the scraped ad pool (measured 0.3744¢).
5258
- // ⚠️ NOT once per scan any more: the deep-capture candidate fallback re-runs this
5259
- // selector for each retry, so the estimate must quote it at
5260
- // GAP_DEEP_CAPTURE_MAX_CANDIDATES (default 3), never at 1.
5445
+ // ⚠️ NOT once per scan any more: the deep-capture candidate fallback re-runs the
5446
+ // video selector for each retry, and static replication independently selects
5447
+ // one image winner. The estimate quotes GAP_DEEP_CAPTURE_MAX_CANDIDATES + 1.
5261
5448
  "openrouter:gap-winner-selection": {
5262
5449
  ourCostCents: 0.4,
5263
5450
  userPriceCents: 0.8
@@ -5269,6 +5456,13 @@ var DEFAULT_PRICING = {
5269
5456
  ourCostCents: 2.5,
5270
5457
  userPriceCents: 5
5271
5458
  },
5459
+ // Unit = one independent multimodal argument extraction for the selected
5460
+ // static-image winner. Quote at the blueprint ceiling until BillingEntry has
5461
+ // enough production volume to establish a narrower measured rate.
5462
+ "openrouter:competitor-image-winner-argument": {
5463
+ ourCostCents: 2.5,
5464
+ userPriceCents: 5
5465
+ },
5272
5466
  // Unit = one multimodal temporal observation of the captured winner MP4.
5273
5467
  // Includes the bounded full video plus measured frame samples and high
5274
5468
  // reasoning. Unmeasured at launch, so quote at 2x the keyframe-only blueprint
@@ -5287,6 +5481,19 @@ var DEFAULT_PRICING = {
5287
5481
  ourCostCents: 0.2,
5288
5482
  userPriceCents: 0.4
5289
5483
  },
5484
+ // Unit = one product-use SHEET derivation: Flash reads the product photos + the seller's raw
5485
+ // text and writes the three lines the video QA judge needs to rule on product MISUSE. Same
5486
+ // model, same effort and the same image budget as the exposure classifier directly above, so
5487
+ // it is priced at that rate rather than re-measured on n=1. Sized ABOVE the measured campaign
5488
+ // cost of the equivalent call (~2.4¢ USER for the sheet+judge PAIR, of which the sheet is the
5489
+ // smaller half) because this table is only the fallback for when OpenRouter omits usage.cost.
5490
+ // ONCE PER TASK, not per clip and not per render attempt: the sheet is derived at ingest and
5491
+ // every QA call on that task reads the stored copy. Quoting it per scene would over-quote a
5492
+ // 3-scene single by 3x and inflate the credit gate for spend that never happens.
5493
+ "openrouter:product-use-sheet": {
5494
+ ourCostCents: 0.2,
5495
+ userPriceCents: 0.4
5496
+ },
5290
5497
  // Unit = one reference-grouping pass over the user's product images (0.1241¢).
5291
5498
  "openrouter:product-reference-grouping": {
5292
5499
  ourCostCents: 0.15,
@@ -5334,6 +5541,68 @@ var DEFAULT_PRICING = {
5334
5541
  ourCostCents: 1,
5335
5542
  userPriceCents: 2
5336
5543
  },
5544
+ // ── Rapid / single_call pipeline: keys that did NOT exist until 2026-08-01 ──
5545
+ //
5546
+ // These labels bill real money every day but had NO entry here, so whenever
5547
+ // OpenRouter omitted `usage.cost` the fallback resolved to `undefined` and the
5548
+ // call was recorded at ZERO. `rapid-plan` alone is the single most expensive
5549
+ // OpenRouter label we have ($116.60 / 30d). The rates below are MEASURED, not
5550
+ // guessed: SUM(ourCostCents)/SUM(units) over 30 days of prod BillingEntry
5551
+ // (neon still-paper-34632546, queried 2026-08-01), at the standard 2x markup.
5552
+ //
5553
+ // None of these keys is read by cost-estimator.service.ts — the rapid pipeline
5554
+ // quotes them through the calibrated STANDARD_PIPELINE_OVERHEAD_CENTS blob — so
5555
+ // adding them changes the billing FALLBACK only and cannot move any quote.
5556
+ // Unit = one rapid plan authoring call (measured 4.3968¢).
5557
+ "openrouter:rapid-plan": {
5558
+ ourCostCents: 4.4,
5559
+ userPriceCents: 8.8
5560
+ },
5561
+ // Unit = one rapid script blueprint call (measured 4.2748¢).
5562
+ "openrouter:rapid-script-blueprint": {
5563
+ ourCostCents: 4.28,
5564
+ userPriceCents: 8.56
5565
+ },
5566
+ // Unit = one whole-video standard QA pass (measured 2.4104¢).
5567
+ "openrouter:video-qa-standard": {
5568
+ ourCostCents: 2.41,
5569
+ userPriceCents: 4.82
5570
+ },
5571
+ // Unit = one frame0 anchor compose (measured 0.7351¢).
5572
+ "openrouter:frame0-anchor-compose": {
5573
+ ourCostCents: 0.74,
5574
+ userPriceCents: 1.48
5575
+ },
5576
+ // Unit = one voiceover↔visual alignment pass (measured 0.9092¢).
5577
+ "openrouter:rapid-voiceover-visual-alignment": {
5578
+ ourCostCents: 0.91,
5579
+ userPriceCents: 1.82
5580
+ },
5581
+ // Unit = one rapid plan character/setting pass (measured 0.5547¢).
5582
+ "openrouter:rapid-plan-character": {
5583
+ ourCostCents: 0.55,
5584
+ userPriceCents: 1.1
5585
+ },
5586
+ // Unit = one segment naturalization pass (measured 0.5470¢).
5587
+ "openrouter:rapid-naturalize": {
5588
+ ourCostCents: 0.55,
5589
+ userPriceCents: 1.1
5590
+ },
5591
+ // Unit = one rapid script validation pass (measured 0.3704¢).
5592
+ "openrouter:rapid-script-validation": {
5593
+ ourCostCents: 0.37,
5594
+ userPriceCents: 0.74
5595
+ },
5596
+ // Unit = one voiceover shortening pass (measured 0.3081¢).
5597
+ "openrouter:rapid-voiceover-shorten": {
5598
+ ourCostCents: 0.31,
5599
+ userPriceCents: 0.62
5600
+ },
5601
+ // Unit = one POV judge call on the rapid plan (measured 0.0769¢).
5602
+ "openrouter:rapid-plan-pov-judge": {
5603
+ ourCostCents: 0.08,
5604
+ userPriceCents: 0.16
5605
+ },
5337
5606
  // Unit = one ffmpeg mix laying the replicated winner's own audio track under our
5338
5607
  // voiceover, producing the extra `ad_final_with_winner_music` deliverable
5339
5608
  // (gap_replication only, behind GAP_WINNER_MUSIC_ENABLED, default OFF). No AI
@@ -6514,9 +6783,107 @@ var LandingConfigSchema = z35.object({
6514
6783
  */
6515
6784
  showVariantPicker: z35.boolean().optional()
6516
6785
  });
6786
+ var FreeformSlotTypeSchema = z35.enum([
6787
+ "text",
6788
+ "image",
6789
+ "price",
6790
+ "cta",
6791
+ "list"
6792
+ ]);
6793
+ var FreeformSlotSchema = z35.object({
6794
+ type: FreeformSlotTypeSchema,
6795
+ /** Human label shown in the editor ("Título", "Imagen principal"). */
6796
+ label: z35.string().min(1).max(60),
6797
+ /** text/price/cta: display text. image: absolute URL. list: unused (see items). */
6798
+ value: z35.string().max(2e3),
6799
+ /** Only for type 'list'. */
6800
+ items: z35.array(z35.string().max(300)).max(12).optional()
6801
+ });
6802
+ var FreeformSectionSchema = z35.object({
6803
+ id: z35.string().regex(/^[a-z0-9][a-z0-9_-]{0,39}$/),
6804
+ label: z35.string().min(1).max(60),
6805
+ /** Sanitized free markup with {{slot:key}} markers and data-cl-* attributes. */
6806
+ layoutHtml: z35.string().min(1).max(16e3),
6807
+ slots: z35.record(z35.string().regex(/^[a-zA-Z0-9_]{1,40}$/), FreeformSlotSchema),
6808
+ /** Section CSS, selectors prefixed with [data-cl-s="<id>"]. */
6809
+ css: z35.string().max(8e3).optional()
6810
+ });
6811
+ var DesignSystemSchema = z35.object({
6812
+ /** Same 4 roles as v1 preset — they feed the --cl-* CSS vars the buy form uses. */
6813
+ colors: z35.object({
6814
+ primary: HexColorSchema,
6815
+ bg: HexColorSchema,
6816
+ text: HexColorSchema,
6817
+ accent: HexColorSchema
6818
+ }),
6819
+ fonts: z35.object({
6820
+ heading: FontNameSchema,
6821
+ body: FontNameSchema
6822
+ }),
6823
+ radius: z35.string().regex(/^\d{1,2}(px|rem|em)$/),
6824
+ /** Prompt-only vocabulary ("brutalist", "editorial"); not rendered. */
6825
+ styleKeywords: z35.array(z35.string().max(40)).max(8).default([])
6826
+ });
6827
+ var FreeformLandingConfigSchema = z35.object({
6828
+ version: z35.literal(2),
6829
+ themeVersion: z35.literal(2),
6830
+ locale: z35.string().min(2),
6831
+ designSystem: DesignSystemSchema,
6832
+ globalCss: z35.string().max(2e4).default(""),
6833
+ sections: z35.array(FreeformSectionSchema).min(1).max(14),
6834
+ /** Buy form is inserted AFTER this section (assembler adds BUY_FORM_MARKER). */
6835
+ buyFormAfterSectionId: z35.string(),
6836
+ /** Assembled + sanitized final HTML — source of truth for render/publish. */
6837
+ html: z35.string().min(1),
6838
+ product: LandingProductSchema,
6839
+ showVariantPicker: z35.boolean().optional(),
6840
+ /**
6841
+ * Suppress the native Shopify buy form (the `BUY_FORM_MARKER` block). The page
6842
+ * CTAs already carry `data-cl-buy-trigger` → they open the ReleaseIt COD popup,
6843
+ * so on ReleaseIt-only stores the native add-to-cart form is a redundant second
6844
+ * buy path (it rendered as a bare "price + Comprar" block). When true the
6845
+ * assembler omits the marker entirely. Undefined/false = keep the native form
6846
+ * (backward compatible with already-assembled configs).
6847
+ */
6848
+ hideNativeBuyForm: z35.boolean().optional()
6849
+ });
6850
+ var AnyLandingConfigSchema = z35.union([
6851
+ FreeformLandingConfigSchema,
6852
+ LandingConfigSchema
6853
+ ]);
6517
6854
  var UpdateLandingConfigResponse = z35.object({
6518
6855
  ok: z35.boolean()
6519
6856
  });
6857
+ var CreateLandingEditRequest = z35.object({
6858
+ message: z35.string().min(3).max(1e3),
6859
+ sectionId: z35.string().optional()
6860
+ });
6861
+ var LandingEditDto = z35.object({
6862
+ id: z35.string(),
6863
+ scope: z35.enum([
6864
+ "section",
6865
+ "full"
6866
+ ]),
6867
+ sectionId: z35.string().nullable(),
6868
+ message: z35.string(),
6869
+ status: z35.enum([
6870
+ "pending",
6871
+ "processing",
6872
+ "applied",
6873
+ "failed",
6874
+ "reverted"
6875
+ ]),
6876
+ costCents: z35.number(),
6877
+ error: z35.string().nullable(),
6878
+ createdAt: z35.string()
6879
+ });
6880
+ var CreateLandingEditResponse = z35.object({
6881
+ edit: LandingEditDto,
6882
+ chargedCredits: z35.number()
6883
+ });
6884
+ var ListLandingEditsResponse = z35.object({
6885
+ edits: z35.array(LandingEditDto)
6886
+ });
6520
6887
 
6521
6888
  // ../../packages/types/src/video-edit.ts
6522
6889
  import { z as z36 } from "zod";
@@ -6563,6 +6930,36 @@ var DropshippingProjectStatusSchema = z37.enum([
6563
6930
  "completed",
6564
6931
  "archived"
6565
6932
  ]);
6933
+ var DROPSHIPPING_LAUNCH_REWARD_CAMPAIGN = "dropshipping-launch-v1";
6934
+ var DropshippingRewardMilestoneSchema = z37.enum([
6935
+ "product_selected",
6936
+ "costing_saved",
6937
+ "product_media_ready",
6938
+ "creatives_completed",
6939
+ "campaign_created"
6940
+ ]);
6941
+ var DROPSHIPPING_LAUNCH_REWARD_CREDITS = {
6942
+ product_selected: 50,
6943
+ costing_saved: 50,
6944
+ product_media_ready: 50,
6945
+ creatives_completed: 150,
6946
+ campaign_created: 200
6947
+ };
6948
+ var DROPSHIPPING_LAUNCH_REWARD_TOTAL_CREDITS = Object.values(DROPSHIPPING_LAUNCH_REWARD_CREDITS).reduce((sum, credits) => sum + credits, 0);
6949
+ var DropshippingProjectRewardMilestoneStateSchema = z37.object({
6950
+ milestone: DropshippingRewardMilestoneSchema,
6951
+ credits: z37.number().int().positive(),
6952
+ available: z37.boolean().optional(),
6953
+ earned: z37.boolean(),
6954
+ grantId: z37.string().uuid().nullable(),
6955
+ grantedAt: z37.string().datetime().nullable()
6956
+ });
6957
+ var DropshippingProjectRewardProgressSchema = z37.object({
6958
+ campaignKey: z37.literal(DROPSHIPPING_LAUNCH_REWARD_CAMPAIGN),
6959
+ totalCredits: z37.number().int().positive(),
6960
+ earnedCredits: z37.number().int().nonnegative(),
6961
+ milestones: z37.array(DropshippingProjectRewardMilestoneStateSchema).length(DropshippingRewardMilestoneSchema.options.length)
6962
+ });
6566
6963
  var DropshippingProjectPhaseSchema = z37.enum([
6567
6964
  "research",
6568
6965
  "costing",
@@ -6720,6 +7117,7 @@ var DropshippingProjectResponseSchema = z37.object({
6720
7117
  version: z37.number().int().positive(),
6721
7118
  phase: DropshippingProjectPhaseSchema,
6722
7119
  nextAction: DropshippingProjectNextActionSchema.nullable(),
7120
+ rewards: DropshippingProjectRewardProgressSchema,
6723
7121
  createdAt: z37.string().datetime(),
6724
7122
  updatedAt: z37.string().datetime()
6725
7123
  });
@@ -6781,12 +7179,17 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
6781
7179
  {
6782
7180
  name: "create_creative_variant",
6783
7181
  confirmation: "credit_consuming",
6784
- schemaHash: "8feeed1e6e6a14bdcb0a1d9d8b03350ce7d8117d926db49f19ca5c5a8f1e7033"
7182
+ schemaHash: "6a777fbe7ad17966d16502e8872e15e4db2dcfacdfd2992d24853340d4f22a00"
6785
7183
  },
6786
7184
  {
6787
7185
  name: "create_product_task",
6788
7186
  confirmation: "credit_consuming",
6789
- schemaHash: "828ee643bb3a05a63c6e63730459d35ce1621cbd559bbc76a6c5fcec9a45654d"
7187
+ schemaHash: "3f03c4886f65a030e03a531c02e0ef879aaf3f5820e90ab3ca04a02350e693e6"
7188
+ },
7189
+ {
7190
+ name: "edit_image_creative",
7191
+ confirmation: "credit_consuming",
7192
+ schemaHash: "ce0abba871436850bd1db0ea4bcdade9ef35420ebd83833e2d5c2dcf70378ec0"
6790
7193
  },
6791
7194
  {
6792
7195
  name: "edit_task_draft",
@@ -6796,7 +7199,7 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
6796
7199
  {
6797
7200
  name: "estimate_creative_variant",
6798
7201
  confirmation: "none",
6799
- schemaHash: "a5601adcb9578319528ca0a96f37a35b613370646a75932d1b3978d593f7e302"
7202
+ schemaHash: "d4453364c0706bf88f480cebc977916f06d222d7eebebc9cfa91165929a1a998"
6800
7203
  },
6801
7204
  {
6802
7205
  name: "find_winning_products",
@@ -6811,17 +7214,17 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
6811
7214
  {
6812
7215
  name: "generate_image_creatives",
6813
7216
  confirmation: "credit_consuming",
6814
- schemaHash: "fb6afcf8571887906340fa0b8dd17196d2814e480b734ad5d374e8810958152e"
7217
+ schemaHash: "bf6a7599dac65c31a43e23badad8fd8e1ab3e8aee8a5a55ac47f9c39268b2fa6"
6815
7218
  },
6816
7219
  {
6817
7220
  name: "generate_landing",
6818
7221
  confirmation: "credit_consuming",
6819
- schemaHash: "539922b8fe611c73a7349b1c3f92632fe21f161d7874a0ef2172abfec5834759"
7222
+ schemaHash: "15465f9bda1ec945cc49f4f4a830d56a17a43b0cb5f6063d5594ddc92c830c21"
6820
7223
  },
6821
7224
  {
6822
7225
  name: "generate_video_creative",
6823
7226
  confirmation: "credit_consuming",
6824
- schemaHash: "94e90d60ad5d68538c31d880a635c5be69d6d204fd2119139735af83ebf0b94a"
7227
+ schemaHash: "c80208df75f2c7cbc459e9ae494a75a0a5f5bc4cf044b3fc9ef9c0e9af2c2452"
6825
7228
  },
6826
7229
  {
6827
7230
  name: "get_meta_campaign_status",
@@ -7182,6 +7585,7 @@ var VideoPresetEnum = z39.enum([
7182
7585
  "ugc_dynamic",
7183
7586
  "ugc_how_to",
7184
7587
  "ugc_unboxing_chaos",
7588
+ "problem_solution",
7185
7589
  "product_showcase",
7186
7590
  "wearable_showcase",
7187
7591
  "cinematic",
@@ -7193,6 +7597,7 @@ var TaskCreativeVariantVideoPresetEnum = z39.enum([
7193
7597
  "auto",
7194
7598
  "ugc_dynamic",
7195
7599
  "ugc_how_to",
7600
+ "problem_solution",
7196
7601
  "product_showcase",
7197
7602
  "cinematic",
7198
7603
  "wild_card",
@@ -7218,6 +7623,11 @@ var VIDEO_PRESET_RESOLUTION = {
7218
7623
  narrativeStyle: "ugc_unboxing_chaos",
7219
7624
  videoModel: "veo"
7220
7625
  },
7626
+ problem_solution: {
7627
+ videoStyle: "ai_dynamic",
7628
+ narrativeStyle: "problem_product_cta",
7629
+ videoModel: "kling"
7630
+ },
7221
7631
  pixar_problem: {
7222
7632
  videoStyle: "ai_dynamic",
7223
7633
  narrativeStyle: "animated_problem_pixar",
@@ -7265,6 +7675,12 @@ function resolveWireVideoTier(tier, durationSeconds) {
7265
7675
  return durationSeconds === 30 ? "standard" : resolveVideoTier(tier);
7266
7676
  }
7267
7677
  __name(resolveWireVideoTier, "resolveWireVideoTier");
7678
+ function assertVideoPresetDurationCompatibility(videoPreset, durationSeconds) {
7679
+ if (videoPreset === "problem_solution" && durationSeconds !== 30) {
7680
+ throw new Error("Problema y Soluci\xF3n solo est\xE1 disponible en videos Multiescena de 30 segundos.");
7681
+ }
7682
+ }
7683
+ __name(assertVideoPresetDurationCompatibility, "assertVideoPresetDurationCompatibility");
7268
7684
  function resolveVideoFormatForDuration(durationSeconds) {
7269
7685
  return durationSeconds === 30 ? "double_30s" : "single_15s";
7270
7686
  }
@@ -7300,6 +7716,7 @@ var PublicNarrativeStyleEnum = z39.enum([
7300
7716
  "review",
7301
7717
  "ugc_how_to",
7302
7718
  "ugc_unboxing_chaos",
7719
+ "problem_product_cta",
7303
7720
  "product_showcase",
7304
7721
  "wearable_showcase",
7305
7722
  "cinematic",
@@ -7941,6 +8358,7 @@ function proxifyOutputs(outputs, proxyBaseUrl) {
7941
8358
  }) ?? url, "rewrite");
7942
8359
  return {
7943
8360
  webappUrl: outputs.webappUrl,
8361
+ adCopy: outputs.adCopy,
7944
8362
  images: outputs.images.map((img, i) => ({
7945
8363
  ...img,
7946
8364
  url: rewrite(img.url, `image-${i}.png`)
@@ -8008,7 +8426,8 @@ async function fetchTaskOutputs(api, taskId) {
8008
8426
  landing: {
8009
8427
  sections
8010
8428
  },
8011
- images
8429
+ images,
8430
+ adCopy: typeof raw.adCreative?.adCopy === "string" && raw.adCreative.adCopy.length ? raw.adCreative.adCopy : void 0
8012
8431
  };
8013
8432
  }
8014
8433
  __name(fetchTaskOutputs, "fetchTaskOutputs");
@@ -8023,6 +8442,10 @@ function renderOutputsMarkdown(outputs) {
8023
8442
  parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
8024
8443
  });
8025
8444
  }
8445
+ if (outputs.adCopy) {
8446
+ parts.push("\n### Ad copy");
8447
+ parts.push(outputs.adCopy);
8448
+ }
8026
8449
  if (outputs.landing.sections.length) {
8027
8450
  parts.push(`
8028
8451
  ### Landing (${outputs.landing.sections.length} secciones)`);
@@ -8056,16 +8479,16 @@ var CreateProductTaskInput = z40.object({
8056
8479
  paymentMethods: z40.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
8057
8480
  productContext: z40.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
8058
8481
  productLabel: z40.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
8059
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
8482
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s y 30s, excepto "problem_solution", que requiere 30s Multiescena. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
8060
8483
  durationSeconds: z40.union([
8061
8484
  z40.literal(15),
8062
8485
  z40.literal(30)
8063
8486
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
8064
8487
  pauseForAngleReview: z40.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
8065
8488
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
8066
- videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez (~el doble de precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
8489
+ videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
8067
8490
  multiAngleVideos: z40.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
8068
- angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_how_to", "ugc_unboxing_chaos", "product_showcase", "wearable_showcase", "cinematic", "wild_card", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
8491
+ angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_how_to", "ugc_unboxing_chaos", "problem_product_cta" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase", "wearable_showcase", "cinematic", "wild_card", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
8069
8492
  outputs: z40.array(z40.enum([
8070
8493
  "landing",
8071
8494
  "video",
@@ -8074,6 +8497,8 @@ var CreateProductTaskInput = z40.object({
8074
8497
  competitorScanEnabled: z40.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
8075
8498
  brandId: z40.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer los outputs generados."),
8076
8499
  creativeDirection: z40.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo) que aplica a los outputs de IMAGEN y LANDING (no al video \u2014 el estilo/ambiente del video se dirige con scriptNotes). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Ej. "amarillo y azul, minimalista". M\xE1x 500 caracteres.'),
8500
+ headline: z40.string().max(120).optional().describe("Opcional. Titular EXACTO a renderizar. Se imprime tal cual: la IA no lo reescribe, no lo acorta y no lo traduce. Se aplica a las IM\xC1GENES y a la portada de la landing de esta task (el video no lo usa; para el video usa scriptNotes). \xDAsalo cuando el copy es de marca y no es negociable. M\xE1x 120 caracteres. Si lo omites, la IA escribe el titular."),
8501
+ subheadline: z40.string().max(160).optional().describe("Opcional. Subt\xEDtulo EXACTO a renderizar, misma regla que headline. Independiente: puedes fijar solo el subt\xEDtulo. M\xE1x 160 caracteres."),
8077
8502
  landingStrategy: LandingStrategyEnum.default("classic").describe('Opcional. Estructura de la landing cl\xE1sica cuando landing est\xE1 entre outputs: "classic" (estructura original probada y recomendada; default del primer rollout), "auto" (la IA decide; experimental), "pain_solution", "social_proof" (Landing Viral: UGC, WhatsApp, Instagram y evidencia social), "benefits_clarity", "offer_value" o "lifestyle_identity". Default "classic".'),
8078
8503
  landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica cuando landing est\xE1 entre outputs: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".')
8079
8504
  }).strict();
@@ -8240,6 +8665,7 @@ __name(resolveStatusTaskKind, "resolveStatusTaskKind");
8240
8665
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
8241
8666
  server.tool("create_product_task", "Crea una tarea de producto desde fotos o desde un proyecto guiado propio. Por defecto genera los 3 outputs (landing + video + image creatives) EN UN MISMO TASK. Devuelve un taskId.\n\nINPUTS REQUERIDOS: images (1-5 fotos del MISMO producto; la primera es la hero) O projectId si el proyecto ya tiene una imagen guardada; adem\xE1s language y country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son inputs de alto impacto en la landing y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto del chat: preg\xFAntale expl\xEDcitamente, incluso si est\xE1 en LATAM.\n\nSi env\xEDas projectId, debe ser el proyecto propio que el usuario acaba de costear; el backend resolver\xE1 la imagen privada y conservar\xE1 todos los controles de cr\xE9ditos/confirmaci\xF3n. Si el usuario adjunta images junto con projectId, esas fotos reemplazan la referencia solo para este task.\n\nANTES de invocar, DILE CLARAMENTE qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task.\n\nUsa SOLO valores declarados en los enums.", CreateProductTaskInput.shape, async (input) => {
8242
8667
  try {
8668
+ assertVideoPresetDurationCompatibility(input.videoPreset, input.durationSeconds);
8243
8669
  if (!input.images && !input.projectId) {
8244
8670
  throw new Error("images es requerido salvo que env\xEDes projectId.");
8245
8671
  }
@@ -8275,6 +8701,8 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
8275
8701
  "options.competitorScanEnabled": input.competitorScanEnabled,
8276
8702
  "options.brandId": input.brandId,
8277
8703
  "options.creativeDirection": input.creativeDirection,
8704
+ "options.headline": input.headline,
8705
+ "options.subheadline": input.subheadline,
8278
8706
  "options.landingStrategy": input.landingStrategy,
8279
8707
  "options.landingImageFormat": input.landingImageFormat,
8280
8708
  pauseForAngleReview: input.pauseForAngleReview,
@@ -8538,7 +8966,10 @@ var CreativeRefs = {
8538
8966
  isCombo: z41.boolean().optional().describe("Opcional. Si true, el creativo presenta el producto como combo/pack. Default false."),
8539
8967
  competitorScanEnabled: z41.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
8540
8968
  brandId: z41.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer el creativo."),
8541
- creativeDirection: z41.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Aplica SOLO a im\xE1genes y landing. Ej. im\xE1genes: "amarillo, azul y rojo, minimalista"; landing: "elegante, paleta fr\xEDa". Para video usa scriptNotes (ambiente/escenario). M\xE1x 500 caracteres.')
8969
+ projectId: z41.string().uuid().optional().describe("Opcional. ID de un proyecto guiado de dropshipping activo del usuario. Enlaza esta generaci\xF3n con su progreso y recompensas."),
8970
+ creativeDirection: z41.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Aplica SOLO a im\xE1genes y landing. Ej. im\xE1genes: "amarillo, azul y rojo, minimalista"; landing: "elegante, paleta fr\xEDa". Para video usa scriptNotes (ambiente/escenario). M\xE1x 500 caracteres.'),
8971
+ headline: z41.string().max(120).optional().describe("Opcional. Titular EXACTO a renderizar en la imagen (o en la portada de la landing). Se imprime tal cual: la IA no lo reescribe, no lo acorta y no lo traduce. \xDAsalo cuando el copy es de marca y no es negociable. M\xE1x 120 caracteres. Si lo omites, la IA escribe el titular."),
8972
+ subheadline: z41.string().max(160).optional().describe("Opcional. Subt\xEDtulo EXACTO a renderizar, misma regla que headline. Independiente: puedes fijar solo el subt\xEDtulo. M\xE1x 160 caracteres.")
8542
8973
  };
8543
8974
  var VideoInput = z41.object({
8544
8975
  images: CreativeRefs.images,
@@ -8555,9 +8986,9 @@ var VideoInput = z41.object({
8555
8986
  z41.literal(15),
8556
8987
  z41.literal(30)
8557
8988
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
8558
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla).'),
8989
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s y 30s, excepto "problem_solution", que requiere 30s Multiescena. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla).'),
8559
8990
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista"; para durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
8560
- videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "720p" = m\xE1xima nitidez (RECOMENDADO, default si se omite); "480p" = render m\xE1s econ\xF3mico. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p).'),
8991
+ videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
8561
8992
  features: z41.array(z41.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
8562
8993
  price: CreativeRefs.price,
8563
8994
  offer: CreativeRefs.offer,
@@ -8568,7 +8999,8 @@ var VideoInput = z41.object({
8568
8999
  pauseForVideoScriptReview: z41.boolean().optional().describe("Opcional. Si true, la tarea pausa despu\xE9s de generar el guion y antes de renderizar. \xDAsalo cuando el usuario quiera editar el guion antes de gastar cr\xE9ditos en el video. Tras la pausa: get_video_script para leerlo, approve_video_script para aprobar (con o sin ediciones). Default false."),
8569
9000
  pauseForAngleReview: z41.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
8570
9001
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
8571
- brandId: CreativeRefs.brandId
9002
+ brandId: CreativeRefs.brandId,
9003
+ projectId: CreativeRefs.projectId
8572
9004
  }).strict();
8573
9005
  var FreeVideoQualityEnum = z41.enum([
8574
9006
  "economical",
@@ -8632,7 +9064,15 @@ var ImageInput = z41.object({
8632
9064
  isCombo: CreativeRefs.isCombo,
8633
9065
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
8634
9066
  brandId: CreativeRefs.brandId,
8635
- creativeDirection: CreativeRefs.creativeDirection
9067
+ projectId: CreativeRefs.projectId,
9068
+ creativeDirection: CreativeRefs.creativeDirection,
9069
+ headline: CreativeRefs.headline,
9070
+ subheadline: CreativeRefs.subheadline
9071
+ });
9072
+ var EditImageCreativeInput = z41.object({
9073
+ taskId: z41.string().uuid().describe("ID de la tarea de imagen ya finalizada."),
9074
+ variantIndex: z41.number().int().min(0).max(10).describe("\xCDndice de la variante a corregir (0 = primera imagen)."),
9075
+ instruction: z41.string().min(1).max(1e3).describe('Qu\xE9 cambiar en esa imagen. Ej. "quita el badge de env\xEDo gratis", "cambia el fondo a blanco".')
8636
9076
  });
8637
9077
  var LandingInput = z41.object({
8638
9078
  images: CreativeRefs.images,
@@ -8649,9 +9089,12 @@ var LandingInput = z41.object({
8649
9089
  isCombo: CreativeRefs.isCombo,
8650
9090
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
8651
9091
  brandId: CreativeRefs.brandId,
9092
+ projectId: CreativeRefs.projectId,
8652
9093
  creativeDirection: CreativeRefs.creativeDirection,
8653
9094
  landingStrategy: LandingStrategyEnum.default("classic").describe('Opcional. Estructura de la landing cl\xE1sica: "classic" (estructura original probada y recomendada; default del primer rollout), "auto" (la IA decide; experimental), "pain_solution" (problema\u2192soluci\xF3n), "social_proof" (Landing Viral: UGC, WhatsApp, Instagram y evidencia social), "benefits_clarity" (beneficios claros), "offer_value" (promoci\xF3n/valor) o "lifestyle_identity" (estilo de vida). Default "classic".'),
8654
- landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".')
9095
+ landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".'),
9096
+ headline: CreativeRefs.headline,
9097
+ subheadline: CreativeRefs.subheadline
8655
9098
  });
8656
9099
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
8657
9100
  server.tool("generate_free_video", 'Genera UN video libre con prompt/guion exacto, opcionalmente con imagen de referencia. Devuelve un taskId y procesa el job de forma as\xEDncrona.\n\nREQUERIDOS: rawPrompt, quality, durationSeconds y videoResolution.\n\nCALIDAD: "economical" = Econ\xF3mico y "high_detail" = Alto detalle. La duraci\xF3n debe pertenecer a la lista admitida por la calidad escogida; si el usuario pide otra, preg\xFAntale una duraci\xF3n v\xE1lida.\n\nSUBT\xCDTULOS: si addSubtitles=true, el rawPrompt completo se usa como texto exacto de subt\xEDtulos y debe tener m\xE1ximo 500 caracteres; no existe una pregunta adicional de transcripci\xF3n.\n\nPara videos normales con narrativa/\xE1ngulo usa generate_video_creative. Para tareas combinadas de producto usa create_product_task.', FreeVideoInput.shape, async (input) => {
@@ -8686,9 +9129,10 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8686
9129
  });
8687
9130
  server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, async (input) => {
8688
9131
  try {
9132
+ assertVideoPresetDurationCompatibility(input.videoPreset, input.durationSeconds);
8689
9133
  const resolved = resolveVideoPreset(input.videoPreset);
8690
9134
  const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
8691
- const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
9135
+ const { videoTier: _publicTier, videoResolution: _vr, projectId, ...rest } = input;
8692
9136
  const wirePayload = {
8693
9137
  ...rest
8694
9138
  };
@@ -8698,6 +9142,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8698
9142
  }
8699
9143
  wirePayload.videoTier = wireTier;
8700
9144
  if (input.videoResolution) wirePayload.videoResolution = input.videoResolution;
9145
+ if (projectId) wirePayload.dropshippingProjectId = projectId;
8701
9146
  return await submitCreative(api, {
8702
9147
  path: "/v1/creatives/videos",
8703
9148
  estimatedSeconds: 300,
@@ -8710,22 +9155,45 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8710
9155
  });
8711
9156
  server.tool("generate_image_creatives", 'Genera N variantes de imagen creativa (ads est\xE1ticos). Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), productTitle, primaryAngle, language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario:\n- price y offer (se renderizan en la imagen \u2014 nunca los asumas; si dice "t\xFA elige", om\xEDtelos).\n- compareAtPrice si hay descuento (ej. "$129.000" para tachar).\n- creativeArchetype y visualTreatment si quiere un look espec\xEDfico (performance vs premium DTC). Si no le importa, om\xEDtelos.\n\nNO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente.\n\nOTROS OPCIONALES: buyerPersona, nVariants (default 3), imageModel, imageStyle, styleMix, aspectRatio (default 4:5), generateAdCopy, pauseForAngleReview, features, brandColors, isCombo, creativeDirection.', ImageInput.shape, async (input) => {
8712
9157
  try {
9158
+ const { projectId, ...rest } = input;
8713
9159
  return await submitCreative(api, {
8714
9160
  path: "/v1/creatives/images",
8715
9161
  estimatedSeconds: 60,
8716
- input,
9162
+ input: {
9163
+ ...rest,
9164
+ ...projectId ? {
9165
+ dropshippingProjectId: projectId
9166
+ } : {}
9167
+ },
8717
9168
  transport
8718
9169
  });
8719
9170
  } catch (e) {
8720
9171
  throw translateError(e);
8721
9172
  }
8722
9173
  });
9174
+ server.tool("edit_image_creative", "Corrige UNA imagen ya generada de una tarea de creativos, sin regenerar el set completo.\n\nCUESTA CR\xC9DITOS: cobra una sola imagen (sin brief ni extracci\xF3n), mucho menos que volver a generar todo. ANTES de invocar, PREG\xDANTALE al usuario si quiere pagar esa correcci\xF3n.\n\nSolo funciona con la tarea en estado `done` \u2014 si a\xFAn est\xE1 procesando, espera con get_task_status. Una tarea en error terminal no se puede corregir.\n\nUsa variantIndex para elegir cu\xE1l de las im\xE1genes corregir (0 = la primera) e instruction para describir el cambio en lenguaje natural.", EditImageCreativeInput.shape, async (input) => {
9175
+ try {
9176
+ const r = await api.postJson(`/v1/creatives/images/${encodeURIComponent(input.taskId)}/regenerate`, {
9177
+ comment: input.instruction,
9178
+ variantIndex: input.variantIndex
9179
+ });
9180
+ return asText(r);
9181
+ } catch (e) {
9182
+ throw translateError(e);
9183
+ }
9184
+ });
8723
9185
  server.tool("generate_landing", 'Genera una landing page cl\xE1sica basada en im\xE1genes, lista para Shopify. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para esos campos, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults): salesAngle, buyerPersona, isCombo, creativeDirection, landingStrategy.', LandingInput.shape, async (input) => {
8724
9186
  try {
9187
+ const { projectId, ...rest } = input;
8725
9188
  return await submitCreative(api, {
8726
9189
  path: "/v1/creatives/landing",
8727
9190
  estimatedSeconds: 60,
8728
- input,
9191
+ input: {
9192
+ ...rest,
9193
+ ...projectId ? {
9194
+ dropshippingProjectId: projectId
9195
+ } : {}
9196
+ },
8729
9197
  transport
8730
9198
  });
8731
9199
  } catch (e) {
@@ -10407,6 +10875,13 @@ var DropshippingProjectPhase = z51.enum([
10407
10875
  "advertising",
10408
10876
  "complete"
10409
10877
  ]);
10878
+ var DropshippingRewardMilestone = z51.enum([
10879
+ "product_selected",
10880
+ "costing_saved",
10881
+ "product_media_ready",
10882
+ "creatives_completed",
10883
+ "campaign_created"
10884
+ ]);
10410
10885
  var TargetCountry = z51.enum([
10411
10886
  "CO",
10412
10887
  "MX",
@@ -10529,6 +11004,19 @@ var CostingResultSchema = z51.object({
10529
11004
  wasPriceAutoAdjusted: z51.boolean(),
10530
11005
  profitabilityStatus: z51.literal("healthy")
10531
11006
  });
11007
+ var RewardProgressSchema = z51.object({
11008
+ campaignKey: z51.literal("dropshipping-launch-v1"),
11009
+ totalCredits: z51.number().int().positive(),
11010
+ earnedCredits: z51.number().int().nonnegative(),
11011
+ milestones: z51.array(z51.object({
11012
+ milestone: DropshippingRewardMilestone,
11013
+ credits: z51.number().int().positive(),
11014
+ available: z51.boolean().optional(),
11015
+ earned: z51.boolean(),
11016
+ grantId: ProjectId.nullable(),
11017
+ grantedAt: z51.string().datetime().nullable()
11018
+ })).length(DropshippingRewardMilestone.options.length)
11019
+ });
10532
11020
  var ProjectResponseSchema = z51.object({
10533
11021
  id: ProjectId,
10534
11022
  taskId: ProjectId.nullable(),
@@ -10548,6 +11036,7 @@ var ProjectResponseSchema = z51.object({
10548
11036
  version: z51.number().int().positive(),
10549
11037
  phase: DropshippingProjectPhase,
10550
11038
  nextAction: DropshippingProjectNextAction.nullable(),
11039
+ rewards: RewardProgressSchema,
10551
11040
  createdAt: z51.string().datetime(),
10552
11041
  updatedAt: z51.string().datetime()
10553
11042
  }).passthrough();
@@ -10565,6 +11054,11 @@ function projectText(raw) {
10565
11054
  nextAction: project.nextAction,
10566
11055
  hasReferenceImage: project.hasReferenceImage,
10567
11056
  costingSaved: Boolean(project.costingResult),
11057
+ rewardProgress: {
11058
+ earnedCredits: project.rewards.earnedCredits,
11059
+ totalCredits: project.rewards.totalCredits,
11060
+ milestones: project.rewards.milestones
11061
+ },
10568
11062
  version: project.version,
10569
11063
  message: `Siguiente paso: ${next}. Relee este proyecto despu\xE9s de cada mutaci\xF3n.`
10570
11064
  })