@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.
- package/dist/http.js +211 -50
- package/dist/stdio.js +211 -50
- 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
|
-
|
|
823
|
-
|
|
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;
|
|
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-
|
|
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.
|
|
872
|
-
"options.
|
|
873
|
-
"options.
|
|
874
|
-
"options.
|
|
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
|
-
|
|
975
|
-
|
|
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,
|
|
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/
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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
|
|
1093
|
-
var ShopifyInput =
|
|
1094
|
-
taskId:
|
|
1095
|
-
shopDomain:
|
|
1096
|
-
numLandingImages:
|
|
1097
|
-
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(),
|
|
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:
|
|
1261
|
+
showVariantPicker: z6.boolean().optional()
|
|
1102
1262
|
});
|
|
1103
|
-
var McpCampaignConfig =
|
|
1104
|
-
dailyBudgetUsd:
|
|
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:
|
|
1108
|
-
campaignName:
|
|
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 =
|
|
1111
|
-
taskId:
|
|
1112
|
-
adAccountId:
|
|
1113
|
-
pixelId:
|
|
1114
|
-
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(),
|
|
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:
|
|
1277
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1118
1278
|
campaignConfig: McpCampaignConfig.optional()
|
|
1119
1279
|
});
|
|
1120
|
-
var TikTokInput =
|
|
1121
|
-
taskId:
|
|
1122
|
-
advertiserId:
|
|
1123
|
-
pixelId:
|
|
1124
|
-
landingUrl:
|
|
1125
|
-
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(),
|
|
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
|
|
1228
|
-
var MetaStatusInput =
|
|
1229
|
-
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.")
|
|
1230
1390
|
});
|
|
1231
|
-
var TikTokStatusInput =
|
|
1232
|
-
campaignId:
|
|
1233
|
-
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.")
|
|
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
|
-
|
|
779
|
-
|
|
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;
|
|
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-
|
|
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.
|
|
828
|
-
"options.
|
|
829
|
-
"options.
|
|
830
|
-
"options.
|
|
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
|
-
|
|
931
|
-
|
|
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,
|
|
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/
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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
|
|
1049
|
-
var ShopifyInput =
|
|
1050
|
-
taskId:
|
|
1051
|
-
shopDomain:
|
|
1052
|
-
numLandingImages:
|
|
1053
|
-
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(),
|
|
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:
|
|
1217
|
+
showVariantPicker: z6.boolean().optional()
|
|
1058
1218
|
});
|
|
1059
|
-
var McpCampaignConfig =
|
|
1060
|
-
dailyBudgetUsd:
|
|
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:
|
|
1064
|
-
campaignName:
|
|
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 =
|
|
1067
|
-
taskId:
|
|
1068
|
-
adAccountId:
|
|
1069
|
-
pixelId:
|
|
1070
|
-
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(),
|
|
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:
|
|
1233
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1074
1234
|
campaignConfig: McpCampaignConfig.optional()
|
|
1075
1235
|
});
|
|
1076
|
-
var TikTokInput =
|
|
1077
|
-
taskId:
|
|
1078
|
-
advertiserId:
|
|
1079
|
-
pixelId:
|
|
1080
|
-
landingUrl:
|
|
1081
|
-
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(),
|
|
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
|
|
1184
|
-
var MetaStatusInput =
|
|
1185
|
-
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.")
|
|
1186
1346
|
});
|
|
1187
|
-
var TikTokStatusInput =
|
|
1188
|
-
campaignId:
|
|
1189
|
-
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.")
|
|
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.
|
|
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.",
|