@productmaker/mcp 3.1.2 → 3.2.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
@@ -8906,7 +8906,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8906
8906
  ], [
8907
8907
  "creative_variant"
8908
8908
  ]),
8909
- defineCapability("create_product_task", "1c2c0153391172e057cbab25c16c79222611f37b9a6ec41a5135aeadec0c574f", "credit_consuming", "creative", [
8909
+ defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
8910
8910
  "credit_consuming",
8911
8911
  "state_change"
8912
8912
  ], [
@@ -8947,6 +8947,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8947
8947
  "productContext"
8948
8948
  ]),
8949
8949
  slot("video_options", false, [
8950
+ "videoPreset",
8951
+ "durationSeconds",
8952
+ "pauseForAngleReview",
8953
+ // MCP-only: WhatsApp runs its own confirmation policy and does not
8954
+ // offer a "decide everything" mode, so the field is not mirrored there.
8955
+ "aiDecidesAll",
8956
+ "videoTier",
8957
+ "videoResolution",
8958
+ "multiAngleVideos",
8959
+ "angleNarratives",
8960
+ "competitorScanEnabled"
8961
+ ], [
8950
8962
  "videoPreset",
8951
8963
  "durationSeconds",
8952
8964
  "pauseForAngleReview",
@@ -9172,7 +9184,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9172
9184
  ], [
9173
9185
  "creative"
9174
9186
  ]),
9175
- defineCapability("generate_landing", "e35e6373ddb92ba09e4adf1acd79387767387817a7b3ec8d83b464f884508fe1", "credit_consuming", "creative", [
9187
+ defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
9176
9188
  "credit_consuming",
9177
9189
  "state_change"
9178
9190
  ], [
@@ -9214,6 +9226,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9214
9226
  "subheadline"
9215
9227
  ]),
9216
9228
  slot("landing_options", false, [
9229
+ "competitorScanEnabled",
9230
+ "landingStrategy",
9231
+ "landingImageFormat",
9232
+ // MCP-only, like create_product_task's aiDecidesAll.
9233
+ "aiDecidesAll"
9234
+ ], [
9217
9235
  "competitorScanEnabled",
9218
9236
  "landingStrategy",
9219
9237
  "landingImageFormat"
@@ -9242,7 +9260,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9242
9260
  fieldCompletion: "all"
9243
9261
  }
9244
9262
  ]),
9245
- defineCapability("generate_video_creative", "6bcd073df66023f8108a671b3027e1f35e9596cf9481ddf13a0282b4c56f5f34", "credit_consuming", "creative", [
9263
+ defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
9246
9264
  "credit_consuming",
9247
9265
  "state_change"
9248
9266
  ], [
@@ -9264,6 +9282,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9264
9282
  "country"
9265
9283
  ]),
9266
9284
  slot("render_options", true, [
9285
+ "aspectRatio",
9286
+ "durationSeconds",
9287
+ "videoPreset",
9288
+ "videoTier",
9289
+ "videoResolution",
9290
+ "features",
9291
+ "cta",
9292
+ "scriptNotes",
9293
+ "pauseForVideoScriptReview",
9294
+ "pauseForAngleReview",
9295
+ // MCP-only, like create_product_task's aiDecidesAll.
9296
+ "aiDecidesAll",
9297
+ "competitorScanEnabled"
9298
+ ], [
9267
9299
  "aspectRatio",
9268
9300
  "durationSeconds",
9269
9301
  "videoPreset",
@@ -11042,7 +11074,7 @@ var START_DROPSHIPPING_PROMPT = [
11042
11074
  "",
11043
11075
  "After I choose a product, call start_dropshipping_project. Ask me for the real supplier cost and required costing inputs before cost_dropshipping_project; never treat a marketplace sale price as supplier cost. When nextAction.kind is cost_product, call cost_product first as a free read-only estimate with those real supplier inputs, carry its price and compareAtPrice into the creative task, then call cost_dropshipping_project with the canonical inputs to persist costing. After every guided mutation, call guide_dropshipping_launch and follow ProductMaker's returned nextAction. Do not maintain a parallel workflow state in chat.",
11044
11076
  "",
11045
- "Use the existing creative/task tools for generation, preserve their estimate and confirmation gates, poll real task status with get_task_status, and answer explicit pauses through resolve_pending_action. Verify actual outputs before calling the creative stage complete.",
11077
+ "Use the existing creative/task tools for generation, preserve their estimate and confirmation gates, and keep polling get_task_status until the task is done, errored, or paused \u2014 a timed-out poll means it is still running, so call again. When it pauses, show me the draft and ask me; only answer it yourself through resolve_pending_action if I delegated that decision to you or the task runs with aiDecidesAll / reviewMode automatic. Verify actual outputs before calling the creative stage complete.",
11046
11078
  "",
11047
11079
  "Before Shopify publication, call list_shopify_shops, check the real task readiness, ask whether the product has variants, show the intended publication, and wait for explicit approval before calling publish_to_shopify. Report the exact blocker when publication is not ready.",
11048
11080
  "",
@@ -11368,7 +11400,7 @@ var ApiClient = class {
11368
11400
  };
11369
11401
 
11370
11402
  // src/index.ts
11371
- var MCP_VERSION = "3.1.2";
11403
+ var MCP_VERSION = "3.2.0";
11372
11404
 
11373
11405
  // src/meta-upload-app.ts
11374
11406
  import { readFileSync } from "fs";
@@ -12906,6 +12938,7 @@ var CreateProductTaskInput = z49.object({
12906
12938
  z49.literal(30)
12907
12939
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
12908
12940
  pauseForAngleReview: z49.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
12941
+ aiDecidesAll: z49.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n, as\xED que `pendingUserAction` no deber\xEDa aparecer. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
12909
12942
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
12910
12943
  videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
12911
12944
  multiAngleVideos: z49.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."),
@@ -13233,6 +13266,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13233
13266
  "options.subheadline": input.subheadline,
13234
13267
  "options.landingStrategy": input.landingStrategy,
13235
13268
  "options.landingImageFormat": input.landingImageFormat,
13269
+ "options.aiDecidesAll": input.aiDecidesAll,
13236
13270
  pauseForAngleReview: input.pauseForAngleReview,
13237
13271
  url: input.productUrl
13238
13272
  }),
@@ -13304,7 +13338,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13304
13338
  throw translateError(e);
13305
13339
  }
13306
13340
  });
