@productmaker/mcp 0.8.0 → 0.10.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 CHANGED
@@ -582,6 +582,10 @@ function resolveVideoTier(tier) {
582
582
  return tier === "basic" ? "grok" : "standard";
583
583
  }
584
584
  __name(resolveVideoTier, "resolveVideoTier");
585
+ var VideoResolutionEnum = z.enum([
586
+ "480p",
587
+ "720p"
588
+ ]);
585
589
  var VideoStyleEnum = z.enum([
586
590
  "ugc",
587
591
  "cartoon_3d",
@@ -931,6 +935,7 @@ var CreateProductTaskInput = z2.object({
931
935
  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.'),
932
936
  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
937
  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).'),
938
+ videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
934
939
  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."),
935
940
  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.'),
936
941
  outputs: z2.array(z2.enum([
@@ -966,6 +971,17 @@ var PendingActionPayload = z2.discriminatedUnion("kind", [
966
971
  z2.object({
967
972
  kind: z2.literal("product_reference_confirmation"),
968
973
  userChoice: ProductReferenceUserChoiceEnum
974
+ }),
975
+ z2.object({
976
+ kind: z2.literal("angle_review_approval"),
977
+ // All edits are optional — empty action body just approves the
978
+ // backend-generated draft as-is. Field shapes mirror ApproveAnglesRequest
979
+ // in @packages/types/wizard.ts (the only path-of-resolution endpoint is
980
+ // POST /v1/tasks/:taskId/approve-angles).
981
+ customAngle: z2.string().min(1).max(480).optional(),
982
+ painPoints: z2.string().max(3e3).optional(),
983
+ benefits: z2.string().max(3e3).optional(),
984
+ features: z2.array(z2.string().min(1).max(300)).max(10).optional()
969
985
  })
970
986
  ]);
971
987
  var ResolvePendingActionInput = z2.object({
@@ -976,7 +992,11 @@ var StatusForResolveSchema = z2.object({
976
992
  pendingUserAction: z2.object({
977
993
  kind: z2.string(),
978
994
  candidateGrouping: z2.string().optional(),
979
- selectedImageUrls: z2.array(z2.string()).optional()
995
+ selectedImageUrls: z2.array(z2.string()).optional(),
996
+ customAngleDraft: z2.string().optional(),
997
+ painPointsDraft: z2.string().optional(),
998
+ benefitsDraft: z2.string().optional(),
999
+ featuresDraft: z2.array(z2.string()).optional()
980
1000
  }).optional()
981
1001
  }).passthrough();
982
1002
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
@@ -1000,6 +1020,11 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1000
1020
  "options.videoStyle": resolved?.videoStyle,
1001
1021
  "options.narrativeStyle": resolved?.narrativeStyle,
1002
1022
  "options.videoTier": wireTier,
1023
+ // Resolution is meaningful ONLY for the basic/grok tier; for every
1024
+ // other tier the backend derives it. Forward it ONLY for grok so the
1025
+ // MCP never presents a "user choice where there is none" (the web has
1026
+ // no resolution knob for standard) — for non-grok it's dropped.
1027
+ "options.videoResolution": wireTier === "grok" ? input.videoResolution : void 0,
1003
1028
  "options.multiAngleVideos": input.multiAngleVideos,
1004
1029
  "options.angleNarratives": input.angleNarratives,
1005
1030
  "options.selectedOutputs": input.outputs,
@@ -1074,7 +1099,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1074
1099
  throw translateError(e);
1075
1100
  }
1076
1101
  });
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) => {
1102
+ 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\n\u2022 `angle_review_approval` \u2014 el usuario opto por `pauseForAngleReview: true` al crear el creativo (en `generate_image_creatives`, `generate_video_creative` o `generate_landing`). La IA ya gener\xF3 el \xE1ngulo + pain points + benefits + features y la tarea est\xE1 pausada esperando que el usuario apruebe o edite antes de renderizar. Lee `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` del `pendingUserAction` para mostr\xE1rselos al usuario en lenguaje natural ("la IA propuso este \xE1ngulo: ... \xBFlo apruebas o cambias algo?"). Payload (TODOS opcionales \u2014 sin nada = aprobar tal cual; con campos = aprobar con ediciones): `customAngle` (override del \xE1ngulo), `painPoints`, `benefits`, `features` (array de 1-10 bullets). Llamar sin ning\xFAn campo es v\xE1lido y aprueba el draft sin cambios.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
1078
1103
  try {
1079
1104
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
1080
1105
  const status = StatusForResolveSchema.parse(raw);
@@ -1104,9 +1129,26 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1104
1129
  });
1105
1130
  return asText(r);
1106
1131
  }
1132
+ case "angle_review_approval": {
1133
+ const body = {};
1134
+ if (pending.customAngleDraft && pending.customAngleDraft.length > 0) {
1135
+ body.customAngle = pending.customAngleDraft;
1136
+ }
1137
+ if (pending.painPointsDraft != null) body.painPoints = pending.painPointsDraft;
1138
+ if (pending.benefitsDraft != null) body.benefits = pending.benefitsDraft;
1139
+ if (pending.featuresDraft != null) body.features = [
1140
+ ...pending.featuresDraft
1141
+ ];
1142
+ if (input.action.customAngle != null) body.customAngle = input.action.customAngle;
1143
+ if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
1144
+ if (input.action.benefits != null) body.benefits = input.action.benefits;
1145
+ if (input.action.features != null) body.features = input.action.features;
1146
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`, body);
1147
+ return asText(r);
1148
+ }
1107
1149
  default: {
1108
- const _exhaustive = input.action.kind;
1109
- throw new Error(`Unsupported pending action kind: ${String(_exhaustive)}`);
1150
+ const _exhaustive = input.action;
1151
+ throw new Error(`Unsupported pending action kind: ${String(_exhaustive.kind)}`);
1110
1152
  }
1111
1153
  }
1112
1154
  } catch (e) {
@@ -1141,6 +1183,7 @@ var VideoInput = z3.object({
1141
1183
  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."),
1142
1184
  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
1185
  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.'),
1186
+ videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); el tier "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
1144
1187
  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"].'),
1145
1188
  price: CreativeRefs.price,
1146
1189
  offer: CreativeRefs.offer,
@@ -1194,7 +1237,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
1194
1237
  }
1195
1238
  const resolved = resolveVideoPreset(input.videoPreset);
1196
1239
  const wireTier = resolveVideoTier(input.videoTier);
1197
- const { videoTier: _publicTier, ...rest } = input;
1240
+ const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
1198
1241
  const wirePayload = {
1199
1242
  ...rest
1200
1243
  };
@@ -1203,6 +1246,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
1203
1246
  wirePayload.narrativeStyle = resolved.narrativeStyle;
1204
1247
  }
1205
1248
  if (wireTier !== void 0) wirePayload.videoTier = wireTier;
1249
+ if (wireTier === "grok" && input.videoResolution) wirePayload.videoResolution = input.videoResolution;
1206
1250
  return await submitCreative(api, {
1207
1251
  path: "/v1/creatives/videos",
1208
1252
  estimatedSeconds: 300,
package/dist/stdio.js CHANGED
@@ -579,6 +579,10 @@ function resolveVideoTier(tier) {
579
579
  return tier === "basic" ? "grok" : "standard";
580
580
  }
581
581
  __name(resolveVideoTier, "resolveVideoTier");
582
+ var VideoResolutionEnum = z.enum([
583
+ "480p",
584
+ "720p"
585
+ ]);
582
586
  var VideoStyleEnum = z.enum([
583
587
  "ugc",
584
588
  "cartoon_3d",
@@ -887,6 +891,7 @@ var CreateProductTaskInput = z2.object({
887
891
  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.'),
888
892
  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
893
  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).'),
894
+ videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
890
895
  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."),
891
896
  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.'),
892
897
  outputs: z2.array(z2.enum([
@@ -922,6 +927,17 @@ var PendingActionPayload = z2.discriminatedUnion("kind", [
922
927
  z2.object({
923
928
  kind: z2.literal("product_reference_confirmation"),
924
929
  userChoice: ProductReferenceUserChoiceEnum
930
+ }),
931
+ z2.object({
932
+ kind: z2.literal("angle_review_approval"),
933
+ // All edits are optional — empty action body just approves the
934
+ // backend-generated draft as-is. Field shapes mirror ApproveAnglesRequest
935
+ // in @packages/types/wizard.ts (the only path-of-resolution endpoint is
936
+ // POST /v1/tasks/:taskId/approve-angles).
937
+ customAngle: z2.string().min(1).max(480).optional(),
938
+ painPoints: z2.string().max(3e3).optional(),
939
+ benefits: z2.string().max(3e3).optional(),
940
+ features: z2.array(z2.string().min(1).max(300)).max(10).optional()
925
941
  })
926
942
  ]);
927
943
  var ResolvePendingActionInput = z2.object({
@@ -932,7 +948,11 @@ var StatusForResolveSchema = z2.object({
932
948
  pendingUserAction: z2.object({
933
949
  kind: z2.string(),
934
950
  candidateGrouping: z2.string().optional(),
935
- selectedImageUrls: z2.array(z2.string()).optional()
951
+ selectedImageUrls: z2.array(z2.string()).optional(),
952
+ customAngleDraft: z2.string().optional(),
953
+ painPointsDraft: z2.string().optional(),
954
+ benefitsDraft: z2.string().optional(),
955
+ featuresDraft: z2.array(z2.string()).optional()
936
956
  }).optional()
937
957
  }).passthrough();
938
958
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
@@ -956,6 +976,11 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
956
976
  "options.videoStyle": resolved?.videoStyle,
957
977
  "options.narrativeStyle": resolved?.narrativeStyle,
958
978
  "options.videoTier": wireTier,
979
+ // Resolution is meaningful ONLY for the basic/grok tier; for every
980
+ // other tier the backend derives it. Forward it ONLY for grok so the
981
+ // MCP never presents a "user choice where there is none" (the web has
982
+ // no resolution knob for standard) — for non-grok it's dropped.
983
+ "options.videoResolution": wireTier === "grok" ? input.videoResolution : void 0,
959
984
  "options.multiAngleVideos": input.multiAngleVideos,
960
985
  "options.angleNarratives": input.angleNarratives,
961
986
  "options.selectedOutputs": input.outputs,
@@ -1030,7 +1055,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1030
1055
  throw translateError(e);
1031
1056
  }
1032
1057
  });
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) => {
1058
+ 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\n\u2022 `angle_review_approval` \u2014 el usuario opto por `pauseForAngleReview: true` al crear el creativo (en `generate_image_creatives`, `generate_video_creative` o `generate_landing`). La IA ya gener\xF3 el \xE1ngulo + pain points + benefits + features y la tarea est\xE1 pausada esperando que el usuario apruebe o edite antes de renderizar. Lee `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` del `pendingUserAction` para mostr\xE1rselos al usuario en lenguaje natural ("la IA propuso este \xE1ngulo: ... \xBFlo apruebas o cambias algo?"). Payload (TODOS opcionales \u2014 sin nada = aprobar tal cual; con campos = aprobar con ediciones): `customAngle` (override del \xE1ngulo), `painPoints`, `benefits`, `features` (array de 1-10 bullets). Llamar sin ning\xFAn campo es v\xE1lido y aprueba el draft sin cambios.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
1034
1059
  try {
1035
1060
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
1036
1061
  const status = StatusForResolveSchema.parse(raw);
@@ -1060,9 +1085,26 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
1060
1085
  });
1061
1086
  return asText(r);
1062
1087
  }
1088
+ case "angle_review_approval": {
1089
+ const body = {};
1090
+ if (pending.customAngleDraft && pending.customAngleDraft.length > 0) {
1091
+ body.customAngle = pending.customAngleDraft;
1092
+ }
1093
+ if (pending.painPointsDraft != null) body.painPoints = pending.painPointsDraft;
1094
+ if (pending.benefitsDraft != null) body.benefits = pending.benefitsDraft;
1095
+ if (pending.featuresDraft != null) body.features = [
1096
+ ...pending.featuresDraft
1097
+ ];
1098
+ if (input.action.customAngle != null) body.customAngle = input.action.customAngle;
1099
+ if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
1100
+ if (input.action.benefits != null) body.benefits = input.action.benefits;
1101
+ if (input.action.features != null) body.features = input.action.features;
1102
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`, body);
1103
+ return asText(r);
1104
+ }
1063
1105
  default: {
1064
- const _exhaustive = input.action.kind;
1065
- throw new Error(`Unsupported pending action kind: ${String(_exhaustive)}`);
1106
+ const _exhaustive = input.action;
1107
+ throw new Error(`Unsupported pending action kind: ${String(_exhaustive.kind)}`);
1066
1108
  }
1067
1109
  }
1068
1110
  } catch (e) {
@@ -1097,6 +1139,7 @@ var VideoInput = z3.object({
1097
1139
  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."),
1098
1140
  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
1141
  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.'),
1142
+ videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); el tier "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
1100
1143
  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"].'),
1101
1144
  price: CreativeRefs.price,
1102
1145
  offer: CreativeRefs.offer,
@@ -1150,7 +1193,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
1150
1193
  }
1151
1194
  const resolved = resolveVideoPreset(input.videoPreset);
1152
1195
  const wireTier = resolveVideoTier(input.videoTier);
1153
- const { videoTier: _publicTier, ...rest } = input;
1196
+ const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
1154
1197
  const wirePayload = {
1155
1198
  ...rest
1156
1199
  };
@@ -1159,6 +1202,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
1159
1202
  wirePayload.narrativeStyle = resolved.narrativeStyle;
1160
1203
  }
1161
1204
  if (wireTier !== void 0) wirePayload.videoTier = wireTier;
1205
+ if (wireTier === "grok" && input.videoResolution) wirePayload.videoResolution = input.videoResolution;
1162
1206
  return await submitCreative(api, {
1163
1207
  path: "/v1/creatives/videos",
1164
1208
  estimatedSeconds: 300,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "0.8.0",
3
+ "version": "0.10.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.",