@productmaker/mcp 6.2.0 → 6.3.1

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.
Files changed (3) hide show
  1. package/dist/http.js +217 -25
  2. package/dist/stdio.js +217 -25
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -619,7 +619,25 @@ var AdaptiveProductReferenceStateSchema = z5.object({
619
619
  * may enrich the candidate later without changing the photos the user
620
620
  * approved; that must not invalidate the answer and reopen the question.
621
621
  */
622
- lastAnsweredSelectedImageUrlsHash: z5.string().min(1).max(128).optional()
622
+ lastAnsweredSelectedImageUrlsHash: z5.string().min(1).max(128).optional(),
623
+ /**
624
+ * SHE PRESSED "SIGUE CON ESTAS FOTOS".
625
+ *
626
+ * The evidence version she pressed it at. The guard is
627
+ * `keptSelectionAtEvidenceVersion <= referenceEvidenceVersion`, so the
628
+ * retraction SURVIVES a later photo change — exactly like every other answer
629
+ * since answers stopped being filtered by evidence version. Recording the
630
+ * version is what makes a retraction taken at a FUTURE version (impossible
631
+ * today, but the ordering is asserted rather than assumed) not count.
632
+ *
633
+ * This is a RETRACTION, and it needs its own field because it cannot be
634
+ * expressed as an answer: the `different_products` answer that opened visual
635
+ * recovery short-circuits `buildAdaptiveProductReferenceDecision` before any
636
+ * later answer is read, so recording one more answer could never close the
637
+ * gate. Bumping the evidence version instead would drop every answer and
638
+ * restart the interview from `sale_structure` — the opposite of "continue".
639
+ */
640
+ keptSelectionAtEvidenceVersion: z5.number().int().positive().optional()
623
641
  });
