@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/http.js CHANGED
@@ -153,7 +153,7 @@ var ApiClient = class {
153
153
  };
154
154
 
155
155
  // src/index.ts
156
- var MCP_VERSION = "2.1.0";
156
+ var MCP_VERSION = "2.3.1";
157
157
 
158
158
  // src/tools/tasks.ts
159
159
  import { z as z40 } from "zod";
@@ -935,7 +935,7 @@ var Product = z4.object({
935
935
  });
936
936
 
937
937
  // ../../packages/types/src/ingest.ts
938
- import { z as z13 } from "zod";
938
+ import { z as z14 } from "zod";
939
939
 
940
940
  // ../../packages/types/src/video-tier.ts
941
941
  import { z as z5 } from "zod";
@@ -947,18 +947,204 @@ var VideoTier = z5.preprocess((v) => v === "grok" ? "single_call" : v, z5.enum([
947
947
  ]));
948
948
  var DEFAULT_VIDEO_TIER = "single_call";
949
949
 
950
+ // ../../packages/types/src/angle.ts
951
+ import { z as z6 } from "zod";
952
+ var ImageWinnerSignal = z6.enum([
953
+ "winner",
954
+ "opportunity",
955
+ "saturated_twist"
956
+ ]);
957
+ var WinnerVisualMode = z6.enum([
958
+ "auto",
959
+ "inspired",
960
+ "original"
961
+ ]);
962
+ var AnchoredIntel = z6.object({
963
+ painPointIndices: z6.array(z6.number().int().nonnegative()).max(10),
964
+ hookIndices: z6.array(z6.number().int().nonnegative()).max(10),
965
+ claimIndices: z6.array(z6.number().int().nonnegative()).max(10),
966
+ sourceAngleType: z6.enum([
967
+ "gap",
968
+ "validated_local",
969
+ "saturated_differentiated",
970
+ "novel"
971
+ ]),
972
+ // SNAPSHOT of the resolved intel strings at angle-generation time.
973
+ // Indices reference into Task.competitorIntel which we treat as immutable
974
+ // post-scan, but defensive: a future re-scan path that rewrites
975
+ // Task.competitorIntel would invalidate the indices silently. Persisting
976
+ // the resolved strings ALONGSIDE the indices means downstream consumers
977
+ // (script writer, landing copy) can safely use the strings directly
978
+ // without re-resolving — staleness-immune. The indices remain useful for
979
+ // analytics ("which intel items did angles cite?") and as a sanity-check.
980
+ resolvedPainPoints: z6.array(z6.string()).max(10).optional(),
981
+ resolvedHooks: z6.array(z6.string()).max(10).optional(),
982
+ resolvedClaims: z6.array(z6.string()).max(10).optional(),
983
+ /**
984
+ * Free-form pillar name describing the AXIS of the product this angle
985
+ * attacks (e.g. "ciencia/mecanismo", "tiempo/ritual", "comparación-con-
986
+ * alternativas", "social-proof", "dolor-emocional", "identidad/persona").
987
+ * The LLM picks the label — we do NOT lock an enum because the right
988
+ * pillar taxonomy varies per product (a supplement's "mecanismo" pillar
989
+ * is different from a gadget's "fricción de uso"). Constraint enforced
990
+ * at generation time: the 3 angles emitted for one task MUST use 3
991
+ * DISTINCT pillars (anti-convergence). Lives inside AnchoredIntel because
992
+ * it ships as analytics metadata alongside the anchoredIntel sourceType +
993
+ * indices, and persists as JSON without a column migration. Optional
994
+ * because angles persisted before this change have no pillar.
995
+ */
996
+ pillar: z6.string().min(1).max(60).optional(),
997
+ // Telemetry: why the angle did NOT import a gapAngle. The legacy single value
998
+ // 'none_relevant' was OVERLOADED (it meant "no gaps existed" AND "gaps existed
999
+ // but none fit" AND was the sanitizer default) which made the gap feature
1000
+ // unmeasurable. Split into distinct, one-meaning-each states:
1001
+ // - persona_mismatch / product_mismatch: the LLM saw gaps and judged them a
1002
+ // wrong-persona / wrong-subcategory fit (trusted from the LLM).
1003
+ // - no_gaps_in_block: there were no gapAngles in the block (or no block).
1004
+ // - gaps_present_not_imported: gaps existed and the angle went novel anyway
1005
+ // (the sanitizer's honest default when the LLM didn't pick persona/product).
1006
+ // - gaps_all_filtered_noise: the reference HAD gaps but every one was dropped
1007
+ // as detection noise (retailer-meta/cross-category) so the LLM saw none.
1008
+ // - n_a: the angle imported a gap (sourceAngleType="gap"), so "why skipped"
1009
+ // is moot.
1010
+ // The deterministic gap-presence states (no_gaps_in_block / gaps_present_not_
1011
+ // imported / gaps_all_filtered_noise) are set by sanitizeAnchoredIntel from
1012
+ // facts it computes (intel.gapAngles.length, intel.gapAnglesFilteredCount), NOT
1013
+ // trusted from the lazy LLM. z.preprocess aliases historical 'none_relevant'
1014
+ // rows → 'no_gaps_in_block' so the JSON column parses with no migration.
1015
+ // Analytics-only: NO runtime path branches on this value.
1016
+ skippedGapReason: z6.preprocess((v) => v === "none_relevant" ? "no_gaps_in_block" : v, z6.enum([
1017
+ "persona_mismatch",
1018
+ "product_mismatch",
1019
+ "no_gaps_in_block",
1020
+ "gaps_present_not_imported",
1021
+ "gaps_all_filtered_noise",
1022
+ "n_a"
1023
+ ])).optional(),
1024
+ // Telemetry: captures the prod-observed case where a "novel" angle actually
1025
+ // reflects a GAP's theme/promise without formally anchoring indices (gapAngles
1026
+ // are a separate array, not index-cited, so a real gap import is reclassified
1027
+ // 'novel' by the empty-indices coherence guard). Self-reported by the LLM:
1028
+ // - gap_imported: the angle's theme imports a gap's logic.
1029
+ // - product_derived: derived purely from the product.
1030
+ // - hybrid: a mix.
1031
+ // SOFT analytics-only signal (the LLM may confabulate) — validated against
1032
+ // known prod cases before trusting aggregates; never a control-flow input.
1033
+ // Optional in storage; on the wire it ships as a required enum with a 'none'
1034
+ // sentinel (strict structured output forbids optional properties) that the
1035
+ // sanitizer maps to undefined.
1036
+ themeSourceType: z6.enum([
1037
+ "gap_imported",
1038
+ "product_derived",
1039
+ "hybrid"
1040
+ ]).optional()
1041
+ });
1042
+ var Angle = z6.object({
1043
+ angle: z6.string().min(1).max(480),
1044
+ buyerPersona: z6.string().min(1).max(360),
1045
+ /** Character description for video (role + appearance + attitude). Drives voice selection and Frame0Spec. */
1046
+ actor: z6.string().min(1).max(360).optional(),
1047
+ // Binary only: voice catalog has no non-binary voices.
1048
+ // See packages/angles/src/index.ts + getAngles coercion in angle.repository.ts.
1049
+ gender: z6.enum([
1050
+ "female",
1051
+ "male"
1052
+ ]).optional(),
1053
+ /** Per-angle user-supplied script/visual notes. Hydrated from the
1054
+ * `Angle.scriptNotes` DB column or the `angleVideoOptions[i].scriptNotes`
1055
+ * passed from the chat. Per-angle overrides global `videoScriptNotes`. */
1056
+ scriptNotes: z6.string().max(2e3).optional(),
1057
+ /** AI-chosen per-angle narrative (set by the batch selector in angles.worker).
1058
+ * Pre-fills the review dropdown; user can override per angle. */
1059
+ narrativeStyle: NarrativeStyle.optional(),
1060
+ /** See AnchoredIntel docs. Optional because angles persisted before this
1061
+ * change have no anchoring captured; new angles ALWAYS emit it (with empty
1062
+ * arrays + 'novel' when intel was unavailable). */
1063
+ anchoredIntel: AnchoredIntel.optional(),
1064
+ /** Server-computed: true when this angle is eligible for the gap_replication
1065
+ * narrative (isReplicationAngle(anchoredIntel, intel) && intel.gapBlueprint
1066
+ * present). Computed server-side (not derived client-side) because the web
1067
+ * only sees this angle's anchoredIntel, not the task's
1068
+ * competitorIntel.gapBlueprint. Gates the review dropdown's gap_replication
1069
+ * option — absent/false = hidden. */
1070
+ gapReplicationEligible: z6.boolean().optional(),
1071
+ /** Server-computed: true when this angle's effective narrative is
1072
+ * gap_replication, which FORCES the multi-shot STANDARD ("Multiescena")
1073
+ * render pipeline regardless of the task's chosen tier (single_call can't
1074
+ * produce the multi-shot replication — see task-results.service.ts and the
1075
+ * render-time tier override). Surfaces "renders in Multiescena" on the
1076
+ * review card so a single_call user isn't surprised by the STANDARD-tier
1077
+ * charge. */
1078
+ rendersMultiShot: z6.boolean().optional(),
1079
+ /** Server-computed product-level origin bucket for the review card's badge
1080
+ * (NOT the internal sourceAngleType taxonomy — see resolveAngleOrigin in
1081
+ * task-results.service.ts): 'gap' = market opportunity competitors aren't
1082
+ * using; 'saturated_twist' = competitors' most-used approach with our own
1083
+ * twist; 'ai_recommended' = AI-authored for this product (default). */
1084
+ angleOrigin: z6.enum([
1085
+ "gap",
1086
+ "saturated_twist",
1087
+ "ai_recommended"
1088
+ ]).optional(),
1089
+ /** Server-computed badge TIER for the replication angle (two-tier UX,
1090
+ * v1.32.0 — see resolveReplicationTier). 'winner' = a validated angle
1091
+ * opportunity backed by a proven ad (anchor gap/saturated → golden badge);
1092
+ * 'opportunity' = a product-coherent replicable winner with no validated
1093
+ * angle gap (anchor 'dominant' → opportunity/twist badge, no overclaim).
1094
+ * Present only on the angle that carries gap_replication; absent otherwise. */
1095
+ replicationTier: z6.enum([
1096
+ "winner",
1097
+ "opportunity"
1098
+ ]).optional(),
1099
+ /** Server-confirmed availability of a coherent, captured IMAGE winner for
1100
+ * this task. The review UI must not claim "anuncio ganador" from the user's
1101
+ * mode choice alone: fail-open scans may legitimately have no winner. */
1102
+ imageWinnerAvailable: z6.boolean().optional(),
1103
+ /** True when `winnerVisualMode=auto` will actually attach the image reference.
1104
+ * A single-image request requires `imageWinnerSignal="winner"`; a multi-image
1105
+ * portfolio may spend one experimental slot on opportunity/twist. Explicit
1106
+ * `inspired` may also apply it; `original` always suppresses it. */
1107
+ imageWinnerAutoApplied: z6.boolean().optional(),
1108
+ /** Server-authored evidence tier for the image reference. The UI renders
1109
+ * this on the inspired control and the selected strategy badge. */
1110
+ imageWinnerSignal: ImageWinnerSignal.optional()
1111
+ });
1112
+ var AdVideo = z6.object({
1113
+ source: z6.enum([
1114
+ "tiktok_shop",
1115
+ "meta_ads"
1116
+ ]),
1117
+ url: z6.string().url(),
1118
+ title: z6.string().optional(),
1119
+ description: z6.string().optional(),
1120
+ metrics: z6.object({
1121
+ impressions: z6.number().int().nonnegative().optional(),
1122
+ views: z6.number().int().nonnegative().optional(),
1123
+ likes: z6.number().int().nonnegative().optional(),
1124
+ ctaClicks: z6.number().int().nonnegative().optional(),
1125
+ diggCount: z6.number().int().nonnegative().optional(),
1126
+ shareCount: z6.number().int().nonnegative().optional(),
1127
+ commentCount: z6.number().int().nonnegative().optional(),
1128
+ playCount: z6.number().int().nonnegative().optional(),
1129
+ collectCount: z6.number().int().nonnegative().optional(),
1130
+ durationSeconds: z6.number().int().nonnegative().optional()
1131
+ }).partial().optional(),
1132
+ country: z6.string().optional(),
1133
+ lastSeen: z6.string().datetime().optional()
1134
+ });
1135
+
950
1136
  // ../../packages/types/src/creative.ts
951
- import { z as z9 } from "zod";
1137
+ import { z as z10 } from "zod";
952
1138
 
953
1139
  // ../../packages/types/src/landing.ts
954
- import { z as z8 } from "zod";
1140
+ import { z as z9 } from "zod";
955
1141
 
956
1142
  // ../../packages/types/src/landing-strategy.ts
957
- import { z as z7 } from "zod";
1143
+ import { z as z8 } from "zod";
958
1144
 
959
1145
  // ../../packages/types/src/landing-role.ts
