@productmaker/mcp 0.4.0 → 0.8.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 +292 -51
- package/dist/stdio.js +292 -51
- 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",
|
|
@@ -525,6 +611,30 @@ var VideoModelEnum = z.enum([
|
|
|
525
611
|
"grok",
|
|
526
612
|
"kling"
|
|
527
613
|
]);
|
|
614
|
+
var ProductReferenceUserChoiceEnum = z.enum([
|
|
615
|
+
"same_product",
|
|
616
|
+
"variants",
|
|
617
|
+
"different_products",
|
|
618
|
+
"combo_distinct"
|
|
619
|
+
]);
|
|
620
|
+
var ProductReferenceCandidateGroupingEnum = z.enum([
|
|
621
|
+
"same_product_same_variant",
|
|
622
|
+
"same_product_different_variants",
|
|
623
|
+
"different_products",
|
|
624
|
+
"unclear"
|
|
625
|
+
]);
|
|
626
|
+
function buildSelectedImageUrlsHash(urls) {
|
|
627
|
+
const input = [
|
|
628
|
+
...urls
|
|
629
|
+
].sort().join("|");
|
|
630
|
+
let hash = 2166136261;
|
|
631
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
632
|
+
hash ^= input.charCodeAt(index);
|
|
633
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
634
|
+
}
|
|
635
|
+
return `fnv1a:${hash.toString(16).padStart(8, "0")}:${urls.length}`;
|
|
636
|
+
}
|
|
637
|
+
__name(buildSelectedImageUrlsHash, "buildSelectedImageUrlsHash");
|
|
528
638
|
var ImageCreativeStyleEnum = z.enum([
|
|
529
639
|
"studio",
|
|
530
640
|
"floating",
|
|
@@ -819,17 +929,16 @@ var CreateProductTaskInput = z2.object({
|
|
|
819
929
|
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
930
|
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
931
|
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.'),
|
|
932
|
+
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.'),
|
|
933
|
+
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
934
|
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;
|
|
935
|
+
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
936
|
outputs: z2.array(z2.enum([
|
|
828
937
|
"landing",
|
|
829
938
|
"video",
|
|
830
939
|
"image"
|
|
831
940
|
])).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
|
-
});
|
|
941
|
+
}).strict();
|
|
833
942
|
var GetTaskStatusInput = z2.object({
|
|
834
943
|
taskId: z2.string().min(1),
|
|
835
944
|
waitSeconds: z2.number().int().min(0).max(120).optional()
|
|
@@ -853,8 +962,25 @@ var EditTaskDraftInput = z2.object({
|
|
|
853
962
|
scriptNotes: z2.string().max(2e3).optional()
|
|
854
963
|
})
|
|
855
964
|
});
|
|
965
|
+
var PendingActionPayload = z2.discriminatedUnion("kind", [
|
|
966
|
+
z2.object({
|
|
967
|
+
kind: z2.literal("product_reference_confirmation"),
|
|
968
|
+
userChoice: ProductReferenceUserChoiceEnum
|
|
969
|
+
})
|
|
970
|
+
]);
|
|
971
|
+
var ResolvePendingActionInput = z2.object({
|
|
972
|
+
taskId: z2.string().min(1),
|
|
973
|
+
action: PendingActionPayload
|
|
974
|
+
}).strict();
|
|
975
|
+
var StatusForResolveSchema = z2.object({
|
|
976
|
+
pendingUserAction: z2.object({
|
|
977
|
+
kind: z2.string(),
|
|
978
|
+
candidateGrouping: z2.string().optional(),
|
|
979
|
+
selectedImageUrls: z2.array(z2.string()).optional()
|
|
980
|
+
}).optional()
|
|
981
|
+
}).passthrough();
|
|
856
982
|
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-
|
|
983
|
+
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
984
|
try {
|
|
859
985
|
const imgs = await resolveImages(input.images, {
|
|
860
986
|
transport
|
|
@@ -863,16 +989,19 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
863
989
|
...img,
|
|
864
990
|
filename: `image-${i}.png`
|
|
865
991
|
}));
|
|
992
|
+
const resolved = resolveVideoPreset(input.videoPreset);
|
|
993
|
+
const wireTier = resolveVideoTier(input.videoTier);
|
|
866
994
|
const fields = flatten({
|
|
867
995
|
"options.languageCode": input.language,
|
|
868
996
|
"options.country": input.country,
|
|
869
997
|
"options.price": input.price,
|
|
870
998
|
"options.offer": input.offer,
|
|
871
|
-
"options.
|
|
872
|
-
"options.
|
|
873
|
-
"options.
|
|
874
|
-
"options.
|
|
999
|
+
"options.videoPreset": input.videoPreset,
|
|
1000
|
+
"options.videoStyle": resolved?.videoStyle,
|
|
1001
|
+
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
1002
|
+
"options.videoTier": wireTier,
|
|
875
1003
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
1004
|
+
"options.angleNarratives": input.angleNarratives,
|
|
876
1005
|
"options.selectedOutputs": input.outputs,
|
|
877
1006
|
url: input.productUrl
|
|
878
1007
|
});
|
|
@@ -888,7 +1017,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
888
1017
|
throw translateError(e);
|
|
889
1018
|
}
|
|
890
1019
|
});
|
|
891
|
-
server.tool("get_task_status",
|
|
1020
|
+
server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` (hoy: `"product_reference_confirmation"` \u2014 el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos), mu\xE9strale al usuario `targetProductLabel` + `reason` en lenguaje natural, recoge su respuesta, y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }` (las `options` v\xE1lidas vienen en el mismo objeto). Sin esto la tarea NUNCA avanza por m\xE1s que esperes. Use this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
|
|
892
1021
|
try {
|
|
893
1022
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
|
|
894
1023
|
const signal = extra?.signal;
|
|
@@ -945,6 +1074,45 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
945
1074
|
throw translateError(e);
|
|
946
1075
|
}
|
|
947
1076
|
});
|
|
1077
|
+
server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Payload: `userChoice` \u2208 { "same_product" (mismo producto, fotos son s\xF3lo \xE1ngulos distintos), "variants" (mismo producto, distintos colores/tallas/sabores), "combo_distinct" (son 2 productos distintos vendidos juntos como combo \u2014 marca la tarea como combo), "different_products" (productos distintos y el usuario quiere generar s\xF3lo uno \u2014 el backend bloquea hasta que selecciones im\xE1genes de un solo producto; normalmente NO es la respuesta correcta para destrabar) }. PREG\xDANTALE al usuario antes de invocar \u2014 `pendingUserAction.targetProductLabel` y `reason` te dan contexto para formularle la pregunta en lenguaje natural. Usa SOLO valores presentes en `pendingUserAction.options`.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
|
|
1078
|
+
try {
|
|
1079
|
+
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
1080
|
+
const status = StatusForResolveSchema.parse(raw);
|
|
1081
|
+
const pending = status.pendingUserAction;
|
|
1082
|
+
if (!pending) {
|
|
1083
|
+
throw new Error("No hay acci\xF3n pendiente para esta tarea \u2014 llama get_task_status primero para confirmar el estado.");
|
|
1084
|
+
}
|
|
1085
|
+
if (pending.kind !== input.action.kind) {
|
|
1086
|
+
throw new Error(`La tarea espera una acci\xF3n de tipo "${pending.kind}" pero mandaste "${input.action.kind}". Reusa el mismo kind que viste en pendingUserAction.`);
|
|
1087
|
+
}
|
|
1088
|
+
switch (input.action.kind) {
|
|
1089
|
+
case "product_reference_confirmation": {
|
|
1090
|
+
const candidateGrouping = pending.candidateGrouping;
|
|
1091
|
+
const selectedImageUrls = pending.selectedImageUrls ?? [];
|
|
1092
|
+
if (!candidateGrouping || selectedImageUrls.length === 0) {
|
|
1093
|
+
throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
|
|
1094
|
+
}
|
|
1095
|
+
const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
|
|
1096
|
+
productReferenceConfirmation: {
|
|
1097
|
+
candidateGrouping,
|
|
1098
|
+
userChoice: input.action.userChoice,
|
|
1099
|
+
// Server overwrites with its own server-side computation;
|
|
1100
|
+
// we send the matching MCP-mirror value so the wire body
|
|
1101
|
+
// is internally coherent if the server skips overwrite.
|
|
1102
|
+
selectedImageUrlsHash: buildSelectedImageUrlsHash(selectedImageUrls)
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
return asText(r);
|
|
1106
|
+
}
|
|
1107
|
+
default: {
|
|
1108
|
+
const _exhaustive = input.action.kind;
|
|
1109
|
+
throw new Error(`Unsupported pending action kind: ${String(_exhaustive)}`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
throw translateError(e);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
948
1116
|
}
|
|
949
1117
|
__name(registerTaskTools, "registerTaskTools");
|
|
950
1118
|
|
|
@@ -971,15 +1139,15 @@ var VideoInput = z3.object({
|
|
|
971
1139
|
"Auto"
|
|
972
1140
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
973
1141
|
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.'),
|
|
1142
|
+
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).'),
|
|
1143
|
+
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
1144
|
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
1145
|
price: CreativeRefs.price,
|
|
979
1146
|
offer: CreativeRefs.offer,
|
|
980
1147
|
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
|
-
|
|
1148
|
+
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.'),
|
|
1149
|
+
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.")
|
|
1150
|
+
}).strict();
|
|
983
1151
|
var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
|
|
984
1152
|
var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
|
|
985
1153
|
var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
|
|
@@ -1019,12 +1187,26 @@ var LandingInput = z3.object({
|
|
|
1019
1187
|
isCombo: CreativeRefs.isCombo
|
|
1020
1188
|
});
|
|
1021
1189
|
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,
|
|
1190
|
+
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
1191
|
try {
|
|
1192
|
+
if (input.videoTier === "basic" && (input.durationSeconds ?? 15) > 15) {
|
|
1193
|
+
throw new Error('videoTier="basic" s\xF3lo admite durationSeconds \u2264 15. Para 30s usa "standard".');
|
|
1194
|
+
}
|
|
1195
|
+
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1196
|
+
const wireTier = resolveVideoTier(input.videoTier);
|
|
1197
|
+
const { videoTier: _publicTier, ...rest } = input;
|
|
1198
|
+
const wirePayload = {
|
|
1199
|
+
...rest
|
|
1200
|
+
};
|
|
1201
|
+
if (resolved) {
|
|
1202
|
+
wirePayload.videoStyle = resolved.videoStyle;
|
|
1203
|
+
wirePayload.narrativeStyle = resolved.narrativeStyle;
|
|
1204
|
+
}
|
|
1205
|
+
if (wireTier !== void 0) wirePayload.videoTier = wireTier;
|
|
1024
1206
|
return await submitCreative(api, {
|
|
1025
1207
|
path: "/v1/creatives/videos",
|
|
1026
1208
|
estimatedSeconds: 300,
|
|
1027
|
-
input,
|
|
1209
|
+
input: wirePayload,
|
|
1028
1210
|
transport
|
|
1029
1211
|
});
|
|
1030
1212
|
} catch (e) {
|
|
@@ -1058,10 +1240,68 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1058
1240
|
}
|
|
1059
1241
|
__name(registerCreativeTools, "registerCreativeTools");
|
|
1060
1242
|
|
|
1061
|
-
// src/tools/
|
|
1243
|
+
// src/tools/script-review.ts
|
|
1062
1244
|
import { z as z4 } from "zod";
|
|
1245
|
+
var GetVideoScriptInput = z4.object({
|
|
1246
|
+
taskId: z4.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
|
|
1247
|
+
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.')
|
|
1248
|
+
}).strict();
|
|
1249
|
+
var ApproveSceneEdit = z4.object({
|
|
1250
|
+
index: z4.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
|
|
1251
|
+
dialogue: z4.string().min(1).refine((v) => v.trim().length > 0, {
|
|
1252
|
+
message: "dialogue no puede ser s\xF3lo espacios"
|
|
1253
|
+
}).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.")
|
|
1254
|
+
}).strict();
|
|
1255
|
+
var ApproveVideoScriptInput = z4.object({
|
|
1256
|
+
taskId: z4.string().min(1).describe("ID de la tarea."),
|
|
1257
|
+
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.'),
|
|
1258
|
+
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.")
|
|
1259
|
+
}).strict();
|
|
1260
|
+
var RegenerateVideoScriptInput = z4.object({
|
|
1261
|
+
taskId: z4.string().min(1).describe("ID de la tarea."),
|
|
1262
|
+
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.'),
|
|
1263
|
+
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).')
|
|
1264
|
+
}).strict();
|
|
1265
|
+
function registerScriptReviewTools(server, api) {
|
|
1266
|
+
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) => {
|
|
1267
|
+
try {
|
|
1268
|
+
const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
|
|
1269
|
+
const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
|
|
1270
|
+
return asText(r);
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
throw translateError(e);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
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) => {
|
|
1276
|
+
try {
|
|
1277
|
+
const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/regenerate`, {
|
|
1278
|
+
angleKey: input.angleKey,
|
|
1279
|
+
narrativeStyle: input.narrativeStyle
|
|
1280
|
+
});
|
|
1281
|
+
return asText(r);
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
throw translateError(e);
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
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) => {
|
|
1287
|
+
try {
|
|
1288
|
+
const body = {
|
|
1289
|
+
scenes: input.scenes
|
|
1290
|
+
};
|
|
1291
|
+
if (input.angleKey) body.angleKey = input.angleKey;
|
|
1292
|
+
const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
|
|
1293
|
+
return asText(r);
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
throw translateError(e);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
__name(registerScriptReviewTools, "registerScriptReviewTools");
|
|
1300
|
+
|
|
1301
|
+
// src/tools/connections.ts
|
|
1302
|
+
import { z as z5 } from "zod";
|
|
1063
1303
|
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.",
|
|
1304
|
+
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
1305
|
try {
|
|
1066
1306
|
const r = await api.getJson("/v1/shopify/shops");
|
|
1067
1307
|
return asText(r);
|
|
@@ -1069,7 +1309,7 @@ function registerConnectionTools(server, api) {
|
|
|
1069
1309
|
throw translateError(e);
|
|
1070
1310
|
}
|
|
1071
1311
|
});
|
|
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.",
|
|
1312
|
+
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
1313
|
try {
|
|
1074
1314
|
const r = await api.getJson("/v1/facebook/ad-accounts");
|
|
1075
1315
|
return asText(r);
|
|
@@ -1077,7 +1317,7 @@ function registerConnectionTools(server, api) {
|
|
|
1077
1317
|
throw translateError(e);
|
|
1078
1318
|
}
|
|
1079
1319
|
});
|
|
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.",
|
|
1320
|
+
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
1321
|
try {
|
|
1082
1322
|
const r = await api.getJson("/v1/tiktok/advertisers");
|
|
1083
1323
|
return asText(r);
|
|
@@ -1089,40 +1329,40 @@ function registerConnectionTools(server, api) {
|
|
|
1089
1329
|
__name(registerConnectionTools, "registerConnectionTools");
|
|
1090
1330
|
|
|
1091
1331
|
// src/tools/publish.ts
|
|
1092
|
-
import { z as
|
|
1093
|
-
var ShopifyInput =
|
|
1094
|
-
taskId:
|
|
1095
|
-
shopDomain:
|
|
1096
|
-
numLandingImages:
|
|
1097
|
-
variantIndex:
|
|
1332
|
+
import { z as z6 } from "zod";
|
|
1333
|
+
var ShopifyInput = z6.object({
|
|
1334
|
+
taskId: z6.string().min(1),
|
|
1335
|
+
shopDomain: z6.string().min(1),
|
|
1336
|
+
numLandingImages: z6.number().int().min(1).max(7).optional(),
|
|
1337
|
+
variantIndex: z6.number().int().min(0).optional(),
|
|
1098
1338
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1099
1339
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
1100
1340
|
* a single-option product page. Defaults to false. */
|
|
1101
|
-
showVariantPicker:
|
|
1341
|
+
showVariantPicker: z6.boolean().optional()
|
|
1102
1342
|
});
|
|
1103
|
-
var McpCampaignConfig =
|
|
1104
|
-
dailyBudgetUsd:
|
|
1343
|
+
var McpCampaignConfig = z6.object({
|
|
1344
|
+
dailyBudgetUsd: z6.number().positive().optional(),
|
|
1105
1345
|
/** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
|
|
1106
1346
|
* ad set). TikTok takes only the first entry. */
|
|
1107
|
-
targetCountries:
|
|
1108
|
-
campaignName:
|
|
1347
|
+
targetCountries: z6.array(z6.string().length(2)).max(1).optional(),
|
|
1348
|
+
campaignName: z6.string().trim().min(1).max(100).optional()
|
|
1109
1349
|
});
|
|
1110
|
-
var MetaInput =
|
|
1111
|
-
taskId:
|
|
1112
|
-
adAccountId:
|
|
1113
|
-
pixelId:
|
|
1114
|
-
landingUrl:
|
|
1350
|
+
var MetaInput = z6.object({
|
|
1351
|
+
taskId: z6.string().min(1),
|
|
1352
|
+
adAccountId: z6.string().min(1),
|
|
1353
|
+
pixelId: z6.string().min(1).optional(),
|
|
1354
|
+
landingUrl: z6.string().url().optional(),
|
|
1115
1355
|
/** Select which angles to publish by 0-based index. Omit to publish all
|
|
1116
1356
|
* successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
|
|
1117
|
-
angleIndexes:
|
|
1357
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1118
1358
|
campaignConfig: McpCampaignConfig.optional()
|
|
1119
1359
|
});
|
|
1120
|
-
var TikTokInput =
|
|
1121
|
-
taskId:
|
|
1122
|
-
advertiserId:
|
|
1123
|
-
pixelId:
|
|
1124
|
-
landingUrl:
|
|
1125
|
-
angleIndexes:
|
|
1360
|
+
var TikTokInput = z6.object({
|
|
1361
|
+
taskId: z6.string().min(1),
|
|
1362
|
+
advertiserId: z6.string().min(1),
|
|
1363
|
+
pixelId: z6.string().min(1).optional(),
|
|
1364
|
+
landingUrl: z6.string().url().optional(),
|
|
1365
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1126
1366
|
campaignConfig: McpCampaignConfig.optional()
|
|
1127
1367
|
});
|
|
1128
1368
|
function registerPublishTools(server, api) {
|
|
@@ -1224,13 +1464,13 @@ function toTikTokCampaignConfig(cfg) {
|
|
|
1224
1464
|
__name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
|
|
1225
1465
|
|
|
1226
1466
|
// src/tools/campaigns.ts
|
|
1227
|
-
import { z as
|
|
1228
|
-
var MetaStatusInput =
|
|
1229
|
-
campaignId:
|
|
1467
|
+
import { z as z7 } from "zod";
|
|
1468
|
+
var MetaStatusInput = z7.object({
|
|
1469
|
+
campaignId: z7.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
|
|
1230
1470
|
});
|
|
1231
|
-
var TikTokStatusInput =
|
|
1232
|
-
campaignId:
|
|
1233
|
-
advertiserId:
|
|
1471
|
+
var TikTokStatusInput = z7.object({
|
|
1472
|
+
campaignId: z7.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
|
|
1473
|
+
advertiserId: z7.string().min(1).describe("The same advertiserId used to publish the campaign.")
|
|
1234
1474
|
});
|
|
1235
1475
|
function registerCampaignTools(server, api) {
|
|
1236
1476
|
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 +1588,7 @@ function createServer(opts) {
|
|
|
1348
1588
|
registerCreativeTools(server, api, {
|
|
1349
1589
|
transport
|
|
1350
1590
|
});
|
|
1591
|
+
registerScriptReviewTools(server, api);
|
|
1351
1592
|
registerConnectionTools(server, api);
|
|
1352
1593
|
registerPublishTools(server, api);
|
|
1353
1594
|
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",
|
|
@@ -522,6 +608,30 @@ var VideoModelEnum = z.enum([
|
|
|
522
608
|
"grok",
|
|
523
609
|
"kling"
|
|
524
610
|
]);
|
|
611
|
+
var ProductReferenceUserChoiceEnum = z.enum([
|
|
612
|
+
"same_product",
|
|
613
|
+
"variants",
|
|
614
|
+
"different_products",
|
|
615
|
+
"combo_distinct"
|
|
616
|
+
]);
|
|
617
|
+
var ProductReferenceCandidateGroupingEnum = z.enum([
|
|
618
|
+
"same_product_same_variant",
|
|
619
|
+
"same_product_different_variants",
|
|
620
|
+
"different_products",
|
|
621
|
+
"unclear"
|
|
622
|
+
]);
|
|
623
|
+
function buildSelectedImageUrlsHash(urls) {
|
|
624
|
+
const input = [
|
|
625
|
+
...urls
|
|
626
|
+
].sort().join("|");
|
|
627
|
+
let hash = 2166136261;
|
|
628
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
629
|
+
hash ^= input.charCodeAt(index);
|
|
630
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
631
|
+
}
|
|
632
|
+
return `fnv1a:${hash.toString(16).padStart(8, "0")}:${urls.length}`;
|
|
633
|
+
}
|
|
634
|
+
__name(buildSelectedImageUrlsHash, "buildSelectedImageUrlsHash");
|
|
525
635
|
var ImageCreativeStyleEnum = z.enum([
|
|
526
636
|
"studio",
|
|
527
637
|
"floating",
|
|
@@ -775,17 +885,16 @@ var CreateProductTaskInput = z2.object({
|
|
|
775
885
|
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
886
|
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
887
|
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.'),
|
|
888
|
+
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.'),
|
|
889
|
+
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
890
|
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;
|
|
891
|
+
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
892
|
outputs: z2.array(z2.enum([
|
|
784
893
|
"landing",
|
|
785
894
|
"video",
|
|
786
895
|
"image"
|
|
787
896
|
])).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
|
-
});
|
|
897
|
+
}).strict();
|
|
789
898
|
var GetTaskStatusInput = z2.object({
|
|
790
899
|
taskId: z2.string().min(1),
|
|
791
900
|
waitSeconds: z2.number().int().min(0).max(120).optional()
|
|
@@ -809,8 +918,25 @@ var EditTaskDraftInput = z2.object({
|
|
|
809
918
|
scriptNotes: z2.string().max(2e3).optional()
|
|
810
919
|
})
|
|
811
920
|
});
|
|
921
|
+
var PendingActionPayload = z2.discriminatedUnion("kind", [
|
|
922
|
+
z2.object({
|
|
923
|
+
kind: z2.literal("product_reference_confirmation"),
|
|
924
|
+
userChoice: ProductReferenceUserChoiceEnum
|
|
925
|
+
})
|
|
926
|
+
]);
|
|
927
|
+
var ResolvePendingActionInput = z2.object({
|
|
928
|
+
taskId: z2.string().min(1),
|
|
929
|
+
action: PendingActionPayload
|
|
930
|
+
}).strict();
|
|
931
|
+
var StatusForResolveSchema = z2.object({
|
|
932
|
+
pendingUserAction: z2.object({
|
|
933
|
+
kind: z2.string(),
|
|
934
|
+
candidateGrouping: z2.string().optional(),
|
|
935
|
+
selectedImageUrls: z2.array(z2.string()).optional()
|
|
936
|
+
}).optional()
|
|
937
|
+
}).passthrough();
|
|
812
938
|
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-
|
|
939
|
+
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
940
|
try {
|
|
815
941
|
const imgs = await resolveImages(input.images, {
|
|
816
942
|
transport
|
|
@@ -819,16 +945,19 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
819
945
|
...img,
|
|
820
946
|
filename: `image-${i}.png`
|
|
821
947
|
}));
|
|
948
|
+
const resolved = resolveVideoPreset(input.videoPreset);
|
|
949
|
+
const wireTier = resolveVideoTier(input.videoTier);
|
|
822
950
|
const fields = flatten({
|
|
823
951
|
"options.languageCode": input.language,
|
|
824
952
|
"options.country": input.country,
|
|
825
953
|
"options.price": input.price,
|
|
826
954
|
"options.offer": input.offer,
|
|
827
|
-
"options.
|
|
828
|
-
"options.
|
|
829
|
-
"options.
|
|
830
|
-
"options.
|
|
955
|
+
"options.videoPreset": input.videoPreset,
|
|
956
|
+
"options.videoStyle": resolved?.videoStyle,
|
|
957
|
+
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
958
|
+
"options.videoTier": wireTier,
|
|
831
959
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
960
|
+
"options.angleNarratives": input.angleNarratives,
|
|
832
961
|
"options.selectedOutputs": input.outputs,
|
|
833
962
|
url: input.productUrl
|
|
834
963
|
});
|
|
@@ -844,7 +973,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
844
973
|
throw translateError(e);
|
|
845
974
|
}
|
|
846
975
|
});
|
|
847
|
-
server.tool("get_task_status",
|
|
976
|
+
server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` (hoy: `"product_reference_confirmation"` \u2014 el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos), mu\xE9strale al usuario `targetProductLabel` + `reason` en lenguaje natural, recoge su respuesta, y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }` (las `options` v\xE1lidas vienen en el mismo objeto). Sin esto la tarea NUNCA avanza por m\xE1s que esperes. Use this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
|
|
848
977
|
try {
|
|
849
978
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
|
|
850
979
|
const signal = extra?.signal;
|
|
@@ -901,6 +1030,45 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
901
1030
|
throw translateError(e);
|
|
902
1031
|
}
|
|
903
1032
|
});
|
|
1033
|
+
server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Payload: `userChoice` \u2208 { "same_product" (mismo producto, fotos son s\xF3lo \xE1ngulos distintos), "variants" (mismo producto, distintos colores/tallas/sabores), "combo_distinct" (son 2 productos distintos vendidos juntos como combo \u2014 marca la tarea como combo), "different_products" (productos distintos y el usuario quiere generar s\xF3lo uno \u2014 el backend bloquea hasta que selecciones im\xE1genes de un solo producto; normalmente NO es la respuesta correcta para destrabar) }. PREG\xDANTALE al usuario antes de invocar \u2014 `pendingUserAction.targetProductLabel` y `reason` te dan contexto para formularle la pregunta en lenguaje natural. Usa SOLO valores presentes en `pendingUserAction.options`.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
|
|
1034
|
+
try {
|
|
1035
|
+
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
1036
|
+
const status = StatusForResolveSchema.parse(raw);
|
|
1037
|
+
const pending = status.pendingUserAction;
|
|
1038
|
+
if (!pending) {
|
|
1039
|
+
throw new Error("No hay acci\xF3n pendiente para esta tarea \u2014 llama get_task_status primero para confirmar el estado.");
|
|
1040
|
+
}
|
|
1041
|
+
if (pending.kind !== input.action.kind) {
|
|
1042
|
+
throw new Error(`La tarea espera una acci\xF3n de tipo "${pending.kind}" pero mandaste "${input.action.kind}". Reusa el mismo kind que viste en pendingUserAction.`);
|
|
1043
|
+
}
|
|
1044
|
+
switch (input.action.kind) {
|
|
1045
|
+
case "product_reference_confirmation": {
|
|
1046
|
+
const candidateGrouping = pending.candidateGrouping;
|
|
1047
|
+
const selectedImageUrls = pending.selectedImageUrls ?? [];
|
|
1048
|
+
if (!candidateGrouping || selectedImageUrls.length === 0) {
|
|
1049
|
+
throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
|
|
1050
|
+
}
|
|
1051
|
+
const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
|
|
1052
|
+
productReferenceConfirmation: {
|
|
1053
|
+
candidateGrouping,
|
|
1054
|
+
userChoice: input.action.userChoice,
|
|
1055
|
+
// Server overwrites with its own server-side computation;
|
|
1056
|
+
// we send the matching MCP-mirror value so the wire body
|
|
1057
|
+
// is internally coherent if the server skips overwrite.
|
|
1058
|
+
selectedImageUrlsHash: buildSelectedImageUrlsHash(selectedImageUrls)
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
return asText(r);
|
|
1062
|
+
}
|
|
1063
|
+
default: {
|
|
1064
|
+
const _exhaustive = input.action.kind;
|
|
1065
|
+
throw new Error(`Unsupported pending action kind: ${String(_exhaustive)}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
throw translateError(e);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
904
1072
|
}
|
|
905
1073
|
__name(registerTaskTools, "registerTaskTools");
|
|
906
1074
|
|
|
@@ -927,15 +1095,15 @@ var VideoInput = z3.object({
|
|
|
927
1095
|
"Auto"
|
|
928
1096
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
929
1097
|
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.'),
|
|
1098
|
+
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).'),
|
|
1099
|
+
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
1100
|
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
1101
|
price: CreativeRefs.price,
|
|
935
1102
|
offer: CreativeRefs.offer,
|
|
936
1103
|
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
|
-
|
|
1104
|
+
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.'),
|
|
1105
|
+
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.")
|
|
1106
|
+
}).strict();
|
|
939
1107
|
var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
|
|
940
1108
|
var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
|
|
941
1109
|
var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
|
|
@@ -975,12 +1143,26 @@ var LandingInput = z3.object({
|
|
|
975
1143
|
isCombo: CreativeRefs.isCombo
|
|
976
1144
|
});
|
|
977
1145
|
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,
|
|
1146
|
+
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
1147
|
try {
|
|
1148
|
+
if (input.videoTier === "basic" && (input.durationSeconds ?? 15) > 15) {
|
|
1149
|
+
throw new Error('videoTier="basic" s\xF3lo admite durationSeconds \u2264 15. Para 30s usa "standard".');
|
|
1150
|
+
}
|
|
1151
|
+
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1152
|
+
const wireTier = resolveVideoTier(input.videoTier);
|
|
1153
|
+
const { videoTier: _publicTier, ...rest } = input;
|
|
1154
|
+
const wirePayload = {
|
|
1155
|
+
...rest
|
|
1156
|
+
};
|
|
1157
|
+
if (resolved) {
|
|
1158
|
+
wirePayload.videoStyle = resolved.videoStyle;
|
|
1159
|
+
wirePayload.narrativeStyle = resolved.narrativeStyle;
|
|
1160
|
+
}
|
|
1161
|
+
if (wireTier !== void 0) wirePayload.videoTier = wireTier;
|
|
980
1162
|
return await submitCreative(api, {
|
|
981
1163
|
path: "/v1/creatives/videos",
|
|
982
1164
|
estimatedSeconds: 300,
|
|
983
|
-
input,
|
|
1165
|
+
input: wirePayload,
|
|
984
1166
|
transport
|
|
985
1167
|
});
|
|
986
1168
|
} catch (e) {
|
|
@@ -1014,10 +1196,68 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1014
1196
|
}
|
|
1015
1197
|
__name(registerCreativeTools, "registerCreativeTools");
|
|
1016
1198
|
|
|
1017
|
-
// src/tools/
|
|
1199
|
+
// src/tools/script-review.ts
|
|
1018
1200
|
import { z as z4 } from "zod";
|
|
1201
|
+
var GetVideoScriptInput = z4.object({
|
|
1202
|
+
taskId: z4.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
|
|
1203
|
+
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.')
|
|
1204
|
+
}).strict();
|
|
1205
|
+
var ApproveSceneEdit = z4.object({
|
|
1206
|
+
index: z4.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
|
|
1207
|
+
dialogue: z4.string().min(1).refine((v) => v.trim().length > 0, {
|
|
1208
|
+
message: "dialogue no puede ser s\xF3lo espacios"
|
|
1209
|
+
}).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.")
|
|
1210
|
+
}).strict();
|
|
1211
|
+
var ApproveVideoScriptInput = z4.object({
|
|
1212
|
+
taskId: z4.string().min(1).describe("ID de la tarea."),
|
|
1213
|
+
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.'),
|
|
1214
|
+
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.")
|
|
1215
|
+
}).strict();
|
|
1216
|
+
var RegenerateVideoScriptInput = z4.object({
|
|
1217
|
+
taskId: z4.string().min(1).describe("ID de la tarea."),
|
|
1218
|
+
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.'),
|
|
1219
|
+
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).')
|
|
1220
|
+
}).strict();
|
|
1221
|
+
function registerScriptReviewTools(server, api) {
|
|
1222
|
+
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) => {
|
|
1223
|
+
try {
|
|
1224
|
+
const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
|
|
1225
|
+
const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
|
|
1226
|
+
return asText(r);
|
|
1227
|
+
} catch (e) {
|
|
1228
|
+
throw translateError(e);
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
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) => {
|
|
1232
|
+
try {
|
|
1233
|
+
const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/regenerate`, {
|
|
1234
|
+
angleKey: input.angleKey,
|
|
1235
|
+
narrativeStyle: input.narrativeStyle
|
|
1236
|
+
});
|
|
1237
|
+
return asText(r);
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
throw translateError(e);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
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) => {
|
|
1243
|
+
try {
|
|
1244
|
+
const body = {
|
|
1245
|
+
scenes: input.scenes
|
|
1246
|
+
};
|
|
1247
|
+
if (input.angleKey) body.angleKey = input.angleKey;
|
|
1248
|
+
const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
|
|
1249
|
+
return asText(r);
|
|
1250
|
+
} catch (e) {
|
|
1251
|
+
throw translateError(e);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
__name(registerScriptReviewTools, "registerScriptReviewTools");
|
|
1256
|
+
|
|
1257
|
+
// src/tools/connections.ts
|
|
1258
|
+
import { z as z5 } from "zod";
|
|
1019
1259
|
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.",
|
|
1260
|
+
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
1261
|
try {
|
|
1022
1262
|
const r = await api.getJson("/v1/shopify/shops");
|
|
1023
1263
|
return asText(r);
|
|
@@ -1025,7 +1265,7 @@ function registerConnectionTools(server, api) {
|
|
|
1025
1265
|
throw translateError(e);
|
|
1026
1266
|
}
|
|
1027
1267
|
});
|
|
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.",
|
|
1268
|
+
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
1269
|
try {
|
|
1030
1270
|
const r = await api.getJson("/v1/facebook/ad-accounts");
|
|
1031
1271
|
return asText(r);
|
|
@@ -1033,7 +1273,7 @@ function registerConnectionTools(server, api) {
|
|
|
1033
1273
|
throw translateError(e);
|
|
1034
1274
|
}
|
|
1035
1275
|
});
|
|
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.",
|
|
1276
|
+
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
1277
|
try {
|
|
1038
1278
|
const r = await api.getJson("/v1/tiktok/advertisers");
|
|
1039
1279
|
return asText(r);
|
|
@@ -1045,40 +1285,40 @@ function registerConnectionTools(server, api) {
|
|
|
1045
1285
|
__name(registerConnectionTools, "registerConnectionTools");
|
|
1046
1286
|
|
|
1047
1287
|
// src/tools/publish.ts
|
|
1048
|
-
import { z as
|
|
1049
|
-
var ShopifyInput =
|
|
1050
|
-
taskId:
|
|
1051
|
-
shopDomain:
|
|
1052
|
-
numLandingImages:
|
|
1053
|
-
variantIndex:
|
|
1288
|
+
import { z as z6 } from "zod";
|
|
1289
|
+
var ShopifyInput = z6.object({
|
|
1290
|
+
taskId: z6.string().min(1),
|
|
1291
|
+
shopDomain: z6.string().min(1),
|
|
1292
|
+
numLandingImages: z6.number().int().min(1).max(7).optional(),
|
|
1293
|
+
variantIndex: z6.number().int().min(0).optional(),
|
|
1054
1294
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1055
1295
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
1056
1296
|
* a single-option product page. Defaults to false. */
|
|
1057
|
-
showVariantPicker:
|
|
1297
|
+
showVariantPicker: z6.boolean().optional()
|
|
1058
1298
|
});
|
|
1059
|
-
var McpCampaignConfig =
|
|
1060
|
-
dailyBudgetUsd:
|
|
1299
|
+
var McpCampaignConfig = z6.object({
|
|
1300
|
+
dailyBudgetUsd: z6.number().positive().optional(),
|
|
1061
1301
|
/** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
|
|
1062
1302
|
* ad set). TikTok takes only the first entry. */
|
|
1063
|
-
targetCountries:
|
|
1064
|
-
campaignName:
|
|
1303
|
+
targetCountries: z6.array(z6.string().length(2)).max(1).optional(),
|
|
1304
|
+
campaignName: z6.string().trim().min(1).max(100).optional()
|
|
1065
1305
|
});
|
|
1066
|
-
var MetaInput =
|
|
1067
|
-
taskId:
|
|
1068
|
-
adAccountId:
|
|
1069
|
-
pixelId:
|
|
1070
|
-
landingUrl:
|
|
1306
|
+
var MetaInput = z6.object({
|
|
1307
|
+
taskId: z6.string().min(1),
|
|
1308
|
+
adAccountId: z6.string().min(1),
|
|
1309
|
+
pixelId: z6.string().min(1).optional(),
|
|
1310
|
+
landingUrl: z6.string().url().optional(),
|
|
1071
1311
|
/** Select which angles to publish by 0-based index. Omit to publish all
|
|
1072
1312
|
* successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
|
|
1073
|
-
angleIndexes:
|
|
1313
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1074
1314
|
campaignConfig: McpCampaignConfig.optional()
|
|
1075
1315
|
});
|
|
1076
|
-
var TikTokInput =
|
|
1077
|
-
taskId:
|
|
1078
|
-
advertiserId:
|
|
1079
|
-
pixelId:
|
|
1080
|
-
landingUrl:
|
|
1081
|
-
angleIndexes:
|
|
1316
|
+
var TikTokInput = z6.object({
|
|
1317
|
+
taskId: z6.string().min(1),
|
|
1318
|
+
advertiserId: z6.string().min(1),
|
|
1319
|
+
pixelId: z6.string().min(1).optional(),
|
|
1320
|
+
landingUrl: z6.string().url().optional(),
|
|
1321
|
+
angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
|
|
1082
1322
|
campaignConfig: McpCampaignConfig.optional()
|
|
1083
1323
|
});
|
|
1084
1324
|
function registerPublishTools(server, api) {
|
|
@@ -1180,13 +1420,13 @@ function toTikTokCampaignConfig(cfg) {
|
|
|
1180
1420
|
__name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
|
|
1181
1421
|
|
|
1182
1422
|
// src/tools/campaigns.ts
|
|
1183
|
-
import { z as
|
|
1184
|
-
var MetaStatusInput =
|
|
1185
|
-
campaignId:
|
|
1423
|
+
import { z as z7 } from "zod";
|
|
1424
|
+
var MetaStatusInput = z7.object({
|
|
1425
|
+
campaignId: z7.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
|
|
1186
1426
|
});
|
|
1187
|
-
var TikTokStatusInput =
|
|
1188
|
-
campaignId:
|
|
1189
|
-
advertiserId:
|
|
1427
|
+
var TikTokStatusInput = z7.object({
|
|
1428
|
+
campaignId: z7.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
|
|
1429
|
+
advertiserId: z7.string().min(1).describe("The same advertiserId used to publish the campaign.")
|
|
1190
1430
|
});
|
|
1191
1431
|
function registerCampaignTools(server, api) {
|
|
1192
1432
|
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 +1544,7 @@ function createServer(opts) {
|
|
|
1304
1544
|
registerCreativeTools(server, api, {
|
|
1305
1545
|
transport
|
|
1306
1546
|
});
|
|
1547
|
+
registerScriptReviewTools(server, api);
|
|
1307
1548
|
registerConnectionTools(server, api);
|
|
1308
1549
|
registerPublishTools(server, api);
|
|
1309
1550
|
registerCampaignTools(server, api);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.",
|