@productmaker/mcp 0.4.0 → 0.7.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.
Files changed (3) hide show
  1. package/dist/http.js +211 -50
  2. package/dist/stdio.js +211 -50
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -496,6 +496,92 @@ __name(translateError, "translateError");
496
496
  import { z } from "zod";
497
497
  var Lang = z.string().min(2).max(10);
498
498
  var Country = z.string().min(2).max(3);
499
+ var VideoPresetEnum = z.enum([
500
+ "auto",
501
+ "ugc_dynamic",
502
+ "ugc_unboxing_chaos",
503
+ "ugc_curiosity_reveal",
504
+ "ugc_listicle_3",
505
+ "ugc_skeptic_confession",
506
+ "ugc_demo_review",
507
+ "problem_solution",
508
+ "pixar_problem",
509
+ "pixar_sassy",
510
+ "product_showcase",
511
+ "wearable_showcase"
512
+ ]);
513
+ var VIDEO_PRESET_RESOLUTION = {
514
+ auto: null,
515
+ ugc_dynamic: {
516
+ videoStyle: "ai_dynamic",
517
+ narrativeStyle: "review",
518
+ videoModel: "veo"
519
+ },
520
+ ugc_unboxing_chaos: {
521
+ videoStyle: "ai_dynamic",
522
+ narrativeStyle: "ugc_unboxing_chaos",
523
+ videoModel: "veo"
524
+ },
525
+ ugc_curiosity_reveal: {
526
+ videoStyle: "ai_dynamic",
527
+ narrativeStyle: "ugc_curiosity_reveal",
528
+ videoModel: "veo"
529
+ },
530
+ ugc_listicle_3: {
531
+ videoStyle: "ai_dynamic",
532
+ narrativeStyle: "ugc_listicle_3",
533
+ videoModel: "kling"
534
+ },
535
+ ugc_skeptic_confession: {
536
+ videoStyle: "ai_dynamic",
537
+ narrativeStyle: "ugc_skeptic_confession",
538
+ videoModel: "veo"
539
+ },
540
+ ugc_demo_review: {
541
+ videoStyle: "ai_dynamic",
542
+ narrativeStyle: "ugc_demo_review",
543
+ videoModel: "kling"
544
+ },
545
+ problem_solution: {
546
+ videoStyle: "ai_dynamic",
547
+ narrativeStyle: "problem_product_cta",
548
+ videoModel: "kling"
549
+ },
550
+ pixar_problem: {
551
+ videoStyle: "ai_dynamic",
552
+ narrativeStyle: "animated_problem_pixar",
553
+ videoModel: "kling"
554
+ },
555
+ pixar_sassy: {
556
+ videoStyle: "ai_dynamic",
557
+ narrativeStyle: "sassy_product_pixar",
558
+ videoModel: "seedance"
559
+ },
560
+ product_showcase: {
561
+ videoStyle: "product_only",
562
+ narrativeStyle: "product_showcase",
563
+ videoModel: "veo"
564
+ },
565
+ wearable_showcase: {
566
+ videoStyle: "ugc",
567
+ narrativeStyle: "wearable_showcase",
568
+ videoModel: "veo"
569
+ }
570
+ };
571
+ function resolveVideoPreset(preset) {
572
+ if (!preset) return null;
573
+ return VIDEO_PRESET_RESOLUTION[preset] ?? null;
574
+ }
575
+ __name(resolveVideoPreset, "resolveVideoPreset");
576
+ var VideoTierEnum = z.enum([
577
+ "basic",
578
+ "standard"
579
+ ]);
580
+ function resolveVideoTier(tier) {
581
+ if (!tier) return void 0;
582
+ return tier === "basic" ? "grok" : "standard";
583
+ }
584
+ __name(resolveVideoTier, "resolveVideoTier");
499
585
  var VideoStyleEnum = z.enum([
500
586
  "ugc",
501
587
  "cartoon_3d",
@@ -819,17 +905,16 @@ var CreateProductTaskInput = z2.object({
819
905
  country: Country.describe('C\xF3digo ISO del pa\xEDs objetivo. Ejemplos: "CO", "MX", "US", "AR". Define moneda, m\xE9todos de pago locales y tono. REQUERIDO \u2014 si el usuario no lo especific\xF3, PREG\xDANTALE antes de invocar. NUNCA infieras desde el contexto del chat.'),
820
906
  price: z2.string().optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
821
907
  offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
822
- videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual del video. "ugc" = creador hablando a c\xE1mara (default, mejor CTR LATAM). "cartoon_3d" = animaci\xF3n 3D. "product_only" = sin actor. "ai_dynamic" = cinematogr\xE1fico AI. Si se omite, default "ugc".'),
823
- narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" = UGC cl\xE1sico multi-hook (default, alta conversi\xF3n). "problem_product_cta" = problema\u2192soluci\xF3n. "product_showcase" = features. "metaphor_product_cta" = met\xE1fora visual. "wearable_showcase" = para ropa/accesorios. "ugc_unboxing_chaos" = unboxing agresivo. "ugc_curiosity_reveal" = hook con producto oculto. "ugc_listicle_3" = 3 razones (30s recomendado). "ugc_skeptic_confession" = esc\xE9ptico\u2192conversi\xF3n (30s recomendado). "ugc_demo_review" = demo en uso a lo largo de d\xEDas. Si se omite, default "review".'),
824
- videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). "bytedance"/"seedance" = TikTok-style. "kling"/"wan" = alternativos. "infinitalk" = lipsync largo. "grok" = grok-imagine. Si se omite, el sistema escoge seg\xFAn costo/calidad.'),
908
+ 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). Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones, ideal 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, ideal 30s), "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.'),
909
+ videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido si la duraci\xF3n del video es 15s. "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si el usuario no menciona estilo, OMITE este campo y el backend usa su default. Para duraci\xF3n 30s s\xF3lo aplica "standard" (Realista est\xE1 vetada).'),
825
910
  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."),
826
- angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; usa los mismos valores que narrativeStyle. Si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Ej: ["animated_problem_pixar","review","ugc_skeptic_confession"] para 3 \xE1ngulos distintos. Requiere multiAngleVideos:true (o videoAngleCount>1) para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan.'),
911
+ angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional, override avanzado). Usa los mismos identificadores de narrativa que videoPreset (ej. "animated_problem_pixar", "review", "ugc_skeptic_confession"). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan seg\xFAn el producto.'),
827
912
  outputs: z2.array(z2.enum([
828
913
  "landing",
829
914
  "video",
830
915
  "image"
831
916
  ])).min(2).max(3).optional().describe('Opcional. Qu\xE9 outputs generar. Default: todos ["landing","video","image"]. Para 2 outputs: ["landing","image"] (sin video), ["video","image"] (sin landing), ["landing","video"] (sin imagen). Para 1 solo output, usa generate_landing / generate_video_creative / generate_image_creatives.')
