@productmaker/mcp 6.3.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 +156 -11
  2. package/dist/stdio.js +156 -11
  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
  ]),
@@ -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.3.0";
13277
+ var MCP_VERSION = "6.3.1";
13171
13278
 
13172
13279
  // src/meta-upload-app.ts
13173
13280
  import { readFileSync } from "fs";
@@ -14266,6 +14373,7 @@ var McpErrorCode = {
14266
14373
  SHOPIFY_TOKEN_EXPIRED: "SHOPIFY_TOKEN_EXPIRED",
14267
14374
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "SHOPIFY_SCOPE_UPGRADE_REQUIRED",
14268
14375
  SHOPIFY_PUBLISH_FAILED: "SHOPIFY_PUBLISH_FAILED",
14376
+ SHOPIFY_NO_PUBLISHED_THEME: "SHOPIFY_NO_PUBLISHED_THEME",
14269
14377
  // Meta / Facebook Ads
14270
14378
  META_NO_FB_PAGE: "META_NO_FB_PAGE",
14271
14379
  META_PORTFOLIO_MISMATCH: "META_PORTFOLIO_MISMATCH",
@@ -14333,6 +14441,7 @@ var CODE_MESSAGES = {
14333
14441
  SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
14334
14442
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
14335
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.",
14336
14445
  // Meta — high-frequency
14337
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).",
14338
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.",
@@ -14518,6 +14627,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
14518
14627
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
14519
14628
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
14520
14629
  McpErrorCode.SHOPIFY_NOT_CONNECTED,
14630
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
14521
14631
  McpErrorCode.TIKTOK_NOT_CONNECTED,
14522
14632
  McpErrorCode.NO_SHOPIFY_SHOPS,
14523
14633
  McpErrorCode.NO_META_AD_ACCOUNTS
@@ -14543,6 +14653,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
14543
14653
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
14544
14654
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
14545
14655
  McpErrorCode.SHOPIFY_PUBLISH_FAILED,
14656
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
14546
14657
  // Meta
14547
14658
  McpErrorCode.META_NO_FB_PAGE,
14548
14659
  McpErrorCode.META_NO_ADS_CREATED,
@@ -14970,9 +15081,14 @@ function renderAngleReview(pending, authorizedVideoCount) {
14970
15081
  __name(renderAngleReview, "renderAngleReview");
14971
15082
  function renderReferenceConfirmation(pending) {
14972
15083
  if (pending.mode === "visual_recovery") {
14973
- 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
15086
+
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
14974
15090
 
14975
- ${escapeMarkdownText(pending.reason ?? "Necesitamos fotos claras adicionales para continuar.")}
15091
+ ${body}
14976
15092
 
14977
15093
  Abre esta tarea en ProductMaker Web para reemplazar o completar las fotos.`;
14978
15094
  }
@@ -15461,6 +15577,8 @@ var StatusForResolveSchema = z54.object({
15461
15577
  "question",
15462
15578
  "visual_recovery"
15463
15579
  ]).optional(),
15580
+ /** Hand-mirrored from packages/types result.ts — see CLAUDE.md § MCP Server Sync. */
15581
+ canKeepSelection: z54.boolean().optional(),
15464
15582
  decisionKey: z54.string().optional(),
15465
15583
  questionOrdinal: z54.number().int().min(1).max(5).optional(),
15466
15584
  questionCeiling: z54.union([
@@ -15959,7 +16077,19 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
15959
16077
  const candidateGrouping = pending.candidateGrouping;
15960
16078
  const selectedImageUrls = pending.selectedImageUrls ?? [];
15961
16079
  if (pending.mode === "visual_recovery") {
15962
- 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);
15963
16093
  }
15964
16094
  if (!candidateGrouping || selectedImageUrls.length === 0) {
15965
16095
  throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
@@ -18677,6 +18807,16 @@ function renderMeta(meta) {
18677
18807
  ].filter(Boolean);
18678
18808
  }
18679
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");
18680
18820
  function renderCandidate(candidate, index) {
18681
18821
  const name = candidate.nameLocal ?? candidate.nameOriginal;
18682
18822
  const lines = [
@@ -18692,7 +18832,12 @@ function renderCandidate(candidate, index) {
18692
18832
  ]);
18693
18833
  lines.push("", ...renderMeta(candidate.meta));
18694
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")}`);
18695
- 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
+ }
18696
18841
  return lines.join("\n");
18697
18842
  }
18698
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.3.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
  ]),