624
642
  var AdaptiveProductReferenceQuestionOptionSchema = z5.object({
625
643
  value: ProductReferenceUserChoiceSchema,
@@ -3078,7 +3096,8 @@ var ShopifyPublishResponse = z22.object({
3078
3096
  var ShopifyPublishErrorResponse = z22.object({
3079
3097
  code: z22.enum([
3080
3098
  "SHOPIFY_NOT_CONNECTED",
3081
- "SHOPIFY_PUBLISH_FAILED"
3099
+ "SHOPIFY_PUBLISH_FAILED",
3100
+ "SHOPIFY_NO_PUBLISHED_THEME"
3082
3101
  ]),
3083
3102
  message: z22.string().optional(),
3084
3103
  authUrl: z22.string().url().optional(),
@@ -3220,9 +3239,16 @@ var ConfirmProductReferencesRequest = z23.object({
3220
3239
  referenceEvidenceVersion: z23.number().int().positive().optional(),
3221
3240
  expectedEvidenceHash: z23.string().min(1).max(128).optional(),
3222
3241
  recovery: z23.object({
3242
+ /**
3243
+ * `keep_selection` is NOT a recovery in the billing sense: it commits
3244
+ * no new evidence, re-enqueues no extract, and costs nothing. It rides
3245
+ * on this field because it answers the same card, and the server
3246
+ * branches on it BEFORE the billed recovery path.
3247
+ */
3223
3248
  action: z23.enum([
3224
3249
  "replace_selection",
3225
- "refine_selection"
3250
+ "refine_selection",
3251
+ "keep_selection"
3226
3252
  ]),
3227
3253
  affectedRefKeys: z23.array(z23.string().min(1).max(200)).min(1).max(5).optional(),
3228
3254
  expectedReferenceVersionId: z23.string().uuid().optional(),
@@ -3230,6 +3256,16 @@ var ConfirmProductReferencesRequest = z23.object({
3230
3256
  uploadedImages: z23.array(ProductReferenceRecoveryUpload).max(INGEST_MAX_REFERENCE_IMAGES).optional()
3231
3257
  }).optional()
3232
3258
  }).superRefine((value, context) => {
3259
+ if (value.recovery?.action === "keep_selection" && value.recovery.uploadedImages?.length) {
3260
+ context.addIssue({
3261
+ code: z23.ZodIssueCode.custom,
3262
+ path: [
3263
+ "recovery",
3264
+ "uploadedImages"
3265
+ ],
3266
+ message: "keep_selection_does_not_accept_uploads"
3267
+ });
3268
+ }
3233
3269
  if (value.recovery?.action === "refine_selection" && value.recovery.uploadedImages?.length) {
3234
3270
  context.addIssue({
3235
3271
  code: z23.ZodIssueCode.custom,
@@ -3797,6 +3833,16 @@ var PendingUserActionSchema = z26.discriminatedUnion("kind", [
3797
3833
  referenceEvidenceVersion: z26.number().int().positive().optional(),
3798
3834
  expectedEvidenceHash: z26.string().min(1).max(128).optional(),
3799
3835
  question: AdaptiveProductReferenceQuestionSchema.optional(),
3836
+ /**
3837
+ * TRUE when "sigue con estas fotos" is a real exit from this card.
3838
+ *
3839
+ * Absent means the seller genuinely has to supply pixels we do not have,
3840
+ * and offering to continue would be a lie: nothing is selected at all, or
3841
+ * a combo component was never photographed. Every client reads this — web,
3842
+ * MCP and WhatsApp — so no surface can invent an exit the server did not
3843
+ * offer, nor omit one it did.
3844
+ */
3845
+ canKeepSelection: z26.boolean().optional(),
3800
3846
  recovery: z26.object({
3801
3847
  kind: z26.literal("combo_component_evidence"),
3802
3848
  minimumImageCount: z26.number().int().min(2).max(5),
@@ -3973,6 +4019,28 @@ var StatusResponse = z26.object({
3973
4019
  * narrower fact than `needsReview` — see the identical field on `TaskSummary`
3974
4020
  * below for why conflating the two produced a call to action that led nowhere. */
3975
4021
  hasAnswerableGate: z26.boolean().optional(),
4022
+ /**
4023
+ * "Is the ANGLE gate — and only that gate — still open?"
4024
+ *
4025
+ * `needsReview` is an OR over every gate (reference, angles, SCRIPT review,
4026
+ * grouping). The chat used to read that OR as proof that the server was
4027
+ * waiting on ANGLES, because the two conditions it paired it with
4028
+ * (`stageStatuses.video_generator === 'pending'` + `status === 'processing'`)
4029
+ * are ALSO true throughout script review. So the moment a script became
4030
+ * ready — a gate the seller had already passed the angle card to reach — the
4031
+ * chat "recovered" the angle review card over a run that was already
4032
+ * dispatched, released its own submit guard, and re-enabled «Generar». The
4033
+ * seller pressed it again; the second resume lost the compare-and-clear and
4034
+ * answered 201 with a plain status body, so the no-op read as success and the
4035
+ * loop repeated (measured 2026-08-27: three resumes in 16s, two of them
4036
+ * no-ops).
4037
+ *
4038
+ * Named rather than inferred, for the same reason as `hasAnswerableGate`: a
4039
+ * gate added tomorrow widens `needsReview` and would silently re-arm that
4040
+ * recovery path. Absent (undefined) means "an older API that cannot answer" —
4041
+ * clients must fall back to `pauseForAngleReview` alone, never to the OR.
4042
+ */
4043
+ awaitingAngleReview: z26.boolean().optional(),
3976
4044
  /** Which landing pipeline produced this task's landing. 'code' tasks have no
3977
4045
  * classic LandingImage rows — the result lives in the inline code editor. */
3978
4046
  landingMode: LandingModeSchema.optional(),
@@ -10516,6 +10584,10 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10516
10584
  defineCapability("cost_product", "6d1f85a999dbdabae4eda406ff4689cdff2016df375b5b6741b91f4308d819af", "none", "read_only", [
10517
10585
  "read"
10518
10586
  ], [
10587
+ slot("product_reference", false, [], [
10588
+ "selectedProductTitle",
10589
+ "selectedProductUrl"
10590
+ ], "server"),
10519
10591
  slot("market", true, [
10520
10592
  "country"
10521
10593
  ]),
@@ -10586,7 +10658,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10586
10658
  ], [
10587
10659
  "creative_variant"
10588
10660
  ]),
10589
- defineCapability("create_product_task", "ba700dbec8fb6454d55f996ae4567b9280256ae3b62fceb77fd729b9aa21425c", "credit_consuming", "creative", [
10661
+ defineCapability("create_product_task", "13a037c7365cc4c30d81a1d89515e663a3de92ffd4138a117fa32ecb629bca44", "credit_consuming", "creative", [
10590
10662
  "credit_consuming",
10591
10663
  "state_change"
10592
10664
  ], [
@@ -10817,7 +10889,8 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10817
10889
  "images"
10818
10890
  ], [
10819
10891
  "imageMessageIds",
10820
- "projectId"
10892
+ "projectId",
10893
+ "productUrl"
10821
10894
  ]),
10822
10895
  slot("product_identity", true, [
10823
10896
  "productTitle"
@@ -10889,7 +10962,8 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10889
10962
  "images"
10890
10963
  ], [
10891
10964
  "imageMessageIds",
10892
- "projectId"
10965
+ "projectId",
10966
+ "productUrl"
10893
10967
  ]),
10894
10968
  slot("locale", true, [
10895
10969
  "language",
@@ -10956,6 +11030,15 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10956
11030
  explicitOmissionAllowed: false,
10957
11031
  fieldCompletion: "all"
10958
11032
  },
11033
+ {
11034
+ slot: "sales_context",
11035
+ askBeforePaidExecution: true,
11036
+ explicitOmissionAllowed: false,
11037
+ fieldCompletion: "any",
11038
+ completionFields: [
11039
+ "salesAngle"
11040
+ ]
11041
+ },
10959
11042
  {
10960
11043
  slot: "commercial_offer",
10961
11044
  askBeforePaidExecution: true,
@@ -10971,7 +11054,8 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10971
11054
  "images"
10972
11055
  ], [
10973
11056
  "imageMessageIds",
10974
- "projectId"
11057
+ "projectId",
11058
+ "productUrl"
10975
11059
  ]),
10976
11060
  slot("product_identity", true, [
10977
11061
  "title"
@@ -11024,6 +11108,25 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
11024
11108
  "product_reference"
11025
11109
  ], [
11026
11110
  "video"
11111
+ ], SHARED_CHANNEL_EXPOSURE, [
11112
+ {
11113
+ slot: "product_reference",
11114
+ askBeforePaidExecution: true,
11115
+ explicitOmissionAllowed: false,
11116
+ fieldCompletion: "any"
11117
+ },
11118
+ {
11119
+ slot: "locale",
11120
+ askBeforePaidExecution: true,
11121
+ explicitOmissionAllowed: false,
11122
+ fieldCompletion: "all"
11123
+ },
11124
+ {
11125
+ slot: "commercial_offer",
11126
+ askBeforePaidExecution: true,
11127
+ explicitOmissionAllowed: true,
11128
+ fieldCompletion: "all"
11129
+ }
11027
11130
  ]),
11028
11131
  defineCapability("get_meta_campaign_status", "92077241cf4e9d14edefca410e85715cc383b1433081c3461c925f8fc3d6e8ab", "none", "read_only", [
11029
11132
  "read"
@@ -11908,6 +12011,10 @@ function parsePublicHttpsOrigin(value) {
11908
12011
  }
11909
12012
  __name(parsePublicHttpsOrigin, "parsePublicHttpsOrigin");
11910
12013
 
12014
+ // ../../packages/types/src/whatsapp.ts
12015
+ var WHATSAPP_MAX_AUDIO_BYTES = 16 * 1024 * 1024;
12016
+ var WHATSAPP_MAX_AUDIO_DURATION_SECONDS = 10 * 60;
12017
+
11911
12018
  // src/http.ts
11912
12019
  import express from "express";
11913
12020
  import rateLimit from "express-rate-limit";
@@ -13167,7 +13274,7 @@ var ApiClient = class {
13167
13274
  };
13168
13275
 
13169
13276
  // src/index.ts
13170
- var MCP_VERSION = "6.2.0";
13277
+ var MCP_VERSION = "6.3.1";
13171
13278
 
13172
13279
  // src/meta-upload-app.ts
13173
13280
  import { readFileSync } from "fs";
@@ -13520,6 +13627,35 @@ function sleep(ms, signal) {
13520
13627
  }
13521
13628
  __name(sleep, "sleep");
13522
13629
 
13630
+ // src/polling-continuation.ts
13631
+ var NO_CONTINUATION_STATUSES = /* @__PURE__ */ new Set([
13632
+ "done",
13633
+ "complete",
13634
+ "error",
13635
+ "awaiting_review"
13636
+ ]);
13637
+ var SUGGESTED_WAIT_SECONDS = 45;
13638
+ function buildPollingContinuation(snapshot, opts = {}) {
13639
+ if (NO_CONTINUATION_STATUSES.has(snapshot.status)) return null;
13640
+ if (snapshot.pendingUserAction != null) return null;
13641
+ const suggestedWaitSeconds = opts.suggestedWaitSeconds ?? SUGGESTED_WAIT_SECONDS;
13642
+ return {
13643
+ stillRunning: true,
13644
+ nextAction: "continue_polling_get_task_status",
13645
+ suggestedWaitSeconds,
13646
+ instruction: `La tarea SIGUE CORRIENDO: no termin\xF3, no fall\xF3 y todav\xEDa no hay nada que el usuario deba revisar. Esto NO es un error ni un final de trabajo.
13647
+
13648
+ REGLA: no termines tu turno sin dejar ARMADA la siguiente consulta. Cerrar el turno est\xE1 bien; cerrarlo sin nada que vuelva a preguntar es lo que deja al usuario esperando para siempre una revisi\xF3n que nunca le vas a mostrar.
13649
+
13650
+ Elige seg\xFAn lo que tu host permita:
13651
+ 1. PREFERIDO \u2014 si puedes agendar un mensaje o turno futuro (mensajes programados, recordatorios): agenda uno a ~2 minutos que vuelva a llamar get_task_status con este mismo taskId, dile al usuario que le avisas, y ah\xED s\xED cierra el turno. Es la mejor opci\xF3n: el usuario queda libre y t\xFA vuelves solo.
13652
+ 2. SI NO PUEDES AGENDAR: llama otra vez a get_task_status AHORA MISMO con waitSeconds=${suggestedWaitSeconds}, y repite hasta un estado terminal (done/error) o una acci\xF3n pendiente del usuario. NO cierres el turno con un "te aviso cuando est\xE9": nadie te va a volver a despertar.
13653
+
13654
+ No subas waitSeconds por encima de ${suggestedWaitSeconds} salvo que sepas que tu host aguanta llamadas m\xE1s largas: muchos clientes MCP cancelan a los 60 s y ver\xEDas un error de timeout en vez del estado.`
13655
+ };
13656
+ }
13657
+ __name(buildPollingContinuation, "buildPollingContinuation");
13658
+
13523
13659
  // src/errors.ts
13524
13660
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
13525
13661
 
@@ -14237,6 +14373,7 @@ var McpErrorCode = {
14237
14373
  SHOPIFY_TOKEN_EXPIRED: "SHOPIFY_TOKEN_EXPIRED",
14238
14374
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "SHOPIFY_SCOPE_UPGRADE_REQUIRED",
14239
14375
  SHOPIFY_PUBLISH_FAILED: "SHOPIFY_PUBLISH_FAILED",
14376
+ SHOPIFY_NO_PUBLISHED_THEME: "SHOPIFY_NO_PUBLISHED_THEME",
14240
14377
  // Meta / Facebook Ads
14241
14378
  META_NO_FB_PAGE: "META_NO_FB_PAGE",
14242
14379
  META_PORTFOLIO_MISMATCH: "META_PORTFOLIO_MISMATCH",
@@ -14304,6 +14441,7 @@ var CODE_MESSAGES = {
14304
14441
  SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
14305
14442
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
14306
14443
  SHOPIFY_PUBLISH_FAILED: "Shopify rejected the publish. Check that the product/landing does not already exist on the store and that your Shopify plan allows additional products.",
14444
+ SHOPIFY_NO_PUBLISHED_THEME: "The Shopify store has no published theme, so there is nowhere to place the landing. Ask the user to publish a theme (Online Store > Themes > Publish) and retry.",
14307
14445
  // Meta — high-frequency
14308
14446
  META_NO_FB_PAGE: "No Facebook Page is associated with this account. Create one at https://www.facebook.com/pages/create before publishing (Meta requires a Page to run ads).",
14309
14447
  META_PORTFOLIO_MISMATCH: "The selected Meta portfolio does not contain the selected ad account. Call `list_meta_destinations` again and choose an account from that portfolio.",
@@ -14489,6 +14627,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
14489
14627
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
14490
14628
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
14491
14629
  McpErrorCode.SHOPIFY_NOT_CONNECTED,
14630
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
14492
14631
  McpErrorCode.TIKTOK_NOT_CONNECTED,
14493
14632
  McpErrorCode.NO_SHOPIFY_SHOPS,
14494
14633
  McpErrorCode.NO_META_AD_ACCOUNTS
@@ -14514,6 +14653,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
14514
14653
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
14515
14654
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
14516
14655
  McpErrorCode.SHOPIFY_PUBLISH_FAILED,
14656
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
14517
14657
  // Meta
14518
14658
  McpErrorCode.META_NO_FB_PAGE,
14519
14659
  McpErrorCode.META_NO_ADS_CREATED,
@@ -14941,9 +15081,14 @@ function renderAngleReview(pending, authorizedVideoCount) {
14941
15081
  __name(renderAngleReview, "renderAngleReview");
14942
15082
  function renderReferenceConfirmation(pending) {
14943
15083
  if (pending.mode === "visual_recovery") {
14944
- return `## Faltan fotos del producto
15084
+ const body = escapeMarkdownText(pending.reason ?? "Necesitamos revisar las fotos antes de continuar.");
15085
+ return pending.canKeepSelection ? `## Revisa las fotos del producto
14945
15086
 
14946
- ${escapeMarkdownText(pending.reason ?? "Necesitamos fotos claras adicionales para continuar.")}
15087
+ ${body}
15088
+
15089
+ Puedes **seguir con estas fotos** (sin costo adicional) llamando \`resolve_pending_action\` con \`kind: "product_reference_confirmation"\`, o abrir la tarea en ProductMaker Web para subir otras.` : `## Faltan fotos del producto
15090
+
15091
+ ${body}
14947
15092
 
14948
15093
  Abre esta tarea en ProductMaker Web para reemplazar o completar las fotos.`;
14949
15094
  }
@@ -15205,7 +15350,7 @@ var CreateProductTaskInput = z54.object({
15205
15350
  paymentMethods: z54.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
15206
15351
  productContext: z54.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
15207
15352
  productLabel: z54.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
15208
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s (single_call) y 30s (Multiescena). Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
15353
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s (single_call) y 30s (Multiescena). Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (idea imposible: un truco visual que en la vida real no podr\xEDa pasar, y al cierre la invitaci\xF3n a comprar), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
15209
15354
  durationSeconds: z54.union([
15210
15355
  z54.literal(15),
15211
15356
  z54.literal(30)
@@ -15432,6 +15577,8 @@ var StatusForResolveSchema = z54.object({
15432
15577
  "question",
15433
15578
  "visual_recovery"
15434
15579
  ]).optional(),
15580
+ /** Hand-mirrored from packages/types result.ts — see CLAUDE.md § MCP Server Sync. */
15581
+ canKeepSelection: z54.boolean().optional(),
15435
15582
  decisionKey: z54.string().optional(),
15436
15583
  questionOrdinal: z54.number().int().min(1).max(5).optional(),
15437
15584
  questionCeiling: z54.union([
@@ -15752,18 +15899,36 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
15752
15899
  const snapshot = await pollStatus(fetcher, input.waitSeconds ?? 0, signal);
15753
15900
  if (snapshot.status !== "done") {
15754
15901
  const review = renderPendingActionMarkdown(snapshot);
15755
- return review ? {
15756
- // Human-visible content stays presentation-only. Resolution
15757
- // refetches the server-owned snapshot, so protocol IDs/hashes
15758
- // never need to be narrated to the user.
15759
- content: [
15760
- {
15761
- type: "text",
15762
- text: review
15763
- }
15764
- ],
15765
- structuredContent: snapshot
15766
- } : asText(snapshot);
15902
+ if (review) {
15903
+ return {
15904
+ content: [
15905
+ {
15906
+ type: "text",
15907
+ text: review
15908
+ }
15909
+ ],
15910
+ structuredContent: snapshot
15911
+ };
15912
+ }
15913
+ const continuation = buildPollingContinuation(snapshot);
15914
+ if (continuation) {
15915
+ return {
15916
+ content: [
15917
+ {
15918
+ type: "text",
15919
+ text: JSON.stringify({
15920
+ ...snapshot,
15921
+ pollingContinuation: continuation
15922
+ })
15923
+ },
15924
+ {
15925
+ type: "text",
15926
+ text: continuation.instruction
15927
+ }
15928
+ ]
15929
+ };
15930
+ }
15931
+ return asText(snapshot);
15767
15932
  }
15768
15933
  if (snapshot.kind === "service") {
15769
15934
  const result = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-result`, {
@@ -15912,7 +16077,19 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
15912
16077
  const candidateGrouping = pending.candidateGrouping;
15913
16078
  const selectedImageUrls = pending.selectedImageUrls ?? [];
15914
16079
  if (pending.mode === "visual_recovery") {
15915
- throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
16080
+ if (!pending.canKeepSelection) {
16081
+ throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
16082
+ }
16083
+ const kept = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
16084
+ selectedImageUrls: pending.selectedImageUrls ?? [],
16085
+ recovery: {
16086
+ action: "keep_selection",
16087
+ ...pending.expectedEvidenceHash ? {
16088
+ expectedEvidenceHash: pending.expectedEvidenceHash
16089
+ } : {}
16090
+ }
16091
+ });
16092
+ return asText(kept);
15916
16093
  }
15917
16094
  if (!candidateGrouping || selectedImageUrls.length === 0) {
15918
16095
  throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
@@ -18630,6 +18807,16 @@ function renderMeta(meta) {
18630
18807
  ].filter(Boolean);
18631
18808
  }
18632
18809
  __name(renderMeta, "renderMeta");
18810
+ function renderDiscoveryLinks(links) {
18811
+ if (!links) return [];
18812
+ return [
18813
+ links.originalProductUrl ? `- [Producto original en TikTok Shop](${links.originalProductUrl})` : "- Producto original en TikTok Shop: no disponible",
18814
+ `- [Buscar d\xF3nde comprarlo](${links.googleSearchUrl})`,
18815
+ links.metaAdUrl ? `- [Anuncio relacionado encontrado en Meta](${links.metaAdUrl}) \u2014 comp\xE1ralo; puede ser una variante o un producto parecido` : "- Anuncio relacionado en Meta: no disponible",
18816
+ links.kaloDataUrl ? `- [Datos en KaloData](${links.kaloDataUrl})` : "- Datos en KaloData: no disponibles"
18817
+ ];
18818
+ }
18819
+ __name(renderDiscoveryLinks, "renderDiscoveryLinks");
18633
18820
  function renderCandidate(candidate, index) {
18634
18821
  const name = candidate.nameLocal ?? candidate.nameOriginal;
18635
18822
  const lines = [
@@ -18645,7 +18832,12 @@ function renderCandidate(candidate, index) {
18645
18832
  ]);
18646
18833
  lines.push("", ...renderMeta(candidate.meta));
18647
18834
  lines.push("", `- **Venta (30d):** ${num(candidate.units30d)} unidades \xB7 ${num(candidate.revenueUsd30d, " USD")} \xB7 crecimiento ${num(candidate.growthPct30d, "%")} \xB7 precio ${num(candidate.priceUsd, " USD")}`);
18648
- if (candidate.productUrl) lines.push(`- [Ver producto](${candidate.productUrl})`);
18835
+ const discoveryLines = renderDiscoveryLinks(candidate.discoveryLinks);
18836
+ if (discoveryLines.length > 0) {
18837
+ lines.push("", "**D\xF3nde encontrarlo:**", ...discoveryLines);
18838
+ } else if (candidate.productUrl) {
18839
+ lines.push(`- [Ver producto](${candidate.productUrl})`);
18840
+ }
18649
18841
  return lines.join("\n");
18650
18842
  }
18651
18843
  __name(renderCandidate, "renderCandidate");
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 = "6.2.0";
267
+ var MCP_VERSION = "6.3.1";
268
268
 
269
269
  // src/meta-upload-app.ts
270
270
  import { readFileSync } from "fs";
@@ -950,7 +950,25 @@ var AdaptiveProductReferenceStateSchema = z5.object({
950
950
  * may enrich the candidate later without changing the photos the user
951
951
  * approved; that must not invalidate the answer and reopen the question.
952
952
  */
953
- lastAnsweredSelectedImageUrlsHash: z5.string().min(1).max(128).optional()
953
+ lastAnsweredSelectedImageUrlsHash: z5.string().min(1).max(128).optional(),
954
+ /**
955
+ * SHE PRESSED "SIGUE CON ESTAS FOTOS".
956
+ *
957
+ * The evidence version she pressed it at. The guard is
958
+ * `keptSelectionAtEvidenceVersion <= referenceEvidenceVersion`, so the
959
+ * retraction SURVIVES a later photo change — exactly like every other answer
960
+ * since answers stopped being filtered by evidence version. Recording the
961
+ * version is what makes a retraction taken at a FUTURE version (impossible
962
+ * today, but the ordering is asserted rather than assumed) not count.
963
+ *
964
+ * This is a RETRACTION, and it needs its own field because it cannot be
965
+ * expressed as an answer: the `different_products` answer that opened visual
966
+ * recovery short-circuits `buildAdaptiveProductReferenceDecision` before any
967
+ * later answer is read, so recording one more answer could never close the
968
+ * gate. Bumping the evidence version instead would drop every answer and
969
+ * restart the interview from `sale_structure` — the opposite of "continue".
970
+ */
971
+ keptSelectionAtEvidenceVersion: z5.number().int().positive().optional()
954
972
  });
955
973
  var AdaptiveProductReferenceQuestionOptionSchema = z5.object({
956
974
  value: ProductReferenceUserChoiceSchema,
@@ -3409,7 +3427,8 @@ var ShopifyPublishResponse = z22.object({
3409
3427
  var ShopifyPublishErrorResponse = z22.object({
3410
3428
  code: z22.enum([
3411
3429
  "SHOPIFY_NOT_CONNECTED",
3412
- "SHOPIFY_PUBLISH_FAILED"
3430
+ "SHOPIFY_PUBLISH_FAILED",
3431
+ "SHOPIFY_NO_PUBLISHED_THEME"
3413
3432
  ]),
3414
3433
  message: z22.string().optional(),
3415
3434
  authUrl: z22.string().url().optional(),
@@ -3551,9 +3570,16 @@ var ConfirmProductReferencesRequest = z23.object({
3551
3570
  referenceEvidenceVersion: z23.number().int().positive().optional(),
3552
3571
  expectedEvidenceHash: z23.string().min(1).max(128).optional(),
3553
3572
  recovery: z23.object({
3573
+ /**
3574
+ * `keep_selection` is NOT a recovery in the billing sense: it commits
3575
+ * no new evidence, re-enqueues no extract, and costs nothing. It rides
3576
+ * on this field because it answers the same card, and the server
3577
+ * branches on it BEFORE the billed recovery path.
3578
+ */
3554
3579
  action: z23.enum([
3555
3580
  "replace_selection",
3556
- "refine_selection"
3581
+ "refine_selection",
3582
+ "keep_selection"
3557
3583
  ]),
3558
3584
  affectedRefKeys: z23.array(z23.string().min(1).max(200)).min(1).max(5).optional(),
3559
3585
  expectedReferenceVersionId: z23.string().uuid().optional(),
@@ -3561,6 +3587,16 @@ var ConfirmProductReferencesRequest = z23.object({
3561
3587
  uploadedImages: z23.array(ProductReferenceRecoveryUpload).max(INGEST_MAX_REFERENCE_IMAGES).optional()
3562
3588
  }).optional()
3563
3589
  }).superRefine((value, context) => {
3590
+ if (value.recovery?.action === "keep_selection" && value.recovery.uploadedImages?.length) {
3591
+ context.addIssue({
3592
+ code: z23.ZodIssueCode.custom,
3593
+ path: [
3594
+ "recovery",
3595
+ "uploadedImages"
3596
+ ],
3597
+ message: "keep_selection_does_not_accept_uploads"
3598
+ });
3599
+ }
3564
3600
  if (value.recovery?.action === "refine_selection" && value.recovery.uploadedImages?.length) {
3565
3601
  context.addIssue({
3566
3602
  code: z23.ZodIssueCode.custom,
@@ -4128,6 +4164,16 @@ var PendingUserActionSchema = z26.discriminatedUnion("kind", [
4128
4164
  referenceEvidenceVersion: z26.number().int().positive().optional(),
4129
4165
  expectedEvidenceHash: z26.string().min(1).max(128).optional(),
4130
4166
  question: AdaptiveProductReferenceQuestionSchema.optional(),
4167
+ /**
4168
+ * TRUE when "sigue con estas fotos" is a real exit from this card.
4169
+ *
4170
+ * Absent means the seller genuinely has to supply pixels we do not have,
4171
+ * and offering to continue would be a lie: nothing is selected at all, or
4172
+ * a combo component was never photographed. Every client reads this — web,
4173
+ * MCP and WhatsApp — so no surface can invent an exit the server did not
4174
+ * offer, nor omit one it did.
4175
+ */
4176
+ canKeepSelection: z26.boolean().optional(),
4131
4177
  recovery: z26.object({
4132
4178
  kind: z26.literal("combo_component_evidence"),
4133
4179
  minimumImageCount: z26.number().int().min(2).max(5),
@@ -4304,6 +4350,28 @@ var StatusResponse = z26.object({
4304
4350
  * narrower fact than `needsReview` — see the identical field on `TaskSummary`
4305
4351
  * below for why conflating the two produced a call to action that led nowhere. */
4306
4352
  hasAnswerableGate: z26.boolean().optional(),
4353
+ /**
4354
+ * "Is the ANGLE gate — and only that gate — still open?"
4355
+ *
4356
+ * `needsReview` is an OR over every gate (reference, angles, SCRIPT review,
4357
+ * grouping). The chat used to read that OR as proof that the server was
4358
+ * waiting on ANGLES, because the two conditions it paired it with
4359
+ * (`stageStatuses.video_generator === 'pending'` + `status === 'processing'`)
4360
+ * are ALSO true throughout script review. So the moment a script became
4361
+ * ready — a gate the seller had already passed the angle card to reach — the
4362
+ * chat "recovered" the angle review card over a run that was already
4363
+ * dispatched, released its own submit guard, and re-enabled «Generar». The
4364
+ * seller pressed it again; the second resume lost the compare-and-clear and
4365
+ * answered 201 with a plain status body, so the no-op read as success and the
4366
+ * loop repeated (measured 2026-08-27: three resumes in 16s, two of them
4367
+ * no-ops).
4368
+ *
4369
+ * Named rather than inferred, for the same reason as `hasAnswerableGate`: a
4370
+ * gate added tomorrow widens `needsReview` and would silently re-arm that
4371
+ * recovery path. Absent (undefined) means "an older API that cannot answer" —
4372
+ * clients must fall back to `pauseForAngleReview` alone, never to the OR.
4373
+ */
4374
+ awaitingAngleReview: z26.boolean().optional(),
4307
4375
  /** Which landing pipeline produced this task's landing. 'code' tasks have no
4308
4376
  * classic LandingImage rows — the result lives in the inline code editor. */
4309
4377
  landingMode: LandingModeSchema.optional(),
@@ -10847,6 +10915,10 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10847
10915
  defineCapability("cost_product", "6d1f85a999dbdabae4eda406ff4689cdff2016df375b5b6741b91f4308d819af", "none", "read_only", [
10848
10916
  "read"
10849
10917
  ], [
10918
+ slot("product_reference", false, [], [
10919
+ "selectedProductTitle",
10920
+ "selectedProductUrl"
10921
+ ], "server"),
10850
10922
  slot("market", true, [
10851
10923
  "country"
10852
10924
  ]),
@@ -10917,7 +10989,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10917
10989
  ], [
10918
10990
  "creative_variant"
10919
10991
  ]),
10920
- defineCapability("create_product_task", "ba700dbec8fb6454d55f996ae4567b9280256ae3b62fceb77fd729b9aa21425c", "credit_consuming", "creative", [
10992
+ defineCapability("create_product_task", "13a037c7365cc4c30d81a1d89515e663a3de92ffd4138a117fa32ecb629bca44", "credit_consuming", "creative", [
10921
10993
  "credit_consuming",
10922
10994
  "state_change"
10923
10995
  ], [
@@ -11148,7 +11220,8 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
11148
11220
  "images"
11149
11221
  ], [
11150
11222
  "imageMessageIds",
11151
- "projectId"
11223
+ "projectId",
11224
+ "productUrl"
11152
11225
  ]),
11153
11226
  slot("product_identity", true, [
11154
11227
  "productTitle"
@@ -11220,7 +11293,8 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
11220
11293
  "images"
11221
11294
  ], [
11222
11295
  "imageMessageIds",
11223
- "projectId"
11296
+ "projectId",
11297
+ "productUrl"
11224
11298
  ]),
11225
11299
  slot("locale", true, [
11226
11300
  "language",
@@ -11287,6 +11361,15 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
11287
11361
  explicitOmissionAllowed: false,
11288
11362
  fieldCompletion: "all"
11289
11363
  },
11364
+ {
11365
+ slot: "sales_context",
11366
+ askBeforePaidExecution: true,
11367
+ explicitOmissionAllowed: false,
11368
+ fieldCompletion: "any",
11369
+ completionFields: [
11370
+ "salesAngle"
11371
+ ]
11372
+ },
11290
11373
  {
11291
11374
  slot: "commercial_offer",
11292
11375
  askBeforePaidExecution: true,
@@ -11302,7 +11385,8 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
11302
11385
  "images"
11303
11386
  ], [
11304
11387
  "imageMessageIds",
11305
- "projectId"
11388
+ "projectId",
11389
+ "productUrl"
11306
11390
  ]),
11307
11391
  slot("product_identity", true, [
11308
11392
  "title"
@@ -11355,6 +11439,25 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
11355
11439
  "product_reference"
11356
11440
  ], [
11357
11441
  "video"
11442
+ ], SHARED_CHANNEL_EXPOSURE, [
11443
+ {
11444
+ slot: "product_reference",
11445
+ askBeforePaidExecution: true,
11446
+ explicitOmissionAllowed: false,
11447
+ fieldCompletion: "any"
11448
+ },
11449
+ {
11450
+ slot: "locale",
11451
+ askBeforePaidExecution: true,
11452
+ explicitOmissionAllowed: false,
11453
+ fieldCompletion: "all"
11454
+ },
11455
+ {
11456
+ slot: "commercial_offer",
11457
+ askBeforePaidExecution: true,
11458
+ explicitOmissionAllowed: true,
11459
+ fieldCompletion: "all"
11460
+ }
11358
11461
  ]),
11359
11462
  defineCapability("get_meta_campaign_status", "92077241cf4e9d14edefca410e85715cc383b1433081c3461c925f8fc3d6e8ab", "none", "read_only", [
11360
11463
  "read"
@@ -11983,6 +12086,10 @@ var ReferralDashboardOverviewSchema = z51.object({
11983
12086
  monthlyRegistrations: z51.array(ReferralDashboardMonthlyRegistrationSchema).refine((months) => months.length === 6, "Expected six monthly registration buckets")
11984
12087
  });
11985
12088
 
12089
+ // ../../packages/types/src/whatsapp.ts
12090
+ var WHATSAPP_MAX_AUDIO_BYTES = 16 * 1024 * 1024;
12091
+ var WHATSAPP_MAX_AUDIO_DURATION_SECONDS = 10 * 60;
12092
+
11986
12093
  // src/tools/tasks.ts
11987
12094
  import { z as z53 } from "zod";
11988
12095
 
@@ -12265,6 +12372,35 @@ function sleep(ms, signal) {
12265
12372
  }
12266
12373
  __name(sleep, "sleep");
12267
12374
 
12375
+ // src/polling-continuation.ts
12376
+ var NO_CONTINUATION_STATUSES = /* @__PURE__ */ new Set([
12377
+ "done",
12378
+ "complete",
12379
+ "error",
12380
+ "awaiting_review"
12381
+ ]);
12382
+ var SUGGESTED_WAIT_SECONDS = 45;
12383
+ function buildPollingContinuation(snapshot, opts = {}) {
12384
+ if (NO_CONTINUATION_STATUSES.has(snapshot.status)) return null;
12385
+ if (snapshot.pendingUserAction != null) return null;
12386
+ const suggestedWaitSeconds = opts.suggestedWaitSeconds ?? SUGGESTED_WAIT_SECONDS;
12387
+ return {
12388
+ stillRunning: true,
12389
+ nextAction: "continue_polling_get_task_status",
12390
+ suggestedWaitSeconds,
12391
+ instruction: `La tarea SIGUE CORRIENDO: no termin\xF3, no fall\xF3 y todav\xEDa no hay nada que el usuario deba revisar. Esto NO es un error ni un final de trabajo.
12392
+
12393
+ REGLA: no termines tu turno sin dejar ARMADA la siguiente consulta. Cerrar el turno est\xE1 bien; cerrarlo sin nada que vuelva a preguntar es lo que deja al usuario esperando para siempre una revisi\xF3n que nunca le vas a mostrar.
12394
+
12395
+ Elige seg\xFAn lo que tu host permita:
12396
+ 1. PREFERIDO \u2014 si puedes agendar un mensaje o turno futuro (mensajes programados, recordatorios): agenda uno a ~2 minutos que vuelva a llamar get_task_status con este mismo taskId, dile al usuario que le avisas, y ah\xED s\xED cierra el turno. Es la mejor opci\xF3n: el usuario queda libre y t\xFA vuelves solo.
12397
+ 2. SI NO PUEDES AGENDAR: llama otra vez a get_task_status AHORA MISMO con waitSeconds=${suggestedWaitSeconds}, y repite hasta un estado terminal (done/error) o una acci\xF3n pendiente del usuario. NO cierres el turno con un "te aviso cuando est\xE9": nadie te va a volver a despertar.
12398
+
12399
+ No subas waitSeconds por encima de ${suggestedWaitSeconds} salvo que sepas que tu host aguanta llamadas m\xE1s largas: muchos clientes MCP cancelan a los 60 s y ver\xEDas un error de timeout en vez del estado.`
12400
+ };
12401
+ }
12402
+ __name(buildPollingContinuation, "buildPollingContinuation");
12403
+
12268
12404
  // src/errors.ts
12269
12405
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
12270
12406
 
@@ -12982,6 +13118,7 @@ var McpErrorCode = {
12982
13118
  SHOPIFY_TOKEN_EXPIRED: "SHOPIFY_TOKEN_EXPIRED",
12983
13119
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "SHOPIFY_SCOPE_UPGRADE_REQUIRED",
12984
13120
  SHOPIFY_PUBLISH_FAILED: "SHOPIFY_PUBLISH_FAILED",
13121
+ SHOPIFY_NO_PUBLISHED_THEME: "SHOPIFY_NO_PUBLISHED_THEME",
12985
13122
  // Meta / Facebook Ads
12986
13123
  META_NO_FB_PAGE: "META_NO_FB_PAGE",
12987
13124
  META_PORTFOLIO_MISMATCH: "META_PORTFOLIO_MISMATCH",
@@ -13049,6 +13186,7 @@ var CODE_MESSAGES = {
13049
13186
  SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
13050
13187
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
13051
13188
  SHOPIFY_PUBLISH_FAILED: "Shopify rejected the publish. Check that the product/landing does not already exist on the store and that your Shopify plan allows additional products.",
13189
+ SHOPIFY_NO_PUBLISHED_THEME: "The Shopify store has no published theme, so there is nowhere to place the landing. Ask the user to publish a theme (Online Store > Themes > Publish) and retry.",
13052
13190
  // Meta — high-frequency
13053
13191
  META_NO_FB_PAGE: "No Facebook Page is associated with this account. Create one at https://www.facebook.com/pages/create before publishing (Meta requires a Page to run ads).",
13054
13192
  META_PORTFOLIO_MISMATCH: "The selected Meta portfolio does not contain the selected ad account. Call `list_meta_destinations` again and choose an account from that portfolio.",
@@ -13234,6 +13372,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
13234
13372
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
13235
13373
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
13236
13374
  McpErrorCode.SHOPIFY_NOT_CONNECTED,
13375
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
13237
13376
  McpErrorCode.TIKTOK_NOT_CONNECTED,
13238
13377
  McpErrorCode.NO_SHOPIFY_SHOPS,
13239
13378
  McpErrorCode.NO_META_AD_ACCOUNTS
@@ -13259,6 +13398,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
13259
13398
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
13260
13399
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
13261
13400
  McpErrorCode.SHOPIFY_PUBLISH_FAILED,
13401
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
13262
13402
  // Meta
13263
13403
  McpErrorCode.META_NO_FB_PAGE,
13264
13404
  McpErrorCode.META_NO_ADS_CREATED,
@@ -13739,9 +13879,14 @@ function renderAngleReview(pending, authorizedVideoCount) {
13739
13879
  __name(renderAngleReview, "renderAngleReview");
13740
13880
  function renderReferenceConfirmation(pending) {
13741
13881
  if (pending.mode === "visual_recovery") {
13742
- return `## Faltan fotos del producto
13882
+ const body = escapeMarkdownText(pending.reason ?? "Necesitamos revisar las fotos antes de continuar.");
13883
+ return pending.canKeepSelection ? `## Revisa las fotos del producto
13743
13884
 
13744
- ${escapeMarkdownText(pending.reason ?? "Necesitamos fotos claras adicionales para continuar.")}
13885
+ ${body}
13886
+
13887
+ Puedes **seguir con estas fotos** (sin costo adicional) llamando \`resolve_pending_action\` con \`kind: "product_reference_confirmation"\`, o abrir la tarea en ProductMaker Web para subir otras.` : `## Faltan fotos del producto
13888
+
13889
+ ${body}
13745
13890
 
13746
13891
  Abre esta tarea en ProductMaker Web para reemplazar o completar las fotos.`;
13747
13892
  }
@@ -14003,7 +14148,7 @@ var CreateProductTaskInput = z53.object({
14003
14148
  paymentMethods: z53.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
14004
14149
  productContext: z53.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
14005
14150
  productLabel: z53.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
14006
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s (single_call) y 30s (Multiescena). Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
14151
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Todos los presets funcionan a 15s (single_call) y 30s (Multiescena). Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "problem_solution" (problema concreto, producto como soluci\xF3n y CTA), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (idea imposible: un truco visual que en la vida real no podr\xEDa pasar, y al cierre la invitaci\xF3n a comprar), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
14007
14152
  durationSeconds: z53.union([
14008
14153
  z53.literal(15),
14009
14154
  z53.literal(30)
@@ -14230,6 +14375,8 @@ var StatusForResolveSchema = z53.object({
14230
14375
  "question",
14231
14376
  "visual_recovery"
14232
14377
  ]).optional(),
14378
+ /** Hand-mirrored from packages/types result.ts — see CLAUDE.md § MCP Server Sync. */
14379
+ canKeepSelection: z53.boolean().optional(),
14233
14380
  decisionKey: z53.string().optional(),
14234
14381
  questionOrdinal: z53.number().int().min(1).max(5).optional(),
14235
14382
  questionCeiling: z53.union([
@@ -14550,18 +14697,36 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
14550
14697
  const snapshot = await pollStatus(fetcher, input.waitSeconds ?? 0, signal);
14551
14698
  if (snapshot.status !== "done") {
14552
14699
  const review = renderPendingActionMarkdown(snapshot);
14553
- return review ? {
14554
- // Human-visible content stays presentation-only. Resolution
14555
- // refetches the server-owned snapshot, so protocol IDs/hashes
14556
- // never need to be narrated to the user.
14557
- content: [
14558
- {
14559
- type: "text",
14560
- text: review
14561
- }
14562
- ],
14563
- structuredContent: snapshot
14564
- } : asText(snapshot);
14700
+ if (review) {
14701
+ return {
14702
+ content: [
14703
+ {
14704
+ type: "text",
14705
+ text: review
14706
+ }
14707
+ ],
14708
+ structuredContent: snapshot
14709
+ };
14710
+ }
14711
+ const continuation = buildPollingContinuation(snapshot);
14712
+ if (continuation) {
14713
+ return {
14714
+ content: [
14715
+ {
14716
+ type: "text",
14717
+ text: JSON.stringify({
14718
+ ...snapshot,
14719
+ pollingContinuation: continuation
14720
+ })
14721
+ },
14722
+ {
14723
+ type: "text",
14724
+ text: continuation.instruction
14725
+ }
14726
+ ]
14727
+ };
14728
+ }
14729
+ return asText(snapshot);
14565
14730
  }
14566
14731
  if (snapshot.kind === "service") {
14567
14732
  const result = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-result`, {
@@ -14710,7 +14875,19 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
14710
14875
  const candidateGrouping = pending.candidateGrouping;
14711
14876
  const selectedImageUrls = pending.selectedImageUrls ?? [];
14712
14877
  if (pending.mode === "visual_recovery") {
14713
- throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
14878
+ if (!pending.canKeepSelection) {
14879
+ throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
14880
+ }
14881
+ const kept = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
14882
+ selectedImageUrls: pending.selectedImageUrls ?? [],
14883
+ recovery: {
14884
+ action: "keep_selection",
14885
+ ...pending.expectedEvidenceHash ? {
14886
+ expectedEvidenceHash: pending.expectedEvidenceHash
14887
+ } : {}
14888
+ }
14889
+ });
14890
+ return asText(kept);
14714
14891
  }
14715
14892
  if (!candidateGrouping || selectedImageUrls.length === 0) {
14716
14893
  throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
@@ -17428,6 +17605,16 @@ function renderMeta(meta) {
17428
17605
  ].filter(Boolean);
17429
17606
  }
17430
17607
  __name(renderMeta, "renderMeta");
17608
+ function renderDiscoveryLinks(links) {
17609
+ if (!links) return [];
17610
+ return [
17611
+ links.originalProductUrl ? `- [Producto original en TikTok Shop](${links.originalProductUrl})` : "- Producto original en TikTok Shop: no disponible",
17612
+ `- [Buscar d\xF3nde comprarlo](${links.googleSearchUrl})`,
17613
+ links.metaAdUrl ? `- [Anuncio relacionado encontrado en Meta](${links.metaAdUrl}) \u2014 comp\xE1ralo; puede ser una variante o un producto parecido` : "- Anuncio relacionado en Meta: no disponible",
17614
+ links.kaloDataUrl ? `- [Datos en KaloData](${links.kaloDataUrl})` : "- Datos en KaloData: no disponibles"
17615
+ ];
17616
+ }
17617
+ __name(renderDiscoveryLinks, "renderDiscoveryLinks");
17431
17618
  function renderCandidate(candidate, index) {
17432
17619
  const name = candidate.nameLocal ?? candidate.nameOriginal;
17433
17620
  const lines = [
@@ -17443,7 +17630,12 @@ function renderCandidate(candidate, index) {
17443
17630
  ]);
17444
17631
  lines.push("", ...renderMeta(candidate.meta));
17445
17632
  lines.push("", `- **Venta (30d):** ${num(candidate.units30d)} unidades \xB7 ${num(candidate.revenueUsd30d, " USD")} \xB7 crecimiento ${num(candidate.growthPct30d, "%")} \xB7 precio ${num(candidate.priceUsd, " USD")}`);
17446
- if (candidate.productUrl) lines.push(`- [Ver producto](${candidate.productUrl})`);
17633
+ const discoveryLines = renderDiscoveryLinks(candidate.discoveryLinks);
17634
+ if (discoveryLines.length > 0) {
17635
+ lines.push("", "**D\xF3nde encontrarlo:**", ...discoveryLines);
17636
+ } else if (candidate.productUrl) {
17637
+ lines.push(`- [Ver producto](${candidate.productUrl})`);
17638
+ }
17447
17639
  return lines.join("\n");
17448
17640
  }
17449
17641
  __name(renderCandidate, "renderCandidate");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "6.2.0",
3
+ "version": "6.3.1",
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.",