@productmaker/mcp 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,6 +78,7 @@ Sign up at [productmaker.app](https://productmaker.app) and create a key (prefix
78
78
  | `regenerate_video_script` | Re-roll the pending video script (optionally with a different narrative style) |
79
79
  | `resolve_pending_action` | Resolve a gate the pipeline is waiting on (product-reference confirmation or angle-review approval) when `get_task_status` returns `pendingUserAction` |
80
80
  | `list_shopify_shops` | List connected Shopify stores |
81
+ | `list_shopify_products` | Search products in a connected Shopify store before choosing `targetProductId` for `publish_to_shopify` |
81
82
  | `list_meta_ad_accounts` | List connected Meta ad accounts |
82
83
  | `list_tiktok_advertisers` | List connected TikTok Ads advertiser accounts |
83
84
  | `publish_to_shopify` | Publish a completed task to a Shopify store (returns markdown with landing + product URLs) |
@@ -86,6 +87,29 @@ Sign up at [productmaker.app](https://productmaker.app) and create a key (prefix
86
87
  | `get_meta_campaign_status` | Read-only status + lifetime insights (spend/impressions/clicks/reach) of a Meta campaign |
87
88
  | `get_tiktok_campaign_status` | Read-only status + lifetime report of a TikTok campaign (requires `advertiserId`) |
88
89
 
90
+ ## Video duration and winner review
91
+
92
+ `create_product_task` and `generate_video_creative` accept `durationSeconds`:
93
+ use `15` for the default single-call render or `30` for the existing
94
+ approximately-30-second multi-scene format. Thirty-second renders are billed
95
+ per scene, so tell the user about the higher cost before requesting one. The
96
+ MCP keeps the internal `standard` tier hidden and selects it only from this
97
+ explicit duration choice.
98
+
99
+ To let a user decide after competition analysis, pass
100
+ `pauseForAngleReview: true` when creating the task. When `get_task_status`
101
+ returns `pendingUserAction.kind = "angle_review_approval"` with a
102
+ `gapDurationChoice`, relay `recommendedSec` and `estimatedDeltaCredits` to the
103
+ user. After explicit confirmation, call `resolve_pending_action` with
104
+ `action: { kind: "angle_review_approval", gapDurationSeconds: 30 }` to upgrade
105
+ only the recommended angle. Omit the field or send `15` to keep the quoted
106
+ duration. A 30-second upgrade is rejected unless the current status contains a
107
+ matching 30-second recommendation.
108
+
109
+ The resolver uses the task kind from status to call the matching backend
110
+ approval contract, preserving multi-output autopilot tasks and the incremental
111
+ credit gate for the upgrade.
112
+
89
113
  ## Multiple photos per product
90
114
 
91
115
  Creative tools accept an `images` array of **1–5 photos of the same product**. The first entry is the hero; the rest are alternate angles. Don't mix different products in one call — that's a separate task each time.
package/dist/http.js CHANGED
@@ -113,7 +113,7 @@ var ApiClient = class {
113
113
  };
114
114
 
115
115
  // src/index.ts
116
- var MCP_VERSION = "1.13.0";
116
+ var MCP_VERSION = "1.15.0";
117
117
 
118
118
  // src/tools/tasks.ts
119
119
  import { z as z2 } from "zod";
@@ -476,6 +476,14 @@ function resolveVideoTier(_tier) {
476
476
  return "single_call";
477
477
  }
478
478
  __name(resolveVideoTier, "resolveVideoTier");
479
+ function resolveWireVideoTier(tier, durationSeconds) {
480
+ return durationSeconds === 30 ? "standard" : resolveVideoTier(tier);
481
+ }
482
+ __name(resolveWireVideoTier, "resolveWireVideoTier");
483
+ function resolveVideoFormatForDuration(durationSeconds) {
484
+ return durationSeconds === 30 ? "double_30s" : "single_15s";
485
+ }
486
+ __name(resolveVideoFormatForDuration, "resolveVideoFormatForDuration");
479
487
  var VideoResolutionEnum = z.enum([
480
488
  "480p",
481
489
  "720p"
@@ -1192,8 +1200,13 @@ var CreateProductTaskInput = z2.object({
1192
1200
  paymentMethods: z2.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.'),
1193
1201
  productContext: z2.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."),
1194
1202
  productLabel: z2.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.'),
1195
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo de video. Default "auto" (la IA escoge tras analizar el producto). Opciones actuales: "auto", "ugc_dynamic" (UGC Review), "ugc_unboxing_chaos" (Unboxing), "problem_solution" (problema\u2192soluci\xF3n), "product_showcase" (Producto en acci\xF3n: una prueba funcional), "wearable_showcase" (modelaje con la prenda/accesorio puesto), "cinematic" (historia visual premium), "pixar_problem" (problema animado Pixar), "pixar_sassy" (producto que habla Pixar). V\xEDa MCP todo video renderiza a 15s single_call. Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
1196
- videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. \xDAnico valor disponible v\xEDa MCP: "basic" = "Realista": render de 15s con audio nativo, el m\xE1s natural. El estilo "Multiescena" (multi-toma, 30s) NO est\xE1 disponible v\xEDa MCP por su costo (se factura por escena) \u2014 si el usuario lo pide, dile que lo genere desde la web. Omitir el campo equivale a "basic".'),
1203
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s; 30s usa el ritmo Multiescena. Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso), "problem_solution" (problema\u2192producto\u2192CTA), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla), "product_showcase" (solo producto, sin actor), "wearable_showcase" (ropa/accesorios puestos). Si el usuario pide algo fuera de esta lista (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.'),
1204
+ durationSeconds: z2.union([
1205
+ z2.literal(15),
1206
+ z2.literal(30)
1207
+ ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
1208
+ pauseForAngleReview: z2.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."),
1209
+ 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.'),
1197
1210
  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).'),
1198
1211
  multiAngleVideos: z2.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."),
1199
1212
  angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_unboxing_chaos", "problem_product_cta", "product_showcase", "wearable_showcase", "cinematic", "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.'),
@@ -1251,13 +1264,19 @@ var PendingActionPayload = z2.discriminatedUnion("kind", [
1251
1264
  z2.object({
1252
1265
  kind: z2.literal("angle_review_approval"),
1253
1266
  // All edits are optional — empty action body just approves the
1254
- // backend-generated draft as-is. Field shapes mirror ApproveAnglesRequest
1255
- // in @packages/types/wizard.ts (the only path-of-resolution endpoint is
1256
- // POST /v1/tasks/:taskId/approve-angles).
1267
+ // backend-generated draft as-is. Field shapes mirror the angle-review
1268
+ // request contracts in @packages/types/wizard.ts; the handler routes to
1269
+ // the dedicated video/image/autopilot endpoint based on StatusResponse.kind.
1257
1270
  customAngle: z2.string().min(1).max(480).optional(),
1258
- painPoints: z2.string().max(3e3).optional(),
1259
- benefits: z2.string().max(3e3).optional(),
1260
- features: z2.array(z2.string().min(1).max(300)).max(10).optional()
1271
+ // Keep the shared MCP action within the strictest downstream contract:
1272
+ // autopilot/image resume endpoints cap these fields at 1500 characters.
1273
+ painPoints: z2.string().max(1500).optional(),
1274
+ benefits: z2.string().max(1500).optional(),
1275
+ features: z2.array(z2.string().min(1).max(300)).max(10).optional(),
1276
+ gapDurationSeconds: z2.union([
1277
+ z2.literal(15),
1278
+ z2.literal(30)
1279
+ ]).optional().describe("Opcional. Solo para aceptar expl\xEDcitamente la recomendaci\xF3n gapDurationChoice del status. 30 env\xEDa el override double_30s para el \xE1ngulo recomendado; 15 u omitido conserva la duraci\xF3n cotizada.")
1261
1280
  })
1262
1281
  ]);
1263
1282
  var ResolvePendingActionInput = z2.object({
@@ -1269,6 +1288,51 @@ function resolveVariantConfirmVideoTier(action) {
1269
1288
  }
1270
1289
  __name(resolveVariantConfirmVideoTier, "resolveVariantConfirmVideoTier");
1271
1290
  var StatusForResolveSchema = z2.object({
1291
+ // Metadata used to select the backend resume contract. Older API responses
1292
+ // may omit these fields, so the resolver keeps the generic fallback.
1293
+ kind: z2.enum([
1294
+ "autopilot",
1295
+ "video",
1296
+ "image",
1297
+ "landing",
1298
+ "mix"
1299
+ ]).optional(),
1300
+ selectedOutputs: z2.array(z2.enum([
1301
+ "landing",
1302
+ "video",
1303
+ "image"
1304
+ ])).optional(),
1305
+ videoAngleCount: z2.number().int().min(1).max(3).optional(),
1306
+ ugcFormat: z2.enum([
1307
+ "single_8s",
1308
+ "single_15s",
1309
+ "double_30s"
1310
+ ]).optional(),
1311
+ videoStyle: z2.enum([
1312
+ "ugc",
1313
+ "cartoon_3d",
1314
+ "product_only",
1315
+ "ai_dynamic"
1316
+ ]).optional(),
1317
+ videoModel: z2.enum([
1318
+ "veo",
1319
+ "wan",
1320
+ "bytedance",
1321
+ "seedance",
1322
+ "grok",
1323
+ "kling"
1324
+ ]).optional(),
1325
+ videoTier: z2.enum([
1326
+ "legacy",
1327
+ "standard",
1328
+ "pro",
1329
+ "single_call"
1330
+ ]).optional(),
1331
+ videoResolution: z2.enum([
1332
+ "480p",
1333
+ "720p"
1334
+ ]).optional(),
1335
+ narrativeStyle: NarrativeStyleEnum.optional(),
1272
1336
  pendingUserAction: z2.object({
1273
1337
  kind: z2.string(),
1274
1338
  candidateGrouping: z2.string().optional(),
@@ -1278,10 +1342,8 @@ var StatusForResolveSchema = z2.object({
1278
1342
  painPointsDraft: z2.string().optional(),
1279
1343
  benefitsDraft: z2.string().optional(),
1280
1344
  featuresDraft: z2.array(z2.string()).optional(),
1281
- // Informational-only recommendation (see GapDurationChoiceMirror in
1282
- // shared.ts). Not read by the switch below — v1 has no consent input for
1283
- // it — mirrored here purely so it survives the parse instead of being
1284
- // silently stripped by this object's default unknown-key behavior.
1345
+ // Recommendation consumed only when the caller explicitly sends
1346
+ // gapDurationSeconds: 30 in the angle approval action.
1285
1347
  gapDurationChoice: GapDurationChoiceMirror.optional()
1286
1348
  }).optional()
1287
1349
  }).passthrough();
@@ -1302,6 +1364,23 @@ function formatOutputList(outputs) {
1302
1364
  return `${labels.slice(0, -1).join(" + ")} + ${labels[labels.length - 1]}`;
1303
1365
  }
1304
1366
  __name(formatOutputList, "formatOutputList");
1367
+ function resolveSelectedAngleIndices(status, gapDurationChoice) {
1368
+ const count = Math.min(3, Math.max(status.videoAngleCount ?? 1, gapDurationChoice ? gapDurationChoice.angleIndex + 1 : 1));
1369
+ return Array.from({
1370
+ length: count
1371
+ }, (_, index) => index);
1372
+ }
1373
+ __name(resolveSelectedAngleIndices, "resolveSelectedAngleIndices");
1374
+ function resolveStatusTaskKind(status) {
1375
+ if (status.kind) return status.kind;
1376
+ const outputs = status.selectedOutputs ?? [];
1377
+ if (outputs.length === 1 && outputs[0] === "video") return "video";
1378
+ if (outputs.includes("video")) return "autopilot";
1379
+ if (outputs.length === 1 && outputs[0] === "image") return "image";
1380
+ if (outputs.length === 1 && outputs[0] === "landing") return "landing";
1381
+ return void 0;
1382
+ }
1383
+ __name(resolveStatusTaskKind, "resolveStatusTaskKind");
1305
1384
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
1306
1385
  server.tool("create_product_task", 'Crea una tarea de producto desde una o varias fotos. 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), language, 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 el usuario est\xE1 en LATAM. Si te dice "t\xFA elige", omite esos campos.\n\nANTES de invocar, DILE CLARAMENTE al usuario qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task. Ejemplo: "Voy a generar landing + video en un mismo task; esto consume cr\xE9ditos de ambos outputs". Si el usuario s\xF3lo quiere un output, usa generate_landing / generate_video_creative / generate_image_creatives en vez de este tool.\n\nSi el usuario tiene varias fotos del MISMO producto (frente, lateral, detalle, uso), incl\xFAyelas todas en images[] hasta 5. Si tiene fotos de productos DIFERENTES, son llamadas separadas (una tarea por producto).\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos):\n- videoPreset (tipo/narrativa del video \u2014 \xFAnico campo de estilo)\n- multiAngleVideos (booleano)\n- angleNarratives (override per-angle de la narrativa, avanzado)\n- outputs (subconjunto de {landing,video,image}); omitido = ["landing","video","image"] en un mismo task\n- productUrl\n- creativeDirection (direcci\xF3n creativa para im\xE1genes y landing; el estilo del video se dirige con scriptNotes)\n\nGUION: para revisar/editar el guion del video antes de renderizar, usa `generate_video_creative` con `pauseForVideoScriptReview: true` en vez de este tool. Despu\xE9s de la pausa: get_video_script para leer, approve_video_script para aprobar con ediciones.\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado (ej. "estilo anime", "modelo X"), p\xEDdele una opci\xF3n v\xE1lida del enum correspondiente.', CreateProductTaskInput.shape, async (input) => {
1307
1386
  try {
@@ -1314,7 +1393,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1314
1393
  }));
1315
1394
  const selectedOutputs = input.outputs ?? DEFAULT_PRODUCT_TASK_OUTPUTS;
1316
1395
  const resolved = resolveVideoPreset(input.videoPreset);
1317
- const wireTier = resolveVideoTier(input.videoTier);
1396
+ const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
1318
1397
  const fields = flatten({
1319
1398
  "options.languageCode": input.language,
1320
1399
  "options.country": input.country,
@@ -1328,6 +1407,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1328
1407
  "options.videoStyle": resolved?.videoStyle,
1329
1408
  "options.narrativeStyle": resolved?.narrativeStyle,
1330
1409
  "options.videoTier": wireTier,
1410
+ "options.ugcFormat": resolveVideoFormatForDuration(input.durationSeconds),
1331
1411
  "options.videoResolution": input.videoResolution,
1332
1412
  "options.multiAngleVideos": input.multiAngleVideos,
1333
1413
  "options.angleNarratives": input.angleNarratives,
@@ -1337,6 +1417,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1337
1417
  "options.creativeDirection": input.creativeDirection,
1338
1418
  "options.landingStrategy": input.landingStrategy,
1339
1419
  "options.landingImageFormat": input.landingImageFormat,
1420
+ pauseForAngleReview: input.pauseForAngleReview,
1340
1421
  url: input.productUrl
1341
1422
  });
1342
1423
  const r = await api.postMultipart("/v1/ingest", {
@@ -1354,7 +1435,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1354
1435
  throw translateError(e);
1355
1436
  }
1356
1437
  });
1357
- server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`), es solo informativo: indica que el anuncio de referencia dura m\xE1s que la duraci\xF3n cotizada para esta tarea y cu\xE1nto costar\xEDa extenderla a `recommendedSec` segundos (el costo extra viene en `estimatedDeltaCredits`, si est\xE1 disponible), pero el MCP no tiene forma de aceptar ese cambio \u2014 com\xE9ntaselo al usuario y dile que lo active desde la web si le interesa.\nUse this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
1438
+ server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`), mu\xE9strale al usuario la duraci\xF3n recomendada y `estimatedDeltaCredits` si aparece. Con confirmaci\xF3n expl\xEDcita, llama `resolve_pending_action` con `action: { kind: "angle_review_approval", gapDurationSeconds: 30 }` para actualizar solo ese \xE1ngulo. Omitir el campo o enviar 15 conserva la duraci\xF3n cotizada; 30 se rechaza si no hay una recomendaci\xF3n pendiente de 30s.\nUse this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
1358
1439
  try {
1359
1440
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
1360
1441
  const signal = extra?.signal;
@@ -1411,7 +1492,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1411
1492
  throw translateError(e);
1412
1493
  }
1413
1494
  });
1414
- server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Payload: `userChoice` \u2208 { "same_product" (mismo producto, fotos son s\xF3lo \xE1ngulos distintos), "variants" (mismo producto, distintos colores/tallas/sabores), "combo_distinct" (son 2 productos distintos vendidos juntos como combo \u2014 marca la tarea como combo), "different_products" (productos distintos y el usuario quiere generar s\xF3lo uno \u2014 el backend bloquea hasta que selecciones im\xE1genes de un solo producto; normalmente NO es la respuesta correcta para destrabar) }. PREG\xDANTALE al usuario antes de invocar \u2014 `pendingUserAction.targetProductLabel` y `reason` te dan contexto para formularle la pregunta en lenguaje natural. Usa SOLO valores presentes en `pendingUserAction.options`. Si el status trae `proposedProductNames` (lista propuesta de nombres), el combo es INCIERTO: mu\xE9strale los nombres al usuario y, si confirma que son productos distintos, manda `userChoice: "combo_distinct"` JUNTO CON `confirmedProductNames` (la lista confirmada o editada, 2-8 nombres) para separarlo en productos distintos; si el usuario dice que es un solo producto, manda `userChoice: "same_product"` (sin `confirmedProductNames`). Cuando el producto son VARIANTES DE COLOR (mismo producto, distintos colores) puedes ofrecer un video "Multiescena" que muestra una toma por color: manda `userChoice: "variants"` JUNTO CON `useMultiSceneShowcase: true`. AVISA al usuario que ese modo se factura por escena (m\xE1s caro que el render normal de una sola toma) antes de activarlo; si prefiere el render normal, manda solo `userChoice: "variants"` sin el flag.\n\n\u2022 `angle_review_approval` \u2014 el usuario opto por `pauseForAngleReview: true` al crear el creativo (en `generate_image_creatives`, `generate_video_creative` o `generate_landing`). La IA ya gener\xF3 el \xE1ngulo + pain points + benefits + features y la tarea est\xE1 pausada esperando que el usuario apruebe o edite antes de renderizar. Lee `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` del `pendingUserAction` para mostr\xE1rselos al usuario en lenguaje natural ("la IA propuso este \xE1ngulo: ... \xBFlo apruebas o cambias algo?"). Payload (TODOS opcionales \u2014 sin nada = aprobar tal cual; con campos = aprobar con ediciones): `customAngle` (override del \xE1ngulo), `painPoints`, `benefits`, `features` (array de 1-10 bullets). Llamar sin ning\xFAn campo es v\xE1lido y aprueba el draft sin cambios.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
1495
+ server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 confirma c\xF3mo agrupar las fotos usando solo los valores de `pendingUserAction.options`.\n\n\u2022 `angle_review_approval` \u2014 aprueba o edita el draft despu\xE9s de `pauseForAngleReview: true`. Puedes enviar `customAngle`, `painPoints`, `benefits` y `features`; todos son opcionales y el MCP conserva el draft persistido cuando no los cambias. Si `pendingUserAction.gapDurationChoice` recomienda 30s para un winner, muestra al usuario `recommendedSec` y `estimatedDeltaCredits`, pide confirmaci\xF3n expl\xEDcita y env\xEDa `gapDurationSeconds: 30` en esta misma acci\xF3n. El MCP valida que exista una recomendaci\xF3n pendiente de 30s y entonces actualiza solo el \xE1ngulo recomendado con `double_30s`; omitir el campo o enviar 15 mantiene la duraci\xF3n cotizada.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
1415
1496
  try {
1416
1497
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
1417
1498
  const status = StatusForResolveSchema.parse(raw);
@@ -1464,7 +1545,108 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1464
1545
  if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
1465
1546
  if (input.action.benefits != null) body.benefits = input.action.benefits;
1466
1547
  if (input.action.features != null) body.features = input.action.features;
1467
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`, body);
1548
+ if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
1549
+ throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
1550
+ }
1551
+ if (input.action.gapDurationSeconds === 30) {
1552
+ const choice = pending.gapDurationChoice;
1553
+ if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
1554
+ if (choice.recommendedSec !== 30) {
1555
+ throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
1556
+ }
1557
+ body.angleVideoOptions = [
1558
+ {
1559
+ angleIndex: choice.angleIndex,
1560
+ ugcFormat: "double_30s"
1561
+ }
1562
+ ];
1563
+ }
1564
+ const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
1565
+ const resolvedTaskKind = resolveStatusTaskKind(status);
1566
+ const isVideoTask = resolvedTaskKind === "video";
1567
+ const isAutopilotTask = resolvedTaskKind === "autopilot";
1568
+ if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
1569
+ const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
1570
+ const optionsByIndex = new Map(existingOptions.map((option) => [
1571
+ option.angleIndex,
1572
+ {
1573
+ ...option
1574
+ }
1575
+ ]));
1576
+ for (const angleIndex of selectedAngleIndices) {
1577
+ optionsByIndex.set(angleIndex, {
1578
+ ...optionsByIndex.get(angleIndex) ?? {
1579
+ angleIndex
1580
+ },
1581
+ angle: body.customAngle
1582
+ });
1583
+ }
1584
+ body.angleVideoOptions = [
1585
+ ...optionsByIndex.values()
1586
+ ].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
1587
+ }
1588
+ if (isVideoTask) {
1589
+ body.selectedAngleIndices = selectedAngleIndices;
1590
+ const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
1591
+ return asText(r2);
1592
+ }
1593
+ if (isAutopilotTask) {
1594
+ const resumeBody = {
1595
+ selectedAngleIndices,
1596
+ ...status.ugcFormat ? {
1597
+ ugcFormat: status.ugcFormat
1598
+ } : {
1599
+ ugcFormat: "single_15s"
1600
+ },
1601
+ ...status.videoStyle ? {
1602
+ videoStyle: status.videoStyle
1603
+ } : {},
1604
+ ...status.videoModel ? {
1605
+ videoModel: status.videoModel
1606
+ } : {},
1607
+ ...status.videoTier ? {
1608
+ videoTier: status.videoTier
1609
+ } : {},
1610
+ ...status.videoResolution ? {
1611
+ videoResolution: status.videoResolution
1612
+ } : {},
1613
+ ...status.narrativeStyle ? {
1614
+ narrativeStyle: status.narrativeStyle
1615
+ } : {},
1616
+ ...body.customAngle != null ? {
1617
+ customAngleOverride: body.customAngle
1618
+ } : {},
1619
+ ...body.painPoints != null ? {
1620
+ painPoints: body.painPoints
1621
+ } : {},
1622
+ ...body.benefits != null ? {
1623
+ benefits: body.benefits
1624
+ } : {},
1625
+ ...body.angleVideoOptions ? {
1626
+ angleVideoOptions: body.angleVideoOptions
1627
+ } : {}
1628
+ };
1629
+ if (body.features !== void 0) {
1630
+ await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
1631
+ features: body.features
1632
+ });
1633
+ }
1634
+ const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
1635
+ return asText(r2);
1636
+ }
1637
+ const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
1638
+ const imageBody = status.kind === "image" ? {
1639
+ ...body.customAngle != null ? {
1640
+ customAngle: body.customAngle
1641
+ } : {},
1642
+ ...body.painPoints != null ? {
1643
+ painPoints: body.painPoints
1644
+ } : {},
1645
+ ...body.features != null ? {
1646
+ features: body.features
1647
+ } : {}
1648
+ } : body;
1649
+ const r = await api.postJson(route, imageBody);
1468
1650
  return asText(r);
1469
1651
  }
1470
1652
  default: {
@@ -1508,9 +1690,12 @@ var VideoInput = z3.object({
1508
1690
  "16:9",
1509
1691
  "Auto"
1510
1692
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
1511
- durationSeconds: z3.number().int().min(5).max(15).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-15). Default 15s. Para hooks cortos usa 8. Duraciones mayores a 15s (video Multiescena de 30s) NO est\xE1n disponibles v\xEDa MCP \u2014 ind\xEDcale al usuario que las genere desde la web."),
1512
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo de video. Default "auto". Opciones actuales: "auto", "ugc_dynamic" (UGC Review), "ugc_unboxing_chaos" (Unboxing), "problem_solution" (problema\u2192soluci\xF3n), "product_showcase" (Producto en acci\xF3n: una prueba funcional), "wearable_showcase" (modelaje con la prenda/accesorio puesto), "cinematic" (historia visual premium), "pixar_problem" (problema animado Pixar), "pixar_sassy" (producto que habla Pixar). V\xEDa MCP todo video renderiza a m\xE1x. 15s.'),
1513
- videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. \xDAnico valor disponible v\xEDa MCP: "basic" = "Realista": render de 15s con audio nativo, el m\xE1s natural. El estilo "Multiescena" (multi-toma, 30s) NO est\xE1 disponible v\xEDa MCP por su costo (se factura por escena) \u2014 si el usuario lo pide, dile que lo genere desde la web. Omitir el campo equivale a "basic".'),
1693
+ durationSeconds: z3.union([
1694
+ z3.literal(15),
1695
+ z3.literal(30)
1696
+ ]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
1697
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan tanto a 15s como a 30s; el formato de 30s conserva el ritmo multi-escena. Opciones: "auto", "ugc_dynamic" (UGC cl\xE1sico testimonial multi-hook), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "cinematic" (historia visual premium), "pixar_problem" (problema animado 3D estilo Pixar), "pixar_sassy" (producto que habla con personalidad), "product_showcase" (sin actor, solo producto), "wearable_showcase" (ropa/accesorios puestos).'),
1698
+ 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.'),
1514
1699
  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).'),
1515
1700
  features: z3.array(z3.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
1516
1701
  price: CreativeRefs.price,
@@ -1520,6 +1705,7 @@ var VideoInput = z3.object({
1520
1705
  cta: z3.string().optional().describe('Opcional. Call-to-action final del video. Ej. "Compra ahora", "Pide el tuyo hoy", "Link en bio". Si se omite, la IA escribe uno.'),
1521
1706
  scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
1522
1707
  pauseForVideoScriptReview: z3.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."),
1708
+ pauseForAngleReview: z3.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."),
1523
1709
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
1524
1710
  brandId: CreativeRefs.brandId
1525
1711
  }).strict();
@@ -1575,10 +1761,10 @@ var LandingInput = z3.object({
1575
1761
  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".')
1576
1762
  });
1577
1763
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
1578
- 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 (m\xE1x 15), 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: v\xEDa MCP todos los videos se generan en estilo "Realista" (render \xFAnico de 15s con audio nativo). El estilo "Multiescena" (30s, multi-toma) NO est\xE1 disponible por MCP por su costo \u2014 si el usuario lo pide, dile que lo genere desde la web.\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.', VideoInput.shape, async (input) => {
1764
+ 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) => {
1579
1765
  try {
1580
1766
  const resolved = resolveVideoPreset(input.videoPreset);
1581
- const wireTier = resolveVideoTier(input.videoTier);
1767
+ const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
1582
1768
  const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
1583
1769
  const wirePayload = {
1584
1770
  ...rest
@@ -1695,6 +1881,23 @@ function registerConnectionTools(server, api) {
1695
1881
  throw translateError(e);
1696
1882
  }
1697
1883
  });
1884
+ server.tool("list_shopify_products", 'Busca productos en una tienda Shopify conectada para elegir el target de una republicaci\xF3n. Usa el `id` devuelto junto con `publicationMode="existing"` en `publish_to_shopify`. Si `taskId` se proporciona, la respuesta tambi\xE9n indica el producto publicado actualmente para esa tarea y tienda. No modifica Shopify.', z5.object({
1885
+ shopDomain: z5.string().min(3).max(120),
1886
+ query: z5.string().trim().max(120).optional(),
1887
+ taskId: z5.string().uuid().optional()
1888
+ }).shape, async ({ shopDomain, query, taskId }) => {
1889
+ try {
1890
+ const params = new URLSearchParams({
1891
+ shopDomain
1892
+ });
1893
+ if (query) params.set("query", query);
1894
+ if (taskId) params.set("taskId", taskId);
1895
+ const r = await api.getJson(`/v1/shopify/products?${params.toString()}`);
1896
+ return asText(r);
1897
+ } catch (e) {
1898
+ throw translateError(e);
1899
+ }
1900
+ });
1698
1901
  server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z5.object({}).shape, async () => {
1699
1902
  try {
1700
1903
  const r = await api.getJson("/v1/facebook/ad-accounts");
package/dist/stdio.js CHANGED
@@ -110,7 +110,7 @@ var ApiClient = class {
110
110
  };
111
111
 
112
112
  // src/index.ts
113
- var MCP_VERSION = "1.13.0";
113
+ var MCP_VERSION = "1.15.0";
114
114
 
115
115
  // src/tools/tasks.ts
116
116
  import { z as z2 } from "zod";
@@ -473,6 +473,14 @@ function resolveVideoTier(_tier) {
473
473
  return "single_call";
474
474
  }
475
475
  __name(resolveVideoTier, "resolveVideoTier");
476
+ function resolveWireVideoTier(tier, durationSeconds) {
477
+ return durationSeconds === 30 ? "standard" : resolveVideoTier(tier);
478
+ }
479
+ __name(resolveWireVideoTier, "resolveWireVideoTier");
480
+ function resolveVideoFormatForDuration(durationSeconds) {
481
+ return durationSeconds === 30 ? "double_30s" : "single_15s";
482
+ }
483
+ __name(resolveVideoFormatForDuration, "resolveVideoFormatForDuration");
476
484
  var VideoResolutionEnum = z.enum([
477
485
  "480p",
478
486
  "720p"
@@ -1148,8 +1156,13 @@ var CreateProductTaskInput = z2.object({
1148
1156
  paymentMethods: z2.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.'),
1149
1157
  productContext: z2.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."),
1150
1158
  productLabel: z2.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.'),
1151
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo de video. Default "auto" (la IA escoge tras analizar el producto). Opciones actuales: "auto", "ugc_dynamic" (UGC Review), "ugc_unboxing_chaos" (Unboxing), "problem_solution" (problema\u2192soluci\xF3n), "product_showcase" (Producto en acci\xF3n: una prueba funcional), "wearable_showcase" (modelaje con la prenda/accesorio puesto), "cinematic" (historia visual premium), "pixar_problem" (problema animado Pixar), "pixar_sassy" (producto que habla Pixar). V\xEDa MCP todo video renderiza a 15s single_call. Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
1152
- videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. \xDAnico valor disponible v\xEDa MCP: "basic" = "Realista": render de 15s con audio nativo, el m\xE1s natural. El estilo "Multiescena" (multi-toma, 30s) NO est\xE1 disponible v\xEDa MCP por su costo (se factura por escena) \u2014 si el usuario lo pide, dile que lo genere desde la web. Omitir el campo equivale a "basic".'),
1159
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s; 30s usa el ritmo Multiescena. Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso), "problem_solution" (problema\u2192producto\u2192CTA), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla), "product_showcase" (solo producto, sin actor), "wearable_showcase" (ropa/accesorios puestos). Si el usuario pide algo fuera de esta lista (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.'),
1160
+ durationSeconds: z2.union([
1161
+ z2.literal(15),
1162
+ z2.literal(30)
1163
+ ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
1164
+ pauseForAngleReview: z2.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."),
1165
+ 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.'),
1153
1166
  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).'),
1154
1167
  multiAngleVideos: z2.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."),
1155
1168
  angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_unboxing_chaos", "problem_product_cta", "product_showcase", "wearable_showcase", "cinematic", "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.'),
@@ -1207,13 +1220,19 @@ var PendingActionPayload = z2.discriminatedUnion("kind", [
1207
1220
  z2.object({
1208
1221
  kind: z2.literal("angle_review_approval"),
1209
1222
  // All edits are optional — empty action body just approves the
1210
- // backend-generated draft as-is. Field shapes mirror ApproveAnglesRequest
1211
- // in @packages/types/wizard.ts (the only path-of-resolution endpoint is
1212
- // POST /v1/tasks/:taskId/approve-angles).
1223
+ // backend-generated draft as-is. Field shapes mirror the angle-review
1224
+ // request contracts in @packages/types/wizard.ts; the handler routes to
1225
+ // the dedicated video/image/autopilot endpoint based on StatusResponse.kind.
1213
1226
  customAngle: z2.string().min(1).max(480).optional(),
1214
- painPoints: z2.string().max(3e3).optional(),
1215
- benefits: z2.string().max(3e3).optional(),
1216
- features: z2.array(z2.string().min(1).max(300)).max(10).optional()
1227
+ // Keep the shared MCP action within the strictest downstream contract:
1228
+ // autopilot/image resume endpoints cap these fields at 1500 characters.
1229
+ painPoints: z2.string().max(1500).optional(),
1230
+ benefits: z2.string().max(1500).optional(),
1231
+ features: z2.array(z2.string().min(1).max(300)).max(10).optional(),
1232
+ gapDurationSeconds: z2.union([
1233
+ z2.literal(15),
1234
+ z2.literal(30)
1235
+ ]).optional().describe("Opcional. Solo para aceptar expl\xEDcitamente la recomendaci\xF3n gapDurationChoice del status. 30 env\xEDa el override double_30s para el \xE1ngulo recomendado; 15 u omitido conserva la duraci\xF3n cotizada.")
1217
1236
  })
1218
1237
  ]);
1219
1238
  var ResolvePendingActionInput = z2.object({
@@ -1225,6 +1244,51 @@ function resolveVariantConfirmVideoTier(action) {
1225
1244
  }
1226
1245
  __name(resolveVariantConfirmVideoTier, "resolveVariantConfirmVideoTier");
1227
1246
  var StatusForResolveSchema = z2.object({
1247
+ // Metadata used to select the backend resume contract. Older API responses
1248
+ // may omit these fields, so the resolver keeps the generic fallback.
1249
+ kind: z2.enum([
1250
+ "autopilot",
1251
+ "video",
1252
+ "image",
1253
+ "landing",
1254
+ "mix"
1255
+ ]).optional(),
1256
+ selectedOutputs: z2.array(z2.enum([
1257
+ "landing",
1258
+ "video",
1259
+ "image"
1260
+ ])).optional(),
1261
+ videoAngleCount: z2.number().int().min(1).max(3).optional(),
1262
+ ugcFormat: z2.enum([
1263
+ "single_8s",
1264
+ "single_15s",
1265
+ "double_30s"
1266
+ ]).optional(),
1267
+ videoStyle: z2.enum([
1268
+ "ugc",
1269
+ "cartoon_3d",
1270
+ "product_only",
1271
+ "ai_dynamic"
1272
+ ]).optional(),
1273
+ videoModel: z2.enum([
1274
+ "veo",
1275
+ "wan",
1276
+ "bytedance",
1277
+ "seedance",
1278
+ "grok",
1279
+ "kling"
1280
+ ]).optional(),
1281
+ videoTier: z2.enum([
1282
+ "legacy",
1283
+ "standard",
1284
+ "pro",
1285
+ "single_call"
1286
+ ]).optional(),
1287
+ videoResolution: z2.enum([
1288
+ "480p",
1289
+ "720p"
1290
+ ]).optional(),
1291
+ narrativeStyle: NarrativeStyleEnum.optional(),
1228
1292
  pendingUserAction: z2.object({
1229
1293
  kind: z2.string(),
1230
1294
  candidateGrouping: z2.string().optional(),
@@ -1234,10 +1298,8 @@ var StatusForResolveSchema = z2.object({
1234
1298
  painPointsDraft: z2.string().optional(),
1235
1299
  benefitsDraft: z2.string().optional(),
1236
1300
  featuresDraft: z2.array(z2.string()).optional(),
1237
- // Informational-only recommendation (see GapDurationChoiceMirror in
1238
- // shared.ts). Not read by the switch below — v1 has no consent input for
1239
- // it — mirrored here purely so it survives the parse instead of being
1240
- // silently stripped by this object's default unknown-key behavior.
1301
+ // Recommendation consumed only when the caller explicitly sends
1302
+ // gapDurationSeconds: 30 in the angle approval action.
1241
1303
  gapDurationChoice: GapDurationChoiceMirror.optional()
1242
1304
  }).optional()
1243
1305
  }).passthrough();
@@ -1258,6 +1320,23 @@ function formatOutputList(outputs) {
1258
1320
  return `${labels.slice(0, -1).join(" + ")} + ${labels[labels.length - 1]}`;
1259
1321
  }
1260
1322
  __name(formatOutputList, "formatOutputList");
1323
+ function resolveSelectedAngleIndices(status, gapDurationChoice) {
1324
+ const count = Math.min(3, Math.max(status.videoAngleCount ?? 1, gapDurationChoice ? gapDurationChoice.angleIndex + 1 : 1));
1325
+ return Array.from({
1326
+ length: count
1327
+ }, (_, index) => index);
1328
+ }
1329
+ __name(resolveSelectedAngleIndices, "resolveSelectedAngleIndices");
1330
+ function resolveStatusTaskKind(status) {
1331
+ if (status.kind) return status.kind;
1332
+ const outputs = status.selectedOutputs ?? [];
1333
+ if (outputs.length === 1 && outputs[0] === "video") return "video";
1334
+ if (outputs.includes("video")) return "autopilot";
1335
+ if (outputs.length === 1 && outputs[0] === "image") return "image";
1336
+ if (outputs.length === 1 && outputs[0] === "landing") return "landing";
1337
+ return void 0;
1338
+ }
1339
+ __name(resolveStatusTaskKind, "resolveStatusTaskKind");
1261
1340
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
1262
1341
  server.tool("create_product_task", 'Crea una tarea de producto desde una o varias fotos. 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), language, 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 el usuario est\xE1 en LATAM. Si te dice "t\xFA elige", omite esos campos.\n\nANTES de invocar, DILE CLARAMENTE al usuario qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task. Ejemplo: "Voy a generar landing + video en un mismo task; esto consume cr\xE9ditos de ambos outputs". Si el usuario s\xF3lo quiere un output, usa generate_landing / generate_video_creative / generate_image_creatives en vez de este tool.\n\nSi el usuario tiene varias fotos del MISMO producto (frente, lateral, detalle, uso), incl\xFAyelas todas en images[] hasta 5. Si tiene fotos de productos DIFERENTES, son llamadas separadas (una tarea por producto).\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos):\n- videoPreset (tipo/narrativa del video \u2014 \xFAnico campo de estilo)\n- multiAngleVideos (booleano)\n- angleNarratives (override per-angle de la narrativa, avanzado)\n- outputs (subconjunto de {landing,video,image}); omitido = ["landing","video","image"] en un mismo task\n- productUrl\n- creativeDirection (direcci\xF3n creativa para im\xE1genes y landing; el estilo del video se dirige con scriptNotes)\n\nGUION: para revisar/editar el guion del video antes de renderizar, usa `generate_video_creative` con `pauseForVideoScriptReview: true` en vez de este tool. Despu\xE9s de la pausa: get_video_script para leer, approve_video_script para aprobar con ediciones.\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado (ej. "estilo anime", "modelo X"), p\xEDdele una opci\xF3n v\xE1lida del enum correspondiente.', CreateProductTaskInput.shape, async (input) => {
1263
1342
  try {
@@ -1270,7 +1349,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1270
1349
  }));
1271
1350
  const selectedOutputs = input.outputs ?? DEFAULT_PRODUCT_TASK_OUTPUTS;
1272
1351
  const resolved = resolveVideoPreset(input.videoPreset);
1273
- const wireTier = resolveVideoTier(input.videoTier);
1352
+ const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
1274
1353
  const fields = flatten({
1275
1354
  "options.languageCode": input.language,
1276
1355
  "options.country": input.country,
@@ -1284,6 +1363,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1284
1363
  "options.videoStyle": resolved?.videoStyle,
1285
1364
  "options.narrativeStyle": resolved?.narrativeStyle,
1286
1365
  "options.videoTier": wireTier,
1366
+ "options.ugcFormat": resolveVideoFormatForDuration(input.durationSeconds),
1287
1367
  "options.videoResolution": input.videoResolution,
1288
1368
  "options.multiAngleVideos": input.multiAngleVideos,
1289
1369
  "options.angleNarratives": input.angleNarratives,
@@ -1293,6 +1373,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1293
1373
  "options.creativeDirection": input.creativeDirection,
1294
1374
  "options.landingStrategy": input.landingStrategy,
1295
1375
  "options.landingImageFormat": input.landingImageFormat,
1376
+ pauseForAngleReview: input.pauseForAngleReview,
1296
1377
  url: input.productUrl
1297
1378
  });
1298
1379
  const r = await api.postMultipart("/v1/ingest", {
@@ -1310,7 +1391,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1310
1391
  throw translateError(e);
1311
1392
  }
1312
1393
  });
1313
- server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`), es solo informativo: indica que el anuncio de referencia dura m\xE1s que la duraci\xF3n cotizada para esta tarea y cu\xE1nto costar\xEDa extenderla a `recommendedSec` segundos (el costo extra viene en `estimatedDeltaCredits`, si est\xE1 disponible), pero el MCP no tiene forma de aceptar ese cambio \u2014 com\xE9ntaselo al usuario y dile que lo active desde la web si le interesa.\nUse this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
1394
+ server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`), mu\xE9strale al usuario la duraci\xF3n recomendada y `estimatedDeltaCredits` si aparece. Con confirmaci\xF3n expl\xEDcita, llama `resolve_pending_action` con `action: { kind: "angle_review_approval", gapDurationSeconds: 30 }` para actualizar solo ese \xE1ngulo. Omitir el campo o enviar 15 conserva la duraci\xF3n cotizada; 30 se rechaza si no hay una recomendaci\xF3n pendiente de 30s.\nUse this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
1314
1395
  try {
1315
1396
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
1316
1397
  const signal = extra?.signal;
@@ -1367,7 +1448,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1367
1448
  throw translateError(e);
1368
1449
  }
1369
1450
  });
1370
- server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Payload: `userChoice` \u2208 { "same_product" (mismo producto, fotos son s\xF3lo \xE1ngulos distintos), "variants" (mismo producto, distintos colores/tallas/sabores), "combo_distinct" (son 2 productos distintos vendidos juntos como combo \u2014 marca la tarea como combo), "different_products" (productos distintos y el usuario quiere generar s\xF3lo uno \u2014 el backend bloquea hasta que selecciones im\xE1genes de un solo producto; normalmente NO es la respuesta correcta para destrabar) }. PREG\xDANTALE al usuario antes de invocar \u2014 `pendingUserAction.targetProductLabel` y `reason` te dan contexto para formularle la pregunta en lenguaje natural. Usa SOLO valores presentes en `pendingUserAction.options`. Si el status trae `proposedProductNames` (lista propuesta de nombres), el combo es INCIERTO: mu\xE9strale los nombres al usuario y, si confirma que son productos distintos, manda `userChoice: "combo_distinct"` JUNTO CON `confirmedProductNames` (la lista confirmada o editada, 2-8 nombres) para separarlo en productos distintos; si el usuario dice que es un solo producto, manda `userChoice: "same_product"` (sin `confirmedProductNames`). Cuando el producto son VARIANTES DE COLOR (mismo producto, distintos colores) puedes ofrecer un video "Multiescena" que muestra una toma por color: manda `userChoice: "variants"` JUNTO CON `useMultiSceneShowcase: true`. AVISA al usuario que ese modo se factura por escena (m\xE1s caro que el render normal de una sola toma) antes de activarlo; si prefiere el render normal, manda solo `userChoice: "variants"` sin el flag.\n\n\u2022 `angle_review_approval` \u2014 el usuario opto por `pauseForAngleReview: true` al crear el creativo (en `generate_image_creatives`, `generate_video_creative` o `generate_landing`). La IA ya gener\xF3 el \xE1ngulo + pain points + benefits + features y la tarea est\xE1 pausada esperando que el usuario apruebe o edite antes de renderizar. Lee `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` del `pendingUserAction` para mostr\xE1rselos al usuario en lenguaje natural ("la IA propuso este \xE1ngulo: ... \xBFlo apruebas o cambias algo?"). Payload (TODOS opcionales \u2014 sin nada = aprobar tal cual; con campos = aprobar con ediciones): `customAngle` (override del \xE1ngulo), `painPoints`, `benefits`, `features` (array de 1-10 bullets). Llamar sin ning\xFAn campo es v\xE1lido y aprueba el draft sin cambios.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
1451
+ server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 confirma c\xF3mo agrupar las fotos usando solo los valores de `pendingUserAction.options`.\n\n\u2022 `angle_review_approval` \u2014 aprueba o edita el draft despu\xE9s de `pauseForAngleReview: true`. Puedes enviar `customAngle`, `painPoints`, `benefits` y `features`; todos son opcionales y el MCP conserva el draft persistido cuando no los cambias. Si `pendingUserAction.gapDurationChoice` recomienda 30s para un winner, muestra al usuario `recommendedSec` y `estimatedDeltaCredits`, pide confirmaci\xF3n expl\xEDcita y env\xEDa `gapDurationSeconds: 30` en esta misma acci\xF3n. El MCP valida que exista una recomendaci\xF3n pendiente de 30s y entonces actualiza solo el \xE1ngulo recomendado con `double_30s`; omitir el campo o enviar 15 mantiene la duraci\xF3n cotizada.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
1371
1452
  try {
1372
1453
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
1373
1454
  const status = StatusForResolveSchema.parse(raw);
@@ -1420,7 +1501,108 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1420
1501
  if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
1421
1502
  if (input.action.benefits != null) body.benefits = input.action.benefits;
1422
1503
  if (input.action.features != null) body.features = input.action.features;
1423
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`, body);
1504
+ if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
1505
+ throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
1506
+ }
1507
+ if (input.action.gapDurationSeconds === 30) {
1508
+ const choice = pending.gapDurationChoice;
1509
+ if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
1510
+ if (choice.recommendedSec !== 30) {
1511
+ throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
1512
+ }
1513
+ body.angleVideoOptions = [
1514
+ {
1515
+ angleIndex: choice.angleIndex,
1516
+ ugcFormat: "double_30s"
1517
+ }
1518
+ ];
1519
+ }
1520
+ const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
1521
+ const resolvedTaskKind = resolveStatusTaskKind(status);
1522
+ const isVideoTask = resolvedTaskKind === "video";
1523
+ const isAutopilotTask = resolvedTaskKind === "autopilot";
1524
+ if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
1525
+ const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
1526
+ const optionsByIndex = new Map(existingOptions.map((option) => [
1527
+ option.angleIndex,
1528
+ {
1529
+ ...option
1530
+ }
1531
+ ]));
1532
+ for (const angleIndex of selectedAngleIndices) {
1533
+ optionsByIndex.set(angleIndex, {
1534
+ ...optionsByIndex.get(angleIndex) ?? {
1535
+ angleIndex
1536
+ },
1537
+ angle: body.customAngle
1538
+ });
1539
+ }
1540
+ body.angleVideoOptions = [
1541
+ ...optionsByIndex.values()
1542
+ ].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
1543
+ }
1544
+ if (isVideoTask) {
1545
+ body.selectedAngleIndices = selectedAngleIndices;
1546
+ const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
1547
+ return asText(r2);
1548
+ }
1549
+ if (isAutopilotTask) {
1550
+ const resumeBody = {
1551
+ selectedAngleIndices,
1552
+ ...status.ugcFormat ? {
1553
+ ugcFormat: status.ugcFormat
1554
+ } : {
1555
+ ugcFormat: "single_15s"
1556
+ },
1557
+ ...status.videoStyle ? {
1558
+ videoStyle: status.videoStyle
1559
+ } : {},
1560
+ ...status.videoModel ? {
1561
+ videoModel: status.videoModel
1562
+ } : {},
1563
+ ...status.videoTier ? {
1564
+ videoTier: status.videoTier
1565
+ } : {},
1566
+ ...status.videoResolution ? {
1567
+ videoResolution: status.videoResolution
1568
+ } : {},
1569
+ ...status.narrativeStyle ? {
1570
+ narrativeStyle: status.narrativeStyle
1571
+ } : {},
1572
+ ...body.customAngle != null ? {
1573
+ customAngleOverride: body.customAngle
1574
+ } : {},
1575
+ ...body.painPoints != null ? {
1576
+ painPoints: body.painPoints
1577
+ } : {},
1578
+ ...body.benefits != null ? {
1579
+ benefits: body.benefits
1580
+ } : {},
1581
+ ...body.angleVideoOptions ? {
1582
+ angleVideoOptions: body.angleVideoOptions
1583
+ } : {}
1584
+ };
1585
+ if (body.features !== void 0) {
1586
+ await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
1587
+ features: body.features
1588
+ });
1589
+ }
1590
+ const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
1591
+ return asText(r2);
1592
+ }
1593
+ const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
1594
+ const imageBody = status.kind === "image" ? {
1595
+ ...body.customAngle != null ? {
1596
+ customAngle: body.customAngle
1597
+ } : {},
1598
+ ...body.painPoints != null ? {
1599
+ painPoints: body.painPoints
1600
+ } : {},
1601
+ ...body.features != null ? {
1602
+ features: body.features
1603
+ } : {}
1604
+ } : body;
1605
+ const r = await api.postJson(route, imageBody);
1424
1606
  return asText(r);
1425
1607
  }