@@ -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
 
@@ -13011,6 +13118,7 @@ var McpErrorCode = {
13011
13118
  SHOPIFY_TOKEN_EXPIRED: "SHOPIFY_TOKEN_EXPIRED",
13012
13119
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "SHOPIFY_SCOPE_UPGRADE_REQUIRED",
13013
13120
  SHOPIFY_PUBLISH_FAILED: "SHOPIFY_PUBLISH_FAILED",
13121
+ SHOPIFY_NO_PUBLISHED_THEME: "SHOPIFY_NO_PUBLISHED_THEME",
13014
13122
  // Meta / Facebook Ads
13015
13123
  META_NO_FB_PAGE: "META_NO_FB_PAGE",
13016
13124
  META_PORTFOLIO_MISMATCH: "META_PORTFOLIO_MISMATCH",
@@ -13078,6 +13186,7 @@ var CODE_MESSAGES = {
13078
13186
  SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
13079
13187
  SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
13080
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.",
13081
13190
  // Meta — high-frequency
13082
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).",
13083
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.",
@@ -13263,6 +13372,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
13263
13372
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
13264
13373
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
13265
13374
  McpErrorCode.SHOPIFY_NOT_CONNECTED,
13375
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
13266
13376
  McpErrorCode.TIKTOK_NOT_CONNECTED,
13267
13377
  McpErrorCode.NO_SHOPIFY_SHOPS,
13268
13378
  McpErrorCode.NO_META_AD_ACCOUNTS
@@ -13288,6 +13398,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
13288
13398
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
13289
13399
  McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
13290
13400
  McpErrorCode.SHOPIFY_PUBLISH_FAILED,
13401
+ McpErrorCode.SHOPIFY_NO_PUBLISHED_THEME,
13291
13402
  // Meta
13292
13403
  McpErrorCode.META_NO_FB_PAGE,
13293
13404
  McpErrorCode.META_NO_ADS_CREATED,
@@ -13768,9 +13879,14 @@ function renderAngleReview(pending, authorizedVideoCount) {
13768
13879
  __name(renderAngleReview, "renderAngleReview");
13769
13880
  function renderReferenceConfirmation(pending) {
13770
13881
  if (pending.mode === "visual_recovery") {
13771
- 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
13884
+
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
13772
13888
 
13773
- ${escapeMarkdownText(pending.reason ?? "Necesitamos fotos claras adicionales para continuar.")}
13889
+ ${body}
13774
13890
 
13775
13891
  Abre esta tarea en ProductMaker Web para reemplazar o completar las fotos.`;
13776
13892
  }
@@ -14259,6 +14375,8 @@ var StatusForResolveSchema = z53.object({
14259
14375
  "question",
14260
14376
  "visual_recovery"
14261
14377
  ]).optional(),
14378
+ /** Hand-mirrored from packages/types result.ts — see CLAUDE.md § MCP Server Sync. */
14379
+ canKeepSelection: z53.boolean().optional(),
14262
14380
  decisionKey: z53.string().optional(),
14263
14381
  questionOrdinal: z53.number().int().min(1).max(5).optional(),
14264
14382
  questionCeiling: z53.union([
@@ -14757,7 +14875,19 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
14757
14875
  const candidateGrouping = pending.candidateGrouping;
14758
14876
  const selectedImageUrls = pending.selectedImageUrls ?? [];
14759
14877
  if (pending.mode === "visual_recovery") {
14760
- 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);
14761
14891
  }
14762
14892
  if (!candidateGrouping || selectedImageUrls.length === 0) {
14763
14893
  throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
@@ -17475,6 +17605,16 @@ function renderMeta(meta) {
17475
17605
  ].filter(Boolean);
17476
17606
  }
17477
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");
17478
17618
  function renderCandidate(candidate, index) {
17479
17619
  const name = candidate.nameLocal ?? candidate.nameOriginal;
17480
17620
  const lines = [
@@ -17490,7 +17630,12 @@ function renderCandidate(candidate, index) {
17490
17630
  ]);
17491
17631
  lines.push("", ...renderMeta(candidate.meta));
17492
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")}`);
17493
- 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
+ }
17494
17639
  return lines.join("\n");
17495
17640
  }
17496
17641
  __name(renderCandidate, "renderCandidate");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "6.3.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.",