13307
- 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, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO: muestra el draft y llama `resolve_pending_action` repitiendo el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
13341
+ 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, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSIGUE LLAM\xC1NDOLA hasta que el estado sea terminal (`done` o `error`) o aparezca `pendingUserAction`. Un retorno sin ninguna de esas dos cosas significa que la tarea sigue corriendo y que alcanzaste el timeout, NO que se colg\xF3 ni que termin\xF3: vuelve a llamarla con waitSeconds alto. Si dejas de llamarla, la tarea sigue avanzando sin que nadie la mire y el usuario se queda esperando en silencio.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO esperando una decisi\xF3n del USUARIO: muestra el draft, PREG\xDANTALE y espera su respuesta. Resp\xF3ndela t\xFA mismo con `resolve_pending_action` \xFAnicamente si el usuario ya te deleg\xF3 esa decisi\xF3n \u2014 porque te lo dijo en la conversaci\xF3n, o porque la tarea corre con `aiDecidesAll` / `reviewMode: automatic`. Nunca deduzcas la delegaci\xF3n del contenido del draft: que la propuesta te parezca obvia o correcta no es un permiso. Al responder, repite el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
13308
13342
  try {
13309
13343
  const signal = extra?.signal;
13310
13344
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
@@ -13387,7 +13421,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13387
13421
  throw translateError(e);
13388
13422
  }
13389
13423
  });
