@productmaker/mcp 3.1.2 → 3.3.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
@@ -2600,7 +2600,13 @@ var VideoGenerator = z18.object({
2600
2600
  videoResolution: VideoResolution.optional(),
2601
2601
  /** Requested controls and the effective provider route that produced this result. */
2602
2602
  execution: VideoExecutionContract.optional(),
2603
- adCopy: z18.string().optional()
2603
+ adCopy: z18.string().optional(),
2604
+ /** TRUE when the seller APPROVED a script for this angle that the plan path
2605
+ * could not honor, so a fresh script was re-authored and rendered instead
2606
+ * (PR #1470). The angle still delivered — nothing was lost and nothing is
2607
+ * refundable — but the video is NOT the script they signed off on, and the
2608
+ * UI must say so. Absent (never `false`) on every honest delivery. */
2609
+ scriptRewritten: z18.boolean().optional()
2604
2610
  });
2605
2611
  var VideoScriptSceneForReview = z18.object({
2606
2612
  index: z18.number().int().nonnegative(),
@@ -4136,8 +4142,20 @@ var AdminUserListResponse = z27.object({
4136
4142
  page: z27.number().int().positive(),
4137
4143
  pageSize: z27.number().int().positive()
4138
4144
  });
4145
+ var AdminCreditGrantReason = z27.enum([
4146
+ "compensation",
4147
+ "goodwill",
4148
+ "promo",
4149
+ "internal-testing",
4150
+ "correction"
4151
+ ]);
4139
4152
  var UpdateUserCreditsRequest = z27.object({
4140
- creditCents: z27.number().nonnegative()
4153
+ creditCents: z27.number().nonnegative(),
4154
+ reason: AdminCreditGrantReason,
4155
+ /** Free-text detail. Required when the enum alone does not explain the grant. */
4156
+ notes: z27.string().trim().min(1).max(500).optional(),
4157
+ /** The task the grant compensates, when there is one. */
4158
+ taskId: z27.string().uuid().optional()
4141
4159
  });
4142
4160
  var QualityReportStatus = z27.enum([
4143
4161
  "open",
@@ -8906,7 +8924,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8906
8924
  ], [
8907
8925
  "creative_variant"
8908
8926
  ]),
8909
- defineCapability("create_product_task", "1c2c0153391172e057cbab25c16c79222611f37b9a6ec41a5135aeadec0c574f", "credit_consuming", "creative", [
8927
+ defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
8910
8928
  "credit_consuming",
8911
8929
  "state_change"
8912
8930
  ], [
@@ -8947,6 +8965,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8947
8965
  "productContext"
8948
8966
  ]),
8949
8967
  slot("video_options", false, [
8968
+ "videoPreset",
8969
+ "durationSeconds",
8970
+ "pauseForAngleReview",
8971
+ // MCP-only: WhatsApp runs its own confirmation policy and does not
8972
+ // offer a "decide everything" mode, so the field is not mirrored there.
8973
+ "aiDecidesAll",
8974
+ "videoTier",
8975
+ "videoResolution",
8976
+ "multiAngleVideos",
8977
+ "angleNarratives",
8978
+ "competitorScanEnabled"
8979
+ ], [
8950
8980
  "videoPreset",
8951
8981
  "durationSeconds",
8952
8982
  "pauseForAngleReview",
@@ -9172,7 +9202,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9172
9202
  ], [
9173
9203
  "creative"
9174
9204
  ]),
9175
- defineCapability("generate_landing", "e35e6373ddb92ba09e4adf1acd79387767387817a7b3ec8d83b464f884508fe1", "credit_consuming", "creative", [
9205
+ defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
9176
9206
  "credit_consuming",
9177
9207
  "state_change"
9178
9208
  ], [
@@ -9214,6 +9244,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9214
9244
  "subheadline"
9215
9245
  ]),
9216
9246
  slot("landing_options", false, [
9247
+ "competitorScanEnabled",
9248
+ "landingStrategy",
9249
+ "landingImageFormat",
9250
+ // MCP-only, like create_product_task's aiDecidesAll.
9251
+ "aiDecidesAll"
9252
+ ], [
9217
9253
  "competitorScanEnabled",
9218
9254
  "landingStrategy",
9219
9255
  "landingImageFormat"
@@ -9242,7 +9278,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9242
9278
  fieldCompletion: "all"
9243
9279
  }
9244
9280
  ]),
9245
- defineCapability("generate_video_creative", "6bcd073df66023f8108a671b3027e1f35e9596cf9481ddf13a0282b4c56f5f34", "credit_consuming", "creative", [
9281
+ defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
9246
9282
  "credit_consuming",
9247
9283
  "state_change"
9248
9284
  ], [
@@ -9264,6 +9300,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9264
9300
  "country"
9265
9301
  ]),
9266
9302
  slot("render_options", true, [
9303
+ "aspectRatio",
9304
+ "durationSeconds",
9305
+ "videoPreset",
9306
+ "videoTier",
9307
+ "videoResolution",
9308
+ "features",
9309
+ "cta",
9310
+ "scriptNotes",
9311
+ "pauseForVideoScriptReview",
9312
+ "pauseForAngleReview",
9313
+ // MCP-only, like create_product_task's aiDecidesAll.
9314
+ "aiDecidesAll",
9315
+ "competitorScanEnabled"
9316
+ ], [
9267
9317
  "aspectRatio",
9268
9318
  "durationSeconds",
9269
9319
  "videoPreset",
@@ -11042,7 +11092,7 @@ var START_DROPSHIPPING_PROMPT = [
11042
11092
  "",
11043
11093
  "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
11094
  "",
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.",
11095
+ "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
11096
  "",
11047
11097
  "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
11098
  "",
@@ -11368,7 +11418,7 @@ var ApiClient = class {
11368
11418
  };
11369
11419
 
11370
11420
  // src/index.ts
11371
- var MCP_VERSION = "3.1.2";
11421
+ var MCP_VERSION = "3.3.0";
11372
11422
 
11373
11423
  // src/meta-upload-app.ts
11374
11424
  import { readFileSync } from "fs";
@@ -12818,7 +12868,12 @@ async function fetchTaskOutputs(api, taskId, options) {
12818
12868
  return {
12819
12869
  angleKey: vg.angleKey ?? "",
12820
12870
  angleIndex: vg.angleIndex ?? 0,
12821
- variants: variants2.length ? variants2 : fallback
12871
+ variants: variants2.length ? variants2 : fallback,
12872
+ // Only ever true — an honest delivery leaves the key off entirely so no
12873
+ // consumer has to distinguish `false` from "the API is older than this".
12874
+ ...vg.scriptRewritten === true ? {
12875
+ scriptRewritten: true
12876
+ } : {}
12822
12877
  };
12823
12878
  }).filter((v) => v.angleKey && v.variants.length);
12824
12879
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -12881,6 +12936,9 @@ function renderOutputsMarkdown(outputs) {
12881
12936
  for (const variant of v.variants) {
12882
12937
  parts.push(`- [${escapeMdLabel(v.angleKey)} \xB7 ${escapeMdLabel(variant.kind)}](${variant.url})`);
12883
12938
  }
12939
+ if (v.scriptRewritten) {
12940
+ parts.push(` \u26A0\uFE0F No pudimos usar el guion que aprobaste para este \xE1ngulo, as\xED que generamos el video con un guion nuevo. Si no te sirve, ed\xEDtalo o vuelve a generarlo.`);
12941
+ }
12884
12942
  }
12885
12943
  }
12886
12944
  return parts.join("\n");
@@ -12906,6 +12964,7 @@ var CreateProductTaskInput = z49.object({
12906
12964
  z49.literal(30)
12907
12965
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
12908
12966
  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."),
12967
+ 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
12968
  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
12969
  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
12970
  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 +13292,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13233
13292
  "options.subheadline": input.subheadline,
13234
13293
  "options.landingStrategy": input.landingStrategy,
13235
13294
  "options.landingImageFormat": input.landingImageFormat,
13295
+ "options.aiDecidesAll": input.aiDecidesAll,
13236
13296
  pauseForAngleReview: input.pauseForAngleReview,
13237
13297
  url: input.productUrl
13238
13298
  }),
@@ -13304,7 +13364,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13304
13364
  throw translateError(e);
13305
13365
  }
13306
13366
  });
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) => {
13367
+ 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
13368
  try {
13309
13369
  const signal = extra?.signal;
13310
13370
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
@@ -13387,7 +13447,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13387
13447
  throw translateError(e);
13388
13448
  }
13389
13449
  });
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) => {
13450
+ 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
13451
  try {
13392
13452
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
13393
13453
  const status = StatusForResolveSchema.parse(raw);
@@ -13690,6 +13750,7 @@ var VideoInput = z50.object({
13690
13750
  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
13751
  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
13752
  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."),
13753
+ 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
13754
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
13694
13755
  brandId: CreativeRefs.brandId,
13695
13756
  projectId: CreativeRefs.projectId
@@ -13767,6 +13828,7 @@ var EditImageCreativeInput = z50.object({
13767
13828
  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
13829
  });
13769
13830
  var LandingInput = z50.object({
13831
+ 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
13832
  images: CreativeRefs.images,
13771
13833
  language: CreativeRefs.language,
13772
13834
  country: CreativeRefs.country,
@@ -14973,7 +15035,7 @@ function uploadSessionText(result) {
14973
15035
  text: [
14974
15036
  `### Prepara tu ${mediaLabel} en ProductMaker`,
14975
15037
  `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.`,
15038
+ `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
15039
  `Este enlace expira: ${result.expiresAt}`
14978
15040
  ].join("\n\n")
14979
15041
  }
@@ -14988,7 +15050,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
14988
15050
  function registerMediaUploadTools(server, api) {
14989
15051
  registerAppTool(server, "request_media_upload", {
14990
15052
  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.",
15053
+ 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
15054
  inputSchema: UploadSessionToolInputShape,
14993
15055
  outputSchema: UniversalUploadStructuredResponse.shape,
14994
15056
  _meta: {
@@ -15028,7 +15090,7 @@ function registerMediaUploadTools(server, api) {
15028
15090
  }
15029
15091
  });
15030
15092
  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.",
15093
+ 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
15094
  inputSchema: UploadStatusInput.shape,
15033
15095
  _meta: {
15034
15096
  "openai/widgetAccessible": true
@@ -15948,7 +16010,7 @@ var CostProjectInput = z62.object({
15948
16010
  var GuideInput = z62.object({
15949
16011
  projectId: ProjectId.optional()
15950
16012
  }).strict();
15951
- var DropshippingProjectNextAction = z62.discriminatedUnion("kind", [
16013
+ var NEXT_ACTION_KINDS = [
15952
16014
  "cost_product",
15953
16015
  "provide_product_media",
15954
16016
  "generate_creatives",
@@ -15962,7 +16024,8 @@ var DropshippingProjectNextAction = z62.discriminatedUnion("kind", [
15962
16024
  "create_meta_campaign",
15963
16025
  "repair_meta_campaign",
15964
16026
  "review_paused_campaign"
15965
- ].map((kind) => z62.object({
16027
+ ];
16028
+ var DropshippingProjectNextAction = z62.discriminatedUnion("kind", NEXT_ACTION_KINDS.map((kind) => z62.object({
15966
16029
  kind: z62.literal(kind),
15967
16030
  href: z62.string().min(1)
15968
16031
  })));
@@ -16036,9 +16099,25 @@ var ProjectResponseSchema = z62.object({
16036
16099
  createdAt: z62.string().datetime(),
16037
16100
  updatedAt: z62.string().datetime()
16038
16101
  }).passthrough();
16102
+ var NEXT_ACTION_GUIDANCE = {
16103
+ cost_product: "Llama `cost_dropshipping_project` con el costo real del proveedor. Nunca lo inventes: p\xEDdeselo al usuario.",
16104
+ provide_product_media: "Llama `request_media_upload` con mediaKind=image y uploadPurpose=product_reference. Cuando el usuario diga que ya subi\xF3, llama `check_media_upload` y usa los assetId. No pidas ruta local ni URL.",
16105
+ generate_creatives: "Llama `create_product_task` con los assetId del producto. Antes dile al usuario qu\xE9 outputs vas a generar y confirma precio y oferta.",
16106
+ wait_for_creatives: "No hay nada que hacer todav\xEDa: la tarea est\xE1 corriendo. Llama `get_task_status` y espera. No crees otra tarea.",
16107
+ repair_creatives: "La tarea fall\xF3. Llama `get_task_status` para leer el error y expl\xEDcaselo al usuario antes de reintentar nada.",
16108
+ complete_creative_set: "Faltan outputs por terminar. Llama `get_task_status` y revisa si hay una acci\xF3n pendiente que resolver con `resolve_pending_action`.",
16109
+ connect_shopify: "Llama `list_shopify_shops`. Si no hay ninguna tienda conectada, el usuario debe conectarla en la web: d\xE1selo como paso manual.",
16110
+ publish_storefront: "Llama `publish_to_shopify` con el taskId y la tienda elegida.",
16111
+ wait_for_storefront: "La publicaci\xF3n en Shopify est\xE1 en curso. Llama `get_task_status` y espera; no publiques otra vez.",
16112
+ connect_meta: "Llama `list_meta_destinations`. Si no hay cuentas, el usuario debe conectar Meta en la web: d\xE1selo como paso manual.",
16113
+ create_meta_campaign: "Llama `list_meta_destinations`, pregunta en el chat por portfolioId, adAccountId y pageId, y luego `publish_to_meta`. La campa\xF1a queda PAUSED.",
16114
+ repair_meta_campaign: "La campa\xF1a qued\xF3 en error o pendiente. Llama `get_meta_campaign_status` y expl\xEDcale al usuario qu\xE9 pas\xF3 antes de reintentar.",
16115
+ review_paused_campaign: "Todo est\xE1 listo. La campa\xF1a qued\xF3 PAUSED a prop\xF3sito: dile al usuario que la active \xE9l mismo en Ads Manager."
16116
+ };
16039
16117
  function projectText(raw) {
16040
16118
  const project = ProjectResponseSchema.parse(raw);
16041
- const next = project.nextAction ? `${project.nextAction.kind} (${project.nextAction.href})` : "ninguna";
16119
+ const kind = project.nextAction?.kind;
16120
+ const guidance = kind ? NEXT_ACTION_GUIDANCE[kind] : void 0;
16042
16121
  return {
16043
16122
  content: [
16044
16123
  {
@@ -16048,6 +16127,9 @@ function projectText(raw) {
16048
16127
  productName: project.productName,
16049
16128
  phase: project.phase,
16050
16129
  nextAction: project.nextAction,
16130
+ // The href is a web anchor and is useless to an agent; the guidance
16131
+ // is the executable form of the same step.
16132
+ nextActionGuidance: guidance ?? null,
16051
16133
  hasReferenceImage: project.hasReferenceImage,
16052
16134
  costingSaved: Boolean(project.costingResult),
16053
16135
  rewardProgress: {
@@ -16056,7 +16138,7 @@ function projectText(raw) {
16056
16138
  milestones: project.rewards.milestones
16057
16139
  },
16058
16140
  version: project.version,
16059
- message: `Siguiente paso: ${next}. Relee este proyecto despu\xE9s de cada mutaci\xF3n.`
16141
+ message: guidance ? `Siguiente paso: ${guidance} Relee este proyecto despu\xE9s de cada mutaci\xF3n.` : "El proyecto no tiene ning\xFAn paso pendiente."
16060
16142
  })
16061
16143
  }
16062
16144
  ]
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.3.0";
268
268
 
269
269
  // src/meta-upload-app.ts
270
270
  import { readFileSync } from "fs";
@@ -3221,7 +3221,13 @@ var VideoGenerator = z18.object({
3221
3221
  videoResolution: VideoResolution.optional(),
3222
3222
  /** Requested controls and the effective provider route that produced this result. */
3223
3223
  execution: VideoExecutionContract.optional(),
3224
- adCopy: z18.string().optional()
3224
+ adCopy: z18.string().optional(),
3225
+ /** TRUE when the seller APPROVED a script for this angle that the plan path
3226
+ * could not honor, so a fresh script was re-authored and rendered instead
3227
+ * (PR #1470). The angle still delivered — nothing was lost and nothing is
3228
+ * refundable — but the video is NOT the script they signed off on, and the
3229
+ * UI must say so. Absent (never `false`) on every honest delivery. */
3230
+ scriptRewritten: z18.boolean().optional()
3225
3231
  });
3226
3232
  var VideoScriptSceneForReview = z18.object({
3227
3233
  index: z18.number().int().nonnegative(),
@@ -4757,8 +4763,20 @@ var AdminUserListResponse = z27.object({
4757
4763
  page: z27.number().int().positive(),
4758
4764
  pageSize: z27.number().int().positive()
4759
4765
  });
4766
+ var AdminCreditGrantReason = z27.enum([
4767
+ "compensation",
4768
+ "goodwill",
4769
+ "promo",
4770
+ "internal-testing",
4771
+ "correction"
4772
+ ]);
4760
4773
  var UpdateUserCreditsRequest = z27.object({
4761
- creditCents: z27.number().nonnegative()
4774
+ creditCents: z27.number().nonnegative(),
4775
+ reason: AdminCreditGrantReason,
4776
+ /** Free-text detail. Required when the enum alone does not explain the grant. */
4777
+ notes: z27.string().trim().min(1).max(500).optional(),
4778
+ /** The task the grant compensates, when there is one. */
4779
+ taskId: z27.string().uuid().optional()
4762
4780
  });
4763
4781
  var QualityReportStatus = z27.enum([
4764
4782
  "open",
@@ -9527,7 +9545,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9527
9545
  ], [
9528
9546
  "creative_variant"
9529
9547
  ]),
9530
- defineCapability("create_product_task", "1c2c0153391172e057cbab25c16c79222611f37b9a6ec41a5135aeadec0c574f", "credit_consuming", "creative", [
9548
+ defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
9531
9549
  "credit_consuming",
9532
9550
  "state_change"
9533
9551
  ], [
@@ -9568,6 +9586,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9568
9586
  "productContext"
9569
9587
  ]),
9570
9588
  slot("video_options", false, [
9589
+ "videoPreset",
9590
+ "durationSeconds",
9591
+ "pauseForAngleReview",
9592
+ // MCP-only: WhatsApp runs its own confirmation policy and does not
9593
+ // offer a "decide everything" mode, so the field is not mirrored there.
9594
+ "aiDecidesAll",
9595
+ "videoTier",
9596
+ "videoResolution",
9597
+ "multiAngleVideos",
9598
+ "angleNarratives",
9599
+ "competitorScanEnabled"
9600
+ ], [
9571
9601
  "videoPreset",
9572
9602
  "durationSeconds",
9573
9603
  "pauseForAngleReview",
@@ -9793,7 +9823,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9793
9823
  ], [
9794
9824
  "creative"
9795
9825
  ]),
9796
- defineCapability("generate_landing", "e35e6373ddb92ba09e4adf1acd79387767387817a7b3ec8d83b464f884508fe1", "credit_consuming", "creative", [
9826
+ defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
9797
9827
  "credit_consuming",
9798
9828
  "state_change"
9799
9829
  ], [
@@ -9835,6 +9865,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9835
9865
  "subheadline"
9836
9866
  ]),
9837
9867
  slot("landing_options", false, [
9868
+ "competitorScanEnabled",
9869
+ "landingStrategy",
9870
+ "landingImageFormat",
9871
+ // MCP-only, like create_product_task's aiDecidesAll.
9872
+ "aiDecidesAll"
9873
+ ], [
9838
9874
  "competitorScanEnabled",
9839
9875
  "landingStrategy",
9840
9876
  "landingImageFormat"
@@ -9863,7 +9899,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9863
9899
  fieldCompletion: "all"
9864
9900
  }
9865
9901
  ]),
9866
- defineCapability("generate_video_creative", "6bcd073df66023f8108a671b3027e1f35e9596cf9481ddf13a0282b4c56f5f34", "credit_consuming", "creative", [
9902
+ defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
9867
9903
  "credit_consuming",
9868
9904
  "state_change"
9869
9905
  ], [
@@ -9885,6 +9921,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9885
9921
  "country"
9886
9922
  ]),
9887
9923
  slot("render_options", true, [
9924
+ "aspectRatio",
9925
+ "durationSeconds",
9926
+ "videoPreset",
9927
+ "videoTier",
9928
+ "videoResolution",
9929
+ "features",
9930
+ "cta",
9931
+ "scriptNotes",
9932
+ "pauseForVideoScriptReview",
9933
+ "pauseForAngleReview",
9934
+ // MCP-only, like create_product_task's aiDecidesAll.
9935
+ "aiDecidesAll",
9936
+ "competitorScanEnabled"
9937
+ ], [
9888
9938
  "aspectRatio",
9889
9939
  "durationSeconds",
9890
9940
  "videoPreset",
@@ -11620,7 +11670,12 @@ async function fetchTaskOutputs(api, taskId, options) {
11620
11670
  return {
11621
11671
  angleKey: vg.angleKey ?? "",
11622
11672
  angleIndex: vg.angleIndex ?? 0,
11623
- variants: variants2.length ? variants2 : fallback
11673
+ variants: variants2.length ? variants2 : fallback,
11674
+ // Only ever true — an honest delivery leaves the key off entirely so no
11675
+ // consumer has to distinguish `false` from "the API is older than this".
11676
+ ...vg.scriptRewritten === true ? {
11677
+ scriptRewritten: true
11678
+ } : {}
11624
11679
  };
11625
11680
  }).filter((v) => v.angleKey && v.variants.length);
11626
11681
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -11683,6 +11738,9 @@ function renderOutputsMarkdown(outputs) {
11683
11738
  for (const variant of v.variants) {
11684
11739
  parts.push(`- [${escapeMdLabel(v.angleKey)} \xB7 ${escapeMdLabel(variant.kind)}](${variant.url})`);
11685
11740
  }
11741
+ if (v.scriptRewritten) {
11742
+ parts.push(` \u26A0\uFE0F No pudimos usar el guion que aprobaste para este \xE1ngulo, as\xED que generamos el video con un guion nuevo. Si no te sirve, ed\xEDtalo o vuelve a generarlo.`);
11743
+ }
11686
11744
  }
11687
11745
  }
11688
11746
  return parts.join("\n");
@@ -11708,6 +11766,7 @@ var CreateProductTaskInput = z48.object({
11708
11766
  z48.literal(30)
11709
11767
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
11710
11768
  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."),
11769
+ 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
11770
  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
11771
  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
11772
  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 +12094,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12035
12094
  "options.subheadline": input.subheadline,
12036
12095
  "options.landingStrategy": input.landingStrategy,
12037
12096
  "options.landingImageFormat": input.landingImageFormat,
12097
+ "options.aiDecidesAll": input.aiDecidesAll,
12038
12098
  pauseForAngleReview: input.pauseForAngleReview,
12039
12099
  url: input.productUrl
12040
12100
  }),
@@ -12106,7 +12166,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12106
12166
  throw translateError(e);
12107
12167
  }
12108
12168
  });
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) => {
12169
+ 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
12170
  try {
12111
12171
  const signal = extra?.signal;
12112
12172
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
@@ -12189,7 +12249,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12189
12249
  throw translateError(e);
12190
12250
  }
12191
12251
  });
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) => {
12252
+ 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
12253
  try {
12194
12254
  const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
12195
12255
  const status = StatusForResolveSchema.parse(raw);
@@ -12492,6 +12552,7 @@ var VideoInput = z49.object({
12492
12552
  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
12553
  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
12554
  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."),
12555
+ 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
12556
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
12496
12557
  brandId: CreativeRefs.brandId,
12497
12558
  projectId: CreativeRefs.projectId
@@ -12569,6 +12630,7 @@ var EditImageCreativeInput = z49.object({
12569
12630
  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
12631
  });
12571
12632
  var LandingInput = z49.object({
12633
+ 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
12634
  images: CreativeRefs.images,
12573
12635
  language: CreativeRefs.language,
12574
12636
  country: CreativeRefs.country,
@@ -13775,7 +13837,7 @@ function uploadSessionText(result) {
13775
13837
  text: [
13776
13838
  `### Prepara tu ${mediaLabel} en ProductMaker`,
13777
13839
  `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.`,
13840
+ `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
13841
  `Este enlace expira: ${result.expiresAt}`
13780
13842
  ].join("\n\n")
13781
13843
  }
@@ -13790,7 +13852,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
13790
13852
  function registerMediaUploadTools(server, api) {
13791
13853
  registerAppTool(server, "request_media_upload", {
13792
13854
  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.",
13855
+ 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
13856
  inputSchema: UploadSessionToolInputShape,
13795
13857
  outputSchema: UniversalUploadStructuredResponse.shape,
13796
13858
  _meta: {
@@ -13830,7 +13892,7 @@ function registerMediaUploadTools(server, api) {
13830
13892
  }
13831
13893
  });
13832
13894
  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.",
13895
+ 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
13896
  inputSchema: UploadStatusInput.shape,
13835
13897
  _meta: {
13836
13898
  "openai/widgetAccessible": true
@@ -14750,7 +14812,7 @@ var CostProjectInput = z61.object({
14750
14812
  var GuideInput = z61.object({
14751
14813
  projectId: ProjectId.optional()
14752
14814
  }).strict();
14753
- var DropshippingProjectNextAction = z61.discriminatedUnion("kind", [
14815
+ var NEXT_ACTION_KINDS = [
14754
14816
  "cost_product",
14755
14817
  "provide_product_media",
14756
14818
  "generate_creatives",
@@ -14764,7 +14826,8 @@ var DropshippingProjectNextAction = z61.discriminatedUnion("kind", [
14764
14826
  "create_meta_campaign",
14765
14827
  "repair_meta_campaign",
14766
14828
  "review_paused_campaign"
14767
- ].map((kind) => z61.object({
14829
+ ];
14830
+ var DropshippingProjectNextAction = z61.discriminatedUnion("kind", NEXT_ACTION_KINDS.map((kind) => z61.object({
14768
14831
  kind: z61.literal(kind),
14769
14832
  href: z61.string().min(1)
14770
14833
  })));
@@ -14838,9 +14901,25 @@ var ProjectResponseSchema = z61.object({
14838
14901
  createdAt: z61.string().datetime(),
14839
14902
  updatedAt: z61.string().datetime()
14840
14903
  }).passthrough();
14904
+ var NEXT_ACTION_GUIDANCE = {
14905
+ cost_product: "Llama `cost_dropshipping_project` con el costo real del proveedor. Nunca lo inventes: p\xEDdeselo al usuario.",
14906
+ provide_product_media: "Llama `request_media_upload` con mediaKind=image y uploadPurpose=product_reference. Cuando el usuario diga que ya subi\xF3, llama `check_media_upload` y usa los assetId. No pidas ruta local ni URL.",
14907
+ generate_creatives: "Llama `create_product_task` con los assetId del producto. Antes dile al usuario qu\xE9 outputs vas a generar y confirma precio y oferta.",
14908
+ wait_for_creatives: "No hay nada que hacer todav\xEDa: la tarea est\xE1 corriendo. Llama `get_task_status` y espera. No crees otra tarea.",
14909
+ repair_creatives: "La tarea fall\xF3. Llama `get_task_status` para leer el error y expl\xEDcaselo al usuario antes de reintentar nada.",
14910
+ complete_creative_set: "Faltan outputs por terminar. Llama `get_task_status` y revisa si hay una acci\xF3n pendiente que resolver con `resolve_pending_action`.",
14911
+ connect_shopify: "Llama `list_shopify_shops`. Si no hay ninguna tienda conectada, el usuario debe conectarla en la web: d\xE1selo como paso manual.",
14912
+ publish_storefront: "Llama `publish_to_shopify` con el taskId y la tienda elegida.",
14913
+ wait_for_storefront: "La publicaci\xF3n en Shopify est\xE1 en curso. Llama `get_task_status` y espera; no publiques otra vez.",
14914
+ connect_meta: "Llama `list_meta_destinations`. Si no hay cuentas, el usuario debe conectar Meta en la web: d\xE1selo como paso manual.",
14915
+ create_meta_campaign: "Llama `list_meta_destinations`, pregunta en el chat por portfolioId, adAccountId y pageId, y luego `publish_to_meta`. La campa\xF1a queda PAUSED.",
14916
+ repair_meta_campaign: "La campa\xF1a qued\xF3 en error o pendiente. Llama `get_meta_campaign_status` y expl\xEDcale al usuario qu\xE9 pas\xF3 antes de reintentar.",
14917
+ review_paused_campaign: "Todo est\xE1 listo. La campa\xF1a qued\xF3 PAUSED a prop\xF3sito: dile al usuario que la active \xE9l mismo en Ads Manager."
14918
+ };
14841
14919
  function projectText(raw) {
14842
14920
  const project = ProjectResponseSchema.parse(raw);
14843
- const next = project.nextAction ? `${project.nextAction.kind} (${project.nextAction.href})` : "ninguna";
14921
+ const kind = project.nextAction?.kind;
14922
+ const guidance = kind ? NEXT_ACTION_GUIDANCE[kind] : void 0;
14844
14923
  return {
14845
14924
  content: [
14846
14925
  {
@@ -14850,6 +14929,9 @@ function projectText(raw) {
14850
14929
  productName: project.productName,
14851
14930
  phase: project.phase,
14852
14931
  nextAction: project.nextAction,
14932
+ // The href is a web anchor and is useless to an agent; the guidance
14933
+ // is the executable form of the same step.
14934
+ nextActionGuidance: guidance ?? null,
14853
14935
  hasReferenceImage: project.hasReferenceImage,
14854
14936
  costingSaved: Boolean(project.costingResult),
14855
14937
  rewardProgress: {
@@ -14858,7 +14940,7 @@ function projectText(raw) {
14858
14940
  milestones: project.rewards.milestones
14859
14941
  },
14860
14942
  version: project.version,
14861
- message: `Siguiente paso: ${next}. Relee este proyecto despu\xE9s de cada mutaci\xF3n.`
14943
+ message: guidance ? `Siguiente paso: ${guidance} Relee este proyecto despu\xE9s de cada mutaci\xF3n.` : "El proyecto no tiene ning\xFAn paso pendiente."
14862
14944
  })
14863
14945
  }
14864
14946
  ]
@@ -14932,7 +15014,7 @@ var START_DROPSHIPPING_PROMPT = [
14932
15014
  "",
14933
15015
  "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
15016
  "",
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.",
15017
+ "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
15018
  "",
14937
15019
  "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
15020
  "",
@@ -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.3.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.",