960
- import { z as z6 } from "zod";
961
- var LandingRole = z6.enum([
1146
+ import { z as z7 } from "zod";
1147
+ var LandingRole = z7.enum([
962
1148
  "hero",
963
1149
  "benefits1",
964
1150
  "socialproof",
@@ -970,7 +1156,7 @@ var LandingRole = z6.enum([
970
1156
  "authority",
971
1157
  "logistics"
972
1158
  ]);
973
- var LandingRoleLegacy = z6.enum([
1159
+ var LandingRoleLegacy = z7.enum([
974
1160
  "hero",
975
1161
  "benefits1",
976
1162
  "benefits2",
@@ -996,7 +1182,7 @@ var LANDING_STRATEGIES = [
996
1182
  "offer_value",
997
1183
  "lifestyle_identity"
998
1184
  ];
999
- var LandingStrategySchema = z7.enum(LANDING_STRATEGIES);
1185
+ var LandingStrategySchema = z8.enum(LANDING_STRATEGIES);
1000
1186
  var EffectiveLandingStrategySchema = LandingStrategySchema.exclude([
1001
1187
  "auto"
1002
1188
  ]);
@@ -1007,8 +1193,8 @@ var SEMANTIC_LANDING_STRATEGIES = [
1007
1193
  "offer_value",
1008
1194
  "lifestyle_identity"
1009
1195
  ];
1010
- var SemanticLandingStrategySchema = z7.enum(SEMANTIC_LANDING_STRATEGIES);
1011
- var LandingVisualStyleFamilySchema = z7.enum([
1196
+ var SemanticLandingStrategySchema = z8.enum(SEMANTIC_LANDING_STRATEGIES);
1197
+ var LandingVisualStyleFamilySchema = z8.enum([
1012
1198
  "editorial_dtc",
1013
1199
  "ugc_scrapbook",
1014
1200
  "technical_diagram",
@@ -1016,39 +1202,39 @@ var LandingVisualStyleFamilySchema = z7.enum([
1016
1202
  "warm_organic",
1017
1203
  "cinematic_problem_solution"
1018
1204
  ]);
1019
- var LandingVisualTypographySchema = z7.enum([
1205
+ var LandingVisualTypographySchema = z8.enum([
1020
1206
  "serif_editorial",
1021
1207
  "sans_modern",
1022
1208
  "display_bold",
1023
1209
  "handwritten_warm"
1024
1210
  ]);
1025
- var LandingVisualCompositionSchema = z7.enum([
1211
+ var LandingVisualCompositionSchema = z8.enum([
1026
1212
  "full_bleed",
1027
1213
  "editorial_panels",
1028
1214
  "cards_collage",
1029
1215
  "diagrammatic",
1030
1216
  "commerce_modules"
1031
1217
  ]);
1032
- var LandingVisualImageTreatmentSchema = z7.enum([
1218
+ var LandingVisualImageTreatmentSchema = z8.enum([
1033
1219
  "studio_editorial",
1034
1220
  "phone_ugc",
1035
1221
  "documentary_lifestyle",
1036
1222
  "technical_macro",
1037
1223
  "high_contrast_direct_response"
1038
1224
  ]);
1039
- var LandingVisualDensitySchema = z7.enum([
1225
+ var LandingVisualDensitySchema = z8.enum([
1040
1226
  "minimal",
1041
1227
  "balanced",
1042
1228
  "rich"
1043
1229
  ]);
1044
- var LandingVisualSignatureSchema = z7.object({
1230
+ var LandingVisualSignatureSchema = z8.object({
1045
1231
  styleFamily: LandingVisualStyleFamilySchema,
1046
1232
  typography: LandingVisualTypographySchema,
1047
1233
  composition: LandingVisualCompositionSchema,
1048
1234
  imageTreatment: LandingVisualImageTreatmentSchema,
1049
1235
  density: LandingVisualDensitySchema,
1050
- paletteIntent: z7.string().trim().min(1).max(160),
1051
- recurringMotif: z7.string().trim().min(1).max(160)
1236
+ paletteIntent: z8.string().trim().min(1).max(160),
1237
+ recurringMotif: z8.string().trim().min(1).max(160)
1052
1238
  });
1053
1239
  var LANDING_SECTION_KEYS = [
1054
1240
  "problem_hook",
@@ -1090,18 +1276,18 @@ var LANDING_SECTION_KEYS = [
1090
1276
  "lifestyle_fit",
1091
1277
  "join_lifestyle"
1092
1278
  ];
1093
- var LandingSectionKeySchema = z7.enum(LANDING_SECTION_KEYS);
1094
- var LandingSectionPlanEntrySchema = z7.object({
1279
+ var LandingSectionKeySchema = z8.enum(LANDING_SECTION_KEYS);
1280
+ var LandingSectionPlanEntrySchema = z8.object({
1095
1281
  /** Stable persistence/regeneration identifier. Never shown to the user. */
1096
1282
  role: LandingRole,
1097
1283
  /** Strategy-specific section identity shown in UI and used by prompts. */
1098
1284
  sectionKey: LandingSectionKeySchema,
1099
- index: z7.number().int().min(1).max(10)
1285
+ index: z8.number().int().min(1).max(10)
1100
1286
  });
1101
- var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefine((plan, ctx) => {
1287
+ var LandingSectionPlanSchema = z8.array(LandingSectionPlanEntrySchema).superRefine((plan, ctx) => {
1102
1288
  if (plan.length !== 7 && plan.length !== 10) {
1103
1289
  ctx.addIssue({
1104
- code: z7.ZodIssueCode.custom,
1290
+ code: z8.ZodIssueCode.custom,
1105
1291
  message: "sectionPlan must contain exactly 7 or 10 entries"
1106
1292
  });
1107
1293
  }
@@ -1110,13 +1296,13 @@ var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefi
1110
1296
  const positions = new Set(plan.map((entry) => entry.index));
1111
1297
  if (roles.size !== plan.length) {
1112
1298
  ctx.addIssue({
1113
- code: z7.ZodIssueCode.custom,
1299
+ code: z8.ZodIssueCode.custom,
1114
1300
  message: "sectionPlan roles must be unique"
1115
1301
  });
1116
1302
  }
1117
1303
  if (keys.size !== plan.length) {
1118
1304
  ctx.addIssue({
1119
- code: z7.ZodIssueCode.custom,
1305
+ code: z8.ZodIssueCode.custom,
1120
1306
  message: "sectionPlan sectionKeys must be unique"
1121
1307
  });
1122
1308
  }
@@ -1125,48 +1311,50 @@ var LandingSectionPlanSchema = z7.array(LandingSectionPlanEntrySchema).superRefi
1125
1311
  }, (_, index) => index + 1);
1126
1312
  if (positions.size !== plan.length || !requiredPositions.every((position) => positions.has(position))) {
1127
1313
  ctx.addIssue({
1128
- code: z7.ZodIssueCode.custom,
1314
+ code: z8.ZodIssueCode.custom,
1129
1315
  message: "sectionPlan indexes must be contiguous from 1 through plan length"
1130
1316
  });
1131
1317
  }
1132
1318
  });
1133
1319
 
1134
1320
  // ../../packages/types/src/landing.ts
1135
- var LandingImageReadable = z8.object({
1136
- index: z8.number().int().min(1).max(20),
1321
+ var LandingImageReadable = z9.object({
1322
+ index: z9.number().int().min(1).max(20),
1137
1323
  /** Child task that owns this image when variants are rendered alongside the root task. */
1138
- variantTaskId: z8.string().uuid().optional(),
1324
+ variantTaskId: z9.string().uuid().optional(),
1139
1325
  role: LandingRoleLegacy,
1140
1326
  /** Strategy-specific display identity; absent on legacy tasks. */
1141
1327
  sectionKey: LandingSectionKeySchema.optional(),
1142
- url: z8.string().min(1),
1143
- width: z8.number().int(),
1144
- height: z8.number().int(),
1145
- notes: z8.string().optional()
1328
+ url: z9.string().min(1),
1329
+ width: z9.number().int(),
1330
+ height: z9.number().int(),
1331
+ notes: z9.string().optional()
1146
1332
  });
1147
- var LandingImage = z8.object({
1148
- index: z8.number().int().min(1).max(10),
1333
+ var LandingImage = z9.object({
1334
+ index: z9.number().int().min(1).max(10),
1149
1335
  role: LandingRole,
1150
1336
  sectionKey: LandingSectionKeySchema.optional(),
1151
- url: z8.string().min(1),
1152
- width: z8.number().int(),
1153
- height: z8.number().int(),
1154
- notes: z8.string().optional()
1337
+ url: z9.string().min(1),
1338
+ width: z9.number().int(),
1339
+ height: z9.number().int(),
1340
+ notes: z9.string().optional()
1155
1341
  });
1156
1342
 
1157
1343
  // ../../packages/types/src/creative.ts
1158
- var VideoCreativeAspectRatio = z9.enum([
1344
+ var VideoCreativeAspectRatio = z10.enum([
1159
1345
  "16:9",
1160
1346
  "9:16",
1161
1347
  "Auto"
1162
1348
  ]);
1163
- var ImageCreativeStyle = z9.enum([
1349
+ var PINNED_HEADLINE_MAX_CHARS = 120;
1350
+ var PINNED_SUBHEADLINE_MAX_CHARS = 160;
1351
+ var ImageCreativeStyle = z10.enum([
1164
1352
  "studio",
1165
1353
  "floating",
1166
1354
  "ingredients",
1167
1355
  "in_use"
1168
1356
  ]);
1169
- var ImageCreativeArchetype = z9.enum([
1357
+ var ImageCreativeArchetype = z10.enum([
1170
1358
  "direct_response_product_ad",
1171
1359
  "hero_outcome",
1172
1360
  "offer_value_prop",
@@ -1175,215 +1363,228 @@ var ImageCreativeArchetype = z9.enum([
1175
1363
  "problem_solution",
1176
1364
  "lifestyle_identity"
1177
1365
  ]);
1178
- var ImageCreativeVisualTreatment = z9.enum([
1366
+ var ImageCreativeVisualTreatment = z10.enum([
1179
1367
  "performance_product_ad",
1180
1368
  "premium_dtc_carousel"
1181
1369
  ]);
1182
- var ImageCreativeAspectRatio = z9.enum([
1370
+ var ImageCreativeAspectRatio = z10.enum([
1183
1371
  "1:1",
1184
1372
  "4:5",
1185
1373
  "9:16",
1186
1374
  "16:9"
1187
1375
  ]);
1188
- var VideoCreativeVariant = z9.object({
1189
- id: z9.string(),
1190
- assetKey: z9.string(),
1191
- url: z9.string().url().optional(),
1192
- thumbnailUrl: z9.string().url().optional(),
1193
- label: z9.string().optional(),
1194
- format: z9.string().optional()
1195
- });
1196
- var VideoCreativeVideo = z9.object({
1197
- provider: z9.string(),
1198
- model: z9.string(),
1199
- durationSeconds: z9.number().int().positive().optional(),
1200
- variants: z9.array(VideoCreativeVariant).default([]),
1201
- clipUrls: z9.array(z9.string().url()).optional(),
1202
- adCopy: z9.string().optional()
1203
- });
1204
- var VideoCreativeContext = z9.object({
1205
- productTitle: z9.string(),
1206
- primaryAngle: z9.string(),
1207
- buyerPersona: z9.string().optional(),
1208
- brandId: z9.string().uuid().optional(),
1209
- features: z9.array(z9.string()).default([]),
1210
- scriptNotes: z9.string().optional(),
1211
- cta: z9.string().optional(),
1212
- language: z9.string(),
1376
+ var VideoCreativeVariant = z10.object({
1377
+ id: z10.string(),
1378
+ assetKey: z10.string(),
1379
+ url: z10.string().url().optional(),
1380
+ thumbnailUrl: z10.string().url().optional(),
1381
+ label: z10.string().optional(),
1382
+ format: z10.string().optional()
1383
+ });
1384
+ var VideoCreativeVideo = z10.object({
1385
+ provider: z10.string(),
1386
+ model: z10.string(),
1387
+ durationSeconds: z10.number().int().positive().optional(),
1388
+ variants: z10.array(VideoCreativeVariant).default([]),
1389
+ clipUrls: z10.array(z10.string().url()).optional(),
1390
+ adCopy: z10.string().optional()
1391
+ });
1392
+ var VideoCreativeContext = z10.object({
1393
+ productTitle: z10.string(),
1394
+ primaryAngle: z10.string(),
1395
+ buyerPersona: z10.string().optional(),
1396
+ brandId: z10.string().uuid().optional(),
1397
+ features: z10.array(z10.string()).default([]),
1398
+ scriptNotes: z10.string().optional(),
1399
+ cta: z10.string().optional(),
1400
+ language: z10.string(),
1213
1401
  aspectRatio: VideoCreativeAspectRatio,
1214
- durationSeconds: z9.number().int().positive()
1402
+ durationSeconds: z10.number().int().positive()
1215
1403
  });
1216
- var VideoCreativeJobResult = z9.object({
1404
+ var VideoCreativeJobResult = z10.object({
1217
1405
  video: VideoCreativeVideo,
1218
1406
  context: VideoCreativeContext,
1219
- createdAt: z9.string().datetime().optional()
1407
+ createdAt: z10.string().datetime().optional()
1220
1408
  });
1221
- var VideoCreativeJobStatusEnum = z9.enum([
1409
+ var VideoCreativeJobStatusEnum = z10.enum([
1222
1410
  "pending",
1223
1411
  "processing",
1224
1412
  "completed",
1225
1413
  "error"
1226
1414
  ]);
1227
- var VideoCreativeJobStatus = z9.object({
1228
- jobId: z9.string(),
1415
+ var VideoCreativeJobStatus = z10.object({
1416
+ jobId: z10.string(),
1229
1417
  status: VideoCreativeJobStatusEnum,
1230
- progress: z9.number().min(0).max(100).optional(),
1231
- etaSeconds: z9.number().int().nonnegative().optional(),
1232
- message: z9.string().optional(),
1233
- error: z9.string().optional(),
1418
+ progress: z10.number().min(0).max(100).optional(),
1419
+ etaSeconds: z10.number().int().nonnegative().optional(),
1420
+ message: z10.string().optional(),
1421
+ error: z10.string().optional(),
1234
1422
  /**
1235
1423
  * Primary angle that the backend will use for generation.
1236
1424
  * Can be user-provided (request.primaryAngle) or generated (angles[0]).
1237
1425
  */
1238
- primaryAngle: z9.string().optional(),
1239
- primaryAngleSource: z9.enum([
1426
+ primaryAngle: z10.string().optional(),
1427
+ primaryAngleSource: z10.enum([
1240
1428
  "user",
1241
1429
  "generated"
1242
1430
  ]).optional(),
1243
- buyerPersona: z9.string().optional(),
1431
+ buyerPersona: z10.string().optional(),
1244
1432
  result: VideoCreativeJobResult.nullable().optional()
1245
1433
  });
1246
- var VideoCreativeJobPayload = z9.object({
1247
- jobId: z9.string().optional(),
1434
+ var VideoCreativeJobPayload = z10.object({
1435
+ jobId: z10.string().optional(),
1248
1436
  status: VideoCreativeJobStatusEnum.optional(),
1249
- progress: z9.number().min(0).max(100).optional(),
1250
- etaSeconds: z9.number().int().nonnegative().optional(),
1251
- message: z9.string().optional(),
1252
- error: z9.string().optional(),
1253
- primaryAngle: z9.string().optional(),
1254
- primaryAngleSource: z9.enum([
1437
+ progress: z10.number().min(0).max(100).optional(),
1438
+ etaSeconds: z10.number().int().nonnegative().optional(),
1439
+ message: z10.string().optional(),
1440
+ error: z10.string().optional(),
1441
+ primaryAngle: z10.string().optional(),
1442
+ primaryAngleSource: z10.enum([
1255
1443
  "user",
1256
1444
  "generated"
1257
1445
  ]).optional(),
1258
- buyerPersona: z9.string().optional(),
1446
+ buyerPersona: z10.string().optional(),
1259
1447
  result: VideoCreativeJobResult.nullable().optional()
1260
1448
  });
1261
- var VideoCreativeHistoryEntry = z9.object({
1262
- id: z9.string(),
1263
- createdAt: z9.string().datetime(),
1449
+ var VideoCreativeHistoryEntry = z10.object({
1450
+ id: z10.string(),
1451
+ createdAt: z10.string().datetime(),
1264
1452
  status: VideoCreativeJobStatusEnum,
1265
1453
  result: VideoCreativeJobResult.nullable(),
1266
- error: z9.string().optional()
1454
+ error: z10.string().optional()
1455
+ });
1456
+ var AdCreative = z10.object({
1457
+ provider: z10.string(),
1458
+ model: z10.string(),
1459
+ prompt: z10.string().min(1),
1460
+ url: z10.string().min(1),
1461
+ width: z10.number().int().positive(),
1462
+ height: z10.number().int().positive(),
1463
+ error: z10.string().optional(),
1464
+ variants: z10.array(z10.string().url()).optional(),
1465
+ /** One-to-one with `variants`: null for ProductMaker-original images and the
1466
+ * evidence tier for the competitor-reference variant. */
1467
+ variantSignals: z10.array(ImageWinnerSignal.nullable()).optional(),
1468
+ createdAt: z10.string().datetime().optional(),
1469
+ adCopy: z10.string().optional()
1470
+ }).superRefine((creative, ctx) => {
1471
+ if (creative.variantSignals !== void 0 && (creative.variants === void 0 || creative.variantSignals.length !== creative.variants.length)) {
1472
+ ctx.addIssue({
1473
+ code: z10.ZodIssueCode.custom,
1474
+ path: [
1475
+ "variantSignals"
1476
+ ],
1477
+ message: "variantSignals must align one-to-one with variants"
1478
+ });
1479
+ }
1267
1480
  });
1268
- var AdCreative = z9.object({
1269
- provider: z9.string(),
1270
- model: z9.string(),
1271
- prompt: z9.string().min(1),
1272
- url: z9.string().min(1),
1273
- width: z9.number().int().positive(),
1274
- height: z9.number().int().positive(),
1275
- error: z9.string().optional(),
1276
- variants: z9.array(z9.string().url()).optional(),
1277
- createdAt: z9.string().datetime().optional(),
1278
- adCopy: z9.string().optional()
1279
- });
1280
- var ImageCreativeCopy = z9.object({
1281
- title: z9.string().optional(),
1282
- subtitle: z9.string().optional(),
1283
- headline: z9.string().optional(),
1284
- bullets: z9.array(z9.string()).optional(),
1285
- discount: z9.string().optional(),
1286
- promoSticker: z9.string().optional(),
1287
- colors: z9.array(z9.string()).optional()
1481
+ var ImageCreativeCopy = z10.object({
1482
+ title: z10.string().optional(),
1483
+ subtitle: z10.string().optional(),
1484
+ headline: z10.string().optional(),
1485
+ bullets: z10.array(z10.string()).optional(),
1486
+ discount: z10.string().optional(),
1487
+ promoSticker: z10.string().optional(),
1488
+ colors: z10.array(z10.string()).optional()
1288
1489
  }).partial();
1289
- var ImageCreativeVariant = z9.object({
1290
- id: z9.string(),
1291
- url: z9.string().url(),
1292
- downloadUrl: z9.string().url().optional()
1293
- });
1294
- var ImageCreativeResult = z9.object({
1295
- provider: z9.string(),
1296
- model: z9.string(),
1297
- prompt: z9.string(),
1298
- width: z9.number().int().positive(),
1299
- height: z9.number().int().positive(),
1300
- variants: z9.array(ImageCreativeVariant).min(1),
1490
+ var ImageCreativeVariant = z10.object({
1491
+ id: z10.string(),
1492
+ url: z10.string().url(),
1493
+ downloadUrl: z10.string().url().optional()
1494
+ });
1495
+ var ImageCreativeResult = z10.object({
1496
+ provider: z10.string(),
1497
+ model: z10.string(),
1498
+ prompt: z10.string(),
1499
+ width: z10.number().int().positive(),
1500
+ height: z10.number().int().positive(),
1501
+ variants: z10.array(ImageCreativeVariant).min(1),
1301
1502
  copy: ImageCreativeCopy.optional(),
1302
- createdAt: z9.string().datetime().optional(),
1303
- adCopy: z9.string().optional()
1304
- });
1305
- var ImageCreativeEditableRequest = z9.object({
1306
- price: z9.string(),
1307
- offer: z9.string(),
1308
- compareAtPrice: z9.string(),
1309
- country: z9.string()
1503
+ createdAt: z10.string().datetime().optional(),
1504
+ adCopy: z10.string().optional()
1505
+ });
1506
+ var ImageCreativeEditableRequest = z10.object({
1507
+ price: z10.string(),
1508
+ offer: z10.string(),
1509
+ compareAtPrice: z10.string(),
1510
+ country: z10.string()
1310
1511
  }).partial();
1311
- var ImageCreativeJobStatusEnum = z9.enum([
1512
+ var ImageCreativeJobStatusEnum = z10.enum([
1312
1513
  "pending",
1313
1514
  "processing",
1314
1515
  "completed",
1315
1516
  "error"
1316
1517
  ]);
1317
- var ImageCreativeJobStatus = z9.object({
1318
- jobId: z9.string().uuid(),
1518
+ var ImageCreativeJobStatus = z10.object({
1519
+ jobId: z10.string().uuid(),
1319
1520
  status: ImageCreativeJobStatusEnum,
1320
- error: z9.string().optional(),
1521
+ error: z10.string().optional(),
1321
1522
  result: ImageCreativeResult.nullable().optional(),
1322
1523
  request: ImageCreativeEditableRequest.optional()
1323
1524
  });
1324
- var ImageCreativeJobPayload = z9.object({
1325
- jobId: z9.string().uuid(),
1525
+ var ImageCreativeJobPayload = z10.object({
1526
+ jobId: z10.string().uuid(),
1326
1527
  status: ImageCreativeJobStatusEnum
1327
1528
  });
1328
- var ImageCreativeHistoryEntry = z9.object({
1329
- id: z9.string().uuid(),
1330
- createdAt: z9.string().datetime(),
1529
+ var ImageCreativeHistoryEntry = z10.object({
1530
+ id: z10.string().uuid(),
1531
+ createdAt: z10.string().datetime(),
1331
1532
  status: ImageCreativeJobStatusEnum,
1332
1533
  result: ImageCreativeResult.nullable(),
1333
- error: z9.string().optional()
1534
+ error: z10.string().optional()
1334
1535
  });
1335
- var LandingCreativeJobStatusEnum = z9.enum([
1536
+ var LandingCreativeJobStatusEnum = z10.enum([
1336
1537
  "pending",
1337
1538
  "processing",
1338
1539
  "completed",
1339
1540
  "error"
1340
1541
  ]);
1341
- var LandingCreativeResult = z9.object({
1342
- images: z9.array(LandingImageReadable).max(20),
1343
- createdAt: z9.string().datetime().optional()
1542
+ var LandingCreativeResult = z10.object({
1543
+ images: z10.array(LandingImageReadable).max(20),
1544
+ createdAt: z10.string().datetime().optional()
1344
1545
  });
1345
- var LandingCreativeJobStatus = z9.object({
1346
- jobId: z9.string().uuid(),
1546
+ var LandingCreativeJobStatus = z10.object({
1547
+ jobId: z10.string().uuid(),
1347
1548
  status: LandingCreativeJobStatusEnum,
1348
- error: z9.string().optional(),
1549
+ error: z10.string().optional(),
1349
1550
  result: LandingCreativeResult.nullable().optional()
1350
1551
  });
1351
- var LandingCreativeJobPayload = z9.object({
1352
- jobId: z9.string().uuid(),
1552
+ var LandingCreativeJobPayload = z10.object({
1553
+ jobId: z10.string().uuid(),
1353
1554
  status: LandingCreativeJobStatusEnum
1354
1555
  });
1355
- var LandingCreativeHistoryEntry = z9.object({
1356
- id: z9.string().uuid(),
1357
- createdAt: z9.string().datetime(),
1556
+ var LandingCreativeHistoryEntry = z10.object({
1557
+ id: z10.string().uuid(),
1558
+ createdAt: z10.string().datetime(),
1358
1559
  status: LandingCreativeJobStatusEnum,
1359
1560
  result: LandingCreativeResult.nullable(),
1360
- error: z9.string().optional()
1561
+ error: z10.string().optional()
1361
1562
  });
1362
- var CreativeGeneratorJob = z9.object({
1363
- taskId: z9.string().uuid(),
1364
- cleanImagesWaitAttempt: z9.number().int().nonnegative().optional()
1563
+ var CreativeGeneratorJob = z10.object({
1564
+ taskId: z10.string().uuid(),
1565
+ cleanImagesWaitAttempt: z10.number().int().nonnegative().optional()
1365
1566
  });
1366
1567
 
1367
1568
  // ../../packages/types/src/product-label.ts
1368
- import { z as z10 } from "zod";
1569
+ import { z as z11 } from "zod";
1369
1570
  var PRODUCT_LABEL_MAX_CHARS = 80;
1370
- var ProductLabelSchema = z10.string().transform((value) => value.trim()).pipe(z10.string().max(PRODUCT_LABEL_MAX_CHARS)).optional();
1571
+ var ProductLabelSchema = z11.string().transform((value) => value.trim()).pipe(z11.string().max(PRODUCT_LABEL_MAX_CHARS)).optional();
1371
1572
 
1372
1573
  // ../../packages/types/src/landing-mode.ts
1373
- import { z as z11 } from "zod";
1374
- var LandingModeSchema = z11.enum([
1574
+ import { z as z12 } from "zod";
1575
+ var LandingModeSchema = z12.enum([
1375
1576
  "classic",
1376
1577
  "code"
1377
1578
  ]);
1378
1579
 
1379
1580
  // ../../packages/types/src/landing-image-format.ts
1380
- import { z as z12 } from "zod";
1581
+ import { z as z13 } from "zod";
1381
1582
  var LANDING_IMAGE_FORMATS = [
1382
1583
  "9:16",
1383
1584
  "4:5",
1384
1585
  "1:1"
1385
1586
  ];
1386
- var LandingImageFormatSchema = z12.enum(LANDING_IMAGE_FORMATS);
1587
+ var LandingImageFormatSchema = z13.enum(LANDING_IMAGE_FORMATS);
1387
1588
 
1388
1589
  // ../../packages/types/src/ingest.ts
1389
1590
  var INGEST_MAX_REFERENCE_IMAGES = 5;
@@ -1402,9 +1603,9 @@ var INGEST_ALLOWED_IMAGE_MIME_TYPES = [
1402
1603
  ];
1403
1604
  var PRODUCT_CONTEXT_MAX_CHARS = 3e3;
1404
1605
  var INGEST_ALLOWED_IMAGE_MIME_TYPE_SET = new Set(INGEST_ALLOWED_IMAGE_MIME_TYPES);
1405
- var IngestOptions = z13.object({
1606
+ var IngestOptions = z14.object({
1406
1607
  /** Optional owner-scoped guided journey context. Never bypasses ingest gates. */
1407
- dropshippingProjectId: z13.string().uuid().optional(),
1608
+ dropshippingProjectId: z14.string().uuid().optional(),
1408
1609
  languageCode: LanguageCodeSchema.default("es-CO"),
1409
1610
  ugcFormat: VideoFormat.default("single_15s"),
1410
1611
  country: CountrySchema,
@@ -1417,26 +1618,33 @@ var IngestOptions = z13.object({
1417
1618
  videoTier: VideoTier.default(DEFAULT_VIDEO_TIER),
1418
1619
  narrativeStyle: NarrativeStyle.default("review"),
1419
1620
  videoPreset: VideoPreset.optional(),
1420
- multiAngleVideos: z13.boolean().default(false),
1421
- videoAngleCount: z13.number().int().min(1).max(3).optional(),
1621
+ multiAngleVideos: z14.boolean().default(false),
1622
+ videoAngleCount: z14.number().int().min(1).max(3).optional(),
1422
1623
  /** Per-angle narrative override (positional). angleNarratives[i] forces the
1423
1624
  * narrative of angle position i, overriding the batch auto-selector; a list
1424
1625
  * shorter than the generated angle count leaves the remaining positions
1425
1626
  * auto-selected. Empty/omitted → all angles auto-select as before. */
1426
- angleNarratives: z13.array(NarrativeStyle).max(3).optional(),
1427
- // 720p is the recommended default for every tier (flipped 2026-06-22 — grok provider
1428
- // unified per-second pricing, so 720p is only a small premium and worth the sharpness).
1627
+ angleNarratives: z14.array(NarrativeStyle).max(3).optional(),
1628
+ // 480p is the recommended default for every tier (flipped back 2026-07-30 — the
1629
+ // 720p premium is real but small, and users on the entry plan were defaulting
1630
+ // into it while burning half a month of credits on one video).
1429
1631
  // This Zod default is what callers that omit the field (MCP/API) actually get,
1430
- // so it must agree with resolveSingleCallAwareResolution's unset fallback (720p).
1431
- videoResolution: VideoResolution.default("720p"),
1432
- generateAdCopy: z13.boolean().default(true),
1433
- generateAiMusic: z13.boolean().optional(),
1632
+ // so it must agree with resolveSingleCallAwareResolution's unset fallback (480p).
1633
+ videoResolution: VideoResolution.default("480p"),
1634
+ generateAdCopy: z14.boolean().default(true),
1635
+ generateAiMusic: z14.boolean().optional(),
1434
1636
  voiceId: VoiceId.optional(),
1435
- isCombo: z13.boolean().default(false),
1436
- productContext: z13.string().transform((value) => value.trim()).pipe(z13.string().max(PRODUCT_CONTEXT_MAX_CHARS)).optional(),
1437
- creativeDirection: z13.string().transform((value) => value.trim()).pipe(z13.string().max(500)).optional(),
1637
+ isCombo: z14.boolean().default(false),
1638
+ productContext: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PRODUCT_CONTEXT_MAX_CHARS)).optional(),
1639
+ creativeDirection: z14.string().transform((value) => value.trim()).pipe(z14.string().max(500)).optional(),
1640
+ /** Exact copy the user pinned. Rendered VERBATIM on the image creatives this task
1641
+ * produces and on the landing hero — never on video, which steers via scriptNotes.
1642
+ * `.pipe(...max)` REJECTS over-cap instead of truncating: silently shortening a pin
1643
+ * would ship copy the user never wrote, which is the bug the pin exists to prevent. */
1644
+ headline: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PINNED_HEADLINE_MAX_CHARS)).optional(),
1645
+ subheadline: z14.string().transform((value) => value.trim()).pipe(z14.string().max(PINNED_SUBHEADLINE_MAX_CHARS)).optional(),
1438
1646
  productLabel: ProductLabelSchema,
1439
- selectedOutputs: z13.array(z13.enum([
1647
+ selectedOutputs: z14.array(z14.enum([
1440
1648
  "landing",
1441
1649
  "video",
1442
1650
  "image"
@@ -1448,26 +1656,26 @@ var IngestOptions = z13.object({
1448
1656
  landingStrategy: LandingStrategySchema.optional(),
1449
1657
  /** Aspect ratio for classic image landing blocks. Ignored by code landing. Absent ⇒ '9:16'. */
1450
1658
  landingImageFormat: LandingImageFormatSchema.optional(),
1451
- imageStyle: z13.string().optional(),
1452
- creativeArchetype: ImageCreativeArchetype.or(z13.literal("ai_decides")).optional(),
1453
- visualTreatment: ImageCreativeVisualTreatment.or(z13.literal("ai_decides")).optional(),
1454
- aspectRatio: z13.string().optional(),
1455
- brandColors: z13.string().optional(),
1456
- nVariants: z13.number().int().min(1).max(6).optional(),
1457
- imageModel: z13.string().optional(),
1458
- styleMix: z13.string().optional(),
1459
- aiDecidesAll: z13.boolean().optional(),
1659
+ imageStyle: z14.string().optional(),
1660
+ creativeArchetype: ImageCreativeArchetype.or(z14.literal("ai_decides")).optional(),
1661
+ visualTreatment: ImageCreativeVisualTreatment.or(z14.literal("ai_decides")).optional(),
1662
+ aspectRatio: z14.string().optional(),
1663
+ brandColors: z14.string().optional(),
1664
+ nVariants: z14.number().int().min(1).max(6).optional(),
1665
+ imageModel: z14.string().optional(),
1666
+ styleMix: z14.string().optional(),
1667
+ aiDecidesAll: z14.boolean().optional(),
1460
1668
  /** Opt out of the competitor-spy analysis. When false the worker skips
1461
1669
  * enqueueCompetitorScan entirely, saving ~$0.25-0.35 user-visible per
1462
1670
  * scan. Default true preserves the existing flow; users with their own
1463
1671
  * competitor intel or in privacy-sensitive niches can disable. */
1464
- competitorScanEnabled: z13.boolean().default(true),
1465
- brandId: z13.string().uuid().optional()
1672
+ competitorScanEnabled: z14.boolean().default(true),
1673
+ brandId: z14.string().uuid().optional()
1466
1674
  }).partial().superRefine((data, ctx) => {
1467
1675
  const message = validatePriceInversion(data.price, data.compareAtPrice);
1468
1676
  if (message) {
1469
1677
  ctx.addIssue({
1470
- code: z13.ZodIssueCode.custom,
1678
+ code: z14.ZodIssueCode.custom,
1471
1679
  path: [
1472
1680
  "compareAtPrice"
1473
1681
  ],
@@ -1478,11 +1686,11 @@ var IngestOptions = z13.object({
1478
1686
  });
1479
1687
  }
1480
1688
  });
1481
- var IngestRequest = z13.object({
1482
- url: z13.string().url(),
1689
+ var IngestRequest = z14.object({
1690
+ url: z14.string().url(),
1483
1691
  options: IngestOptions.optional()
1484
1692
  });
1485
- var VideoOptionsOverride = z13.object({
1693
+ var VideoOptionsOverride = z14.object({
1486
1694
  videoModel: VideoModel.optional(),
1487
1695
  videoTier: VideoTier.optional(),
1488
1696
  ugcFormat: VideoFormat.optional(),
@@ -1491,38 +1699,38 @@ var VideoOptionsOverride = z13.object({
1491
1699
  videoResolution: VideoResolution.optional(),
1492
1700
  voiceId: VoiceId.optional(),
1493
1701
  /** Actor/character description (role + appearance + attitude). Drives voice and video character. */
1494
- actor: z13.string().max(360).optional()
1702
+ actor: z14.string().max(360).optional()
1495
1703
  });
1496
1704
  var AngleVideoOptions = VideoOptionsOverride.extend({
1497
- angleIndex: z13.number().int().nonnegative(),
1705
+ angleIndex: z14.number().int().nonnegative(),
1498
1706
  /** Per-angle angle/hook text override. When provided, replaces the auto-generated angle. */
1499
- angle: z13.string().max(480).optional(),
1707
+ angle: z14.string().max(480).optional(),
1500
1708
  /** Per-angle creative direction (camera/action/setting/tone). NOT spoken dialogue — the script is authored/edited at script review. */
1501
- scriptNotes: z13.string().max(2e3).optional(),
1709
+ scriptNotes: z14.string().max(2e3).optional(),
1502
1710
  /** Per-angle buyer persona override (describes the target audience) */
1503
- buyerPersona: z13.string().max(360).optional(),
1711
+ buyerPersona: z14.string().max(360).optional(),
1504
1712
  /** Per-angle actor/character description (role + appearance + attitude). Drives voice and video character. */
1505
- actor: z13.string().max(360).optional()
1506
- });
1507
- var RapidScriptBlueprintShotForApproval = z13.object({
1508
- id: z13.string().min(1),
1509
- beatSource: z13.string().min(1),
1510
- durationSec: z13.number().positive(),
1511
- role: z13.string().min(1).max(120),
1512
- text: z13.string().min(1).max(320),
1513
- visualIntent: z13.string().min(1).max(500).optional(),
1514
- variantKey: z13.string().min(1).max(120).optional(),
1515
- targetWords: z13.number().int().positive().optional(),
1516
- minWords: z13.number().int().positive(),
1517
- maxWords: z13.number().int().positive()
1713
+ actor: z14.string().max(360).optional()
1714
+ });
1715
+ var RapidScriptBlueprintShotForApproval = z14.object({
1716
+ id: z14.string().min(1),
1717
+ beatSource: z14.string().min(1),
1718
+ durationSec: z14.number().positive(),
1719
+ role: z14.string().min(1).max(120),
1720
+ text: z14.string().min(1).max(320),
1721
+ visualIntent: z14.string().min(1).max(500).optional(),
1722
+ variantKey: z14.string().min(1).max(120).optional(),
1723
+ targetWords: z14.number().int().positive().optional(),
1724
+ minWords: z14.number().int().positive(),
1725
+ maxWords: z14.number().int().positive()
1518
1726
  }).refine((shot) => shot.maxWords >= shot.minWords, {
1519
1727
  message: "maxWords must be greater than or equal to minWords",
1520
1728
  path: [
1521
1729
  "maxWords"
1522
1730
  ]
1523
1731
  });
1524
- var RapidScriptBlueprintForApproval = z13.object({
1525
- shots: z13.array(RapidScriptBlueprintShotForApproval).min(1).max(10)
1732
+ var RapidScriptBlueprintForApproval = z14.object({
1733
+ shots: z14.array(RapidScriptBlueprintShotForApproval).min(1).max(10)
1526
1734
  }).refine((blueprint) => {
1527
1735
  const ids = blueprint.shots.map((shot) => shot.id);
1528
1736
  return new Set(ids).size === ids.length;
@@ -1535,27 +1743,27 @@ var RapidScriptBlueprintForApproval = z13.object({
1535
1743
  var AngleVideoOptionsWithBlueprint = AngleVideoOptions.extend({
1536
1744
  rapidScriptBlueprint: RapidScriptBlueprintForApproval.optional()
1537
1745
  });
1538
- var ProductImageSelection = z13.string().min(1).max(2048).refine((value) => !value.includes("\0"), "Invalid image reference");
1539
- var IngestResumePayload = z13.object({
1540
- selectedAngleIndices: z13.array(z13.number().int().nonnegative()).min(1).max(3),
1746
+ var ProductImageSelection = z14.string().min(1).max(2048).refine((value) => !value.includes("\0"), "Invalid image reference");
1747
+ var IngestResumePayload = z14.object({
1748
+ selectedAngleIndices: z14.array(z14.number().int().nonnegative()).min(1).max(3),
1541
1749
  ugcFormat: VideoFormat.default("single_15s"),
1542
1750
  videoStyle: VideoStyle.default("ugc"),
1543
1751
  videoModel: VideoModel.default("veo"),
1544
1752
  videoTier: VideoTier.optional(),
1545
1753
  narrativeStyle: NarrativeStyle.default("review"),
1546
- // 720p is the recommended default for every tier (flipped 2026-06-22) — keep in sync
1547
- // with IngestOptions.videoResolution and resolveSingleCallAwareResolution.
1548
- videoResolution: VideoResolution.default("720p"),
1549
- pauseForVideoScriptReview: z13.boolean().optional(),
1550
- generateAiMusic: z13.boolean().optional(),
1551
- scriptNotes: z13.string().max(2e3).optional(),
1754
+ // 480p is the recommended default for every tier (flipped back 2026-07-30) — keep in
1755
+ // sync with IngestOptions.videoResolution and resolveSingleCallAwareResolution.
1756
+ videoResolution: VideoResolution.default("480p"),
1757
+ pauseForVideoScriptReview: z14.boolean().optional(),
1758
+ generateAiMusic: z14.boolean().optional(),
1759
+ scriptNotes: z14.string().max(2e3).optional(),
1552
1760
  languageCode: LanguageCodeSchema.optional(),
1553
1761
  country: CountrySchema.optional(),
1554
1762
  price: PriceSchema.optional(),
1555
1763
  offer: OfferSchema.optional(),
1556
1764
  compareAtPrice: CompareAtPriceSchema.optional(),
1557
1765
  paymentMethods: PaymentMethodsSchema.optional(),
1558
- angleVideoOptions: z13.array(AngleVideoOptionsWithBlueprint).max(3).refine((opts) => {
1766
+ angleVideoOptions: z14.array(AngleVideoOptionsWithBlueprint).max(3).refine((opts) => {
1559
1767
  if (!opts?.length) return true;
1560
1768
  const indices = opts.map((o) => o.angleIndex);
1561
1769
  return new Set(indices).size === indices.length;
@@ -1564,191 +1772,29 @@ var IngestResumePayload = z13.object({
1564
1772
  }).optional(),
1565
1773
  voiceId: VoiceId.optional(),
1566
1774
  /** Global actor/character description (role + appearance + attitude). Default for all angles. */
1567
- actor: z13.string().max(360).optional(),
1775
+ actor: z14.string().max(360).optional(),
1568
1776
  /** Custom angle override for landing/creative (uses this instead of first angle) */
1569
- customAngleOverride: z13.string().min(1).max(480).optional(),
1777
+ customAngleOverride: z14.string().min(1).max(480).optional(),
1570
1778
  /** Pain points to emphasize in landings, creatives, and videos */
1571
- painPoints: z13.string().max(1500).optional(),
1779
+ painPoints: z14.string().max(1500).optional(),
1572
1780
  /** Benefits to highlight in landings, creatives, and videos */
1573
- benefits: z13.string().max(1500).optional(),
1781
+ benefits: z14.string().max(1500).optional(),
1574
1782
  /** Target gender for landing/creative images */
1575
- creativeGender: z13.enum([
1783
+ creativeGender: z14.enum([
1576
1784
  "female",
1577
1785
  "male",
1578
1786
  "mixed"
1579
1787
  ]).optional(),
1788
+ /** Non-blocking static winner preference. Omitted defaults to original. */
1789
+ winnerVisualMode: WinnerVisualMode.optional(),
1580
1790
  /** Which product image URLs to use for this generation (ephemeral selection) */
1581
- selectedImageUrls: z13.array(ProductImageSelection).max(INGEST_MAX_REFERENCE_IMAGES).optional(),
1791
+ selectedImageUrls: z14.array(ProductImageSelection).max(INGEST_MAX_REFERENCE_IMAGES).optional(),
1582
1792
  /** User-confirmed intended product target for the selected reference image set */
1583
1793
  productTargetConfirmation: ProductTargetConfirmationSchema.optional(),
1584
1794
  /** User-confirmed grouping for the selected reference image set */
1585
1795
  productReferenceConfirmation: ProductReferenceConfirmationSchema.optional()
1586
1796
  });
1587
1797
 
1588
- // ../../packages/types/src/angle.ts
1589
- import { z as z14 } from "zod";
1590
- var AnchoredIntel = z14.object({
1591
- painPointIndices: z14.array(z14.number().int().nonnegative()).max(10),
1592
- hookIndices: z14.array(z14.number().int().nonnegative()).max(10),
1593
- claimIndices: z14.array(z14.number().int().nonnegative()).max(10),
1594
- sourceAngleType: z14.enum([
1595
- "gap",
1596
- "validated_local",
1597
- "saturated_differentiated",
1598
- "novel"
1599
- ]),
1600
- // SNAPSHOT of the resolved intel strings at angle-generation time.
1601
- // Indices reference into Task.competitorIntel which we treat as immutable
1602
- // post-scan, but defensive: a future re-scan path that rewrites
1603
- // Task.competitorIntel would invalidate the indices silently. Persisting
1604
- // the resolved strings ALONGSIDE the indices means downstream consumers
1605
- // (script writer, landing copy) can safely use the strings directly
1606
- // without re-resolving — staleness-immune. The indices remain useful for
1607
- // analytics ("which intel items did angles cite?") and as a sanity-check.
1608
- resolvedPainPoints: z14.array(z14.string()).max(10).optional(),
1609
- resolvedHooks: z14.array(z14.string()).max(10).optional(),
1610
- resolvedClaims: z14.array(z14.string()).max(10).optional(),
1611
- /**
1612
- * Free-form pillar name describing the AXIS of the product this angle
1613
- * attacks (e.g. "ciencia/mecanismo", "tiempo/ritual", "comparación-con-
1614
- * alternativas", "social-proof", "dolor-emocional", "identidad/persona").
1615
- * The LLM picks the label — we do NOT lock an enum because the right
1616
- * pillar taxonomy varies per product (a supplement's "mecanismo" pillar
1617
- * is different from a gadget's "fricción de uso"). Constraint enforced
1618
- * at generation time: the 3 angles emitted for one task MUST use 3
1619
- * DISTINCT pillars (anti-convergence). Lives inside AnchoredIntel because
1620
- * it ships as analytics metadata alongside the anchoredIntel sourceType +
1621
- * indices, and persists as JSON without a column migration. Optional
1622
- * because angles persisted before this change have no pillar.
1623
- */
1624
- pillar: z14.string().min(1).max(60).optional(),
1625
- // Telemetry: why the angle did NOT import a gapAngle. The legacy single value
1626
- // 'none_relevant' was OVERLOADED (it meant "no gaps existed" AND "gaps existed
1627
- // but none fit" AND was the sanitizer default) which made the gap feature
1628
- // unmeasurable. Split into distinct, one-meaning-each states:
1629
- // - persona_mismatch / product_mismatch: the LLM saw gaps and judged them a
1630
- // wrong-persona / wrong-subcategory fit (trusted from the LLM).
1631
- // - no_gaps_in_block: there were no gapAngles in the block (or no block).
1632
- // - gaps_present_not_imported: gaps existed and the angle went novel anyway
1633
- // (the sanitizer's honest default when the LLM didn't pick persona/product).
1634
- // - gaps_all_filtered_noise: the reference HAD gaps but every one was dropped
1635
- // as detection noise (retailer-meta/cross-category) so the LLM saw none.
1636
- // - n_a: the angle imported a gap (sourceAngleType="gap"), so "why skipped"
1637
- // is moot.
1638
- // The deterministic gap-presence states (no_gaps_in_block / gaps_present_not_
1639
- // imported / gaps_all_filtered_noise) are set by sanitizeAnchoredIntel from
1640
- // facts it computes (intel.gapAngles.length, intel.gapAnglesFilteredCount), NOT
1641
- // trusted from the lazy LLM. z.preprocess aliases historical 'none_relevant'
1642
- // rows → 'no_gaps_in_block' so the JSON column parses with no migration.
1643
- // Analytics-only: NO runtime path branches on this value.
1644
- skippedGapReason: z14.preprocess((v) => v === "none_relevant" ? "no_gaps_in_block" : v, z14.enum([
1645
- "persona_mismatch",
1646
- "product_mismatch",
1647
- "no_gaps_in_block",
1648
- "gaps_present_not_imported",
1649
- "gaps_all_filtered_noise",
1650
- "n_a"
1651
- ])).optional(),
1652
- // Telemetry: captures the prod-observed case where a "novel" angle actually
1653
- // reflects a GAP's theme/promise without formally anchoring indices (gapAngles
1654
- // are a separate array, not index-cited, so a real gap import is reclassified
1655
- // 'novel' by the empty-indices coherence guard). Self-reported by the LLM:
1656
- // - gap_imported: the angle's theme imports a gap's logic.
1657
- // - product_derived: derived purely from the product.
1658
- // - hybrid: a mix.
1659
- // SOFT analytics-only signal (the LLM may confabulate) — validated against
1660
- // known prod cases before trusting aggregates; never a control-flow input.
1661
- // Optional in storage; on the wire it ships as a required enum with a 'none'
1662
- // sentinel (strict structured output forbids optional properties) that the
1663
- // sanitizer maps to undefined.
1664
- themeSourceType: z14.enum([
1665
- "gap_imported",
1666
- "product_derived",
1667
- "hybrid"
1668
- ]).optional()
1669
- });
1670
- var Angle = z14.object({
1671
- angle: z14.string().min(1).max(480),
1672
- buyerPersona: z14.string().min(1).max(360),
1673
- /** Character description for video (role + appearance + attitude). Drives voice selection and Frame0Spec. */
1674
- actor: z14.string().min(1).max(360).optional(),
1675
- // Binary only: voice catalog has no non-binary voices.
1676
- // See packages/angles/src/index.ts + getAngles coercion in angle.repository.ts.
1677
- gender: z14.enum([
1678
- "female",
1679
- "male"
1680
- ]).optional(),
1681
- /** Per-angle user-supplied script/visual notes. Hydrated from the
1682
- * `Angle.scriptNotes` DB column or the `angleVideoOptions[i].scriptNotes`
1683
- * passed from the chat. Per-angle overrides global `videoScriptNotes`. */
1684
- scriptNotes: z14.string().max(2e3).optional(),
1685
- /** AI-chosen per-angle narrative (set by the batch selector in angles.worker).
1686
- * Pre-fills the review dropdown; user can override per angle. */
1687
- narrativeStyle: NarrativeStyle.optional(),
1688
- /** See AnchoredIntel docs. Optional because angles persisted before this
1689
- * change have no anchoring captured; new angles ALWAYS emit it (with empty
1690
- * arrays + 'novel' when intel was unavailable). */
1691
- anchoredIntel: AnchoredIntel.optional(),
1692
- /** Server-computed: true when this angle is eligible for the gap_replication
1693
- * narrative (isReplicationAngle(anchoredIntel, intel) && intel.gapBlueprint
1694
- * present). Computed server-side (not derived client-side) because the web
1695
- * only sees this angle's anchoredIntel, not the task's
1696
- * competitorIntel.gapBlueprint. Gates the review dropdown's gap_replication
1697
- * option — absent/false = hidden. */
1698
- gapReplicationEligible: z14.boolean().optional(),
1699
- /** Server-computed: true when this angle's effective narrative is
1700
- * gap_replication, which FORCES the multi-shot STANDARD ("Multiescena")
1701
- * render pipeline regardless of the task's chosen tier (single_call can't
1702
- * produce the multi-shot replication — see task-results.service.ts and the
1703
- * render-time tier override). Surfaces "renders in Multiescena" on the
1704
- * review card so a single_call user isn't surprised by the STANDARD-tier
1705
- * charge. */
1706
- rendersMultiShot: z14.boolean().optional(),
1707
- /** Server-computed product-level origin bucket for the review card's badge
1708
- * (NOT the internal sourceAngleType taxonomy — see resolveAngleOrigin in
1709
- * task-results.service.ts): 'gap' = market opportunity competitors aren't
1710
- * using; 'saturated_twist' = competitors' most-used approach with our own
1711
- * twist; 'ai_recommended' = AI-authored for this product (default). */
1712
- angleOrigin: z14.enum([
1713
- "gap",
1714
- "saturated_twist",
1715
- "ai_recommended"
1716
- ]).optional(),
1717
- /** Server-computed badge TIER for the replication angle (two-tier UX,
1718
- * v1.32.0 — see resolveReplicationTier). 'winner' = a validated angle
1719
- * opportunity backed by a proven ad (anchor gap/saturated → golden badge);
1720
- * 'opportunity' = a product-coherent replicable winner with no validated
1721
- * angle gap (anchor 'dominant' → opportunity/twist badge, no overclaim).
1722
- * Present only on the angle that carries gap_replication; absent otherwise. */
1723
- replicationTier: z14.enum([
1724
- "winner",
1725
- "opportunity"
1726
- ]).optional()
1727
- });
1728
- var AdVideo = z14.object({
1729
- source: z14.enum([
1730
- "tiktok_shop",
1731
- "meta_ads"
1732
- ]),
1733
- url: z14.string().url(),
1734
- title: z14.string().optional(),
1735
- description: z14.string().optional(),
1736
- metrics: z14.object({
1737
- impressions: z14.number().int().nonnegative().optional(),
1738
- views: z14.number().int().nonnegative().optional(),
1739
- likes: z14.number().int().nonnegative().optional(),
1740
- ctaClicks: z14.number().int().nonnegative().optional(),
1741
- diggCount: z14.number().int().nonnegative().optional(),
1742
- shareCount: z14.number().int().nonnegative().optional(),
1743
- commentCount: z14.number().int().nonnegative().optional(),
1744
- playCount: z14.number().int().nonnegative().optional(),
1745
- collectCount: z14.number().int().nonnegative().optional(),
1746
- durationSeconds: z14.number().int().nonnegative().optional()
1747
- }).partial().optional(),
1748
- country: z14.string().optional(),
1749
- lastSeen: z14.string().datetime().optional()
1750
- });
1751
-
1752
1798
  // ../../packages/types/src/frame0.ts
1753
1799
  import { z as z15 } from "zod";
1754
1800
  var Frame0SpecVersion = z15.literal("frame0_spec_v1");
@@ -1927,7 +1973,15 @@ var VideoGenerator = z16.object({
1927
1973
  angleKey: z16.string().min(1),
1928
1974
  angleIndex: z16.number().int().nonnegative(),
1929
1975
  angle: z16.string().min(1).max(480).optional(),
1930
- buyerPersona: z16.string().min(1).max(360).optional(),
1976
+ // READ contract: the cap must fit what the generators actually persist, not
1977
+ // what the write surfaces accept. Angle-derived personas are LLM-written and
1978
+ // routinely exceed the 360-char write cap (prod alert 88badc8: 419 chars), and
1979
+ // this schema serializes existing DB rows — a too-tight cap turns a valid row
1980
+ // into a ZodError that 500s the whole `GET /v1/results/:taskId` payload and
1981
+ // hides the entire task. Same reasoning as the landing-images cap in
1982
+ // `result.ts`. Write-side caps (angle.ts, wizard.ts, task-creative-variants.ts)
1983
+ // intentionally stay at 360.
1984
+ buyerPersona: z16.string().min(1).max(1200).optional(),
1931
1985
  script: z16.string().min(1),
1932
1986
  prompt: z16.string().min(1),
1933
1987
  // Empty string allowed: Pro pipeline pauses for script review BEFORE the grid
@@ -2356,6 +2410,8 @@ var ApproveImageAnglesRequest = z19.object({
2356
2410
  painPoints: z19.string().max(1500).optional(),
2357
2411
  /** Features to include in the image */
2358
2412
  features: z19.array(z19.string().min(1).max(240)).max(20).optional(),
2413
+ /** Non-blocking static winner preference. Omitted defaults to original. */
2414
+ winnerVisualMode: WinnerVisualMode.optional(),
2359
2415
  /** Which product image URLs to use for this generation (ephemeral selection) */
2360
2416
  selectedImageUrls: z19.array(ProductImageSelection).max(5).optional(),
2361
2417
  productTargetConfirmation: ProductTargetConfirmationSchema.optional(),
@@ -2380,12 +2436,13 @@ var ImageSuggestionsResponse = z19.object({
2380
2436
  buyerPersona: z19.string().optional()
2381
2437
  });
2382
2438
  var RefineAngleRequest = z19.object({
2383
- /** Refine mode: 'all' refines angle + pains + benefits, 'pain_points'/'benefits' regenerate just that section */
2439
+ /** Refine mode: 'reference' adapts a competitor image winner's selling structure to the user's product. */
2384
2440
  mode: z19.enum([
2385
2441
  "all",
2386
2442
  "pain_points",
2387
2443
  "benefits",
2388
- "video"
2444
+ "video",
2445
+ "reference"
2389
2446
  ]),
2390
2447
  /** Current angle text (used as context for aligned generation) */
2391
2448
  currentAngle: z19.string().max(480).optional(),
@@ -2478,7 +2535,6 @@ var TASK_CREATIVE_VARIANT_TEXT_LIMITS = {
2478
2535
  recommendationRationale: 600
2479
2536
  };
2480
2537
  var TaskCreativeVariantVideoPreset = VideoPreset.exclude([
2481
- "problem_solution",
2482
2538
  "ugc_unboxing_chaos",
2483
2539
  "ugc_curiosity_reveal",
2484
2540
  "ugc_listicle_3",
@@ -2835,7 +2891,10 @@ var StatusResponse = z22.object({
2835
2891
  /** Derived from landingEnabled/videoGeneratorEnabled/adCreativeEnabled flags;
2836
2892
  * canonical [landing, video, image] order regardless of input order.
2837
2893
  * Always present (server derivation has a "legacy = enabled" fallback). */
2838
- selectedOutputs: SelectedOutputsSchema
2894
+ selectedOutputs: SelectedOutputsSchema,
2895
+ /** Persisted image portfolio size so a refreshed review reconstructs the
2896
+ * same visual plan even when browser session state is unavailable. */
2897
+ imageVariantCount: z22.number().int().min(1).max(6).optional()
2839
2898
  });
2840
2899
  var TaskSummary = z22.object({
2841
2900
  taskId: z22.string().uuid(),
@@ -4120,6 +4179,8 @@ var TikTokCreateCampaignResponseSchema = z32.object({
4120
4179
  // ../../packages/types/src/pricing.ts
4121
4180
  var CREDITS_PER_USD_CENT = 10;
4122
4181
  var CREDITS_PER_DOLLAR = 100 * CREDITS_PER_USD_CENT;
4182
+ var KALODATA_USD_CENTS_PER_PROVIDER_CREDIT = 9;
4183
+ var KALODATA_PROVIDER_CREDITS_PER_SOURCE_CEILING = 7.5;
4123
4184
  var DEFAULT_PRICING = {
4124
4185
  // IMPORTANT: despite the field name, these values are in *USD cents* (not USD dollars).
4125
4186
  // We store cents (and sometimes fractional cents) everywhere so we can convert to credits:
@@ -4165,16 +4226,16 @@ var DEFAULT_PRICING = {
4165
4226
  userPriceCents: 0.8
4166
4227
  },
4167
4228
  "openrouter:chat-parse": {
4168
- ourCostCents: 0.05,
4169
- userPriceCents: 0.1
4229
+ ourCostCents: 0.2,
4230
+ userPriceCents: 0.4
4170
4231
  },
4171
4232
  "openrouter:audio-tag-injection": {
4172
- ourCostCents: 0.4,
4173
- userPriceCents: 0.8
4233
+ ourCostCents: 0.1,
4234
+ userPriceCents: 0.2
4174
4235
  },
4175
4236
  "openrouter:product-consolidation": {
4176
- ourCostCents: 0.4,
4177
- userPriceCents: 0.8
4237
+ ourCostCents: 0.07,
4238
+ userPriceCents: 0.14
4178
4239
  },
4179
4240
  "openrouter:video-final-simplify-rewrite": {
4180
4241
  ourCostCents: 0.4,
@@ -4193,12 +4254,12 @@ var DEFAULT_PRICING = {
4193
4254
  userPriceCents: 0.3
4194
4255
  },
4195
4256
  "openrouter:vision-extract-combo": {
4196
- ourCostCents: 0.3,
4197
- userPriceCents: 0.6
4257
+ ourCostCents: 0.6,
4258
+ userPriceCents: 1.2
4198
4259
  },
4199
4260
  "openrouter:rich-scene-analyzer": {
4200
- ourCostCents: 0.15,
4201
- userPriceCents: 0.3
4261
+ ourCostCents: 13.42,
4262
+ userPriceCents: 26.84
4202
4263
  },
4203
4264
  "openrouter:product-info-extractor": {
4204
4265
  ourCostCents: 0.15,
@@ -4538,6 +4599,10 @@ var DEFAULT_PRICING = {
4538
4599
  ourCostCents: 2,
4539
4600
  userPriceCents: 4
4540
4601
  },
4602
+ "kie:image-generation:nano-banana-2-lite": {
4603
+ ourCostCents: 2,
4604
+ userPriceCents: 4
4605
+ },
4541
4606
  "kie:image-generation:nano-banana-2": {
4542
4607
  ourCostCents: 4,
4543
4608
  userPriceCents: 8
@@ -4574,6 +4639,10 @@ var DEFAULT_PRICING = {
4574
4639
  ourCostCents: 3,
4575
4640
  userPriceCents: 6
4576
4641
  },
4642
+ "fal:image-generation:gpt-image-2:1k": {
4643
+ ourCostCents: 1.5,
4644
+ userPriceCents: 6
4645
+ },
4577
4646
  // Video reference pricing varies by model:
4578
4647
  // - gpt-image-2 @ 2K (primary since 2026-04-24): $0.05 / $0.10
4579
4648
  // - nano-banana-2 (fallback for 5xx/timeout): $0.04 / $0.08
@@ -4770,15 +4839,16 @@ var DEFAULT_PRICING = {
4770
4839
  ourCostCents: 2.5,
4771
4840
  userPriceCents: 5
4772
4841
  },
4773
- // Usage spec extraction (Gemini 3 Flash Preview:online)
4842
+ // Usage spec extraction (gemini-3-flash-preview:online). Measured 1.5807¢ — the :online
4843
+ // web-search surcharge is what makes it ~4x the plain-Flash rate it used to be priced at.
4774
4844
  "openrouter:product-usage-extract": {
4775
- ourCostCents: 0.4,
4776
- userPriceCents: 0.8
4845
+ ourCostCents: 1.58,
4846
+ userPriceCents: 3.16
4777
4847
  },
4778
- // Functional demonstration extraction (Gemini 3 Flash Preview:online with web search)
4848
+ // Functional demonstration extraction (gemini-3-flash-preview:online). Measured 1.0643¢.
4779
4849
  "openrouter:functional-demo-extract": {
4780
- ourCostCents: 0.4,
4781
- userPriceCents: 0.8
4850
+ ourCostCents: 1.06,
4851
+ userPriceCents: 2.12
4782
4852
  },
4783
4853
  // Fusion of angles and usage into visual context (Gemini 3 Flash Preview)
4784
4854
  "openrouter:visual-context-fusion": {
@@ -4795,6 +4865,38 @@ var DEFAULT_PRICING = {
4795
4865
  ourCostCents: 2,
4796
4866
  userPriceCents: 4
4797
4867
  },
4868
+ // Free-form (v2) code landings — estimator inputs + usage.cost fallbacks. Edit-chat
4869
+ // rows are the FLAT per-message price (100/300 credits).
4870
+ "openrouter:landing-art-direction": {
4871
+ ourCostCents: 5,
4872
+ userPriceCents: 10
4873
+ },
4874
+ // v2.1: ONE coherent call authors every section (replaced the per-section
4875
+ // 'landing-section-code' fan-out — kept below for legacy BillingEntry rows).
4876
+ "openrouter:landing-section-authoring": {
4877
+ ourCostCents: 20,
4878
+ userPriceCents: 40
4879
+ },
4880
+ "openrouter:landing-section-code": {
4881
+ ourCostCents: 5,
4882
+ userPriceCents: 10
4883
+ },
4884
+ "openrouter:landing-visual-judge": {
4885
+ ourCostCents: 3,
4886
+ userPriceCents: 6
4887
+ },
4888
+ "system:landing-qa": {
4889
+ ourCostCents: 1,
4890
+ userPriceCents: 0
4891
+ },
4892
+ "openrouter:landing-edit-chat-section": {
4893
+ ourCostCents: 5,
4894
+ userPriceCents: 10
4895
+ },
4896
+ "openrouter:landing-edit-chat-full": {
4897
+ ourCostCents: 15,
4898
+ userPriceCents: 30
4899
+ },
4798
4900
  // Product mix helper calls (Gemini 3 Flash Preview)
4799
4901
  "openrouter:product-mix-script-generation": {
4800
4902
  ourCostCents: 0.4,
@@ -4825,15 +4927,18 @@ var DEFAULT_PRICING = {
4825
4927
  ourCostCents: 0.2,
4826
4928
  userPriceCents: 0.4
4827
4929
  },
4828
- // Landing image QA (Gemini 3 Flash Preview, 1 image). Checks anatomy/text/background cutoff.
4930
+ // Landing image QA (gemini-3.6-flash since 2026-08-01, 1-2 images). Anatomy/text/background cutoff.
4931
+ // 1.88¢ = the post-model-swap steady state at prod averages (11,109 prompt / 928 completion
4932
+ // tokens) WITH the prompt-cache breakpoint; without caching it would be ~2.36¢.
4829
4933
  "openrouter:landing-image-qa": {
4830
- ourCostCents: 0.15,
4831
- userPriceCents: 0.3
4934
+ ourCostCents: 1.88,
4935
+ userPriceCents: 3.76
4832
4936
  },
4833
- // Creative image QA (Gemini 3 Flash Preview, 1 image). Checks product fidelity and severe render defects.
4937
+ // Creative image QA (gemini-3-flash-preview — deliberately NOT on 3.6; see ai_models.md).
4938
+ // Measured 0.8196¢ over 30d of prod.
4834
4939
  "openrouter:creative-image-qa": {
4835
- ourCostCents: 0.15,
4836
- userPriceCents: 0.3
4940
+ ourCostCents: 0.82,
4941
+ userPriceCents: 1.64
4837
4942
  },
4838
4943
  // Anatomy check (Gemini 3 Flash Preview, 1 image). Standalone anatomy verification.
4839
4944
  "openrouter:anatomy-check": {
@@ -4872,10 +4977,10 @@ var DEFAULT_PRICING = {
4872
4977
  ourCostCents: 0.15,
4873
4978
  userPriceCents: 0.3
4874
4979
  },
4875
- // Policy rewrite helper (Gemini 3 Flash Preview)
4980
+ // Policy rewrite helper (gemini-3-flash-preview). Measured 0.0416¢ — a short text-only call.
4876
4981
  "openrouter:policy-prompt-rewrite": {
4877
- ourCostCents: 0.4,
4878
- userPriceCents: 0.8
4982
+ ourCostCents: 0.04,
4983
+ userPriceCents: 0.08
4879
4984
  },
4880
4985
  // TTS dialogue rewrite helper (OpenRouter model: google/gemini-3-flash-preview)
4881
4986
  // Our cost: ~$0.0005 USD per request => 0.05 USD cents; keep 2x markup.
@@ -4946,14 +5051,25 @@ var DEFAULT_PRICING = {
4946
5051
  userPriceCents: 0.8
4947
5052
  },
4948
5053
  // single_call: flattens the approved plan into the one Grok render prompt
4949
- // (gemini-3.6-flash, medium reasoning, 8k maxTokens). Sized from 30d of prod entries on
4950
- // the SAME model — rapid-grok-shot-action averaged 2.11¢ our / 4.23¢ user. Only
4951
- // attempt 1 is charged; the flatten's internal retries record COGS only, so this
4952
- // quotes once per render. Real calls bill the actual usage.cost x2; this value is
4953
- // the estimator basis + the fallback when usage.cost is missing.
5054
+ // (gemini-3.6-flash, medium reasoning, 8k maxTokens).
5055
+ //
5056
+ // RE-DERIVED 2026-08-01 from 860 prod rows (July 2026, still-paper-34632546):
5057
+ // mean 4.573¢ our, p50 4.674¢, p95 6.205¢. The previous 2¢ was inferred by
5058
+ // ANALOGY from `rapid-grok-shot-action` (2.11¢) rather than measured on this
5059
+ // label, and was 2.3x too low — real calls bill the actual usage.cost x2, so
5060
+ // the money was always collected correctly, but every QUOTE understated this
5061
+ // line by that factor. 5¢ sits above the mean and the median with headroom,
5062
+ // below the p95.
5063
+ //
5064
+ // ⚠️ This is a PER-CALL rate and the flatten is NOT called once per render:
5065
+ // measured 4.34 calls per (task, angle) on average, because a structurally
5066
+ // valid prompt that exceeds Grok's 4090-BYTE ceiling is regenerated shorter
5067
+ // (up to MAX_FLATTEN_ATTEMPTS). The estimator multiplies this rate by that cap.
5068
+ // Reducing the CALL COUNT is the real cost lever here, not this rate — see the
5069
+ // note on the flatten line in cost-estimator.service.ts.
4954
5070
  "openrouter:single-call-flatten": {
4955
- ourCostCents: 2,
4956
- userPriceCents: 4
5071
+ ourCostCents: 5,
5072
+ userPriceCents: 10
4957
5073
  },
4958
5074
  "openrouter:hand-detector": {
4959
5075
  ourCostCents: 0.15,
@@ -4972,8 +5088,8 @@ var DEFAULT_PRICING = {
4972
5088
  userPriceCents: 0.8
4973
5089
  },
4974
5090
  "openrouter:wearable-product-semantics": {
4975
- ourCostCents: 0.4,
4976
- userPriceCents: 0.8
5091
+ ourCostCents: 0.11,
5092
+ userPriceCents: 0.22
4977
5093
  },
4978
5094
  // Clip Judge — text-only comparison of QA results between original and retry clips
4979
5095
  // (google/gemini-3-flash-preview, text-only). Our cost: ~$0.0006 => 0.06 cents; 2x markup.
@@ -4990,30 +5106,60 @@ var DEFAULT_PRICING = {
4990
5106
  // Image Judge — text-only comparison of QA results between image candidates
4991
5107
  // (google/gemini-3-flash-preview, text-only). Our cost: ~$0.0006 => 0.06 cents; 2x markup.
4992
5108
  "openrouter:image-judge-text": {
4993
- ourCostCents: 0.06,
4994
- userPriceCents: 0.12
5109
+ ourCostCents: 0.39,
5110
+ userPriceCents: 0.78
4995
5111
  },
4996
5112
  // Image Judge — vision comparison of two images + product reference (google/gemini-3-flash-preview)
4997
- // Our cost: ~$0.0015 => 0.15 cents; 2x markup (images are cheaper than video).
5113
+ // Measured 0.8863¢ over 30d of prod (the old ~0.15¢ predated the multi-image comparison).
4998
5114
  "openrouter:image-judge-vision": {
4999
- ourCostCents: 0.15,
5000
- userPriceCents: 0.3
5115
+ ourCostCents: 0.89,
5116
+ userPriceCents: 1.78
5001
5117
  },
5002
5118
  // Video QA — product integrity scanner (Gemini 3 Flash Preview, 5 frames + reference).
5003
5119
  "openrouter:video-product-integrity": {
5004
5120
  ourCostCents: 0.15,
5005
5121
  userPriceCents: 0.3
5006
5122
  },
5007
- // Video QA — independent anatomy arm-scanner (Gemini 3 Flash Preview, one call per
5008
- // sampled frame, up to ARM_SCAN_MAX_FRAMES per clip). Billed PER FRAME (units: 1 per
5009
- // call), so this rate is per frame, not per clip. Sized above the 30d prod average
5010
- // (10.303 entries at $6.81 our => 0.066¢/frame) so the fallback path — used when
5011
- // OpenRouter omits usage.cost — cannot under-charge. Before this key existed those
5012
- // fallbacks hit the `No pricing rule found; skipping` branch and went UNBILLED.
5123
+ // Video QA — anatomy. THREE keys, and they do NOT share a unit. Getting this wrong
5124
+ // over-charges by ~42x, which is exactly what a first pass at the 2026-07-30 change did.
5125
+ //
5126
+ // (a) The arm-scan's per-FRAME counting sweep. RETIRED 2026-07-30 — it has NO producer left.
5127
+ // Its last call site was the whole-video concat gate, removed as redundant with the
5128
+ // per-scene frame judge (b). It was ALSO duration-blind on `double_30s` — the sweep
5129
+ // samples t=0,1,2… capped at 16, so a 30s assembled video was only partly read while all
5130
+ // 16 frames still billed — but NOT on `single_15s` (~15s, covered completely), which is
5131
+ // the dominant format. Redundancy is what carries the removal; the blindness is secondary.
5132
+ // The estimator no longer quotes it either.
5133
+ // The KEY STAYS DEFINED ON PURPOSE. `BillingService.record` treats an unknown key as
5134
+ // "No pricing rule found; skipping" and bills NOTHING — that silent hole already cost us
5135
+ // 10,303 unbilled calls before 2026-07-18. `scanClipArms` is still exported, so if
5136
+ // anything ever calls it again it must land on a rate, not on the skip branch.
5137
+ // Rate stays PER FRAME, sized above the
5138
+ // 30d prod average (10303 rows => 0.132c user/frame).
5013
5139
  "openrouter:video-anatomy-arm-scan": {
5014
5140
  ourCostCents: 0.1,
5015
5141
  userPriceCents: 0.2
5016
5142
  },
5143
+ // (b) The frame JUDGE that replaced the sweep on the `analyzeVideoClipDJ` path: ALL frames
5144
+ // in ONE multi-image call, so a unit is one call. MEASURED, not estimated —
5145
+ // usage.cost on a real 16-frame judgement at 768px is $0.0417 => 4.17c our. The table is
5146
+ // the FALLBACK used when OpenRouter omits usage.cost, so it must sit at or above that.
5147
+ // Billed PER FRAME (units = frames in the call), like the sweep and for the same reason:
5148
+ // the cost scales with IMAGES and QA_FRAME_JUDGE_FRAMES is env-overridable, so a per-CALL
5149
+ // unit would freeze the fallback charge and the credit-gate quote at the cap the rate
5150
+ // happened to be measured at. Measured $0.0417 over 16 frames => 0.26c/frame; priced at
5151
+ // 0.32c for headroom (n=1 on one clip, and the 1.9k reasoning-token share moves with
5152
+ // QA_DETECTOR_EFFORT). Re-derive from 30d of BillingEntry once this key has volume.
5153
+ "openrouter:video-qa-anatomy-frame-judge": {
5154
+ ourCostCents: 0.32,
5155
+ userPriceCents: 0.64
5156
+ },
5157
+ // (c) Tracing confirmation of a frames-only finding: 3 stills + a 3s 480p slice, one call,
5158
+ // no 16-image prompt — materially cheaper than the judgement it verifies.
5159
+ "openrouter:video-qa-anatomy-tracing-confirm": {
5160
+ ourCostCents: 1.5,
5161
+ userPriceCents: 3
5162
+ },
5017
5163
  // Physics confirmation — skeptical second opinion on D+J floating-object flags
5018
5164
  // (Gemini 3 Flash Preview, ~3 frames per flagged moment, 1 call). Aligned with other vision-QA keys.
5019
5165
  "openrouter:video-physics-confirmation": {
@@ -5055,10 +5201,11 @@ var DEFAULT_PRICING = {
5055
5201
  ourCostCents: 1,
5056
5202
  userPriceCents: 2
5057
5203
  },
5058
- // LLM image ranking (Gemini 3 Flash, ~10 images multi-modal)
5204
+ // LLM image ranking (gemini-3-flash-preview, ~10 images multi-modal). Unit = one CANDIDATE
5205
+ // image, not one call, so the per-unit rate is small: measured 0.0924¢/unit.
5059
5206
  "openrouter:image-ranking": {
5060
- ourCostCents: 0.3,
5061
- userPriceCents: 0.6
5207
+ ourCostCents: 0.09,
5208
+ userPriceCents: 0.18
5062
5209
  },
5063
5210
  // Visual context pass — frame0 description (Gemini 3 Flash Preview, 1 image)
5064
5211
  "openrouter:visual-context-frame0-describe": {
@@ -5147,10 +5294,50 @@ var DEFAULT_PRICING = {
5147
5294
  ourCostCents: 1,
5148
5295
  userPriceCents: 2
5149
5296
  },
5150
- // Product research acquires KaloPilot credits at $0.10 per provider credit;
5151
- // the source worker overrides ourCostCents with the exact reported usage.
5297
+ // ─── Product research (winning products) ─────────────────────────────────
5298
+ // BUNDLE, billed as two rows against the same `productResearchRunId`:
5299
+ // revenue → `system:product-research-report` = 200¢ / 2,000 credits, charged
5300
+ // UP FRONT by purchaseProductResearchRun (which also debits
5301
+ // User.creditCents in the same transaction), and reversed by a
5302
+ // `system:product-research-failure-refund` row when the run fails.
5303
+ // cost → `kalodata:product-research-source` (+ `openrouter:*` analysis),
5304
+ // recorded AFTER the provider answers, with creditOperation
5305
+ // 'ledger_only' — the user was already debited.
5306
+ //
5307
+ // ⚠️ `userPriceCents: 0` on the kalodata row is DELIBERATE, not a missing
5308
+ // charge. Revenue cannot live here: the cost row is written after the
5309
+ // provider bills us, while the price is committed at purchase time, and a
5310
+ // run that fails post-source is refunded in full while its kalodata cost
5311
+ // stands. Any per-provider markup report that divides price÷cost will
5312
+ // therefore show kalodata (and the openrouter product-research labels) at
5313
+ // 0.00×. The cost row carries `bundledRevenue*` metadata so the pairing is
5314
+ // discoverable from the row itself.
5315
+ //
5316
+ // ⚠️ Two traps in aggregating this bundle, both measured on the 27 runs to
5317
+ // 2026-08-01 — read before writing a P&L query:
5318
+ // 1. Refunds are stored as a POSITIVE `userPriceCents` on
5319
+ // `system:product-research-failure-refund` (metadata.target 'refund',
5320
+ // not 'product_research'). Grouping on target='product_research' counts
5321
+ // the purchase and drops its reversal; summing userPriceCents across
5322
+ // the run double-counts instead of netting. Retained revenue is
5323
+ // Σ(label='product-research-report') − Σ(label='product-research-failure-refund').
5324
+ // For those 27 runs that is $54 charged − $30 refunded = $24 kept.
5325
+ // 2. A cost row is per PROVIDER CALL, not per run: `getFreshProductResearchSource`
5326
+ // serves repeat queries from the source cache, so a charged run can have
5327
+ // no kalodata row at all (12 of the 27 did). `bundledRevenueCents` is
5328
+ // therefore context for THIS row — never a revenue total to sum.
5329
+ // Total COGS is Σ ourCostCents over every row with a non-null
5330
+ // productResearchRunId, which needs no per-provider grouping at all.
5331
+ //
5332
+ // The source worker overrides ourCostCents with the exact reported usage
5333
+ // (providerCredits × KALODATA_USD_CENTS_PER_PROVIDER_CREDIT). Nothing reads
5334
+ // this entry today — no cost-estimator key and no credit gate resolves it,
5335
+ // since the gate is a flat `creditCents >= 200` in
5336
+ // purchaseProductResearchRun. It is kept as the audited reference rate so
5337
+ // the estimate stops contradicting the ledger the moment something does
5338
+ // quote from it.
5152
5339
  "kalodata:product-research-source": {
5153
- ourCostCents: 24.9,
5340
+ ourCostCents: KALODATA_PROVIDER_CREDITS_PER_SOURCE_CEILING * KALODATA_USD_CENTS_PER_PROVIDER_CREDIT,
5154
5341
  userPriceCents: 0
5155
5342
  },
5156
5343
  "system:product-research-report": {
@@ -5258,9 +5445,9 @@ var DEFAULT_PRICING = {
5258
5445
  // here (0.0653¢ our/frame) is LOWER, so the prod-derived rate stands per the
5259
5446
  // quote-at-the-ceiling rule. Do not re-add a second key: TS1117 duplicate.
5260
5447
  // Unit = one winner-selection pass over the scraped ad pool (measured 0.3744¢).
5261
- // ⚠️ NOT once per scan any more: the deep-capture candidate fallback re-runs this
5262
- // selector for each retry, so the estimate must quote it at
5263
- // GAP_DEEP_CAPTURE_MAX_CANDIDATES (default 3), never at 1.
5448
+ // ⚠️ NOT once per scan any more: the deep-capture candidate fallback re-runs the
5449
+ // video selector for each retry, and static replication independently selects
5450
+ // one image winner. The estimate quotes GAP_DEEP_CAPTURE_MAX_CANDIDATES + 1.
5264
5451
  "openrouter:gap-winner-selection": {
5265
5452
  ourCostCents: 0.4,
5266
5453
  userPriceCents: 0.8
@@ -5272,6 +5459,13 @@ var DEFAULT_PRICING = {
5272
5459
  ourCostCents: 2.5,
5273
5460
  userPriceCents: 5
5274
5461
  },
5462
+ // Unit = one independent multimodal argument extraction for the selected
5463
+ // static-image winner. Quote at the blueprint ceiling until BillingEntry has
5464
+ // enough production volume to establish a narrower measured rate.
5465
+ "openrouter:competitor-image-winner-argument": {
5466
+ ourCostCents: 2.5,
5467
+ userPriceCents: 5
5468
+ },
5275
5469
  // Unit = one multimodal temporal observation of the captured winner MP4.
5276
5470
  // Includes the bounded full video plus measured frame samples and high
5277
5471
  // reasoning. Unmeasured at launch, so quote at 2x the keyframe-only blueprint
@@ -5290,6 +5484,19 @@ var DEFAULT_PRICING = {
5290
5484
  ourCostCents: 0.2,
5291
5485
  userPriceCents: 0.4
5292
5486
  },
5487
+ // Unit = one product-use SHEET derivation: Flash reads the product photos + the seller's raw
5488
+ // text and writes the three lines the video QA judge needs to rule on product MISUSE. Same
5489
+ // model, same effort and the same image budget as the exposure classifier directly above, so
5490
+ // it is priced at that rate rather than re-measured on n=1. Sized ABOVE the measured campaign
5491
+ // cost of the equivalent call (~2.4¢ USER for the sheet+judge PAIR, of which the sheet is the
5492
+ // smaller half) because this table is only the fallback for when OpenRouter omits usage.cost.
5493
+ // ONCE PER TASK, not per clip and not per render attempt: the sheet is derived at ingest and
5494
+ // every QA call on that task reads the stored copy. Quoting it per scene would over-quote a
5495
+ // 3-scene single by 3x and inflate the credit gate for spend that never happens.
5496
+ "openrouter:product-use-sheet": {
5497
+ ourCostCents: 0.2,
5498
+ userPriceCents: 0.4
5499
+ },
5293
5500
  // Unit = one reference-grouping pass over the user's product images (0.1241¢).
5294
5501
  "openrouter:product-reference-grouping": {
5295
5502
  ourCostCents: 0.15,
@@ -5337,6 +5544,68 @@ var DEFAULT_PRICING = {
5337
5544
  ourCostCents: 1,
5338
5545
  userPriceCents: 2
5339
5546
  },
5547
+ // ── Rapid / single_call pipeline: keys that did NOT exist until 2026-08-01 ──
5548
+ //
5549
+ // These labels bill real money every day but had NO entry here, so whenever
5550
+ // OpenRouter omitted `usage.cost` the fallback resolved to `undefined` and the
5551
+ // call was recorded at ZERO. `rapid-plan` alone is the single most expensive
5552
+ // OpenRouter label we have ($116.60 / 30d). The rates below are MEASURED, not
5553
+ // guessed: SUM(ourCostCents)/SUM(units) over 30 days of prod BillingEntry
5554
+ // (neon still-paper-34632546, queried 2026-08-01), at the standard 2x markup.
5555
+ //
5556
+ // None of these keys is read by cost-estimator.service.ts — the rapid pipeline
5557
+ // quotes them through the calibrated STANDARD_PIPELINE_OVERHEAD_CENTS blob — so
5558
+ // adding them changes the billing FALLBACK only and cannot move any quote.
5559
+ // Unit = one rapid plan authoring call (measured 4.3968¢).
5560
+ "openrouter:rapid-plan": {
5561
+ ourCostCents: 4.4,
5562
+ userPriceCents: 8.8
5563
+ },
5564
+ // Unit = one rapid script blueprint call (measured 4.2748¢).
5565
+ "openrouter:rapid-script-blueprint": {
5566
+ ourCostCents: 4.28,
5567
+ userPriceCents: 8.56
5568
+ },
5569
+ // Unit = one whole-video standard QA pass (measured 2.4104¢).
5570
+ "openrouter:video-qa-standard": {
5571
+ ourCostCents: 2.41,
5572
+ userPriceCents: 4.82
5573
+ },
5574
+ // Unit = one frame0 anchor compose (measured 0.7351¢).
5575
+ "openrouter:frame0-anchor-compose": {
5576
+ ourCostCents: 0.74,
5577
+ userPriceCents: 1.48
5578
+ },
5579
+ // Unit = one voiceover↔visual alignment pass (measured 0.9092¢).
5580
+ "openrouter:rapid-voiceover-visual-alignment": {
5581
+ ourCostCents: 0.91,
5582
+ userPriceCents: 1.82
5583
+ },
5584
+ // Unit = one rapid plan character/setting pass (measured 0.5547¢).
5585
+ "openrouter:rapid-plan-character": {
5586
+ ourCostCents: 0.55,
5587
+ userPriceCents: 1.1
5588
+ },
5589
+ // Unit = one segment naturalization pass (measured 0.5470¢).
5590
+ "openrouter:rapid-naturalize": {
5591
+ ourCostCents: 0.55,
5592
+ userPriceCents: 1.1
5593
+ },
5594
+ // Unit = one rapid script validation pass (measured 0.3704¢).
5595
+ "openrouter:rapid-script-validation": {
5596
+ ourCostCents: 0.37,
5597
+ userPriceCents: 0.74
5598
+ },
5599
+ // Unit = one voiceover shortening pass (measured 0.3081¢).
5600
+ "openrouter:rapid-voiceover-shorten": {
5601
+ ourCostCents: 0.31,
5602
+ userPriceCents: 0.62
5603
+ },
5604
+ // Unit = one POV judge call on the rapid plan (measured 0.0769¢).
5605
+ "openrouter:rapid-plan-pov-judge": {
5606
+ ourCostCents: 0.08,
5607
+ userPriceCents: 0.16
5608
+ },
5340
5609
  // Unit = one ffmpeg mix laying the replicated winner's own audio track under our
5341
5610
  // voiceover, producing the extra `ad_final_with_winner_music` deliverable
5342
5611
  // (gap_replication only, behind GAP_WINNER_MUSIC_ENABLED, default OFF). No AI
@@ -6517,9 +6786,107 @@ var LandingConfigSchema = z35.object({
6517
6786
  */
6518
6787
  showVariantPicker: z35.boolean().optional()
6519
6788
  });
6789
+ var FreeformSlotTypeSchema = z35.enum([
6790
+ "text",
6791
+ "image",
6792
+ "price",
6793
+ "cta",
6794
+ "list"
6795
+ ]);
6796
+ var FreeformSlotSchema = z35.object({
6797
+ type: FreeformSlotTypeSchema,
6798
+ /** Human label shown in the editor ("Título", "Imagen principal"). */
6799
+ label: z35.string().min(1).max(60),
6800
+ /** text/price/cta: display text. image: absolute URL. list: unused (see items). */
6801
+ value: z35.string().max(2e3),
6802
+ /** Only for type 'list'. */
6803
+ items: z35.array(z35.string().max(300)).max(12).optional()
6804
+ });
6805
+ var FreeformSectionSchema = z35.object({
6806
+ id: z35.string().regex(/^[a-z0-9][a-z0-9_-]{0,39}$/),
6807
+ label: z35.string().min(1).max(60),
6808
+ /** Sanitized free markup with {{slot:key}} markers and data-cl-* attributes. */
6809
+ layoutHtml: z35.string().min(1).max(16e3),
6810
+ slots: z35.record(z35.string().regex(/^[a-zA-Z0-9_]{1,40}$/), FreeformSlotSchema),
6811
+ /** Section CSS, selectors prefixed with [data-cl-s="<id>"]. */
6812
+ css: z35.string().max(8e3).optional()
6813
+ });
6814
+ var DesignSystemSchema = z35.object({
6815
+ /** Same 4 roles as v1 preset — they feed the --cl-* CSS vars the buy form uses. */
6816
+ colors: z35.object({
6817
+ primary: HexColorSchema,
6818
+ bg: HexColorSchema,
6819
+ text: HexColorSchema,
6820
+ accent: HexColorSchema
6821
+ }),
6822
+ fonts: z35.object({
6823
+ heading: FontNameSchema,
6824
+ body: FontNameSchema
6825
+ }),
6826
+ radius: z35.string().regex(/^\d{1,2}(px|rem|em)$/),
6827
+ /** Prompt-only vocabulary ("brutalist", "editorial"); not rendered. */
6828
+ styleKeywords: z35.array(z35.string().max(40)).max(8).default([])
6829
+ });
6830
+ var FreeformLandingConfigSchema = z35.object({
6831
+ version: z35.literal(2),
6832
+ themeVersion: z35.literal(2),
6833
+ locale: z35.string().min(2),
6834
+ designSystem: DesignSystemSchema,
6835
+ globalCss: z35.string().max(2e4).default(""),
6836
+ sections: z35.array(FreeformSectionSchema).min(1).max(14),
6837
+ /** Buy form is inserted AFTER this section (assembler adds BUY_FORM_MARKER). */
6838
+ buyFormAfterSectionId: z35.string(),
6839
+ /** Assembled + sanitized final HTML — source of truth for render/publish. */
6840
+ html: z35.string().min(1),
6841
+ product: LandingProductSchema,
6842
+ showVariantPicker: z35.boolean().optional(),
6843
+ /**
6844
+ * Suppress the native Shopify buy form (the `BUY_FORM_MARKER` block). The page
6845
+ * CTAs already carry `data-cl-buy-trigger` → they open the ReleaseIt COD popup,
6846
+ * so on ReleaseIt-only stores the native add-to-cart form is a redundant second
6847
+ * buy path (it rendered as a bare "price + Comprar" block). When true the
6848
+ * assembler omits the marker entirely. Undefined/false = keep the native form
6849
+ * (backward compatible with already-assembled configs).
6850
+ */
6851
+ hideNativeBuyForm: z35.boolean().optional()
6852
+ });
6853
+ var AnyLandingConfigSchema = z35.union([
6854
+ FreeformLandingConfigSchema,
6855
+ LandingConfigSchema
6856
+ ]);
6520
6857
  var UpdateLandingConfigResponse = z35.object({
6521
6858
  ok: z35.boolean()
6522
6859
  });
6860
+ var CreateLandingEditRequest = z35.object({
6861
+ message: z35.string().min(3).max(1e3),
6862
+ sectionId: z35.string().optional()
6863
+ });
6864
+ var LandingEditDto = z35.object({
6865
+ id: z35.string(),
6866
+ scope: z35.enum([
6867
+ "section",
6868
+ "full"
6869
+ ]),
6870
+ sectionId: z35.string().nullable(),
6871
+ message: z35.string(),
6872
+ status: z35.enum([
6873
+ "pending",
6874
+ "processing",
6875
+ "applied",
6876
+ "failed",
6877
+ "reverted"
6878
+ ]),
6879
+ costCents: z35.number(),
6880
+ error: z35.string().nullable(),
6881
+ createdAt: z35.string()
6882
+ });
6883
+ var CreateLandingEditResponse = z35.object({
6884
+ edit: LandingEditDto,
6885
+ chargedCredits: z35.number()
6886
+ });
6887
+ var ListLandingEditsResponse = z35.object({
6888
+ edits: z35.array(LandingEditDto)
6889
+ });
6523
6890
 
6524
6891
  // ../../packages/types/src/video-edit.ts
6525
6892
  import { z as z36 } from "zod";
@@ -6566,6 +6933,36 @@ var DropshippingProjectStatusSchema = z37.enum([
6566
6933
  "completed",
6567
6934
  "archived"
6568
6935
  ]);
6936
+ var DROPSHIPPING_LAUNCH_REWARD_CAMPAIGN = "dropshipping-launch-v1";
6937
+ var DropshippingRewardMilestoneSchema = z37.enum([
6938
+ "product_selected",
6939
+ "costing_saved",
6940
+ "product_media_ready",
6941
+ "creatives_completed",
6942
+ "campaign_created"
6943
+ ]);
6944
+ var DROPSHIPPING_LAUNCH_REWARD_CREDITS = {
6945
+ product_selected: 50,
6946
+ costing_saved: 50,
6947
+ product_media_ready: 50,
6948
+ creatives_completed: 150,
6949
+ campaign_created: 200
6950
+ };
6951
+ var DROPSHIPPING_LAUNCH_REWARD_TOTAL_CREDITS = Object.values(DROPSHIPPING_LAUNCH_REWARD_CREDITS).reduce((sum, credits) => sum + credits, 0);
6952
+ var DropshippingProjectRewardMilestoneStateSchema = z37.object({
6953
+ milestone: DropshippingRewardMilestoneSchema,
6954
+ credits: z37.number().int().positive(),
6955
+ available: z37.boolean().optional(),
6956
+ earned: z37.boolean(),
6957
+ grantId: z37.string().uuid().nullable(),
6958
+ grantedAt: z37.string().datetime().nullable()
6959
+ });
6960
+ var DropshippingProjectRewardProgressSchema = z37.object({
6961
+ campaignKey: z37.literal(DROPSHIPPING_LAUNCH_REWARD_CAMPAIGN),
6962
+ totalCredits: z37.number().int().positive(),
6963
+ earnedCredits: z37.number().int().nonnegative(),
6964
+ milestones: z37.array(DropshippingProjectRewardMilestoneStateSchema).length(DropshippingRewardMilestoneSchema.options.length)
6965
+ });
6569
6966
  var DropshippingProjectPhaseSchema = z37.enum([
6570
6967
  "research",
6571
6968
  "costing",
@@ -6723,6 +7120,7 @@ var DropshippingProjectResponseSchema = z37.object({
6723
7120
  version: z37.number().int().positive(),
6724
7121
  phase: DropshippingProjectPhaseSchema,
6725
7122
  nextAction: DropshippingProjectNextActionSchema.nullable(),
7123
+ rewards: DropshippingProjectRewardProgressSchema,
6726
7124
  createdAt: z37.string().datetime(),
6727
7125
  updatedAt: z37.string().datetime()
6728
7126
  });
@@ -6784,12 +7182,17 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
6784
7182
  {
6785
7183
  name: "create_creative_variant",
6786
7184
  confirmation: "credit_consuming",
6787
- schemaHash: "8feeed1e6e6a14bdcb0a1d9d8b03350ce7d8117d926db49f19ca5c5a8f1e7033"
7185
+ schemaHash: "6a777fbe7ad17966d16502e8872e15e4db2dcfacdfd2992d24853340d4f22a00"
6788
7186
  },
6789
7187
  {
6790
7188
  name: "create_product_task",
6791
7189
  confirmation: "credit_consuming",
6792
- schemaHash: "828ee643bb3a05a63c6e63730459d35ce1621cbd559bbc76a6c5fcec9a45654d"
7190
+ schemaHash: "3f03c4886f65a030e03a531c02e0ef879aaf3f5820e90ab3ca04a02350e693e6"
7191
+ },
7192
+ {
7193
+ name: "edit_image_creative",
7194
+ confirmation: "credit_consuming",
7195
+ schemaHash: "ce0abba871436850bd1db0ea4bcdade9ef35420ebd83833e2d5c2dcf70378ec0"
6793
7196
  },
6794
7197
  {
6795
7198
  name: "edit_task_draft",
@@ -6799,7 +7202,7 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
6799
7202
  {
6800
7203
  name: "estimate_creative_variant",
6801
7204
  confirmation: "none",
6802
- schemaHash: "a5601adcb9578319528ca0a96f37a35b613370646a75932d1b3978d593f7e302"
7205
+ schemaHash: "d4453364c0706bf88f480cebc977916f06d222d7eebebc9cfa91165929a1a998"
6803
7206
  },
6804
7207
  {
6805
7208
  name: "find_winning_products",
@@ -6814,17 +7217,17 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
6814
7217
  {
6815
7218
  name: "generate_image_creatives",
6816
7219
  confirmation: "credit_consuming",
6817
- schemaHash: "fb6afcf8571887906340fa0b8dd17196d2814e480b734ad5d374e8810958152e"
7220
+ schemaHash: "bf6a7599dac65c31a43e23badad8fd8e1ab3e8aee8a5a55ac47f9c39268b2fa6"
6818
7221
  },
6819
7222
  {
6820
7223
  name: "generate_landing",
6821
7224
  confirmation: "credit_consuming",
6822
- schemaHash: "539922b8fe611c73a7349b1c3f92632fe21f161d7874a0ef2172abfec5834759"
7225
+ schemaHash: "15465f9bda1ec945cc49f4f4a830d56a17a43b0cb5f6063d5594ddc92c830c21"
6823
7226
  },
6824
7227
  {
6825
7228
  name: "generate_video_creative",
6826
7229
  confirmation: "credit_consuming",
6827
- schemaHash: "94e90d60ad5d68538c31d880a635c5be69d6d204fd2119139735af83ebf0b94a"
7230
+ schemaHash: "c80208df75f2c7cbc459e9ae494a75a0a5f5bc4cf044b3fc9ef9c0e9af2c2452"
6828
7231
  },
6829
7232
  {
6830
7233
  name: "get_meta_campaign_status",
@@ -7185,6 +7588,7 @@ var VideoPresetEnum = z39.enum([
7185
7588
  "ugc_dynamic",
7186
7589
  "ugc_how_to",
7187
7590
  "ugc_unboxing_chaos",
7591
+ "problem_solution",
7188
7592
  "product_showcase",
7189
7593
  "wearable_showcase",
7190
7594
  "cinematic",
@@ -7196,6 +7600,7 @@ var TaskCreativeVariantVideoPresetEnum = z39.enum([
7196
7600
  "auto",
7197
7601
  "ugc_dynamic",
7198
7602
  "ugc_how_to",
7603
+ "problem_solution",
7199
7604
  "product_showcase",
7200
7605
  "cinematic",
7201
7606
  "wild_card",
@@ -7221,6 +7626,11 @@ var VIDEO_PRESET_RESOLUTION = {
7221
7626
  narrativeStyle: "ugc_unboxing_chaos",
7222
7627
  videoModel: "veo"
7223
7628
  },
7629
+ problem_solution: {
7630
+ videoStyle: "ai_dynamic",
7631
+ narrativeStyle: "problem_product_cta",
7632
+ videoModel: "kling"
7633
+ },
7224
7634
  pixar_problem: {
7225
7635
  videoStyle: "ai_dynamic",
7226
7636
  narrativeStyle: "animated_problem_pixar",
@@ -7268,6 +7678,12 @@ function resolveWireVideoTier(tier, durationSeconds) {
7268
7678
  return durationSeconds === 30 ? "standard" : resolveVideoTier(tier);
7269
7679
  }
7270
7680
  __name(resolveWireVideoTier, "resolveWireVideoTier");
7681
+ function assertVideoPresetDurationCompatibility(videoPreset, durationSeconds) {
7682
+ if (videoPreset === "problem_solution" && durationSeconds !== 30) {
7683
+ throw new Error("Problema y Soluci\xF3n solo est\xE1 disponible en videos Multiescena de 30 segundos.");
7684
+ }
7685
+ }
7686
+ __name(assertVideoPresetDurationCompatibility, "assertVideoPresetDurationCompatibility");
7271
7687
  function resolveVideoFormatForDuration(durationSeconds) {
7272
7688
  return durationSeconds === 30 ? "double_30s" : "single_15s";
7273
7689
  }
@@ -7303,6 +7719,7 @@ var PublicNarrativeStyleEnum = z39.enum([
7303
7719
  "review",
7304
7720
  "ugc_how_to",
7305
7721
  "ugc_unboxing_chaos",
7722
+ "problem_product_cta",
7306
7723
  "product_showcase",
7307
7724
  "wearable_showcase",
7308
7725
  "cinematic",
@@ -7985,6 +8402,7 @@ function proxifyOutputs(outputs, proxyBaseUrl) {
7985
8402
  }) ?? url, "rewrite");
7986
8403
  return {
7987
8404
  webappUrl: outputs.webappUrl,
8405
+ adCopy: outputs.adCopy,
7988
8406
  images: outputs.images.map((img, i) => ({
7989
8407
  ...img,
7990
8408
  url: rewrite(img.url, `image-${i}.png`)
@@ -8052,7 +8470,8 @@ async function fetchTaskOutputs(api, taskId) {
8052
8470
  landing: {
8053
8471
  sections
8054
8472
  },
8055
- images
8473
+ images,
8474
+ adCopy: typeof raw.adCreative?.adCopy === "string" && raw.adCreative.adCopy.length ? raw.adCreative.adCopy : void 0
8056
8475
  };
8057
8476
  }
8058
8477
  __name(fetchTaskOutputs, "fetchTaskOutputs");
@@ -8067,6 +8486,10 @@ function renderOutputsMarkdown(outputs) {
8067
8486
  parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
8068
8487
  });
8069
8488
  }
8489
+ if (outputs.adCopy) {
8490
+ parts.push("\n### Ad copy");
8491
+ parts.push(outputs.adCopy);
8492
+ }
8070
8493
  if (outputs.landing.sections.length) {
8071
8494
  parts.push(`
8072
8495
  ### Landing (${outputs.landing.sections.length} secciones)`);
@@ -8100,16 +8523,16 @@ var CreateProductTaskInput = z40.object({
8100
8523
  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.'),
8101
8524
  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."),
8102
8525
  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.'),
8103
- 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.'),
8526
+ 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.'),
8104
8527
  durationSeconds: z40.union([
8105
8528
  z40.literal(15),
8106
8529
  z40.literal(30)
8107
8530
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
8108
8531
  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."),
8109
8532
  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.'),
8110
- 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).'),
8533
+ 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).'),
8111
8534
  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."),
8112
- 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.'),
8535
+ 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.'),
8113
8536
  outputs: z40.array(z40.enum([
8114
8537
  "landing",
8115
8538
  "video",
@@ -8118,6 +8541,8 @@ var CreateProductTaskInput = z40.object({
8118
8541
  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."),
8119
8542
  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."),
8120
8543
  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.'),
8544
+ 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."),
8545
+ 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."),
8121
8546
  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".'),
8122
8547
  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".')
8123
8548
  }).strict();
@@ -8284,6 +8709,7 @@ __name(resolveStatusTaskKind, "resolveStatusTaskKind");
8284
8709
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
8285
8710
  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) => {
8286
8711
  try {
8712
+ assertVideoPresetDurationCompatibility(input.videoPreset, input.durationSeconds);
8287
8713
  if (!input.images && !input.projectId) {
8288
8714
  throw new Error("images es requerido salvo que env\xEDes projectId.");
8289
8715
  }
@@ -8319,6 +8745,8 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
8319
8745
  "options.competitorScanEnabled": input.competitorScanEnabled,
8320
8746
  "options.brandId": input.brandId,
8321
8747
  "options.creativeDirection": input.creativeDirection,
8748
+ "options.headline": input.headline,
8749
+ "options.subheadline": input.subheadline,
8322
8750
  "options.landingStrategy": input.landingStrategy,
8323
8751
  "options.landingImageFormat": input.landingImageFormat,
8324
8752
  pauseForAngleReview: input.pauseForAngleReview,
@@ -8582,7 +9010,10 @@ var CreativeRefs = {
8582
9010
  isCombo: z41.boolean().optional().describe("Opcional. Si true, el creativo presenta el producto como combo/pack. Default false."),
8583
9011
  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."),
8584
9012
  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."),
8585
- 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.')
9013
+ 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."),
9014
+ 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.'),
9015
+ 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."),
9016
+ 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.")
8586
9017
  };
8587
9018
  var VideoInput = z41.object({
8588
9019
  images: CreativeRefs.images,
@@ -8599,9 +9030,9 @@ var VideoInput = z41.object({
8599
9030
  z41.literal(15),
8600
9031
  z41.literal(30)
8601
9032
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
8602
- 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).'),
9033
+ 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).'),
8603
9034
  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.'),
8604
- 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).'),
9035
+ 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).'),
8605
9036
  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"].'),
8606
9037
  price: CreativeRefs.price,
8607
9038
  offer: CreativeRefs.offer,
@@ -8612,7 +9043,8 @@ var VideoInput = z41.object({
8612
9043
  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."),
8613
9044
  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."),
8614
9045
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
8615
- brandId: CreativeRefs.brandId
9046
+ brandId: CreativeRefs.brandId,
9047
+ projectId: CreativeRefs.projectId
8616
9048
  }).strict();
8617
9049
  var FreeVideoQualityEnum = z41.enum([
8618
9050
  "economical",
@@ -8676,7 +9108,15 @@ var ImageInput = z41.object({
8676
9108
  isCombo: CreativeRefs.isCombo,
8677
9109
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
8678
9110
  brandId: CreativeRefs.brandId,
8679
- creativeDirection: CreativeRefs.creativeDirection
9111
+ projectId: CreativeRefs.projectId,
9112
+ creativeDirection: CreativeRefs.creativeDirection,
9113
+ headline: CreativeRefs.headline,
9114
+ subheadline: CreativeRefs.subheadline
9115
+ });
9116
+ var EditImageCreativeInput = z41.object({
9117
+ taskId: z41.string().uuid().describe("ID de la tarea de imagen ya finalizada."),
9118
+ variantIndex: z41.number().int().min(0).max(10).describe("\xCDndice de la variante a corregir (0 = primera imagen)."),
9119
+ 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".')
8680
9120
  });
8681
9121
  var LandingInput = z41.object({
8682
9122
  images: CreativeRefs.images,
@@ -8693,9 +9133,12 @@ var LandingInput = z41.object({
8693
9133
  isCombo: CreativeRefs.isCombo,
8694
9134
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
8695
9135
  brandId: CreativeRefs.brandId,
9136
+ projectId: CreativeRefs.projectId,
8696
9137
  creativeDirection: CreativeRefs.creativeDirection,
8697
9138
  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".'),
8698
- 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".')
9139
+ 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".'),
9140
+ headline: CreativeRefs.headline,
9141
+ subheadline: CreativeRefs.subheadline
8699
9142
  });
8700
9143
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
8701
9144
  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) => {
@@ -8730,9 +9173,10 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8730
9173
  });
8731
9174
  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) => {
8732
9175
  try {
9176
+ assertVideoPresetDurationCompatibility(input.videoPreset, input.durationSeconds);
8733
9177
  const resolved = resolveVideoPreset(input.videoPreset);
8734
9178
  const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
8735
- const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
9179
+ const { videoTier: _publicTier, videoResolution: _vr, projectId, ...rest } = input;
8736
9180
  const wirePayload = {
8737
9181
  ...rest
8738
9182
  };
@@ -8742,6 +9186,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8742
9186
  }
8743
9187
  wirePayload.videoTier = wireTier;
8744
9188
  if (input.videoResolution) wirePayload.videoResolution = input.videoResolution;
9189
+ if (projectId) wirePayload.dropshippingProjectId = projectId;
8745
9190
  return await submitCreative(api, {
8746
9191
  path: "/v1/creatives/videos",
8747
9192
  estimatedSeconds: 300,
@@ -8754,22 +9199,45 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8754
9199
  });
8755
9200
  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) => {
8756
9201
  try {
9202
+ const { projectId, ...rest } = input;
8757
9203
  return await submitCreative(api, {
8758
9204
  path: "/v1/creatives/images",
8759
9205
  estimatedSeconds: 60,
8760
- input,
9206
+ input: {
9207
+ ...rest,
9208
+ ...projectId ? {
9209
+ dropshippingProjectId: projectId
9210
+ } : {}
9211
+ },
8761
9212
  transport
8762
9213
  });
8763
9214
  } catch (e) {
8764
9215
  throw translateError(e);
8765
9216
  }
8766
9217
  });
9218
+ 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) => {
9219
+ try {
9220
+ const r = await api.postJson(`/v1/creatives/images/${encodeURIComponent(input.taskId)}/regenerate`, {
9221
+ comment: input.instruction,
9222
+ variantIndex: input.variantIndex
9223
+ });
9224
+ return asText(r);
9225
+ } catch (e) {
9226
+ throw translateError(e);
9227
+ }
9228
+ });
8767
9229
  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) => {
8768
9230
  try {
9231
+ const { projectId, ...rest } = input;
8769
9232
  return await submitCreative(api, {
8770
9233
  path: "/v1/creatives/landing",
8771
9234
  estimatedSeconds: 60,
8772
- input,
9235
+ input: {
9236
+ ...rest,
9237
+ ...projectId ? {
9238
+ dropshippingProjectId: projectId
9239
+ } : {}
9240
+ },
8773
9241
  transport
8774
9242
  });
8775
9243
  } catch (e) {
@@ -10451,6 +10919,13 @@ var DropshippingProjectPhase = z51.enum([
10451
10919
  "advertising",
10452
10920
  "complete"
10453
10921
  ]);
10922
+ var DropshippingRewardMilestone = z51.enum([
10923
+ "product_selected",
10924
+ "costing_saved",
10925
+ "product_media_ready",
10926
+ "creatives_completed",
10927
+ "campaign_created"
10928
+ ]);
10454
10929
  var TargetCountry = z51.enum([
10455
10930
  "CO",
10456
10931
  "MX",
@@ -10573,6 +11048,19 @@ var CostingResultSchema = z51.object({
10573
11048
  wasPriceAutoAdjusted: z51.boolean(),
10574
11049
  profitabilityStatus: z51.literal("healthy")
10575
11050
  });
11051
+ var RewardProgressSchema = z51.object({
11052
+ campaignKey: z51.literal("dropshipping-launch-v1"),
11053
+ totalCredits: z51.number().int().positive(),
11054
+ earnedCredits: z51.number().int().nonnegative(),
11055
+ milestones: z51.array(z51.object({
11056
+ milestone: DropshippingRewardMilestone,
11057
+ credits: z51.number().int().positive(),
11058
+ available: z51.boolean().optional(),
11059
+ earned: z51.boolean(),
11060
+ grantId: ProjectId.nullable(),
11061
+ grantedAt: z51.string().datetime().nullable()
11062
+ })).length(DropshippingRewardMilestone.options.length)
11063
+ });
10576
11064
  var ProjectResponseSchema = z51.object({
10577
11065
  id: ProjectId,
10578
11066
  taskId: ProjectId.nullable(),
@@ -10592,6 +11080,7 @@ var ProjectResponseSchema = z51.object({
10592
11080
  version: z51.number().int().positive(),
10593
11081
  phase: DropshippingProjectPhase,
10594
11082
  nextAction: DropshippingProjectNextAction.nullable(),
11083
+ rewards: RewardProgressSchema,
10595
11084
  createdAt: z51.string().datetime(),
10596
11085
  updatedAt: z51.string().datetime()
10597
11086
  }).passthrough();
@@ -10609,6 +11098,11 @@ function projectText(raw) {
10609
11098
  nextAction: project.nextAction,
10610
11099
  hasReferenceImage: project.hasReferenceImage,
10611
11100
  costingSaved: Boolean(project.costingResult),
11101
+ rewardProgress: {
11102
+ earnedCredits: project.rewards.earnedCredits,
11103
+ totalCredits: project.rewards.totalCredits,
11104
+ milestones: project.rewards.milestones
11105
+ },
10612
11106
  version: project.version,
10613
11107
  message: `Siguiente paso: ${next}. Relee este proyecto despu\xE9s de cada mutaci\xF3n.`
10614
11108
  })