13390
- server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
13424
+ server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Esta llamada habla EN NOMBRE DEL USUARIO: \xFAsala solo con una respuesta que \xE9l te haya dado, o cuando te haya delegado la decisi\xF3n de forma expl\xEDcita o v\xEDa `aiDecidesAll` / `reviewMode: automatic`. Si nadie te deleg\xF3 nada, pregunta y espera en vez de llamarla. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
13391
13425
  try {
13392
13426
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
13393
13427
  const status = StatusForResolveSchema.parse(raw);
@@ -13690,6 +13724,7 @@ var VideoInput = z50.object({
13690
13724
  scriptNotes: z50.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.'),
13691
13725
  pauseForVideoScriptReview: z50.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."),
13692
13726
  pauseForAngleReview: z50.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
13727
+ aiDecidesAll: z50.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
13693
13728
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
13694
13729
  brandId: CreativeRefs.brandId,
13695
13730
  projectId: CreativeRefs.projectId
@@ -13767,6 +13802,7 @@ var EditImageCreativeInput = z50.object({
13767
13802
  instruction: z50.string().min(1).max(1e3).describe('Qu\xE9 cambiar en esa imagen. Ej. "quita el badge de env\xEDo gratis", "cambia el fondo a blanco".')
13768
13803
  });
13769
13804
  var LandingInput = z50.object({
13805
+ aiDecidesAll: z50.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
13770
13806
  images: CreativeRefs.images,
13771
13807
  language: CreativeRefs.language,
13772
13808
  country: CreativeRefs.country,
@@ -14973,7 +15009,7 @@ function uploadSessionText(result) {
14973
15009
  text: [
14974
15010
  `### Prepara tu ${mediaLabel} en ProductMaker`,
14975
15011
  `Usa primero el App inline de este chat. Si tu host no soporta el bridge de archivos, usa este fallback: [Abrir carga segura](${result.uploadPageUrl})`,
14976
- `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. Cuando termines, pulsa **OK** dentro del App para que ProductMaker contin\xFAe en este chat.`,
15012
+ `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. El App sube cada archivo solo; cuando quieras seguir, escr\xEDbemelo en el chat.`,
14977
15013
  `Este enlace expira: ${result.expiresAt}`
14978
15014
  ].join("\n\n")
14979
15015
  }
@@ -14988,7 +15024,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
14988
15024
  function registerMediaUploadTools(server, api) {
14989
15025
  registerAppTool(server, "request_media_upload", {
14990
15026
  title: "Subir archivos",
14991
- description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. Tras OK, reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
15027
+ description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
14992
15028
  inputSchema: UploadSessionToolInputShape,
14993
15029
  outputSchema: UniversalUploadStructuredResponse.shape,
14994
15030
  _meta: {
@@ -15028,7 +15064,7 @@ function registerMediaUploadTools(server, api) {
15028
15064
  }
15029
15065
  });
15030
15066
  server.registerTool("check_media_upload", {
15031
- description: "Comprueba la carga universal. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
15067
+ description: "Comprueba la carga universal. Ll\xE1mala cuando el usuario diga que ya subi\xF3 sus archivos: es la \xFAnica forma de enterarte, porque el App no manda mensajes al chat. Si status=pending, el usuario todav\xEDa no ha terminado: d\xEDselo y espera, no inventes un assetId ni abras otra sesi\xF3n de carga. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
15032
15068
  inputSchema: UploadStatusInput.shape,
15033
15069
  _meta: {
15034
15070
  "openai/widgetAccessible": true
package/dist/stdio.js CHANGED
@@ -264,7 +264,7 @@ var ApiClient = class {
264
264
  };
265
265
 
266
266
  // src/index.ts
267
- var MCP_VERSION = "3.1.2";
267
+ var MCP_VERSION = "3.2.0";
268
268
 
269
269
  // src/meta-upload-app.ts
270
270
  import { readFileSync } from "fs";
@@ -9527,7 +9527,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9527
9527
  ], [
9528
9528
  "creative_variant"
9529
9529
  ]),
9530
- defineCapability("create_product_task", "1c2c0153391172e057cbab25c16c79222611f37b9a6ec41a5135aeadec0c574f", "credit_consuming", "creative", [
9530
+ defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
9531
9531
  "credit_consuming",
9532
9532
  "state_change"
9533
9533
  ], [
@@ -9568,6 +9568,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9568
9568
  "productContext"
9569
9569
  ]),
9570
9570
  slot("video_options", false, [
9571
+ "videoPreset",
9572
+ "durationSeconds",
9573
+ "pauseForAngleReview",
9574
+ // MCP-only: WhatsApp runs its own confirmation policy and does not
9575
+ // offer a "decide everything" mode, so the field is not mirrored there.
9576
+ "aiDecidesAll",
9577
+ "videoTier",
9578
+ "videoResolution",
9579
+ "multiAngleVideos",
9580
+ "angleNarratives",
9581
+ "competitorScanEnabled"
9582
+ ], [
9571
9583
  "videoPreset",
9572
9584
  "durationSeconds",
9573
9585
  "pauseForAngleReview",
@@ -9793,7 +9805,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9793
9805
  ], [
9794
9806
  "creative"
9795
9807
  ]),
9796
- defineCapability("generate_landing", "e35e6373ddb92ba09e4adf1acd79387767387817a7b3ec8d83b464f884508fe1", "credit_consuming", "creative", [
9808
+ defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
9797
9809
  "credit_consuming",
9798
9810
  "state_change"
9799
9811
  ], [
@@ -9835,6 +9847,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9835
9847
  "subheadline"
9836
9848
  ]),
9837
9849
  slot("landing_options", false, [
9850
+ "competitorScanEnabled",
9851
+ "landingStrategy",
9852
+ "landingImageFormat",
9853
+ // MCP-only, like create_product_task's aiDecidesAll.
9854
+ "aiDecidesAll"
9855
+ ], [
9838
9856
  "competitorScanEnabled",
9839
9857
  "landingStrategy",
9840
9858
  "landingImageFormat"
@@ -9863,7 +9881,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9863
9881
  fieldCompletion: "all"
9864
9882
  }
9865
9883
  ]),
9866
- defineCapability("generate_video_creative", "6bcd073df66023f8108a671b3027e1f35e9596cf9481ddf13a0282b4c56f5f34", "credit_consuming", "creative", [
9884
+ defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
9867
9885
  "credit_consuming",
9868
9886
  "state_change"
9869
9887
  ], [
@@ -9885,6 +9903,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9885
9903
  "country"
9886
9904
  ]),
9887
9905
  slot("render_options", true, [
9906
+ "aspectRatio",
9907
+ "durationSeconds",
9908
+ "videoPreset",
9909
+ "videoTier",
9910
+ "videoResolution",
9911
+ "features",
9912
+ "cta",
9913
+ "scriptNotes",
9914
+ "pauseForVideoScriptReview",
9915
+ "pauseForAngleReview",
9916
+ // MCP-only, like create_product_task's aiDecidesAll.
9917
+ "aiDecidesAll",
9918
+ "competitorScanEnabled"
9919
+ ], [
9888
9920
  "aspectRatio",
9889
9921
  "durationSeconds",
9890
9922
  "videoPreset",
@@ -11708,6 +11740,7 @@ var CreateProductTaskInput = z48.object({
11708
11740
  z48.literal(30)
11709
11741
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
11710
11742
  pauseForAngleReview: z48.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
11743
+ aiDecidesAll: z48.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n, as\xED que `pendingUserAction` no deber\xEDa aparecer. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
11711
11744
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
11712
11745
  videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
11713
11746
  multiAngleVideos: z48.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."),
@@ -12035,6 +12068,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12035
12068
  "options.subheadline": input.subheadline,
12036
12069
  "options.landingStrategy": input.landingStrategy,
12037
12070
  "options.landingImageFormat": input.landingImageFormat,
12071
+ "options.aiDecidesAll": input.aiDecidesAll,
12038
12072
  pauseForAngleReview: input.pauseForAngleReview,
12039
12073
  url: input.productUrl
12040
12074
  }),
@@ -12106,7 +12140,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12106
12140
  throw translateError(e);
12107
12141
  }
12108
12142
  });
12109
- 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, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO: muestra el draft y llama `resolve_pending_action` repitiendo el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
12143
+ 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, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSIGUE LLAM\xC1NDOLA hasta que el estado sea terminal (`done` o `error`) o aparezca `pendingUserAction`. Un retorno sin ninguna de esas dos cosas significa que la tarea sigue corriendo y que alcanzaste el timeout, NO que se colg\xF3 ni que termin\xF3: vuelve a llamarla con waitSeconds alto. Si dejas de llamarla, la tarea sigue avanzando sin que nadie la mire y el usuario se queda esperando en silencio.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO esperando una decisi\xF3n del USUARIO: muestra el draft, PREG\xDANTALE y espera su respuesta. Resp\xF3ndela t\xFA mismo con `resolve_pending_action` \xFAnicamente si el usuario ya te deleg\xF3 esa decisi\xF3n \u2014 porque te lo dijo en la conversaci\xF3n, o porque la tarea corre con `aiDecidesAll` / `reviewMode: automatic`. Nunca deduzcas la delegaci\xF3n del contenido del draft: que la propuesta te parezca obvia o correcta no es un permiso. Al responder, repite el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
12110
12144
  try {
12111
12145
  const signal = extra?.signal;
12112
12146
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
@@ -12189,7 +12223,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12189
12223
  throw translateError(e);
12190
12224
  }
12191
12225
  });
12192
- server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
12226
+ server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Esta llamada habla EN NOMBRE DEL USUARIO: \xFAsala solo con una respuesta que \xE9l te haya dado, o cuando te haya delegado la decisi\xF3n de forma expl\xEDcita o v\xEDa `aiDecidesAll` / `reviewMode: automatic`. Si nadie te deleg\xF3 nada, pregunta y espera en vez de llamarla. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
12193
12227
  try {
12194
12228
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
12195
12229
  const status = StatusForResolveSchema.parse(raw);
@@ -12492,6 +12526,7 @@ var VideoInput = z49.object({
12492
12526
  scriptNotes: z49.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.'),
12493
12527
  pauseForVideoScriptReview: z49.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."),
12494
12528
  pauseForAngleReview: z49.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
12529
+ aiDecidesAll: z49.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
12495
12530
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
12496
12531
  brandId: CreativeRefs.brandId,
12497
12532
  projectId: CreativeRefs.projectId
@@ -12569,6 +12604,7 @@ var EditImageCreativeInput = z49.object({
12569
12604
  instruction: z49.string().min(1).max(1e3).describe('Qu\xE9 cambiar en esa imagen. Ej. "quita el badge de env\xEDo gratis", "cambia el fondo a blanco".')
12570
12605
  });
12571
12606
  var LandingInput = z49.object({
12607
+ aiDecidesAll: z49.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
12572
12608
  images: CreativeRefs.images,
12573
12609
  language: CreativeRefs.language,
12574
12610
  country: CreativeRefs.country,
@@ -13775,7 +13811,7 @@ function uploadSessionText(result) {
13775
13811
  text: [
13776
13812
  `### Prepara tu ${mediaLabel} en ProductMaker`,
13777
13813
  `Usa primero el App inline de este chat. Si tu host no soporta el bridge de archivos, usa este fallback: [Abrir carga segura](${result.uploadPageUrl})`,
13778
- `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. Cuando termines, pulsa **OK** dentro del App para que ProductMaker contin\xFAe en este chat.`,
13814
+ `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. El App sube cada archivo solo; cuando quieras seguir, escr\xEDbemelo en el chat.`,
13779
13815
  `Este enlace expira: ${result.expiresAt}`
13780
13816
  ].join("\n\n")
13781
13817
  }
@@ -13790,7 +13826,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
13790
13826
  function registerMediaUploadTools(server, api) {
13791
13827
  registerAppTool(server, "request_media_upload", {
13792
13828
  title: "Subir archivos",
13793
- description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. Tras OK, reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
13829
+ description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
13794
13830
  inputSchema: UploadSessionToolInputShape,
13795
13831
  outputSchema: UniversalUploadStructuredResponse.shape,
13796
13832
  _meta: {
@@ -13830,7 +13866,7 @@ function registerMediaUploadTools(server, api) {
13830
13866
  }
13831
13867
  });
13832
13868
  server.registerTool("check_media_upload", {
13833
- description: "Comprueba la carga universal. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
13869
+ description: "Comprueba la carga universal. Ll\xE1mala cuando el usuario diga que ya subi\xF3 sus archivos: es la \xFAnica forma de enterarte, porque el App no manda mensajes al chat. Si status=pending, el usuario todav\xEDa no ha terminado: d\xEDselo y espera, no inventes un assetId ni abras otra sesi\xF3n de carga. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
13834
13870
  inputSchema: UploadStatusInput.shape,
13835
13871
  _meta: {
13836
13872
  "openai/widgetAccessible": true
@@ -14932,7 +14968,7 @@ var START_DROPSHIPPING_PROMPT = [
14932
14968
  "",
14933
14969
  "After I choose a product, call start_dropshipping_project. Ask me for the real supplier cost and required costing inputs before cost_dropshipping_project; never treat a marketplace sale price as supplier cost. When nextAction.kind is cost_product, call cost_product first as a free read-only estimate with those real supplier inputs, carry its price and compareAtPrice into the creative task, then call cost_dropshipping_project with the canonical inputs to persist costing. After every guided mutation, call guide_dropshipping_launch and follow ProductMaker's returned nextAction. Do not maintain a parallel workflow state in chat.",
14934
14970
  "",
14935
- "Use the existing creative/task tools for generation, preserve their estimate and confirmation gates, poll real task status with get_task_status, and answer explicit pauses through resolve_pending_action. Verify actual outputs before calling the creative stage complete.",
14971
+ "Use the existing creative/task tools for generation, preserve their estimate and confirmation gates, and keep polling get_task_status until the task is done, errored, or paused \u2014 a timed-out poll means it is still running, so call again. When it pauses, show me the draft and ask me; only answer it yourself through resolve_pending_action if I delegated that decision to you or the task runs with aiDecidesAll / reviewMode automatic. Verify actual outputs before calling the creative stage complete.",
14936
14972
  "",
14937
14973
  "Before Shopify publication, call list_shopify_shops, check the real task readiness, ask whether the product has variants, show the intended publication, and wait for explicit approval before calling publish_to_shopify. Report the exact blocker when publication is not ready.",
14938
14974
  "",
@@ -214,24 +214,6 @@
214
214
  line-height: 1;
215
215
  }
216
216
 
217
- .complete-button {
218
- justify-self: start;
219
- min-width: 76px;
220
- margin-top: 10px;
221
- padding: 10px 20px;
222
- border: 0;
223
- border-radius: 12px;
224
- background: var(--pm-primary);
225
- color: #ffffff;
226
- cursor: pointer;
227
- font-weight: 780;
228
- }
229
-
230
- .complete-button:disabled {
231
- cursor: not-allowed;
232
- opacity: 0.5;
233
- }
234
-
235
217
  .drop-help {
236
218
  position: absolute;
237
219
  width: 1px;
@@ -511,15 +493,6 @@
511
493
  aria-live="polite"
512
494
  ></p>
513
495
  </div>
514
- <button
515
- class="complete-button"
516
- type="button"
517
- data-testid="meta-upload-complete-btn"
518
- hidden
519
- disabled
520
- >
521
- OK
522
- </button>
523
496
  </section>
524
497
  </article>
525
498
  </main>
@@ -576,9 +549,6 @@
576
549
  batchError: document.querySelector(
577
550
  '[data-testid="meta-upload-batch-error"]',
578
551
  ),
579
- completeButton: document.querySelector(
580
- '[data-testid="meta-upload-complete-btn"]',
581
- ),
582
552
  uploadPrompt: document.querySelector(
583
553
  '[data-testid="meta-upload-prompt"]',
584
554
  ),
@@ -591,7 +561,6 @@
591
561
  disposed: false,
592
562
  initialized: false,
593
563
  standardHostReady: false,
594
- hostCanMessage: false,
595
564
  hostCanServerTools: false,
596
565
  hostContainerDimensions: null,
597
566
  rpcId: 0,
@@ -602,9 +571,6 @@
602
571
  session: null,
603
572
  toolInput: {},
604
573
  items: [],
605
- assets: [],
606
- completionNotified: false,
607
- completionNotificationInFlight: false,
608
574
  consumedBridgeIds: new Set(),
609
575
  consumingBridgeFiles: false,
610
576
  bridgeConsumptionQueued: false,
@@ -677,38 +643,6 @@
677
643
  scheduleIntrinsicSizeReport(true);
678
644
  }
679
645
 
680
- function canMessageHost() {
681
- return (
682
- hasOpenAiFunction("sendFollowUpMessage") || state.hostCanMessage
683
- );
684
- }
685
-
686
- function canCompleteUpload() {
687
- return (
688
- !state.disposed &&
689
- Boolean(state.session) &&
690
- !sessionExpired() &&
691
- !state.busyBatch &&
692
- !state.completionNotified &&
693
- !state.completionNotificationInFlight &&
694
- state.assets.length > 0 &&
695
- state.items.length > 0 &&
696
- state.assets.length === state.items.length &&
697
- state.items.every((item) => item.status === "uploaded") &&
698
- canMessageHost()
699
- );
700
- }
701
-
702
- function renderCompletionAction() {
703
- const hasVerifiedAssets = state.assets.length > 0;
704
- elements.completeButton.hidden =
705
- state.completionNotified || !hasVerifiedAssets || !canMessageHost();
706
- elements.completeButton.disabled = !canCompleteUpload();
707
- elements.completeButton.textContent =
708
- state.completionNotificationInFlight ? "…" : "OK";
709
- scheduleIntrinsicSizeReport(true);
710
- }
711
-
712
646
  function parseFinitePositiveNumber(value) {
713
647
  return typeof value === "number" &&
714
648
  Number.isFinite(value) &&
@@ -927,8 +861,6 @@
927
861
  !state.session ||
928
862
  sessionExpired() ||
929
863
  state.uploadBlocked ||
930
- state.completionNotified ||
931
- state.completionNotificationInFlight ||
932
864
  state.busyBatch ||
933
865
  state.items.length >= safeSessionLimit();
934
866
  elements.input.disabled = disabled;
@@ -1044,7 +976,6 @@
1044
976
  hostContext.containerDimensions,
1045
977
  );
1046
978
  applyHostContainerDimensions();
1047
- state.hostCanMessage = capabilityEnabled(capabilities.message);
1048
979
  state.hostCanServerTools = capabilityEnabled(
1049
980
  capabilities.serverTools,
1050
981
  );
@@ -1152,7 +1083,6 @@
1152
1083
  abortActiveWork();
1153
1084
  for (const item of state.items) revokePreviewUrl(item);
1154
1085
  state.items = [];
1155
- state.assets = [];
1156
1086
  state.consumedBridgeIds.clear();
1157
1087
  state.consumingBridgeFiles = false;
1158
1088
  state.bridgeConsumptionQueued = false;
@@ -1162,8 +1092,6 @@
1162
1092
  renderItems();
1163
1093
  state.uploadBlocked = false;
1164
1094
  state.fallbackRequired = false;
1165
- state.completionNotified = false;
1166
- state.completionNotificationInFlight = false;
1167
1095
  state.workCancelled = false;
1168
1096
  if (state.abortController?.signal.aborted) {
1169
1097
  state.abortController = null;
@@ -1181,9 +1109,9 @@
1181
1109
  } else {
1182
1110
  setFallbackVisibility(state.fallbackRequired);
1183
1111
  setSessionStatus("Sesión lista para recibir archivos.", "neutral");
1112
+ if (sessionChanged) void refreshSessionStatus({ onMount: true });
1184
1113
  }
1185
1114
  setPickerAvailability();
1186
- renderCompletionAction();
1187
1115
  void consumePendingBridgeFiles();
1188
1116
  scheduleIntrinsicSizeReport(true);
1189
1117
  }
@@ -1334,9 +1262,7 @@
1334
1262
  state.disposed ||
1335
1263
  !state.session ||
1336
1264
  sessionExpired() ||
1337
- state.uploadBlocked ||
1338
- state.completionNotified ||
1339
- state.completionNotificationInFlight
1265
+ state.uploadBlocked
1340
1266
  )
1341
1267
  return;
1342
1268
  state.workCancelled = false;
@@ -1355,9 +1281,7 @@
1355
1281
  state.disposed ||
1356
1282
  !state.session ||
1357
1283
  sessionExpired() ||
1358
- state.uploadBlocked ||
1359
- state.completionNotified ||
1360
- state.completionNotificationInFlight
1284
+ state.uploadBlocked
1361
1285
  )
1362
1286
  return;
1363
1287
  const bridge = openAiBridge();
@@ -1531,8 +1455,6 @@
1531
1455
  !state.session ||
1532
1456
  sessionExpired() ||
1533
1457
  state.uploadBlocked ||
1534
- state.completionNotified ||
1535
- state.completionNotificationInFlight ||
1536
1458
  state.busyBatch
1537
1459
  )
1538
1460
  return;
@@ -1552,10 +1474,7 @@
1552
1474
  if (!selected.length) return;
1553
1475
 
1554
1476
  state.busyBatch = true;
1555
- state.assets = [];
1556
- state.completionNotified = false;
1557
1477
  setPickerAvailability();
1558
- renderCompletionAction();
1559
1478
  for (const file of selected) {
1560
1479
  const mediaKind = mediaKindForType(file.type) || "image";
1561
1480
  const item = createItem(file, mediaKind);
@@ -1567,7 +1486,6 @@
1567
1486
  }
1568
1487
  state.busyBatch = false;
1569
1488
  setPickerAvailability();
1570
- renderCompletionAction();
1571
1489
  if (state.items.some((item) => item.status === "uploaded")) {
1572
1490
  await refreshSessionStatus();
1573
1491
  }
@@ -1588,6 +1506,7 @@
1588
1506
  failureStage: "",
1589
1507
  assetId: "",
1590
1508
  previewUrl: "",
1509
+ slotReserved: false,
1591
1510
  };
1592
1511
  }
1593
1512
 
@@ -1610,12 +1529,15 @@
1610
1529
  item.status = "uploaded";
1611
1530
  item.progress = 100;
1612
1531
  item.error = "";
1532
+ item.slotReserved = false;
1613
1533
  emitAnalytics("mcp_media_upload_completed", item.mediaKind);
1614
1534
  } catch (error) {
1615
1535
  const failure = safeFailure(error);
1616
1536
  item.status = "failed";
1617
1537
  item.error = failure.message;
1618
1538
  item.failureStage = failure.stage;
1539
+ item.failureCode = failure.code;
1540
+ void releaseUploadSlot(item);
1619
1541
  emitAnalytics("mcp_media_upload_error", item.mediaKind, {
1620
1542
  failureStage: failure.stage,
1621
1543
  errorType: failure.code,
@@ -1626,6 +1548,10 @@
1626
1548
  "danger",
1627
1549
  true,
1628
1550
  );
1551
+ } else if (failure.code === "resolve_rejected") {
1552
+ // Permanent: the server already refused this exact file. Offering a
1553
+ // retry here is what let one rejection look like nine flaky ones.
1554
+ requireFallback(failure.message, "danger");
1629
1555
  } else if (failure.stage !== "validation") {
1630
1556
  requireFallback(
1631
1557
  "La carga no terminó. Puedes reintentarlo o continuar desde el enlace seguro.",
@@ -1904,6 +1830,9 @@
1904
1830
  item.uploadFile,
1905
1831
  item.uploadId,
1906
1832
  );
1833
+ // From here the server holds a slot for this assetId until the
1834
+ // object lands or the TTL expires.
1835
+ item.slotReserved = true;
1907
1836
  await putFileWithProgress(
1908
1837
  resolved.uploadUrl,
1909
1838
  item.uploadFile,
@@ -1916,6 +1845,7 @@
1916
1845
  return;
1917
1846
  } catch (error) {
1918
1847
  if (error && error.message === "session_expired") throw error;
1848
+ if (error && error.permanent) throw error;
1919
1849
  if (state.disposed || state.workCancelled) throw error;
1920
1850
  lastError = error;
1921
1851
  }
@@ -1933,6 +1863,96 @@
1933
1863
  throw error;
1934
1864
  }
1935
1865
 
1866
+ /**
1867
+ * Classifies a failed resolve.
1868
+ *
1869
+ * 410 means the upload link is spent and the whole session is dead; any
1870
+ * other 4xx means the API refused this particular file (unsupported type,
1871
+ * session already at its file limit). Both are deterministic, and
1872
+ * retrying them three times only produced three identical failures before
1873
+ * telling the user to "retry" something that could never succeed. Only a
1874
+ * server fault or a network blip earns another attempt, so anything below
1875
+ * 500 stops at once and carries the server's own reason to the card.
1876
+ */
1877
+ async function resolveFailure(response) {
1878
+ const status = response.status;
1879
+ const expired = status === 401 || status === 410;
1880
+ const error = new Error(
1881
+ expired ? "session_expired" : "resolve_rejected",
1882
+ );
1883
+ error.safeCode = expired ? "session_expired" : "resolve_rejected";
1884
+ error.safeStage = "resolve";
1885
+ error.permanent = status < 500;
1886
+ error.safeMessage = await resolveFailureMessage(response);
1887
+ return error;
1888
+ }
1889
+
1890
+ async function resolveFailureMessage(response) {
1891
+ // Expiry means one thing and the App says it better than the API does;
1892
+ // a per-file rejection is the opposite, so there the server's own
1893
+ // reason is the only useful text and the copy below is just a floor.
1894
+ if (response.status === 401 || response.status === 410)
1895
+ return "La sesión expiró. Abre el enlace seguro para crear otra.";
1896
+ const fallback =
1897
+ response.status === 413
1898
+ ? "El archivo es demasiado grande para esta carga."
1899
+ : response.status < 500
1900
+ ? "ProductMaker no aceptó este archivo."
1901
+ : "No pudimos preparar la carga. Reintenta o abre la carga segura.";
1902
+ try {
1903
+ const body = await response.json();
1904
+ const detail = isRecord(body)
1905
+ ? boundedString(
1906
+ typeof body.message === "string"
1907
+ ? body.message
1908
+ : Array.isArray(body.message)
1909
+ ? body.message.join(", ")
1910
+ : "",
1911
+ 200,
1912
+ )
1913
+ : "";
1914
+ return detail || fallback;
1915
+ } catch {
1916
+ return fallback;
1917
+ }
1918
+ }
1919
+
1920
+ /**
1921
+ * Hands the session's slot back when an upload will never finish.
1922
+ *
1923
+ * A resolved reservation is held for its full TTL, so one failed or
1924
+ * removed file used to keep a seat for fifteen minutes and every later
1925
+ * pick in that session was refused as "maximum number of files".
1926
+ *
1927
+ * Best effort: this runs on a path that is already failing, and the TTL
1928
+ * still frees the slot on its own, so no error here may reach the user.
1929
+ */
1930
+ async function releaseUploadSlot(item) {
1931
+ if (!item || !item.slotReserved || !state.session) return;
1932
+ item.slotReserved = false;
1933
+ try {
1934
+ const uploadPage = new URL(state.session.uploadPageUrl);
1935
+ const releaseUrl = new URL(
1936
+ state.session.resolveEndpoint.replace(/\/resolve$/, "/release"),
1937
+ uploadPage.origin,
1938
+ );
1939
+ if (!safeReleaseEndpointUrl(releaseUrl)) return;
1940
+ const token =
1941
+ uploadPage.searchParams.get("token") ||
1942
+ new URLSearchParams(uploadPage.hash.slice(1)).get("token");
1943
+ if (!token) return;
1944
+ await fetch(releaseUrl.toString(), {
1945
+ method: "POST",
1946
+ headers: { "content-type": "application/json" },
1947
+ body: JSON.stringify({ token, uploadId: item.uploadId }),
1948
+ credentials: "omit",
1949
+ redirect: "error",
1950
+ });
1951
+ } catch {
1952
+ // The reservation expires on its own; never surface this.
1953
+ }
1954
+ }
1955
+
1936
1956
  async function resolveUpload(file, uploadId) {
1937
1957
  const uploadPage = new URL(state.session.uploadPageUrl);
1938
1958
  const resolveUrl = new URL(
@@ -1967,13 +1987,7 @@
1967
1987
  signal: activeAbortSignal(),
1968
1988
  });
1969
1989
  if (!response.ok) {
1970
- const error = new Error(
1971
- response.status === 401 || response.status === 410
1972
- ? "session_expired"
1973
- : "resolve_failed",
1974
- );
1975
- error.safeStage = "resolve";
1976
- throw error;
1990
+ throw await resolveFailure(response);
1977
1991
  }
1978
1992
  const body = await response.json();
1979
1993
  if (!isRecord(body)) throw new Error("invalid_resolve_response");
@@ -2071,6 +2085,31 @@
2071
2085
  return officialApi || sameOriginWebProxy || localApi || configuredApi;
2072
2086
  }
2073
2087
 
2088
+ /**
2089
+ * Same origin rules as the resolver, for the sibling /release path.
2090
+ *
2091
+ * The web fallback page proxies through /api/upload-media/resolve and has
2092
+ * no release route, so a session using it simply skips the release and
2093
+ * lets the TTL free the slot. The inline App always resolves against the
2094
+ * API directly, which is where an abandoned reservation actually hurts.
2095
+ */
2096
+ function safeReleaseEndpointUrl(url) {
2097
+ if (url.username || url.password) return false;
2098
+ if (url.search || url.hash) return false;
2099
+ const releasePath = "/v1/uploaded-assets/sessions/release";
2100
+ if (url.pathname !== releasePath) return false;
2101
+ if (url.origin === "https://api.productmaker.app") return true;
2102
+ if (
2103
+ url.protocol === "http:" &&
2104
+ ["localhost", "127.0.0.1", "::1"].includes(url.hostname)
2105
+ )
2106
+ return true;
2107
+ return (
2108
+ Boolean(CONFIGURED_RESOLVE_ENDPOINT) &&
2109
+ url.origin === new URL(CONFIGURED_RESOLVE_ENDPOINT).origin
2110
+ );
2111
+ }
2112
+
2074
2113
  function putFileWithProgress(url, file, onProgress) {
2075
2114
  return new Promise((resolve, reject) => {
2076
2115
  const request = new XMLHttpRequest();
@@ -2140,7 +2179,8 @@
2140
2179
  !item ||
2141
2180
  item.status !== "failed" ||
2142
2181
  !item.file ||
2143
- item.failureStage === "validation"
2182
+ item.failureStage === "validation" ||
2183
+ item.failureCode === "resolve_rejected"
2144
2184
  )
2145
2185
  return;
2146
2186
  item.attempts = 0;
@@ -2150,8 +2190,14 @@
2150
2190
 
2151
2191
  function removeItem(index) {
2152
2192
  if (state.busyBatch) return;
2193
+ // A rehydrated item is a receipt for bytes already in staging; dropping
2194
+ // the card here would hide an asset the agent can still see and use.
2195
+ if (state.items[index]?.rehydrated) return;
2153
2196
  const [item] = state.items.splice(index, 1);
2154
- if (item) revokePreviewUrl(item);
2197
+ if (item) {
2198
+ revokePreviewUrl(item);
2199
+ if (item.status !== "uploaded") void releaseUploadSlot(item);
2200
+ }
2155
2201
  renderItems();
2156
2202
  setPickerAvailability();
2157
2203
  }
@@ -2273,6 +2319,7 @@
2273
2319
  if (
2274
2320
  item.status === "failed" &&
2275
2321
  item.failureStage !== "validation" &&
2322
+ item.failureCode !== "resolve_rejected" &&
2276
2323
  item.file
2277
2324
  ) {
2278
2325
  const retry = document.createElement("button");
@@ -2319,7 +2366,6 @@
2319
2366
  fragment.append(card);
2320
2367
  });
2321
2368
  elements.fileList.replaceChildren(fragment);
2322
- renderCompletionAction();
2323
2369
  scheduleIntrinsicSizeReport(true);
2324
2370
  }
2325
2371
 
@@ -2331,10 +2377,54 @@
2331
2377
  return "Pendiente";
2332
2378
  }
2333
2379
 
2334
- async function refreshSessionStatus() {
2380
+ function uploadedStatusMessage(assets) {
2381
+ return assets.length === 1
2382
+ ? "Archivo listo. Escribe en el chat para continuar."
2383
+ : `${assets.length} archivos listos. Escribe en el chat para continuar.`;
2384
+ }
2385
+
2386
+ /**
2387
+ * Rebuilds the file list from assets the session already holds, so a
2388
+ * re-rendered App shows what was uploaded instead of an empty picker.
2389
+ * These items have no local File: they cannot be retried or removed,
2390
+ * because the bytes live in the staging bucket and not in this page.
2391
+ */
2392
+ function adoptAlreadyUploadedAssets(assets) {
2393
+ for (const item of state.items) revokePreviewUrl(item);
2394
+ state.items = assets.map((asset, index) => ({
2395
+ id: `rehydrated-${asset.assetId}`,
2396
+ uploadId: asset.assetId,
2397
+ file: null,
2398
+ displayName: `Archivo ${index + 1}`,
2399
+ uploadFile: null,
2400
+ mediaKind: asset.mediaKind,
2401
+ status: "uploaded",
2402
+ progress: 100,
2403
+ attempts: 0,
2404
+ error: "",
2405
+ failureStage: "",
2406
+ assetId: asset.assetId,
2407
+ previewUrl: "",
2408
+ rehydrated: true,
2409
+ }));
2410
+ renderItems();
2411
+ }
2412
+
2413
+ /**
2414
+ * Reads the staging session's real contents.
2415
+ *
2416
+ * `onMount` marks the call the App makes when a host re-renders a saved
2417
+ * tool result — reopening the chat, switching away and back. That render
2418
+ * starts from an empty widget with no items, so without this the user is
2419
+ * shown a fresh picker for files they already uploaded. On mount an empty
2420
+ * session is the normal case (nothing uploaded yet) and must stay silent;
2421
+ * after an upload the same emptiness means the upload did not land.
2422
+ */
2423
+ async function refreshSessionStatus({ onMount = false } = {}) {
2335
2424
  if (!state.session) return;
2336
2425
  const requestedSessionId = state.session.sessionId;
2337
- setSessionStatus("Comprobando el staging privado…", "neutral");
2426
+ if (!onMount)
2427
+ setSessionStatus("Comprobando el staging privado…", "neutral");
2338
2428
  try {
2339
2429
  const result = await callReadOnlyTool(state.session.checkToolName, {
2340
2430
  sessionId: requestedSessionId,
@@ -2346,9 +2436,8 @@
2346
2436
  )
2347
2437
  return;
2348
2438
  const status = parseStatusResult(result);
2349
- state.assets = status.assets;
2350
- renderCompletionAction();
2351
2439
  if (!status.assets.length) {
2440
+ if (onMount) return;
2352
2441
  requireFallback(
2353
2442
  "La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
2354
2443
  "warning",
@@ -2356,33 +2445,38 @@
2356
2445
  return;
2357
2446
  }
2358
2447
 
2448
+ if (onMount) {
2449
+ // The probe is async: the user can pick a file while it is in
2450
+ // flight. Their selection is the live truth, so never overwrite it
2451
+ // with the snapshot this call started from.
2452
+ if (state.items.length > 0 || state.busyBatch) return;
2453
+ adoptAlreadyUploadedAssets(status.assets);
2454
+ setSessionStatus(uploadedStatusMessage(status.assets), "success");
2455
+ setPickerAvailability();
2456
+ scheduleIntrinsicSizeReport(true);
2457
+ return;
2458
+ }
2459
+
2359
2460
  const partial =
2360
2461
  status.status === "pending" ||
2361
2462
  (state.items.length > 0 &&
2362
2463
  status.assets.length < state.items.length);
2363
- const canContinueInChat = canMessageHost();
2364
2464
  if (partial) {
2365
2465
  requireFallback(
2366
2466
  "Carga parcial verificada. Puedes continuar desde el enlace seguro si algún archivo no terminó.",
2367
2467
  "warning",
2368
2468
  );
2369
- } else if (!canContinueInChat) {
2370
- requireFallback(
2371
- "Carga completada. Este chat no puede continuar desde el widget. Abre el enlace seguro para seguir.",
2372
- "warning",
2373
- );
2374
2469
  } else {
2375
2470
  setFallbackVisibility(state.fallbackRequired);
2376
2471
  }
2377
2472
  setSessionStatus(
2378
2473
  partial
2379
2474
  ? "Carga parcial verificada. Continuaremos solo con los assets listos."
2380
- : canContinueInChat
2381
- ? "Carga completada."
2382
- : "Carga completada. Abre el enlace seguro para seguir.",
2383
- partial || !canContinueInChat ? "warning" : "success",
2475
+ : uploadedStatusMessage(status.assets),
2476
+ partial ? "warning" : "success",
2384
2477
  );
2385
2478
  } catch (error) {
2479
+ if (onMount) return;
2386
2480
  const expired =
2387
2481
  sessionExpired() ||
2388
2482
  (error && /expired|expirad/i.test(String(error.message)));
@@ -2461,82 +2555,6 @@
2461
2555
  return { status, assets };
2462
2556
  }
2463
2557
 
2464
- async function notifyChatUploadComplete() {
2465
- if (
2466
- state.completionNotified ||
2467
- state.completionNotificationInFlight ||
2468
- !state.session
2469
- )
2470
- return;
2471
- const completionSessionId = state.session.sessionId;
2472
- state.completionNotificationInFlight = true;
2473
- setPickerAvailability();
2474
- renderCompletionAction();
2475
- const assets = state.assets
2476
- .map((asset) => `${asset.mediaKind} asset_id ${asset.assetId}`)
2477
- .join(", ");
2478
- const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again. Ask only for the remaining inputs required by my original request.`;
2479
- try {
2480
- const bridge = openAiBridge();
2481
- if (bridge && typeof bridge.sendFollowUpMessage === "function") {
2482
- await bridge.sendFollowUpMessage({ prompt: message });
2483
- if (
2484
- !state.session ||
2485
- state.session.sessionId !== completionSessionId
2486
- )
2487
- return;
2488
- state.completionNotified = true;
2489
- setPickerAvailability();
2490
- renderCompletionAction();
2491
- return;
2492
- }
2493
- if (state.hostCanMessage) {
2494
- const messageResult = await rpcRequest("ui/message", {
2495
- role: "user",
2496
- content: [{ type: "text", text: message }],
2497
- });
2498
- if (isRecord(messageResult) && messageResult.isError === true)
2499
- throw new Error("host_message_rejected");
2500
- if (
2501
- !state.session ||
2502
- state.session.sessionId !== completionSessionId
2503
- )
2504
- return;
2505
- state.completionNotified = true;
2506
- setPickerAvailability();
2507
- renderCompletionAction();
2508
- return;
2509
- }
2510
- setSessionStatus(
2511
- "La carga terminó, pero este host no puede continuar el chat desde el widget. Vuelve al chat para seguir.",
2512
- "warning",
2513
- );
2514
- } catch {
2515
- if (
2516
- !state.session ||
2517
- state.session.sessionId !== completionSessionId
2518
- )
2519
- return;
2520
- setSessionStatus(
2521
- "No pudimos continuar el chat. Intenta nuevamente.",
2522
- "danger",
2523
- );
2524
- } finally {
2525
- if (
2526
- state.session &&
2527
- state.session.sessionId === completionSessionId
2528
- ) {
2529
- state.completionNotificationInFlight = false;
2530
- setPickerAvailability();
2531
- renderCompletionAction();
2532
- }
2533
- }
2534
- }
2535
-
2536
- elements.completeButton.addEventListener("click", () => {
2537
- if (canCompleteUpload()) void notifyChatUploadComplete();
2538
- });
2539
-
2540
2558
  document.addEventListener("DOMContentLoaded", initialize, {
2541
2559
  once: true,
2542
2560
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Product Maker MCP server — cost products, generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from Claude, ChatGPT, or Gemini.",