1426
1608
  default: {
@@ -1464,9 +1646,12 @@ var VideoInput = z3.object({
1464
1646
  "16:9",
1465
1647
  "Auto"
1466
1648
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
1467
- durationSeconds: z3.number().int().min(5).max(15).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-15). Default 15s. Para hooks cortos usa 8. Duraciones mayores a 15s (video Multiescena de 30s) NO est\xE1n disponibles v\xEDa MCP \u2014 ind\xEDcale al usuario que las genere desde la web."),
1468
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo de video. Default "auto". Opciones actuales: "auto", "ugc_dynamic" (UGC Review), "ugc_unboxing_chaos" (Unboxing), "problem_solution" (problema\u2192soluci\xF3n), "product_showcase" (Producto en acci\xF3n: una prueba funcional), "wearable_showcase" (modelaje con la prenda/accesorio puesto), "cinematic" (historia visual premium), "pixar_problem" (problema animado Pixar), "pixar_sassy" (producto que habla Pixar). V\xEDa MCP todo video renderiza a m\xE1x. 15s.'),
1469
- videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. \xDAnico valor disponible v\xEDa MCP: "basic" = "Realista": render de 15s con audio nativo, el m\xE1s natural. El estilo "Multiescena" (multi-toma, 30s) NO est\xE1 disponible v\xEDa MCP por su costo (se factura por escena) \u2014 si el usuario lo pide, dile que lo genere desde la web. Omitir el campo equivale a "basic".'),
1649
+ durationSeconds: z3.union([
1650
+ z3.literal(15),
1651
+ z3.literal(30)
1652
+ ]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
1653
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan tanto a 15s como a 30s; el formato de 30s conserva el ritmo multi-escena. Opciones: "auto", "ugc_dynamic" (UGC cl\xE1sico testimonial multi-hook), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "cinematic" (historia visual premium), "pixar_problem" (problema animado 3D estilo Pixar), "pixar_sassy" (producto que habla con personalidad), "product_showcase" (sin actor, solo producto), "wearable_showcase" (ropa/accesorios puestos).'),
1654
+ 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.'),
1470
1655
  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).'),
1471
1656
  features: z3.array(z3.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
1472
1657
  price: CreativeRefs.price,
@@ -1476,6 +1661,7 @@ var VideoInput = z3.object({
1476
1661
  cta: z3.string().optional().describe('Opcional. Call-to-action final del video. Ej. "Compra ahora", "Pide el tuyo hoy", "Link en bio". Si se omite, la IA escribe uno.'),
1477
1662
  scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
1478
1663
  pauseForVideoScriptReview: z3.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."),
1664
+ pauseForAngleReview: z3.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."),
1479
1665
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
1480
1666
  brandId: CreativeRefs.brandId
1481
1667
  }).strict();
@@ -1531,10 +1717,10 @@ var LandingInput = z3.object({
1531
1717
  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".')
1532
1718
  });
1533
1719
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
1534
- 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 (m\xE1x 15), 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: v\xEDa MCP todos los videos se generan en estilo "Realista" (render \xFAnico de 15s con audio nativo). El estilo "Multiescena" (30s, multi-toma) NO est\xE1 disponible por MCP por su costo \u2014 si el usuario lo pide, dile que lo genere desde la web.\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.', VideoInput.shape, async (input) => {
1720
+ 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) => {
1535
1721
  try {
1536
1722
  const resolved = resolveVideoPreset(input.videoPreset);
1537
- const wireTier = resolveVideoTier(input.videoTier);
1723
+ const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
1538
1724
  const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
1539
1725
  const wirePayload = {
1540
1726
  ...rest
@@ -1651,6 +1837,23 @@ function registerConnectionTools(server, api) {
1651
1837
  throw translateError(e);
1652
1838
  }
1653
1839
  });
1840
+ server.tool("list_shopify_products", 'Busca productos en una tienda Shopify conectada para elegir el target de una republicaci\xF3n. Usa el `id` devuelto junto con `publicationMode="existing"` en `publish_to_shopify`. Si `taskId` se proporciona, la respuesta tambi\xE9n indica el producto publicado actualmente para esa tarea y tienda. No modifica Shopify.', z5.object({
1841
+ shopDomain: z5.string().min(3).max(120),
1842
+ query: z5.string().trim().max(120).optional(),
1843
+ taskId: z5.string().uuid().optional()
1844
+ }).shape, async ({ shopDomain, query, taskId }) => {
1845
+ try {
1846
+ const params = new URLSearchParams({
1847
+ shopDomain
1848
+ });
1849
+ if (query) params.set("query", query);
1850
+ if (taskId) params.set("taskId", taskId);
1851
+ const r = await api.getJson(`/v1/shopify/products?${params.toString()}`);
1852
+ return asText(r);
1853
+ } catch (e) {
1854
+ throw translateError(e);
1855
+ }
1856
+ });
1654
1857
  server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z5.object({}).shape, async () => {
1655
1858
  try {
1656
1859
  const r = await api.getJson("/v1/facebook/ad-accounts");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Product Maker MCP server — cost products, generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from Claude, ChatGPT, or Gemini.",