832
- });
917
+ }).strict();
833
918
  var GetTaskStatusInput = z2.object({
834
919
  taskId: z2.string().min(1),
835
920
  waitSeconds: z2.number().int().min(0).max(120).optional()
@@ -854,7 +939,7 @@ var EditTaskDraftInput = z2.object({
854
939
  })
855
940
  });
856
941
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
857
- 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). 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 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 del chat: preg\xFAntale expl\xEDcitamente, incluso si el usuario est\xE1 en LATAM. Si te dice "t\xFA elige" para price/offer, om\xEDtelos.\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- videoStyle, narrativeStyle, videoModel (enums \u2014 ver descripci\xF3n de cada campo)\n- multiAngleVideos (booleano)\n- outputs (subconjunto de {landing,video,image})\n- productUrl\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.', CreateProductTaskInput.shape, async (input) => {
942
+ 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). 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 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 del chat: preg\xFAntale expl\xEDcitamente, incluso si el usuario est\xE1 en LATAM. Si te dice "t\xFA elige" para price/offer, om\xEDtelos.\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- videoTier (estilo: basic="Realista"/standard="Multiescena")\n- multiAngleVideos (booleano)\n- angleNarratives (override per-angle de la narrativa, avanzado)\n- outputs (subconjunto de {landing,video,image})\n- productUrl\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) => {
858
943
  try {
859
944
  const imgs = await resolveImages(input.images, {
860
945
  transport
@@ -863,16 +948,19 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
863
948
  ...img,
864
949
  filename: `image-${i}.png`
865
950
  }));
951
+ const resolved = resolveVideoPreset(input.videoPreset);
952
+ const wireTier = resolveVideoTier(input.videoTier);
866
953
  const fields = flatten({
867
954
  "options.languageCode": input.language,
868
955
  "options.country": input.country,
869
956
  "options.price": input.price,
870
957
  "options.offer": input.offer,
871
- "options.videoStyle": input.videoStyle,
872
- "options.narrativeStyle": input.narrativeStyle,
873
- "options.angleNarratives": input.angleNarratives,
874
- "options.videoModel": input.videoModel,
958
+ "options.videoPreset": input.videoPreset,
959
+ "options.videoStyle": resolved?.videoStyle,
960
+ "options.narrativeStyle": resolved?.narrativeStyle,
961
+ "options.videoTier": wireTier,
875
962
  "options.multiAngleVideos": input.multiAngleVideos,
963
+ "options.angleNarratives": input.angleNarratives,
876
964
  "options.selectedOutputs": input.outputs,
877
965
  url: input.productUrl
878
966
  });
@@ -971,15 +1059,15 @@ var VideoInput = z3.object({
971
1059
  "Auto"
972
1060
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
973
1061
  durationSeconds: z3.number().int().min(5).max(60).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-60). Default 15s. Para hooks cortos usa 8, para storytelling usa 30."),
974
- narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (UGC cl\xE1sico multi-hook, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase", "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas).'),
975
- videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual. "ugc" (creador hablando, default), "cartoon_3d", "product_only", "ai_dynamic".'),
976
- videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). Ver lista de valores v\xE1lidos.'),
1062
+ 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). 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, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "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).'),
1063
+ videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido cuando durationSeconds \u2264 15 (el handler lo valida). "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si la duraci\xF3n es 30s, s\xF3lo aplica "standard". Si el usuario no menciona estilo, OMITE este campo y el backend usa su default.'),
977
1064
  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"].'),
978
1065
  price: CreativeRefs.price,
979
1066
  offer: CreativeRefs.offer,
980
1067
  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.'),
981
- scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion. Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico".')
982
- });
1068
+ 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.'),
1069
+ 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.")
1070
+ }).strict();
983
1071
  var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
984
1072
  var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
985
1073
  var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
@@ -1019,12 +1107,26 @@ var LandingInput = z3.object({
1019
1107
  isCombo: CreativeRefs.isCombo
1020
1108
  });
