@productmaker/mcp 0.3.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.
- package/dist/http.js +219 -49
- package/dist/stdio.js +219 -49
- package/package.json +23 -14
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",
|
|
@@ -507,7 +593,14 @@ var NarrativeStyleEnum = z.enum([
|
|
|
507
593
|
"problem_product_cta",
|
|
508
594
|
"product_showcase",
|
|
509
595
|
"metaphor_product_cta",
|
|
510
|
-
"wearable_showcase"
|
|
596
|
+
"wearable_showcase",
|
|
597
|
+
"animated_problem_pixar",
|
|
598
|
+
"sassy_product_pixar",
|
|
599
|
+
"ugc_unboxing_chaos",
|
|
600
|
+
"ugc_curiosity_reveal",
|
|
601
|
+
"ugc_listicle_3",
|
|
602
|
+
"ugc_skeptic_confession",
|
|
603
|
+
"ugc_demo_review"
|
|
511
604
|
]);
|
|
512
605
|
var VideoModelEnum = z.enum([
|
|
513
606
|
"veo",
|
|
@@ -812,16 +905,16 @@ var CreateProductTaskInput = z2.object({
|
|
|
812
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.'),
|
|
813
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.'),
|
|
814
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.'),
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
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).'),
|
|
818
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."),
|
|
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.'),
|
|
819
912
|
outputs: z2.array(z2.enum([
|
|
820
913
|
"landing",
|
|
821
914
|
"video",
|
|
822
915
|
"image"
|
|
823
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.')
|
|
824
|
-
});
|
|
917
|
+
}).strict();
|
|
825
918
|
var GetTaskStatusInput = z2.object({
|
|
826
919
|
taskId: z2.string().min(1),
|
|
827
920
|
waitSeconds: z2.number().int().min(0).max(120).optional()
|
|
@@ -846,7 +939,7 @@ var EditTaskDraftInput = z2.object({
|
|
|
846
939
|
})
|
|
847
940
|
});
|
|
848
941
|
function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
|
|
849
|
-
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-
|
|
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) => {
|
|
850
943
|
try {
|
|
851
944
|
const imgs = await resolveImages(input.images, {
|
|
852
945
|
transport
|
|
@@ -855,15 +948,19 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
855
948
|
...img,
|
|
856
949
|
filename: `image-${i}.png`
|
|
857
950
|
}));
|
|
951
|
+
const resolved = resolveVideoPreset(input.videoPreset);
|
|
952
|
+
const wireTier = resolveVideoTier(input.videoTier);
|
|
858
953
|
const fields = flatten({
|
|
859
954
|
"options.languageCode": input.language,
|
|
860
955
|
"options.country": input.country,
|
|
861
956
|
"options.price": input.price,
|
|
862
957
|
"options.offer": input.offer,
|
|
863
|
-
"options.
|
|
864
|
-
"options.
|
|
865
|
-
"options.
|
|
958
|
+
"options.videoPreset": input.videoPreset,
|
|
959
|
+
"options.videoStyle": resolved?.videoStyle,
|
|
960
|
+
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
961
|
+
"options.videoTier": wireTier,
|
|
866
962
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
963
|
+
"options.angleNarratives": input.angleNarratives,
|
|
867
964
|
"options.selectedOutputs": input.outputs,
|
|
868
965
|
url: input.productUrl
|
|
869
966
|
});
|
|
@@ -962,15 +1059,15 @@ var VideoInput = z3.object({
|
|
|
962
1059
|
"Auto"
|
|
963
1060
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
964
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."),
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
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.'),
|
|
968
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"].'),
|
|
969
1065
|
price: CreativeRefs.price,
|
|
970
1066
|
offer: CreativeRefs.offer,
|
|
971
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.'),
|
|
972
|
-
scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion. Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico".')
|
|
973
|
-
|
|
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();
|
|
974
1071
|
var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
|
|
975
1072
|
var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
|
|
976
1073
|
var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
|
|
@@ -1010,12 +1107,26 @@ var LandingInput = z3.object({
|
|
|
1010
1107
|
isCombo: CreativeRefs.isCombo
|
|
1011
1108
|
});
|
|
1012
1109
|
function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
1013
|
-
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,
|
|
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) => {
|
|
1014
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;
|
|
1015
1126
|
return await submitCreative(api, {
|
|
1016
1127
|
path: "/v1/creatives/videos",
|
|
1017
1128
|
estimatedSeconds: 300,
|
|
1018
|
-
input,
|
|
1129
|
+
input: wirePayload,
|
|
1019
1130
|
transport
|
|
1020
1131
|
});
|
|
1021
1132
|
} catch (e) {
|
|
@@ -1049,10 +1160,68 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1049
1160
|
}
|
|
1050
1161
|
__name(registerCreativeTools, "registerCreativeTools");
|
|
1051
1162
|
|
|
1052
|
-
// src/tools/
|
|
1163
|
+
// src/tools/script-review.ts
|
|
1053
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";
|
|
1054
1223
|
function registerConnectionTools(server, api) {
|
|
1055
|
-
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.",
|
|
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 () => {
|
|
1056
1225
|
try {
|
|
1057
1226
|
const r = await api.getJson("/v1/shopify/shops");
|
|
1058
1227
|
return asText(r);
|
|
@@ -1060,7 +1229,7 @@ function registerConnectionTools(server, api) {
|
|
|
1060
1229
|
throw translateError(e);
|
|
1061
1230
|
}
|
|
1062
1231
|
});
|
|
1063
|
-
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.",
|
|
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 () => {
|
|
1064
1233
|
try {
|
|
1065
1234
|
const r = await api.getJson("/v1/facebook/ad-accounts");
|
|
1066
1235
|
return asText(r);
|
|
@@ -1068,7 +1237,7 @@ function registerConnectionTools(server, api) {
|
|
|
1068
1237
|
throw translateError(e);
|
|
1069
1238
|
}
|
|
1070
1239
|
});
|
|
1071
|
-
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.",
|
|
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 () => {
|
|
1072
1241
|
try {
|
|
1073
1242
|
const r = await api.getJson("/v1/tiktok/advertisers");
|
|
1074
1243
|
return asText(r);
|
|
@@ -1080,40 +1249,40 @@ function registerConnectionTools(server, api) {
|
|
|
1080
1249
|
__name(registerConnectionTools, "registerConnectionTools");
|
|
1081
1250
|
|
|
1082
1251
|
// src/tools/publish.ts
|
|
1083
|
-
import { z as
|
|
1084
|
-
var ShopifyInput =
|
|
1085
|
-
taskId:
|
|
1086
|
-
shopDomain:
|
|
1087
|
-
numLandingImages:
|
|
1088
|
-
variantIndex:
|
|
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(),
|
|
1089
1258
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1090
1259
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
1091
1260
|
* a single-option product page. Defaults to false. */
|
|
1092
|
-
showVariantPicker:
|
|
1261
|
+
showVariantPicker: z6.boolean().optional()
|
|
1093
1262
|
});
|
|
1094
|
-
var McpCampaignConfig =
|
|
1095
|
-
dailyBudgetUsd:
|
|
1263
|
+
var McpCampaignConfig = z6.object({
|
|
1264
|
+
dailyBudgetUsd: z6.number().positive().optional(),
|
|
1096
1265
|
/** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
|
|
1097
1266
|
* ad set). TikTok takes only the first entry. */
|
|
1098
|
-
targetCountries:
|
|
1099
|
-
campaignName:
|
|
1267
|
+
targetCountries: z6.array(z6.string().length(2)).max(1).optional(),
|
|
1268
|
+
campaignName: z6.string().trim().min(1).max(100).optional()
|
|
1100
1269
|
});
|
|
1101
|
-
var MetaInput =
|
|
1102
|
-
taskId:
|
|
1103
|
-
adAccountId:
|
|
1104
|
-
pixelId:
|
|
1105
|
-
landingUrl:
|
|
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(),
|
|
1106
1275
|
/** Select which angles to publish by 0-based index. Omit to publish all
|
|
1107
1276
|
* successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
|
|
1108
|
-
angleIndexes:
|
|
1277
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1109
1278
|
campaignConfig: McpCampaignConfig.optional()
|
|
1110
1279
|
});
|
|
1111
|
-
var TikTokInput =
|
|
1112
|
-
taskId:
|
|
1113
|
-
advertiserId:
|
|
1114
|
-
pixelId:
|
|
1115
|
-
landingUrl:
|
|
1116
|
-
angleIndexes:
|
|
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(),
|
|
1117
1286
|
campaignConfig: McpCampaignConfig.optional()
|
|
1118
1287
|
});
|
|
1119
1288
|
function registerPublishTools(server, api) {
|
|
@@ -1215,13 +1384,13 @@ function toTikTokCampaignConfig(cfg) {
|
|
|
1215
1384
|
__name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
|
|
1216
1385
|
|
|
1217
1386
|
// src/tools/campaigns.ts
|
|
1218
|
-
import { z as
|
|
1219
|
-
var MetaStatusInput =
|
|
1220
|
-
campaignId:
|
|
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.")
|
|
1221
1390
|
});
|
|
1222
|
-
var TikTokStatusInput =
|
|
1223
|
-
campaignId:
|
|
1224
|
-
advertiserId:
|
|
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.")
|
|
1225
1394
|
});
|
|
1226
1395
|
function registerCampaignTools(server, api) {
|
|
1227
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) => {
|
|
@@ -1339,6 +1508,7 @@ function createServer(opts) {
|
|
|
1339
1508
|
registerCreativeTools(server, api, {
|
|
1340
1509
|
transport
|
|
1341
1510
|
});
|
|
1511
|
+
registerScriptReviewTools(server, api);
|
|
1342
1512
|
registerConnectionTools(server, api);
|
|
1343
1513
|
registerPublishTools(server, api);
|
|
1344
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",
|
|
@@ -504,7 +590,14 @@ var NarrativeStyleEnum = z.enum([
|
|
|
504
590
|
"problem_product_cta",
|
|
505
591
|
"product_showcase",
|
|
506
592
|
"metaphor_product_cta",
|
|
507
|
-
"wearable_showcase"
|
|
593
|
+
"wearable_showcase",
|
|
594
|
+
"animated_problem_pixar",
|
|
595
|
+
"sassy_product_pixar",
|
|
596
|
+
"ugc_unboxing_chaos",
|
|
597
|
+
"ugc_curiosity_reveal",
|
|
598
|
+
"ugc_listicle_3",
|
|
599
|
+
"ugc_skeptic_confession",
|
|
600
|
+
"ugc_demo_review"
|
|
508
601
|
]);
|
|
509
602
|
var VideoModelEnum = z.enum([
|
|
510
603
|
"veo",
|
|
@@ -768,16 +861,16 @@ var CreateProductTaskInput = z2.object({
|
|
|
768
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.'),
|
|
769
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.'),
|
|
770
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.'),
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
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).'),
|
|
774
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."),
|
|
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.'),
|
|
775
868
|
outputs: z2.array(z2.enum([
|
|
776
869
|
"landing",
|
|
777
870
|
"video",
|
|
778
871
|
"image"
|
|
779
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.')
|
|
780
|
-
});
|
|
873
|
+
}).strict();
|
|
781
874
|
var GetTaskStatusInput = z2.object({
|
|
782
875
|
taskId: z2.string().min(1),
|
|
783
876
|
waitSeconds: z2.number().int().min(0).max(120).optional()
|
|
@@ -802,7 +895,7 @@ var EditTaskDraftInput = z2.object({
|
|
|
802
895
|
})
|
|
803
896
|
});
|
|
804
897
|
function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
|
|
805
|
-
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-
|
|
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) => {
|
|
806
899
|
try {
|
|
807
900
|
const imgs = await resolveImages(input.images, {
|
|
808
901
|
transport
|
|
@@ -811,15 +904,19 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
811
904
|
...img,
|
|
812
905
|
filename: `image-${i}.png`
|
|
813
906
|
}));
|
|
907
|
+
const resolved = resolveVideoPreset(input.videoPreset);
|
|
908
|
+
const wireTier = resolveVideoTier(input.videoTier);
|
|
814
909
|
const fields = flatten({
|
|
815
910
|
"options.languageCode": input.language,
|
|
816
911
|
"options.country": input.country,
|
|
817
912
|
"options.price": input.price,
|
|
818
913
|
"options.offer": input.offer,
|
|
819
|
-
"options.
|
|
820
|
-
"options.
|
|
821
|
-
"options.
|
|
914
|
+
"options.videoPreset": input.videoPreset,
|
|
915
|
+
"options.videoStyle": resolved?.videoStyle,
|
|
916
|
+
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
917
|
+
"options.videoTier": wireTier,
|
|
822
918
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
919
|
+
"options.angleNarratives": input.angleNarratives,
|
|
823
920
|
"options.selectedOutputs": input.outputs,
|
|
824
921
|
url: input.productUrl
|
|
825
922
|
});
|
|
@@ -918,15 +1015,15 @@ var VideoInput = z3.object({
|
|
|
918
1015
|
"Auto"
|
|
919
1016
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
920
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."),
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
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.'),
|
|
924
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"].'),
|
|
925
1021
|
price: CreativeRefs.price,
|
|
926
1022
|
offer: CreativeRefs.offer,
|
|
927
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.'),
|
|
928
|
-
scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion. Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico".')
|
|
929
|
-
|
|
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();
|
|
930
1027
|
var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
|
|
931
1028
|
var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
|
|
932
1029
|
var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
|
|
@@ -966,12 +1063,26 @@ var LandingInput = z3.object({
|
|
|
966
1063
|
isCombo: CreativeRefs.isCombo
|
|
967
1064
|
});
|
|
968
1065
|
function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
969
|
-
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,
|
|
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) => {
|
|
970
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;
|
|
971
1082
|
return await submitCreative(api, {
|
|
972
1083
|
path: "/v1/creatives/videos",
|
|
973
1084
|
estimatedSeconds: 300,
|
|
974
|
-
input,
|
|
1085
|
+
input: wirePayload,
|
|
975
1086
|
transport
|
|
976
1087
|
});
|
|
977
1088
|
} catch (e) {
|
|
@@ -1005,10 +1116,68 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1005
1116
|
}
|
|
1006
1117
|
__name(registerCreativeTools, "registerCreativeTools");
|
|
1007
1118
|
|
|
1008
|
-
// src/tools/
|
|
1119
|
+
// src/tools/script-review.ts
|
|
1009
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";
|
|
1010
1179
|
function registerConnectionTools(server, api) {
|
|
1011
|
-
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.",
|
|
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 () => {
|
|
1012
1181
|
try {
|
|
1013
1182
|
const r = await api.getJson("/v1/shopify/shops");
|
|
1014
1183
|
return asText(r);
|
|
@@ -1016,7 +1185,7 @@ function registerConnectionTools(server, api) {
|
|
|
1016
1185
|
throw translateError(e);
|
|
1017
1186
|
}
|
|
1018
1187
|
});
|
|
1019
|
-
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.",
|
|
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 () => {
|
|
1020
1189
|
try {
|
|
1021
1190
|
const r = await api.getJson("/v1/facebook/ad-accounts");
|
|
1022
1191
|
return asText(r);
|
|
@@ -1024,7 +1193,7 @@ function registerConnectionTools(server, api) {
|
|
|
1024
1193
|
throw translateError(e);
|
|
1025
1194
|
}
|
|
1026
1195
|
});
|
|
1027
|
-
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.",
|
|
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 () => {
|
|
1028
1197
|
try {
|
|
1029
1198
|
const r = await api.getJson("/v1/tiktok/advertisers");
|
|
1030
1199
|
return asText(r);
|
|
@@ -1036,40 +1205,40 @@ function registerConnectionTools(server, api) {
|
|
|
1036
1205
|
__name(registerConnectionTools, "registerConnectionTools");
|
|
1037
1206
|
|
|
1038
1207
|
// src/tools/publish.ts
|
|
1039
|
-
import { z as
|
|
1040
|
-
var ShopifyInput =
|
|
1041
|
-
taskId:
|
|
1042
|
-
shopDomain:
|
|
1043
|
-
numLandingImages:
|
|
1044
|
-
variantIndex:
|
|
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(),
|
|
1045
1214
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1046
1215
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
1047
1216
|
* a single-option product page. Defaults to false. */
|
|
1048
|
-
showVariantPicker:
|
|
1217
|
+
showVariantPicker: z6.boolean().optional()
|
|
1049
1218
|
});
|
|
1050
|
-
var McpCampaignConfig =
|
|
1051
|
-
dailyBudgetUsd:
|
|
1219
|
+
var McpCampaignConfig = z6.object({
|
|
1220
|
+
dailyBudgetUsd: z6.number().positive().optional(),
|
|
1052
1221
|
/** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
|
|
1053
1222
|
* ad set). TikTok takes only the first entry. */
|
|
1054
|
-
targetCountries:
|
|
1055
|
-
campaignName:
|
|
1223
|
+
targetCountries: z6.array(z6.string().length(2)).max(1).optional(),
|
|
1224
|
+
campaignName: z6.string().trim().min(1).max(100).optional()
|
|
1056
1225
|
});
|
|
1057
|
-
var MetaInput =
|
|
1058
|
-
taskId:
|
|
1059
|
-
adAccountId:
|
|
1060
|
-
pixelId:
|
|
1061
|
-
landingUrl:
|
|
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(),
|
|
1062
1231
|
/** Select which angles to publish by 0-based index. Omit to publish all
|
|
1063
1232
|
* successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
|
|
1064
|
-
angleIndexes:
|
|
1233
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1065
1234
|
campaignConfig: McpCampaignConfig.optional()
|
|
1066
1235
|
});
|
|
1067
|
-
var TikTokInput =
|
|
1068
|
-
taskId:
|
|
1069
|
-
advertiserId:
|
|
1070
|
-
pixelId:
|
|
1071
|
-
landingUrl:
|
|
1072
|
-
angleIndexes:
|
|
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(),
|
|
1073
1242
|
campaignConfig: McpCampaignConfig.optional()
|
|
1074
1243
|
});
|
|
1075
1244
|
function registerPublishTools(server, api) {
|
|
@@ -1171,13 +1340,13 @@ function toTikTokCampaignConfig(cfg) {
|
|
|
1171
1340
|
__name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
|
|
1172
1341
|
|
|
1173
1342
|
// src/tools/campaigns.ts
|
|
1174
|
-
import { z as
|
|
1175
|
-
var MetaStatusInput =
|
|
1176
|
-
campaignId:
|
|
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.")
|
|
1177
1346
|
});
|
|
1178
|
-
var TikTokStatusInput =
|
|
1179
|
-
campaignId:
|
|
1180
|
-
advertiserId:
|
|
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.")
|
|
1181
1350
|
});
|
|
1182
1351
|
function registerCampaignTools(server, api) {
|
|
1183
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) => {
|
|
@@ -1295,6 +1464,7 @@ function createServer(opts) {
|
|
|
1295
1464
|
registerCreativeTools(server, api, {
|
|
1296
1465
|
transport
|
|
1297
1466
|
});
|
|
1467
|
+
registerScriptReviewTools(server, api);
|
|
1298
1468
|
registerConnectionTools(server, api);
|
|
1299
1469
|
registerPublishTools(server, api);
|
|
1300
1470
|
registerCampaignTools(server, api);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "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.",
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
},
|
|
38
38
|
"author": "Product Maker AI <support@productmaker.ai> (https://productmaker.app)",
|
|
39
39
|
"license": "MIT",
|
|
40
|
-
"bin": {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
40
|
+
"bin": {
|
|
41
|
+
"productmaker-mcp": "./dist/stdio.js"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -55,14 +55,23 @@
|
|
|
55
55
|
"zod": "^3.23.8"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@packages/types": "workspace:*",
|
|
59
58
|
"@types/express": "^4.17.21",
|
|
60
59
|
"@types/node": "^20.11.0",
|
|
61
60
|
"msw": "^2.4.0",
|
|
62
61
|
"tsup": "^8.3.0",
|
|
63
62
|
"tsx": "^4.16.0",
|
|
64
63
|
"typescript": "^5.4.5",
|
|
65
|
-
"vitest": "^2.1.9"
|
|
64
|
+
"vitest": "^2.1.9",
|
|
65
|
+
"@packages/types": "0.1.0"
|
|
66
66
|
},
|
|
67
|
-
"publishConfig": {
|
|
68
|
-
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsup",
|
|
72
|
+
"dev:stdio": "tsx src/stdio.ts",
|
|
73
|
+
"dev:http": "tsx src/http.ts",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"typecheck": "tsc --noEmit"
|
|
76
|
+
}
|
|
77
|
+
}
|