1021
1109
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
1022
- 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, narrativeStyle, videoStyle, videoModel, features, cta, scriptNotes.\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado, p\xEDdele una opci\xF3n v\xE1lida.', VideoInput.shape, async (input) => {
1110
+ 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, videoPreset (tipo/narrativa del video), videoTier (estilo: basic="Realista"/standard="Multiescena"), features, cta, scriptNotes.\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) => {
1023
1111
  try {
1112
+ if (input.videoTier === "basic" && (input.durationSeconds ?? 15) > 15) {
1113
+ throw new Error('videoTier="basic" s\xF3lo admite durationSeconds \u2264 15. Para 30s usa "standard".');
1114
+ }
1115
+ const resolved = resolveVideoPreset(input.videoPreset);
1116
+ const wireTier = resolveVideoTier(input.videoTier);
1117
+ const { videoTier: _publicTier, ...rest } = input;
1118
+ const wirePayload = {
1119
+ ...rest
1120
+ };
1121
+ if (resolved) {
1122
+ wirePayload.videoStyle = resolved.videoStyle;
1123
+ wirePayload.narrativeStyle = resolved.narrativeStyle;
1124
+ }
1125
+ if (wireTier !== void 0) wirePayload.videoTier = wireTier;
1024
1126
  return await submitCreative(api, {
1025
1127
  path: "/v1/creatives/videos",
1026
1128
  estimatedSeconds: 300,
1027
- input,
1129
+ input: wirePayload,
1028
1130
  transport
1029
1131
  });
1030
1132
  } catch (e) {
@@ -1058,10 +1160,68 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
1058
1160
  }
1059
1161
  __name(registerCreativeTools, "registerCreativeTools");
1060
1162
 
1061
- // src/tools/connections.ts
1163
+ // src/tools/script-review.ts
1062
1164
  import { z as z4 } from "zod";
1165
+ var GetVideoScriptInput = z4.object({
1166
+ taskId: z4.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
1167
+ angleKey: z4.string().min(1).optional().describe('Clave del \xE1ngulo cuyo guion se quiere leer (formato "angle_0", "angle_1", ...). REQUERIDO para tareas multi-angle: si se omite, el backend resuelve a "angle_0" LITERAL (no "el primer pendiente"), as\xED que en multi-angle donde angle_0 ya est\xE9 aprobado/saltado obtendr\xEDas status="approved" y nunca alcanzar\xEDas el \xE1ngulo realmente pendiente. Para single-angle puede omitirse.')
1168
+ }).strict();
1169
+ var ApproveSceneEdit = z4.object({
1170
+ index: z4.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
1171
+ dialogue: z4.string().min(1).refine((v) => v.trim().length > 0, {
1172
+ message: "dialogue no puede ser s\xF3lo espacios"
1173
+ }).describe("Di\xE1logo final de la escena. NO puede estar vac\xEDo ni ser s\xF3lo espacios. Puede ser el original sin cambios o la edici\xF3n del usuario.")
1174
+ }).strict();
1175
+ var ApproveVideoScriptInput = z4.object({
1176
+ taskId: z4.string().min(1).describe("ID de la tarea."),
1177
+ angleKey: z4.string().min(1).optional().describe('Mismo angleKey usado en get_video_script. REQUERIDO en multi-angle (si se omite el backend resuelve a "angle_0" literal). En single-angle puede omitirse.'),
1178
+ scenes: z4.array(ApproveSceneEdit).min(1).describe("Escenas con los di\xE1logos finales. El backend mergea por `index` \u2014 las escenas NO incluidas conservan el di\xE1logo original. Debe haber al menos 1 entrada (scenes vac\xEDo = 400). Para cada entrada que mandes con un dialogue distinto del original, el backend valida minWords \u2264 words \u2264 maxWords y rechaza con 400 si te sales del rango.")
1179
+ }).strict();
1180
+ var RegenerateVideoScriptInput = z4.object({
1181
+ taskId: z4.string().min(1).describe("ID de la tarea."),
1182
+ angleKey: z4.string().min(1).describe('Clave del \xE1ngulo a regenerar. Single-angle: pasa "angle_0". Multi-angle: el mismo que devuelve get_video_script. NO se puede omitir \u2014 el backend lo exige siempre.'),
1183
+ narrativeStyle: NarrativeStyleEnum.describe('Narrativa para el nuevo guion (mismos valores que angleNarratives). REQUERIDO \u2014 el backend NO acepta "mantener la actual"; siempre tienes que elegir una. Si el usuario quiere re-rolear con la misma narrativa, pasa la que ya tiene el \xE1ngulo (la ves en get_video_script o en get_task_status).')
1184
+ }).strict();
1185
+ function registerScriptReviewTools(server, api) {
1186
+ server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId? }] }.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, async (input) => {
1187
+ try {
1188
+ const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
1189
+ const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
1190
+ return asText(r);
1191
+ } catch (e) {
1192
+ throw translateError(e);
1193
+ }
1194
+ });
1195
+ server.tool("regenerate_video_script", 'Re-rolea (regenera) el guion del video pendiente de revisi\xF3n. \xDAsalo cuando:\n- get_video_script devuelve status="error" (el LLM fall\xF3 al redactar).\n- El usuario rechaza el guion entero y prefiere reintentar (no editar l\xEDnea por l\xEDnea).\n- Quieres probar otra narrativa (pasa `narrativeStyle`).\n\nFlujo: llama este tool \u2192 polea get_video_script hasta que vuelva a status="ready" \u2192 mu\xE9strale el nuevo guion al usuario \u2192 approve_video_script.\n\nIMPORTANTE: regenerar CUESTA cr\xE9ditos de LLM. No lo invoques sin confirmaci\xF3n del usuario salvo que el status sea "error" (sin retry el usuario no tiene salida).\n\nMULTI-ANGLE: pasa el mismo `angleKey` que usaste con get_video_script.', RegenerateVideoScriptInput.shape, async (input) => {
1196
+ try {
1197
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/regenerate`, {
1198
+ angleKey: input.angleKey,
1199
+ narrativeStyle: input.narrativeStyle
1200
+ });
1201
+ return asText(r);
1202
+ } catch (e) {
1203
+ throw translateError(e);
1204
+ }
1205
+ });
1206
+ server.tool("approve_video_script", 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO las valida si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas). Si quieres que pasen tal cual, simplemente no las incluyas en scenes[].\n4. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n5. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.', ApproveVideoScriptInput.shape, async (input) => {
1207
+ try {
1208
+ const body = {
1209
+ scenes: input.scenes
1210
+ };
1211
+ if (input.angleKey) body.angleKey = input.angleKey;
1212
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
1213
+ return asText(r);
1214
+ } catch (e) {
1215
+ throw translateError(e);
1216
+ }
1217
+ });
1218
+ }
1219
+ __name(registerScriptReviewTools, "registerScriptReviewTools");
1220
+
1221
+ // src/tools/connections.ts
1222
+ import { z as z5 } from "zod";
1063
1223
  function registerConnectionTools(server, api) {
1064
- server.tool("list_shopify_shops", "Lista las tiendas Shopify conectadas por este usuario. Si la lista est\xE1 vac\xEDa, dile al usuario que conecte una tienda en productmaker.app/integrations antes de intentar publicar.", z4.object({}).shape, async () => {
1224
+ server.tool("list_shopify_shops", "Lista las tiendas Shopify conectadas por este usuario. Si la lista est\xE1 vac\xEDa, dile al usuario que conecte una tienda en productmaker.app/integrations antes de intentar publicar.", z5.object({}).shape, async () => {
1065
1225
  try {
1066
1226
  const r = await api.getJson("/v1/shopify/shops");
1067
1227
  return asText(r);
@@ -1069,7 +1229,7 @@ function registerConnectionTools(server, api) {
1069
1229
  throw translateError(e);
1070
1230
  }
1071
1231
  });
1072
- 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.", z4.object({}).shape, async () => {
1232
+ 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 () => {
1073
1233
  try {
1074
1234
  const r = await api.getJson("/v1/facebook/ad-accounts");
1075
1235
  return asText(r);
@@ -1077,7 +1237,7 @@ function registerConnectionTools(server, api) {
1077
1237
  throw translateError(e);
1078
1238
  }
1079
1239
  });
1080
- server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z4.object({}).shape, async () => {
1240
+ server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z5.object({}).shape, async () => {
1081
1241
  try {
1082
1242
  const r = await api.getJson("/v1/tiktok/advertisers");
1083
1243
  return asText(r);
@@ -1089,40 +1249,40 @@ function registerConnectionTools(server, api) {
1089
1249
  __name(registerConnectionTools, "registerConnectionTools");
1090
1250
 
1091
1251
  // src/tools/publish.ts
1092
- import { z as z5 } from "zod";
1093
- var ShopifyInput = z5.object({
1094
- taskId: z5.string().min(1),
1095
- shopDomain: z5.string().min(1),
1096
- numLandingImages: z5.number().int().min(1).max(7).optional(),
1097
- variantIndex: z5.number().int().min(0).optional(),
1252
+ import { z as z6 } from "zod";
1253
+ var ShopifyInput = z6.object({
1254
+ taskId: z6.string().min(1),
1255
+ shopDomain: z6.string().min(1),
1256
+ numLandingImages: z6.number().int().min(1).max(7).optional(),
1257
+ variantIndex: z6.number().int().min(0).optional(),
1098
1258
  /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
1099
1259
  * When true, the landing renders variant-picker pills; when false, it ships
1100
1260
  * a single-option product page. Defaults to false. */
1101
- showVariantPicker: z5.boolean().optional()
1261
+ showVariantPicker: z6.boolean().optional()
1102
1262
  });
1103
- var McpCampaignConfig = z5.object({
1104
- dailyBudgetUsd: z5.number().positive().optional(),
1263
+ var McpCampaignConfig = z6.object({
1264
+ dailyBudgetUsd: z6.number().positive().optional(),
1105
1265
  /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
1106
1266
  * ad set). TikTok takes only the first entry. */
1107
- targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
1108
- campaignName: z5.string().trim().min(1).max(100).optional()
1267
+ targetCountries: z6.array(z6.string().length(2)).max(1).optional(),
1268
+ campaignName: z6.string().trim().min(1).max(100).optional()
1109
1269
  });
1110
- var MetaInput = z5.object({
1111
- taskId: z5.string().min(1),
1112
- adAccountId: z5.string().min(1),
1113
- pixelId: z5.string().min(1).optional(),
1114
- landingUrl: z5.string().url().optional(),
1270
+ var MetaInput = z6.object({
1271
+ taskId: z6.string().min(1),
1272
+ adAccountId: z6.string().min(1),
1273
+ pixelId: z6.string().min(1).optional(),
1274
+ landingUrl: z6.string().url().optional(),
1115
1275
  /** Select which angles to publish by 0-based index. Omit to publish all
1116
1276
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1117
- angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1277
+ angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1118
1278
  campaignConfig: McpCampaignConfig.optional()
1119
1279
  });
1120
- var TikTokInput = z5.object({
1121
- taskId: z5.string().min(1),
1122
- advertiserId: z5.string().min(1),
1123
- pixelId: z5.string().min(1).optional(),
1124
- landingUrl: z5.string().url().optional(),
1125
- angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1280
+ var TikTokInput = z6.object({
1281
+ taskId: z6.string().min(1),
1282
+ advertiserId: z6.string().min(1),
1283
+ pixelId: z6.string().min(1).optional(),
1284
+ landingUrl: z6.string().url().optional(),
1285
+ angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1126
1286
  campaignConfig: McpCampaignConfig.optional()
1127
1287
  });
1128
1288
  function registerPublishTools(server, api) {
@@ -1224,13 +1384,13 @@ function toTikTokCampaignConfig(cfg) {
1224
1384
  __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
1225
1385
 
1226
1386
  // src/tools/campaigns.ts
1227
- import { z as z6 } from "zod";
1228
- var MetaStatusInput = z6.object({
1229
- campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1387
+ import { z as z7 } from "zod";
1388
+ var MetaStatusInput = z7.object({
1389
+ campaignId: z7.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1230
1390
  });
1231
- var TikTokStatusInput = z6.object({
1232
- campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1233
- advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
1391
+ var TikTokStatusInput = z7.object({
1392
+ campaignId: z7.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1393
+ advertiserId: z7.string().min(1).describe("The same advertiserId used to publish the campaign.")
1234
1394
  });
1235
1395
  function registerCampaignTools(server, api) {
1236
1396
  server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, async (input) => {
@@ -1348,6 +1508,7 @@ function createServer(opts) {
1348
1508
  registerCreativeTools(server, api, {
1349
1509
  transport
1350
1510
  });
1511
+ registerScriptReviewTools(server, api);
1351
1512
  registerConnectionTools(server, api);
1352
1513
  registerPublishTools(server, api);
1353
1514
  registerCampaignTools(server, api);
package/dist/stdio.js CHANGED
@@ -493,6 +493,92 @@ __name(translateError, "translateError");
493
493
  import { z } from "zod";
494
494
  var Lang = z.string().min(2).max(10);
495
495
  var Country = z.string().min(2).max(3);
496
+ var VideoPresetEnum = z.enum([
497
+ "auto",
498
+ "ugc_dynamic",
499
+ "ugc_unboxing_chaos",
500
+ "ugc_curiosity_reveal",
501
+ "ugc_listicle_3",
502
+ "ugc_skeptic_confession",
503
+ "ugc_demo_review",
504
+ "problem_solution",
505
+ "pixar_problem",
506
+ "pixar_sassy",
507
+ "product_showcase",
508
+ "wearable_showcase"
509
+ ]);
510
+ var VIDEO_PRESET_RESOLUTION = {
511
+ auto: null,
512
+ ugc_dynamic: {
513
+ videoStyle: "ai_dynamic",
514
+ narrativeStyle: "review",
515
+ videoModel: "veo"
516
+ },
517
+ ugc_unboxing_chaos: {
518
+ videoStyle: "ai_dynamic",
519
+ narrativeStyle: "ugc_unboxing_chaos",
520
+ videoModel: "veo"
521
+ },
522
+ ugc_curiosity_reveal: {
523
+ videoStyle: "ai_dynamic",
524
+ narrativeStyle: "ugc_curiosity_reveal",
525
+ videoModel: "veo"
526
+ },
527
+ ugc_listicle_3: {
528
+ videoStyle: "ai_dynamic",
529
+ narrativeStyle: "ugc_listicle_3",
530
+ videoModel: "kling"
531
+ },
532
+ ugc_skeptic_confession: {
533
+ videoStyle: "ai_dynamic",
534
+ narrativeStyle: "ugc_skeptic_confession",
535
+ videoModel: "veo"
536
+ },
537
+ ugc_demo_review: {
538
+ videoStyle: "ai_dynamic",
539
+ narrativeStyle: "ugc_demo_review",
540
+ videoModel: "kling"
541
+ },
542
+ problem_solution: {
543
+ videoStyle: "ai_dynamic",
544
+ narrativeStyle: "problem_product_cta",
545
+ videoModel: "kling"
546
+ },
547
+ pixar_problem: {
548
+ videoStyle: "ai_dynamic",
549
+ narrativeStyle: "animated_problem_pixar",
550
+ videoModel: "kling"
551
+ },
552
+ pixar_sassy: {
553
+ videoStyle: "ai_dynamic",
554
+ narrativeStyle: "sassy_product_pixar",
555
+ videoModel: "seedance"
556
+ },
557
+ product_showcase: {
558
+ videoStyle: "product_only",
559
+ narrativeStyle: "product_showcase",
560
+ videoModel: "veo"
561
+ },
562
+ wearable_showcase: {
563
+ videoStyle: "ugc",
564
+ narrativeStyle: "wearable_showcase",
565
+ videoModel: "veo"
566
+ }
567
+ };
568
+ function resolveVideoPreset(preset) {
569
+ if (!preset) return null;
570
+ return VIDEO_PRESET_RESOLUTION[preset] ?? null;
571
+ }
572
+ __name(resolveVideoPreset, "resolveVideoPreset");
573
+ var VideoTierEnum = z.enum([
574
+ "basic",
575
+ "standard"
576
+ ]);
577
+ function resolveVideoTier(tier) {
578
+ if (!tier) return void 0;
579
+ return tier === "basic" ? "grok" : "standard";
580
+ }
581
+ __name(resolveVideoTier, "resolveVideoTier");
496
582
  var VideoStyleEnum = z.enum([
497
583
  "ugc",
498
584
  "cartoon_3d",
@@ -775,17 +861,16 @@ var CreateProductTaskInput = z2.object({
775
861
  country: Country.describe('C\xF3digo ISO del pa\xEDs objetivo. Ejemplos: "CO", "MX", "US", "AR". Define moneda, m\xE9todos de pago locales y tono. REQUERIDO \u2014 si el usuario no lo especific\xF3, PREG\xDANTALE antes de invocar. NUNCA infieras desde el contexto del chat.'),
776
862
  price: z2.string().optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
777
863
  offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
778
- videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual del video. "ugc" = creador hablando a c\xE1mara (default, mejor CTR LATAM). "cartoon_3d" = animaci\xF3n 3D. "product_only" = sin actor. "ai_dynamic" = cinematogr\xE1fico AI. Si se omite, default "ugc".'),
779
- narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" = UGC cl\xE1sico multi-hook (default, alta conversi\xF3n). "problem_product_cta" = problema\u2192soluci\xF3n. "product_showcase" = features. "metaphor_product_cta" = met\xE1fora visual. "wearable_showcase" = para ropa/accesorios. "ugc_unboxing_chaos" = unboxing agresivo. "ugc_curiosity_reveal" = hook con producto oculto. "ugc_listicle_3" = 3 razones (30s recomendado). "ugc_skeptic_confession" = esc\xE9ptico\u2192conversi\xF3n (30s recomendado). "ugc_demo_review" = demo en uso a lo largo de d\xEDas. Si se omite, default "review".'),
780
- videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). "bytedance"/"seedance" = TikTok-style. "kling"/"wan" = alternativos. "infinitalk" = lipsync largo. "grok" = grok-imagine. Si se omite, el sistema escoge seg\xFAn costo/calidad.'),
864
+ 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). Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones, ideal 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, ideal 30s), "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.'),
865
+ videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido si la duraci\xF3n del video es 15s. "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si el usuario no menciona estilo, OMITE este campo y el backend usa su default. Para duraci\xF3n 30s s\xF3lo aplica "standard" (Realista est\xE1 vetada).'),
781
866
  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."),
782
- angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; usa los mismos valores que narrativeStyle. Si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Ej: ["animated_problem_pixar","review","ugc_skeptic_confession"] para 3 \xE1ngulos distintos. Requiere multiAngleVideos:true (o videoAngleCount>1) para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan.'),
867
+ angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional, override avanzado). Usa los mismos identificadores de narrativa que videoPreset (ej. "animated_problem_pixar", "review", "ugc_skeptic_confession"). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan seg\xFAn el producto.'),
783
868
  outputs: z2.array(z2.enum([
784
869
  "landing",
785
870
  "video",
786
871
  "image"
787
872
  ])).min(2).max(3).optional().describe('Opcional. Qu\xE9 outputs generar. Default: todos ["landing","video","image"]. Para 2 outputs: ["landing","image"] (sin video), ["video","image"] (sin landing), ["landing","video"] (sin imagen). Para 1 solo output, usa generate_landing / generate_video_creative / generate_image_creatives.')
788
- });
873
+ }).strict();
789
874
  var GetTaskStatusInput = z2.object({
790
875
  taskId: z2.string().min(1),
791
876
  waitSeconds: z2.number().int().min(0).max(120).optional()
@@ -810,7 +895,7 @@ var EditTaskDraftInput = z2.object({
810
895
  })
811
896
  });
812
897
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
813
- 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). 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 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 del chat: preg\xFAntale expl\xEDcitamente, incluso si el usuario est\xE1 en LATAM. Si te dice "t\xFA elige" para price/offer, om\xEDtelos.\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- videoStyle, narrativeStyle, videoModel (enums \u2014 ver descripci\xF3n de cada campo)\n- multiAngleVideos (booleano)\n- outputs (subconjunto de {landing,video,image})\n- productUrl\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.', CreateProductTaskInput.shape, async (input) => {
898
+ 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). 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 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 del chat: preg\xFAntale expl\xEDcitamente, incluso si el usuario est\xE1 en LATAM. Si te dice "t\xFA elige" para price/offer, om\xEDtelos.\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- videoTier (estilo: basic="Realista"/standard="Multiescena")\n- multiAngleVideos (booleano)\n- angleNarratives (override per-angle de la narrativa, avanzado)\n- outputs (subconjunto de {landing,video,image})\n- productUrl\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) => {
814
899
  try {
815
900
  const imgs = await resolveImages(input.images, {
816
901
  transport
@@ -819,16 +904,19 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
819
904
  ...img,
820
905
  filename: `image-${i}.png`
821
906
  }));
907
+ const resolved = resolveVideoPreset(input.videoPreset);
908
+ const wireTier = resolveVideoTier(input.videoTier);
822
909
  const fields = flatten({
823
910
  "options.languageCode": input.language,
824
911
  "options.country": input.country,
825
912
  "options.price": input.price,
826
913
  "options.offer": input.offer,
827
- "options.videoStyle": input.videoStyle,
828
- "options.narrativeStyle": input.narrativeStyle,
829
- "options.angleNarratives": input.angleNarratives,
830
- "options.videoModel": input.videoModel,
914
+ "options.videoPreset": input.videoPreset,
915
+ "options.videoStyle": resolved?.videoStyle,
916
+ "options.narrativeStyle": resolved?.narrativeStyle,
917
+ "options.videoTier": wireTier,
831
918
  "options.multiAngleVideos": input.multiAngleVideos,
919
+ "options.angleNarratives": input.angleNarratives,
832
920
  "options.selectedOutputs": input.outputs,
833
921
  url: input.productUrl
834
922
  });
@@ -927,15 +1015,15 @@ var VideoInput = z3.object({
927
1015
  "Auto"
928
1016
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
929
1017
  durationSeconds: z3.number().int().min(5).max(60).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-60). Default 15s. Para hooks cortos usa 8, para storytelling usa 30."),
930
- narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (UGC cl\xE1sico multi-hook, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase", "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas).'),
931
- videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual. "ugc" (creador hablando, default), "cartoon_3d", "product_only", "ai_dynamic".'),
932
- videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). Ver lista de valores v\xE1lidos.'),
1018
+ 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). 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, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "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).'),
1019
+ videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido cuando durationSeconds \u2264 15 (el handler lo valida). "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si la duraci\xF3n es 30s, s\xF3lo aplica "standard". Si el usuario no menciona estilo, OMITE este campo y el backend usa su default.'),
933
1020
  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"].'),
934
1021
  price: CreativeRefs.price,
935
1022
  offer: CreativeRefs.offer,
936
1023
  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.'),
937
- scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion. Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico".')
938
- });
1024
+ 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.'),
1025
+ 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.")
1026
+ }).strict();
939
1027
  var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
940
1028
  var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
941
1029
  var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
@@ -975,12 +1063,26 @@ var LandingInput = z3.object({
975
1063
  isCombo: CreativeRefs.isCombo
976
1064
  });
977
1065
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
978
- 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, narrativeStyle, videoStyle, videoModel, features, cta, scriptNotes.\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado, p\xEDdele una opci\xF3n v\xE1lida.', VideoInput.shape, async (input) => {
1066
+ 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, videoPreset (tipo/narrativa del video), videoTier (estilo: basic="Realista"/standard="Multiescena"), features, cta, scriptNotes.\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) => {
979
1067
  try {
1068
+ if (input.videoTier === "basic" && (input.durationSeconds ?? 15) > 15) {
1069
+ throw new Error('videoTier="basic" s\xF3lo admite durationSeconds \u2264 15. Para 30s usa "standard".');
1070
+ }
1071
+ const resolved = resolveVideoPreset(input.videoPreset);
1072
+ const wireTier = resolveVideoTier(input.videoTier);
1073
+ const { videoTier: _publicTier, ...rest } = input;
1074
+ const wirePayload = {
1075
+ ...rest
1076
+ };
1077
+ if (resolved) {
1078
+ wirePayload.videoStyle = resolved.videoStyle;
1079
+ wirePayload.narrativeStyle = resolved.narrativeStyle;
1080
+ }
1081
+ if (wireTier !== void 0) wirePayload.videoTier = wireTier;
980
1082
  return await submitCreative(api, {
981
1083
  path: "/v1/creatives/videos",
982
1084
  estimatedSeconds: 300,
983
- input,
1085
+ input: wirePayload,
984
1086
  transport
985
1087
  });
986
1088
  } catch (e) {
@@ -1014,10 +1116,68 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
1014
1116
  }
1015
1117
  __name(registerCreativeTools, "registerCreativeTools");
1016
1118
 
1017
- // src/tools/connections.ts
1119
+ // src/tools/script-review.ts
1018
1120
  import { z as z4 } from "zod";
1121
+ var GetVideoScriptInput = z4.object({
1122
+ taskId: z4.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
1123
+ angleKey: z4.string().min(1).optional().describe('Clave del \xE1ngulo cuyo guion se quiere leer (formato "angle_0", "angle_1", ...). REQUERIDO para tareas multi-angle: si se omite, el backend resuelve a "angle_0" LITERAL (no "el primer pendiente"), as\xED que en multi-angle donde angle_0 ya est\xE9 aprobado/saltado obtendr\xEDas status="approved" y nunca alcanzar\xEDas el \xE1ngulo realmente pendiente. Para single-angle puede omitirse.')
1124
+ }).strict();
1125
+ var ApproveSceneEdit = z4.object({
1126
+ index: z4.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
1127
+ dialogue: z4.string().min(1).refine((v) => v.trim().length > 0, {
1128
+ message: "dialogue no puede ser s\xF3lo espacios"
1129
+ }).describe("Di\xE1logo final de la escena. NO puede estar vac\xEDo ni ser s\xF3lo espacios. Puede ser el original sin cambios o la edici\xF3n del usuario.")
1130
+ }).strict();
1131
+ var ApproveVideoScriptInput = z4.object({
1132
+ taskId: z4.string().min(1).describe("ID de la tarea."),
1133
+ angleKey: z4.string().min(1).optional().describe('Mismo angleKey usado en get_video_script. REQUERIDO en multi-angle (si se omite el backend resuelve a "angle_0" literal). En single-angle puede omitirse.'),
1134
+ scenes: z4.array(ApproveSceneEdit).min(1).describe("Escenas con los di\xE1logos finales. El backend mergea por `index` \u2014 las escenas NO incluidas conservan el di\xE1logo original. Debe haber al menos 1 entrada (scenes vac\xEDo = 400). Para cada entrada que mandes con un dialogue distinto del original, el backend valida minWords \u2264 words \u2264 maxWords y rechaza con 400 si te sales del rango.")
1135
+ }).strict();
1136
+ var RegenerateVideoScriptInput = z4.object({
1137
+ taskId: z4.string().min(1).describe("ID de la tarea."),
1138
+ angleKey: z4.string().min(1).describe('Clave del \xE1ngulo a regenerar. Single-angle: pasa "angle_0". Multi-angle: el mismo que devuelve get_video_script. NO se puede omitir \u2014 el backend lo exige siempre.'),
1139
+ narrativeStyle: NarrativeStyleEnum.describe('Narrativa para el nuevo guion (mismos valores que angleNarratives). REQUERIDO \u2014 el backend NO acepta "mantener la actual"; siempre tienes que elegir una. Si el usuario quiere re-rolear con la misma narrativa, pasa la que ya tiene el \xE1ngulo (la ves en get_video_script o en get_task_status).')
1140
+ }).strict();
1141
+ function registerScriptReviewTools(server, api) {
1142
+ server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId? }] }.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, async (input) => {
1143
+ try {
1144
+ const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
1145
+ const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
1146
+ return asText(r);
1147
+ } catch (e) {
1148
+ throw translateError(e);
1149
+ }
1150
+ });
1151
+ server.tool("regenerate_video_script", 'Re-rolea (regenera) el guion del video pendiente de revisi\xF3n. \xDAsalo cuando:\n- get_video_script devuelve status="error" (el LLM fall\xF3 al redactar).\n- El usuario rechaza el guion entero y prefiere reintentar (no editar l\xEDnea por l\xEDnea).\n- Quieres probar otra narrativa (pasa `narrativeStyle`).\n\nFlujo: llama este tool \u2192 polea get_video_script hasta que vuelva a status="ready" \u2192 mu\xE9strale el nuevo guion al usuario \u2192 approve_video_script.\n\nIMPORTANTE: regenerar CUESTA cr\xE9ditos de LLM. No lo invoques sin confirmaci\xF3n del usuario salvo que el status sea "error" (sin retry el usuario no tiene salida).\n\nMULTI-ANGLE: pasa el mismo `angleKey` que usaste con get_video_script.', RegenerateVideoScriptInput.shape, async (input) => {
1152
+ try {
1153
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/regenerate`, {
1154
+ angleKey: input.angleKey,
1155
+ narrativeStyle: input.narrativeStyle
1156
+ });
1157
+ return asText(r);
1158
+ } catch (e) {
1159
+ throw translateError(e);
1160
+ }
1161
+ });
1162
+ server.tool("approve_video_script", 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO las valida si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas). Si quieres que pasen tal cual, simplemente no las incluyas en scenes[].\n4. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n5. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.', ApproveVideoScriptInput.shape, async (input) => {
1163
+ try {
1164
+ const body = {
1165
+ scenes: input.scenes
1166
+ };
1167
+ if (input.angleKey) body.angleKey = input.angleKey;
1168
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
1169
+ return asText(r);
1170
+ } catch (e) {
1171
+ throw translateError(e);
1172
+ }
1173
+ });
1174
+ }
1175
+ __name(registerScriptReviewTools, "registerScriptReviewTools");
1176
+
1177
+ // src/tools/connections.ts
1178
+ import { z as z5 } from "zod";
1019
1179
  function registerConnectionTools(server, api) {
1020
- server.tool("list_shopify_shops", "Lista las tiendas Shopify conectadas por este usuario. Si la lista est\xE1 vac\xEDa, dile al usuario que conecte una tienda en productmaker.app/integrations antes de intentar publicar.", z4.object({}).shape, async () => {
1180
+ server.tool("list_shopify_shops", "Lista las tiendas Shopify conectadas por este usuario. Si la lista est\xE1 vac\xEDa, dile al usuario que conecte una tienda en productmaker.app/integrations antes de intentar publicar.", z5.object({}).shape, async () => {
1021
1181
  try {
1022
1182
  const r = await api.getJson("/v1/shopify/shops");
1023
1183
  return asText(r);
@@ -1025,7 +1185,7 @@ function registerConnectionTools(server, api) {
1025
1185
  throw translateError(e);
1026
1186
  }
1027
1187
  });
1028
- 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.", z4.object({}).shape, async () => {
1188
+ 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 () => {
1029
1189
  try {
1030
1190
  const r = await api.getJson("/v1/facebook/ad-accounts");
1031
1191
  return asText(r);
@@ -1033,7 +1193,7 @@ function registerConnectionTools(server, api) {
1033
1193
  throw translateError(e);
1034
1194
  }
1035
1195
  });
1036
- server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z4.object({}).shape, async () => {
1196
+ server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z5.object({}).shape, async () => {
1037
1197
  try {
1038
1198
  const r = await api.getJson("/v1/tiktok/advertisers");
1039
1199
  return asText(r);
@@ -1045,40 +1205,40 @@ function registerConnectionTools(server, api) {
1045
1205
  __name(registerConnectionTools, "registerConnectionTools");
1046
1206
 
1047
1207
  // src/tools/publish.ts
1048
- import { z as z5 } from "zod";
1049
- var ShopifyInput = z5.object({
1050
- taskId: z5.string().min(1),
1051
- shopDomain: z5.string().min(1),
1052
- numLandingImages: z5.number().int().min(1).max(7).optional(),
1053
- variantIndex: z5.number().int().min(0).optional(),
1208
+ import { z as z6 } from "zod";
1209
+ var ShopifyInput = z6.object({
1210
+ taskId: z6.string().min(1),
1211
+ shopDomain: z6.string().min(1),
1212
+ numLandingImages: z6.number().int().min(1).max(7).optional(),
1213
+ variantIndex: z6.number().int().min(0).optional(),
1054
1214
  /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
1055
1215
  * When true, the landing renders variant-picker pills; when false, it ships
1056
1216
  * a single-option product page. Defaults to false. */
1057
- showVariantPicker: z5.boolean().optional()
1217
+ showVariantPicker: z6.boolean().optional()
1058
1218
  });
1059
- var McpCampaignConfig = z5.object({
1060
- dailyBudgetUsd: z5.number().positive().optional(),
1219
+ var McpCampaignConfig = z6.object({
1220
+ dailyBudgetUsd: z6.number().positive().optional(),
1061
1221
  /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
1062
1222
  * ad set). TikTok takes only the first entry. */
1063
- targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
1064
- campaignName: z5.string().trim().min(1).max(100).optional()
1223
+ targetCountries: z6.array(z6.string().length(2)).max(1).optional(),
1224
+ campaignName: z6.string().trim().min(1).max(100).optional()
1065
1225
  });
1066
- var MetaInput = z5.object({
1067
- taskId: z5.string().min(1),
1068
- adAccountId: z5.string().min(1),
1069
- pixelId: z5.string().min(1).optional(),
1070
- landingUrl: z5.string().url().optional(),
1226
+ var MetaInput = z6.object({
1227
+ taskId: z6.string().min(1),
1228
+ adAccountId: z6.string().min(1),
1229
+ pixelId: z6.string().min(1).optional(),
1230
+ landingUrl: z6.string().url().optional(),
1071
1231
  /** Select which angles to publish by 0-based index. Omit to publish all
1072
1232
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1073
- angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1233
+ angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1074
1234
  campaignConfig: McpCampaignConfig.optional()
1075
1235
  });
1076
- var TikTokInput = z5.object({
1077
- taskId: z5.string().min(1),
1078
- advertiserId: z5.string().min(1),
1079
- pixelId: z5.string().min(1).optional(),
1080
- landingUrl: z5.string().url().optional(),
1081
- angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1236
+ var TikTokInput = z6.object({
1237
+ taskId: z6.string().min(1),
1238
+ advertiserId: z6.string().min(1),
1239
+ pixelId: z6.string().min(1).optional(),
1240
+ landingUrl: z6.string().url().optional(),
1241
+ angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1082
1242
  campaignConfig: McpCampaignConfig.optional()
1083
1243
  });
1084
1244
  function registerPublishTools(server, api) {
@@ -1180,13 +1340,13 @@ function toTikTokCampaignConfig(cfg) {
1180
1340
  __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
1181
1341
 
1182
1342
  // src/tools/campaigns.ts
1183
- import { z as z6 } from "zod";
1184
- var MetaStatusInput = z6.object({
1185
- campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1343
+ import { z as z7 } from "zod";
1344
+ var MetaStatusInput = z7.object({
1345
+ campaignId: z7.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1186
1346
  });
1187
- var TikTokStatusInput = z6.object({
1188
- campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1189
- advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
1347
+ var TikTokStatusInput = z7.object({
1348
+ campaignId: z7.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1349
+ advertiserId: z7.string().min(1).describe("The same advertiserId used to publish the campaign.")
1190
1350
  });
1191
1351
  function registerCampaignTools(server, api) {
1192
1352
  server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, async (input) => {
@@ -1304,6 +1464,7 @@ function createServer(opts) {
1304
1464
  registerCreativeTools(server, api, {
1305
1465
  transport
1306
1466
  });
1467
+ registerScriptReviewTools(server, api);
1307
1468
  registerConnectionTools(server, api);
1308
1469
  registerPublishTools(server, api);
1309
1470
  registerCampaignTools(server, api);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Product Maker MCP server — connect Claude Desktop, Claude Code, ChatGPT, or Gemini to Product Maker AI to generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from your AI assistant.",