@productmaker/mcp 3.2.0 → 3.4.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.
Files changed (3) hide show
  1. package/dist/http.js +621 -307
  2. package/dist/stdio.js +611 -306
  3. package/package.json +2 -2
package/dist/http.js CHANGED
@@ -896,6 +896,42 @@ var VideoTier = z5.preprocess((v) => v === "grok" ? "single_call" : v, z5.enum([
896
896
  ]));
897
897
  var DEFAULT_VIDEO_TIER = "single_call";
898
898
 
899
+ // ../../packages/types/src/image-provider.ts
900
+ var GPT_IMAGE_2_RATIOS_BLOCKED_ABOVE_1K = [
901
+ "4:5",
902
+ "5:4"
903
+ ];
904
+ function gptImage2SupportsTier(aspectRatio, tier) {
905
+ if (tier === "gpt-image-2:1k") return true;
906
+ return !GPT_IMAGE_2_RATIOS_BLOCKED_ABOVE_1K.includes(aspectRatio);
907
+ }
908
+ __name(gptImage2SupportsTier, "gptImage2SupportsTier");
909
+ var IMAGE_TIER_UNSUPPORTED_FOR_RATIO_RULE = "image_tier_unsupported_for_ratio";
910
+ var IMAGE_TIER_LIST = [
911
+ "gpt-image-2:1k",
912
+ "gpt-image-2:2k",
913
+ "gpt-image-2:4k"
914
+ ];
915
+ function parseImageTier(imageModel) {
916
+ const normalized = imageModel.trim().toLowerCase();
917
+ return IMAGE_TIER_LIST.includes(normalized) ? normalized : null;
918
+ }
919
+ __name(parseImageTier, "parseImageTier");
920
+ function validateImageTierForRatio(aspectRatio, imageModel) {
921
+ if (!aspectRatio || !imageModel) return null;
922
+ const tier = parseImageTier(imageModel);
923
+ if (!tier) return null;
924
+ const ratio = aspectRatio.trim();
925
+ if (!GPT_IMAGE_2_RATIOS_BLOCKED_ABOVE_1K.includes(ratio)) return null;
926
+ if (gptImage2SupportsTier(ratio, tier)) return null;
927
+ return buildImageTierUnsupportedMessage(ratio);
928
+ }
929
+ __name(validateImageTierForRatio, "validateImageTierForRatio");
930
+ function buildImageTierUnsupportedMessage(aspectRatio) {
931
+ return `El formato ${aspectRatio} solo est\xE1 disponible en calidad est\xE1ndar (1K). Elige 1K, o cambia el formato para usar Premium.`;
932
+ }
933
+ __name(buildImageTierUnsupportedMessage, "buildImageTierUnsupportedMessage");
934
+
899
935
  // ../../packages/types/src/angle.ts
900
936
  import { z as z6 } from "zod";
901
937
  var ImageWinnerSignal = z6.enum([
@@ -2236,6 +2272,19 @@ var IngestOptions = z15.object({
2236
2272
  message
2237
2273
  });
2238
2274
  }
2275
+ const tierMessage = validateImageTierForRatio(data.aspectRatio, data.imageModel);
2276
+ if (tierMessage) {
2277
+ ctx.addIssue({
2278
+ code: z15.ZodIssueCode.custom,
2279
+ path: [
2280
+ "imageModel"
2281
+ ],
2282
+ params: {
2283
+ rule: IMAGE_TIER_UNSUPPORTED_FOR_RATIO_RULE
2284
+ },
2285
+ message: tierMessage
2286
+ });
2287
+ }
2239
2288
  });
2240
2289
  var IngestRequest = z15.object({
2241
2290
  url: z15.string().url(),
@@ -2600,7 +2649,13 @@ var VideoGenerator = z18.object({
2600
2649
  videoResolution: VideoResolution.optional(),
2601
2650
  /** Requested controls and the effective provider route that produced this result. */
2602
2651
  execution: VideoExecutionContract.optional(),
2603
- adCopy: z18.string().optional()
2652
+ adCopy: z18.string().optional(),
2653
+ /** TRUE when the seller APPROVED a script for this angle that the plan path
2654
+ * could not honor, so a fresh script was re-authored and rendered instead
2655
+ * (PR #1470). The angle still delivered — nothing was lost and nothing is
2656
+ * refundable — but the video is NOT the script they signed off on, and the
2657
+ * UI must say so. Absent (never `false`) on every honest delivery. */
2658
+ scriptRewritten: z18.boolean().optional()
2604
2659
  });
2605
2660
  var VideoScriptSceneForReview = z18.object({
2606
2661
  index: z18.number().int().nonnegative(),
@@ -2774,6 +2829,7 @@ var ShopifyPublicationMode = z20.enum([
2774
2829
  "existing",
2775
2830
  "new"
2776
2831
  ]);
2832
+ var SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH = 160;
2777
2833
  var ShopifyPublishRequest = z20.object({
2778
2834
  shopDomain: z20.string().min(3).max(120).optional(),
2779
2835
  /**
@@ -2792,7 +2848,9 @@ var ShopifyPublishRequest = z20.object({
2792
2848
  */
2793
2849
  publicationMode: ShopifyPublicationMode.default("auto"),
2794
2850
  /** Shopify product GID, numeric id, or admin product URL. Required for existing mode. */
2795
- targetProductId: z20.string().trim().min(1).max(300).optional()
2851
+ targetProductId: z20.string().trim().min(1).max(300).optional(),
2852
+ announcementBarEnabled: z20.boolean().optional(),
2853
+ announcementBarText: z20.string().trim().min(1).max(SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH).optional()
2796
2854
  }).superRefine((data, ctx) => {
2797
2855
  if (data.publicationMode === "existing" && !data.targetProductId) {
2798
2856
  ctx.addIssue({
@@ -2803,6 +2861,24 @@ var ShopifyPublishRequest = z20.object({
2803
2861
  message: "Selecciona un producto de Shopify."
2804
2862
  });
2805
2863
  }
2864
+ if (data.announcementBarEnabled && !data.announcementBarText) {
2865
+ ctx.addIssue({
2866
+ code: z20.ZodIssueCode.custom,
2867
+ path: [
2868
+ "announcementBarText"
2869
+ ],
2870
+ message: "Escribe el texto del anuncio."
2871
+ });
2872
+ }
2873
+ if (!data.announcementBarEnabled && data.announcementBarText) {
2874
+ ctx.addIssue({
2875
+ code: z20.ZodIssueCode.custom,
2876
+ path: [
2877
+ "announcementBarText"
2878
+ ],
2879
+ message: "Activa el anuncio para usar texto."
2880
+ });
2881
+ }
2806
2882
  });
2807
2883
  var ShopifyPublicationStatus = z20.enum([
2808
2884
  "pending",
@@ -2885,6 +2961,10 @@ var CreateShopifyBillingChargeRequest = z20.object({
2885
2961
  var CancelShopifySubscriptionRequest = z20.object({
2886
2962
  subscriptionId: z20.string().min(1)
2887
2963
  });
2964
+ var ShopifyAnnouncementBarState = z20.object({
2965
+ enabled: z20.boolean(),
2966
+ text: z20.string().max(SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH).optional()
2967
+ });
2888
2968
  var LandingHealthResponse = z20.object({
2889
2969
  taskId: z20.string(),
2890
2970
  state: z20.enum([
@@ -2926,9 +3006,20 @@ var ApproveAnglesRequest = z21.object({
2926
3006
  "mixed"
2927
3007
  ]).optional()
2928
3008
  });
3009
+ var ApproveAnglesRefusalReason = z21.enum([
3010
+ "photo_review_pending",
3011
+ "photos_not_ready",
3012
+ "reference_out_of_date"
3013
+ ]);
2929
3014
  var ApproveAnglesResponse = z21.object({
2930
3015
  success: z21.boolean(),
2931
- taskId: z21.string().uuid()
3016
+ taskId: z21.string().uuid(),
3017
+ reason: ApproveAnglesRefusalReason.optional()
3018
+ });
3019
+ var IngestResumeRefusalResponse = z21.object({
3020
+ success: z21.literal(false),
3021
+ taskId: z21.string().uuid(),
3022
+ reason: ApproveAnglesRefusalReason.optional()
2932
3023
  });
2933
3024
  var GenerateSuggestionsResponse = z21.object({
2934
3025
  painPoints: z21.array(z21.string()),
@@ -3044,7 +3135,8 @@ var ApproveVideoAnglesRequest = z21.object({
3044
3135
  });
3045
3136
  var ApproveVideoAnglesResponse = z21.object({
3046
3137
  success: z21.boolean(),
3047
- taskId: z21.string().uuid()
3138
+ taskId: z21.string().uuid(),
3139
+ reason: ApproveAnglesRefusalReason.optional()
3048
3140
  });
3049
3141
  var SaveVideoDraftRequest = z21.object({
3050
3142
  /** Global creative direction for all videos. Not spoken dialogue. */
@@ -3076,7 +3168,8 @@ var ApproveImageAnglesRequest = z21.object({
3076
3168
  });
3077
3169
  var ApproveImageAnglesResponse = z21.object({
3078
3170
  success: z21.boolean(),
3079
- taskId: z21.string().uuid()
3171
+ taskId: z21.string().uuid(),
3172
+ reason: ApproveAnglesRefusalReason.optional()
3080
3173
  });
3081
3174
  var SaveImageDraftRequest = z21.object({
3082
3175
  customAngle: z21.string().max(480).optional(),
@@ -3433,6 +3526,10 @@ var MVPResult = z24.object({
3433
3526
  videoAngleCount: z24.number().int().min(1).max(3).optional(),
3434
3527
  adCreative: AdCreative.optional(),
3435
3528
  shopifyPageUrl: z24.string().url().optional(),
3529
+ /** What the announcement bar looks like ON THE PUBLISHED landing right now.
3530
+ * The publish dialog prefills from this: without it the checkbox always opens
3531
+ * unchecked, and republishing anything would read as "remove the bar". */
3532
+ shopifyAnnouncementBar: ShopifyAnnouncementBarState.optional(),
3436
3533
  // Pain points and benefits for image wizard pre-filling
3437
3534
  painPoints: z24.string().optional(),
3438
3535
  benefits: z24.string().optional(),
@@ -3558,7 +3655,19 @@ var StatusResponse = z24.object({
3558
3655
  "done",
3559
3656
  "error"
3560
3657
  ]),
3658
+ /** ENGINEERING string (`Task.error`). Never render this to a seller. */
3561
3659
  error: z24.string().optional(),
3660
+ /**
3661
+ * SELLER-FACING failure copy, in Spanish, already decided by the backend —
3662
+ * render it VERBATIM. Present only for `status: 'error'`.
3663
+ *
3664
+ * Clients used to receive only `error` and guess at it with a local regex
3665
+ * table; measured 2026-08-18 over 187 failed tasks in 60 days, that guess
3666
+ * produced contentless copy ("Error desconocido." / "Error interno") for 123
3667
+ * of them. Populated from `Task.userMessage`, falling back to the same
3668
+ * resolver for rows written before that column existed.
3669
+ */
3670
+ userMessage: z24.string().optional(),
3562
3671
  startedAt: z24.string().datetime().optional(),
3563
3672
  updatedAt: z24.string().datetime().optional(),
3564
3673
  stageStatuses: StageStatusSnapshot,
@@ -3569,6 +3678,10 @@ var StatusResponse = z24.object({
3569
3678
  * campaign target geo when creating Meta ads from this task. */
3570
3679
  targetCountry: z24.string().optional(),
3571
3680
  shopifyPageUrl: z24.string().url().optional(),
3681
+ /** What the announcement bar looks like ON THE PUBLISHED landing right now.
3682
+ * The publish dialog prefills from this: without it the checkbox always opens
3683
+ * unchecked, and republishing anything would read as "remove the bar". */
3684
+ shopifyAnnouncementBar: ShopifyAnnouncementBarState.optional(),
3572
3685
  shopifyShops: z24.array(z24.object({
3573
3686
  shopDomain: z24.string(),
3574
3687
  primaryDomain: z24.string().nullable()
@@ -3579,6 +3692,10 @@ var StatusResponse = z24.object({
3579
3692
  requiresAction: z24.boolean().optional(),
3580
3693
  /** True when the task needs the user's attention rather than passive progress. */
3581
3694
  needsReview: z24.boolean().optional(),
3695
+ /** True when an open gate exists that somebody can actually ANSWER. A strictly
3696
+ * narrower fact than `needsReview` — see the identical field on `TaskSummary`
3697
+ * below for why conflating the two produced a call to action that led nowhere. */
3698
+ hasAnswerableGate: z24.boolean().optional(),
3582
3699
  /** Which landing pipeline produced this task's landing. 'code' tasks have no
3583
3700
  * classic LandingImage rows — the result lives in the inline code editor. */
3584
3701
  landingMode: LandingModeSchema.optional(),
@@ -3649,6 +3766,27 @@ var TaskSummary = z24.object({
3649
3766
  /** True when user action is ACTUALLY needed (angle review or script review ready).
3650
3767
  * Use this for status display to show "Revisar" vs "Progreso". */
3651
3768
  needsReview: z24.boolean().optional(),
3769
+ /**
3770
+ * True when an open gate exists that somebody can actually ANSWER.
3771
+ *
3772
+ * DIFFERENT FACT from `needsReview`, and the difference is the bug. `needsReview`
3773
+ * means "this task is waiting on the seller"; the list badge renders «Revisar»
3774
+ * from it. But a task whose dispatch was lost is also "waiting", with no question
3775
+ * open anywhere — and so is the autopilot angle wait, whose resume button is
3776
+ * measurably INERT (`resumeAfterAngles` CASes on a flag that is false there, so
3777
+ * it returns 200 and dispatches nothing). Both render «Revisar» today and both
3778
+ * lead the seller nowhere. The owner spent hours one night convinced the platform
3779
+ * was frozen, chasing a ghost the badge itself invented.
3780
+ *
3781
+ * Derived from the reconciler's gate registry, NOT from a hand-maintained flag:
3782
+ * being a registry entry is what "answerable" means, because every entry declares
3783
+ * a decider and a deadline at compile time. A gate added tomorrow reports itself
3784
+ * here on its own. See `answerableGateIdsFor` in
3785
+ * apps/api/src/modules/cron/gate-reconciler/gate-registry.ts.
3786
+ *
3787
+ * Render «Revisar» on THIS, not on `needsReview`.
3788
+ */
3789
+ hasAnswerableGate: z24.boolean().optional(),
3652
3790
  createdAt: z24.string().datetime().optional(),
3653
3791
  startedAt: z24.string().datetime().optional(),
3654
3792
  updatedAt: z24.string().datetime().optional(),
@@ -4136,8 +4274,20 @@ var AdminUserListResponse = z27.object({
4136
4274
  page: z27.number().int().positive(),
4137
4275
  pageSize: z27.number().int().positive()
4138
4276
  });
4277
+ var AdminCreditGrantReason = z27.enum([
4278
+ "compensation",
4279
+ "goodwill",
4280
+ "promo",
4281
+ "internal-testing",
4282
+ "correction"
4283
+ ]);
4139
4284
  var UpdateUserCreditsRequest = z27.object({
4140
- creditCents: z27.number().nonnegative()
4285
+ creditCents: z27.number().nonnegative(),
4286
+ reason: AdminCreditGrantReason,
4287
+ /** Free-text detail. Required when the enum alone does not explain the grant. */
4288
+ notes: z27.string().trim().min(1).max(500).optional(),
4289
+ /** The task the grant compensates, when there is one. */
4290
+ taskId: z27.string().uuid().optional()
4141
4291
  });
4142
4292
  var QualityReportStatus = z27.enum([
4143
4293
  "open",
@@ -8726,6 +8876,7 @@ function slot(name, required, mcp2, whatsapp = mcp2, source = "user") {
8726
8876
  }
8727
8877
  __name(slot, "slot");
8728
8878
  var SERVER_SELECTED_OR_ARTIFACT_FIELDS = /* @__PURE__ */ new Set([
8879
+ "actionKind",
8729
8880
  "actorId",
8730
8881
  "adAccountId",
8731
8882
  "advertiserId",
@@ -9550,7 +9701,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9550
9701
  ], [
9551
9702
  "meta_publication"
9552
9703
  ]),
9553
- defineCapability("publish_to_shopify", "ec37111aac872e6be22f34c8b547c1e4b58875f798c48e24ca9ca067a0a22d8b", "external_mutation", "publishing", [
9704
+ defineCapability("publish_to_shopify", "6a75ad9bfe04aed24ed41564011ac785657bab12caccd4d78c1de846ce798e82", "external_mutation", "publishing", [
9554
9705
  "external_mutation",
9555
9706
  "state_change"
9556
9707
  ], [
@@ -9570,7 +9721,13 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9570
9721
  slot("publication_target", false, [
9571
9722
  "publicationMode",
9572
9723
  "targetProductId"
9573
- ])
9724
+ ]),
9725
+ // MCP + web only: the WhatsApp publish tool exposes no announcement fields,
9726
+ // and claiming them here breaks the schema-parity invariant.
9727
+ slot("announcement_bar", false, [
9728
+ "announcementBarEnabled",
9729
+ "announcementBarText"
9730
+ ], [])
9574
9731
  ], [
9575
9732
  "product",
9576
9733
  "shop"
@@ -11400,7 +11557,7 @@ var ApiClient = class {
11400
11557
  };
11401
11558
 
11402
11559
  // src/index.ts
11403
- var MCP_VERSION = "3.2.0";
11560
+ var MCP_VERSION = "3.4.0";
11404
11561
 
11405
11562
  // src/meta-upload-app.ts
11406
11563
  import { readFileSync } from "fs";
@@ -12343,6 +12500,42 @@ var ResearchCategoryEnum = z48.enum([
12343
12500
  "garden",
12344
12501
  "cleaning"
12345
12502
  ]);
12503
+ var READS_INTERNAL = {
12504
+ readOnlyHint: true,
12505
+ destructiveHint: false,
12506
+ idempotentHint: true,
12507
+ openWorldHint: false
12508
+ };
12509
+ var READS_EXTERNAL = {
12510
+ readOnlyHint: true,
12511
+ destructiveHint: false,
12512
+ idempotentHint: true,
12513
+ openWorldHint: true
12514
+ };
12515
+ var CREATES_INTERNAL = {
12516
+ readOnlyHint: false,
12517
+ destructiveHint: false,
12518
+ idempotentHint: false,
12519
+ openWorldHint: false
12520
+ };
12521
+ var CREATES_EXTERNAL = {
12522
+ readOnlyHint: false,
12523
+ destructiveHint: false,
12524
+ idempotentHint: false,
12525
+ openWorldHint: true
12526
+ };
12527
+ var OVERWRITES_INTERNAL = {
12528
+ readOnlyHint: false,
12529
+ destructiveHint: true,
12530
+ idempotentHint: false,
12531
+ openWorldHint: false
12532
+ };
12533
+ var OVERWRITES_EXTERNAL = {
12534
+ readOnlyHint: false,
12535
+ destructiveHint: true,
12536
+ idempotentHint: false,
12537
+ openWorldHint: true
12538
+ };
12346
12539
 
12347
12540
  // src/errors.ts
12348
12541
  var McpErrorCode = {
@@ -12800,6 +12993,18 @@ function translateError(err) {
12800
12993
  }
12801
12994
  __name(translateError, "translateError");
12802
12995
 
12996
+ // src/tools/approve-refusal-message.ts
12997
+ function approveRefusalMessage(reason) {
12998
+ if (reason === "photo_review_pending") {
12999
+ return "No se inici\xF3 la generaci\xF3n: faltan por revisar las fotos del producto. Abre la tarea, aprueba las fotos y vuelve a intentar. No se cobr\xF3 nada.";
13000
+ }
13001
+ if (reason === "photos_not_ready") {
13002
+ return "No se inici\xF3 la generaci\xF3n: todav\xEDa estamos preparando las fotos del producto. Intenta de nuevo en un momento. No se cobr\xF3 nada.";
13003
+ }
13004
+ return "No se inici\xF3 la generaci\xF3n y no se cobr\xF3 nada. Los cambios siguen guardados: vuelve a intentar.";
13005
+ }
13006
+ __name(approveRefusalMessage, "approveRefusalMessage");
13007
+
12803
13008
  // src/tools/task-outputs.ts
12804
13009
  var WEBAPP_BASE = "https://productmaker.app";
12805
13010
  function proxifyOutputs(outputs, proxyBaseUrl) {
@@ -12850,7 +13055,12 @@ async function fetchTaskOutputs(api, taskId, options) {
12850
13055
  return {
12851
13056
  angleKey: vg.angleKey ?? "",
12852
13057
  angleIndex: vg.angleIndex ?? 0,
12853
- variants: variants2.length ? variants2 : fallback
13058
+ variants: variants2.length ? variants2 : fallback,
13059
+ // Only ever true — an honest delivery leaves the key off entirely so no
13060
+ // consumer has to distinguish `false` from "the API is older than this".
13061
+ ...vg.scriptRewritten === true ? {
13062
+ scriptRewritten: true
13063
+ } : {}
12854
13064
  };
12855
13065
  }).filter((v) => v.angleKey && v.variants.length);
12856
13066
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -12913,6 +13123,9 @@ function renderOutputsMarkdown(outputs) {
12913
13123
  for (const variant of v.variants) {
12914
13124
  parts.push(`- [${escapeMdLabel(v.angleKey)} \xB7 ${escapeMdLabel(variant.kind)}](${variant.url})`);
12915
13125
  }
13126
+ if (v.scriptRewritten) {
13127
+ 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.`);
13128
+ }
12916
13129
  }
12917
13130
  }
12918
13131
  return parts.join("\n");
@@ -13219,7 +13432,7 @@ function resolveStatusTaskKind(status) {
13219
13432
  }
13220
13433
  __name(resolveStatusTaskKind, "resolveStatusTaskKind");
13221
13434
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
13222
- server.tool("create_product_task", "Crea una tarea de producto desde fotos o desde un proyecto guiado propio. Por defecto genera los 3 outputs (landing + video + image creatives) EN UN MISMO TASK. Devuelve un taskId.\n\nINPUTS REQUERIDOS: images (1-5 fotos del MISMO producto; la primera es la hero) O projectId si el proyecto ya tiene una imagen guardada; adem\xE1s language y country. Para fotos en una laptop con MCP remoto llama `request_media_upload` con mediaKind=image y luego usa los assetId devueltos dentro de images; no pidas path ni URL cuando el App est\xE9 disponible.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son inputs de alto impacto en la landing y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto del chat: preg\xFAntale expl\xEDcitamente, incluso si est\xE1 en LATAM.\n\nSi env\xEDas projectId, debe ser el proyecto propio que el usuario acaba de costear; el backend resolver\xE1 la imagen privada y conservar\xE1 todos los controles de cr\xE9ditos/confirmaci\xF3n. Si el usuario adjunta images junto con projectId, esas fotos reemplazan la referencia solo para este task.\n\nANTES de invocar, DILE CLARAMENTE qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task.\n\nUsa SOLO valores declarados en los enums.", CreateProductTaskInput.shape, async (input) => {
13435
+ server.tool("create_product_task", "Crea una tarea de producto desde fotos o desde un proyecto guiado propio. Por defecto genera los 3 outputs (landing + video + image creatives) EN UN MISMO TASK. Devuelve un taskId.\n\nINPUTS REQUERIDOS: images (1-5 fotos del MISMO producto; la primera es la hero) O projectId si el proyecto ya tiene una imagen guardada; adem\xE1s language y country. Para fotos en una laptop con MCP remoto llama `request_media_upload` con mediaKind=image y luego usa los assetId devueltos dentro de images; no pidas path ni URL cuando el App est\xE9 disponible.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son inputs de alto impacto en la landing y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto del chat: preg\xFAntale expl\xEDcitamente, incluso si est\xE1 en LATAM.\n\nSi env\xEDas projectId, debe ser el proyecto propio que el usuario acaba de costear; el backend resolver\xE1 la imagen privada y conservar\xE1 todos los controles de cr\xE9ditos/confirmaci\xF3n. Si el usuario adjunta images junto con projectId, esas fotos reemplazan la referencia solo para este task.\n\nANTES de invocar, DILE CLARAMENTE qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task.\n\nUsa SOLO valores declarados en los enums.", CreateProductTaskInput.shape, CREATES_EXTERNAL, async (input) => {
13223
13436
  try {
13224
13437
  if (!input.images && !input.projectId) {
13225
13438
  throw new Error("images es requerido salvo que env\xEDes projectId.");
@@ -13287,7 +13500,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13287
13500
  throw translateError(e);
13288
13501
  }
13289
13502
  });
13290
- server.tool("create_service_video", "Crea un video para un servicio desde su landing. Esta acci\xF3n consume cr\xE9ditos: confirma la intenci\xF3n del usuario antes de llamarla. `automatic` genera directamente; `guided` devuelve conceptos para que el usuario apruebe uno mediante `resolve_pending_action`. La web tambi\xE9n acepta demos y anuncios de referencia; el transporte MCP de video adjunto todav\xEDa falla cerrado hasta poder subir bytes de forma segura.", CreateServiceVideoInput.shape, async (input) => {
13503
+ server.tool("create_service_video", "Crea un video para un servicio desde su landing. Esta acci\xF3n consume cr\xE9ditos: confirma la intenci\xF3n del usuario antes de llamarla. `automatic` genera directamente; `guided` devuelve conceptos para que el usuario apruebe uno mediante `resolve_pending_action`. La web tambi\xE9n acepta demos y anuncios de referencia; el transporte MCP de video adjunto todav\xEDa falla cerrado hasta poder subir bytes de forma segura.", CreateServiceVideoInput.shape, CREATES_EXTERNAL, async (input) => {
13291
13504
  try {
13292
13505
  const created = await api.postJson("/v2/ingest", {
13293
13506
  taskType: "service",
@@ -13338,7 +13551,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13338
13551
  throw translateError(e);
13339
13552
  }
13340
13553
  });
13341
- server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSIGUE LLAM\xC1NDOLA hasta que el estado sea terminal (`done` o `error`) o aparezca `pendingUserAction`. Un retorno sin ninguna de esas dos cosas significa que la tarea sigue corriendo y que alcanzaste el timeout, NO que se colg\xF3 ni que termin\xF3: vuelve a llamarla con waitSeconds alto. Si dejas de llamarla, la tarea sigue avanzando sin que nadie la mire y el usuario se queda esperando en silencio.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO esperando una decisi\xF3n del USUARIO: muestra el draft, PREG\xDANTALE y espera su respuesta. Resp\xF3ndela t\xFA mismo con `resolve_pending_action` \xFAnicamente si el usuario ya te deleg\xF3 esa decisi\xF3n \u2014 porque te lo dijo en la conversaci\xF3n, o porque la tarea corre con `aiDecidesAll` / `reviewMode: automatic`. Nunca deduzcas la delegaci\xF3n del contenido del draft: que la propuesta te parezca obvia o correcta no es un permiso. Al responder, repite el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
13554
+ 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, READS_INTERNAL, async (input, extra) => {
13342
13555
  try {
13343
13556
  const signal = extra?.signal;
13344
13557
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
@@ -13398,7 +13611,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13398
13611
  throw translateError(e);
13399
13612
  }
13400
13613
  });
13401
- server.tool("list_tasks", "Lista de forma read-only las tareas recientes del usuario. \xDAsala para encontrar una tarea previa por nombre o revisar el historial antes de pedir un taskId. Soporta paginaci\xF3n con limit/offset y filtro de texto. No crea, reanuda ni modifica tareas.", ListTasksInput.shape, async (input) => {
13614
+ server.tool("list_tasks", "Lista de forma read-only las tareas recientes del usuario. \xDAsala para encontrar una tarea previa por nombre o revisar el historial antes de pedir un taskId. Soporta paginaci\xF3n con limit/offset y filtro de texto. No crea, reanuda ni modifica tareas.", ListTasksInput.shape, READS_INTERNAL, async (input) => {
13402
13615
  try {
13403
13616
  const qs = new URLSearchParams();
13404
13617
  if (input.limit != null) qs.set("limit", String(input.limit));
@@ -13411,7 +13624,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13411
13624
  throw translateError(e);
13412
13625
  }
13413
13626
  });
13414
- server.tool("edit_task_draft", "Edita el draft de un angle de marketing antes de la generaci\xF3n. Campos editables: angle (texto del angle), buyerPersona, actor (descripci\xF3n del personaje), actorId (ID de un personaje guardado; usa list_actors), gender (female|male), scriptNotes. Use this only before generation completes. Editing already-generated images or videos is not supported.", EditTaskDraftInput.shape, async (input) => {
13627
+ server.tool("edit_task_draft", "Edita el draft de un angle de marketing antes de la generaci\xF3n. Campos editables: angle (texto del angle), buyerPersona, actor (descripci\xF3n del personaje), actorId (ID de un personaje guardado; usa list_actors), gender (female|male), scriptNotes. Use this only before generation completes. Editing already-generated images or videos is not supported.", EditTaskDraftInput.shape, OVERWRITES_INTERNAL, async (input) => {
13415
13628
  try {
13416
13629
  const angle = await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/angles/${input.angleIndex}`, input.angle ?? {});
13417
13630
  return asText({
@@ -13421,257 +13634,273 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
13421
13634
  throw translateError(e);
13422
13635
  }
13423
13636
  });
13424
- server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Esta llamada habla EN NOMBRE DEL USUARIO: \xFAsala solo con una respuesta que \xE9l te haya dado, o cuando te haya delegado la decisi\xF3n de forma expl\xEDcita o v\xEDa `aiDecidesAll` / `reviewMode: automatic`. Si nadie te deleg\xF3 nada, pregunta y espera en vez de llamarla. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
13425
- try {
13426
- const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
13427
- const status = StatusForResolveSchema.parse(raw);
13428
- const pending = status.pendingUserAction;
13429
- if (!pending) {
13430
- throw new Error("No hay acci\xF3n pendiente para esta tarea \u2014 llama get_task_status primero para confirmar el estado.");
13431
- }
13432
- if (pending.kind !== input.action.kind) {
13433
- throw new Error(`La tarea espera una acci\xF3n de tipo "${pending.kind}" pero mandaste "${input.action.kind}". Reusa el mismo kind que viste en pendingUserAction.`);
13434
- }
13435
- switch (input.action.kind) {
13436
- case "product_reference_set_approval": {
13437
- if (pending.scope !== input.action.scope || pending.scopeKey !== input.action.scopeKey || pending.scopeSnapshotId !== input.action.scopeSnapshotId || pending.scopeHash !== input.action.expectedScopeHash || !pending.allowedDecisions?.includes(input.action.decision)) {
13438
- throw new Error("La aprobaci\xF3n de referencias no coincide con el snapshot mostrado; vuelve a llamar get_task_status.");
13439
- }
13440
- if (input.action.decision === "reject" && (!input.action.reason || !input.action.affectedRefKeys?.length || !input.action.recoveryAction)) {
13441
- throw new Error("Una referencia rechazada necesita motivo, referencias afectadas y acci\xF3n de recuperaci\xF3n.");
13442
- }
13443
- if (input.action.decision === "correct_view" && (!input.action.reason || !input.action.affectedRefKeys?.length)) {
13444
- throw new Error("Una correcci\xF3n de \xE1ngulo necesita motivo y referencias afectadas.");
13445
- }
13446
- if (input.action.decision === "correct_view" && input.action.affectedRefKeys?.length !== 1) {
13447
- throw new Error("Corrige una sola vista por intento.");
13448
- }
13449
- if (pending.phase === "preparation" && input.action.decision === "approve" && (!pending.selectedRefKeys || pending.selectedRefKeys.length === 0)) {
13450
- throw new Error("No hay fotos seleccionadas para preparar. Abre la tarea en ProductMaker Web y elige al menos una.");
13451
- }
13452
- const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/resolve-product-reference-set`, {
13453
- ...input.action,
13454
- ...pending.phase === "preparation" && input.action.decision === "approve" && pending.selectedRefKeys ? {
13455
- selectedRefKeys: pending.selectedRefKeys
13456
- } : {}
13457
- });
13458
- return asText(result);
13637
+ server.tool(
13638
+ "resolve_pending_action",
13639
+ "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.",
13640
+ ResolvePendingActionInput.shape,
13641
+ // Approving the gate RELEASES the pipeline into paid generation at the video
13642
+ // and image providers (registry effects: credit_consuming + state_change),
13643
+ // and decision=reject discards the approved reference set. Both halves, hence
13644
+ // destructive AND open-world.
13645
+ OVERWRITES_EXTERNAL,
13646
+ async (input) => {
13647
+ try {
13648
+ const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
13649
+ const status = StatusForResolveSchema.parse(raw);
13650
+ const pending = status.pendingUserAction;
13651
+ if (!pending) {
13652
+ throw new Error("No hay acci\xF3n pendiente para esta tarea \u2014 llama get_task_status primero para confirmar el estado.");
13459
13653
  }
13460
- case "product_reference_confirmation": {
13461
- const candidateGrouping = pending.candidateGrouping;
13462
- const selectedImageUrls = pending.selectedImageUrls ?? [];
13463
- if (pending.mode === "visual_recovery") {
13464
- throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
13465
- }
13466
- if (!candidateGrouping || selectedImageUrls.length === 0) {
13467
- throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
13468
- }
13469
- const userChoice = input.action.userChoice;
13470
- if (!userChoice) {
13471
- throw new Error("La pregunta adaptativa necesita userChoice.");
13472
- }
13473
- const offeredChoices = pending.question?.options.map((option) => option.value) ?? pending.options;
13474
- if (!offeredChoices) {
13475
- throw new Error("Estado inconsistente: falta la pregunta adaptativa con sus opciones verificables. Vuelve a llamar get_task_status.");
13476
- }
13477
- if (!offeredChoices.includes(userChoice)) {
13478
- throw new Error(`La opci\xF3n "${userChoice}" no fue ofrecida en la pregunta pendiente. Elige una opci\xF3n mostrada por get_task_status.`);
13479
- }
13480
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
13481
- productReferenceConfirmation: {
13482
- candidateGrouping,
13483
- userChoice,
13484
- // Server overwrites with its own server-side computation;
13485
- // we send the matching MCP-mirror value so the wire body
13486
- // is internally coherent if the server skips overwrite.
13487
- selectedImageUrlsHash: buildSelectedImageUrlsHash(selectedImageUrls)
13488
- },
13489
- ...input.action.confirmedProductNames ? {
13490
- confirmedProductNames: input.action.confirmedProductNames
13491
- } : {},
13492
- ...pending.decisionKey && pending.referenceEvidenceVersion && pending.expectedEvidenceHash ? {
13493
- decisionKey: pending.decisionKey,
13494
- referenceEvidenceVersion: pending.referenceEvidenceVersion,
13495
- expectedEvidenceHash: pending.expectedEvidenceHash
13496
- } : {},
13497
- // Multi-scene color showcase → upgrade to STANDARD. Gated to the
13498
- // variants choice client-side so the wire body stays coherent; the
13499
- // backend re-gates on variant_set grouping.
13500
- ...resolveVariantConfirmVideoTier(input.action) === "standard" ? {
13501
- videoTier: "standard"
13502
- } : {}
13503
- });
13504
- return asText(r);
13654
+ if (pending.kind !== input.action.kind) {
13655
+ throw new Error(`La tarea espera una acci\xF3n de tipo "${pending.kind}" pero mandaste "${input.action.kind}". Reusa el mismo kind que viste en pendingUserAction.`);
13505
13656
  }
13506
- case "angle_review_approval": {
13507
- const body = {};
13508
- if (pending.customAngleDraft && pending.customAngleDraft.length > 0) {
13509
- body.customAngle = pending.customAngleDraft;
13657
+ switch (input.action.kind) {
13658
+ case "product_reference_set_approval": {
13659
+ if (pending.scope !== input.action.scope || pending.scopeKey !== input.action.scopeKey || pending.scopeSnapshotId !== input.action.scopeSnapshotId || pending.scopeHash !== input.action.expectedScopeHash || !pending.allowedDecisions?.includes(input.action.decision)) {
13660
+ throw new Error("La aprobaci\xF3n de referencias no coincide con el snapshot mostrado; vuelve a llamar get_task_status.");
13661
+ }
13662
+ if (input.action.decision === "reject" && (!input.action.reason || !input.action.affectedRefKeys?.length || !input.action.recoveryAction)) {
13663
+ throw new Error("Una referencia rechazada necesita motivo, referencias afectadas y acci\xF3n de recuperaci\xF3n.");
13664
+ }
13665
+ if (input.action.decision === "correct_view" && (!input.action.reason || !input.action.affectedRefKeys?.length)) {
13666
+ throw new Error("Una correcci\xF3n de \xE1ngulo necesita motivo y referencias afectadas.");
13667
+ }
13668
+ if (input.action.decision === "correct_view" && input.action.affectedRefKeys?.length !== 1) {
13669
+ throw new Error("Corrige una sola vista por intento.");
13670
+ }
13671
+ if (pending.phase === "preparation" && input.action.decision === "approve" && (!pending.selectedRefKeys || pending.selectedRefKeys.length === 0)) {
13672
+ throw new Error("No hay fotos seleccionadas para preparar. Abre la tarea en ProductMaker Web y elige al menos una.");
13673
+ }
13674
+ const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/resolve-product-reference-set`, {
13675
+ ...input.action,
13676
+ ...pending.phase === "preparation" && input.action.decision === "approve" && pending.selectedRefKeys ? {
13677
+ selectedRefKeys: pending.selectedRefKeys
13678
+ } : {}
13679
+ });
13680
+ return asText(result);
13510
13681
  }
13511
- if (pending.painPointsDraft != null) body.painPoints = pending.painPointsDraft;
13512
- if (pending.benefitsDraft != null) body.benefits = pending.benefitsDraft;
13513
- if (pending.featuresDraft != null) body.features = [
13514
- ...pending.featuresDraft
13515
- ];
13516
- if (input.action.customAngle != null) body.customAngle = input.action.customAngle;
13517
- if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
13518
- if (input.action.benefits != null) body.benefits = input.action.benefits;
13519
- if (input.action.features != null) body.features = input.action.features;
13520
- if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
13521
- throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
13682
+ case "product_reference_confirmation": {
13683
+ const candidateGrouping = pending.candidateGrouping;
13684
+ const selectedImageUrls = pending.selectedImageUrls ?? [];
13685
+ if (pending.mode === "visual_recovery") {
13686
+ throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
13687
+ }
13688
+ if (!candidateGrouping || selectedImageUrls.length === 0) {
13689
+ throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
13690
+ }
13691
+ const userChoice = input.action.userChoice;
13692
+ if (!userChoice) {
13693
+ throw new Error("La pregunta adaptativa necesita userChoice.");
13694
+ }
13695
+ const offeredChoices = pending.question?.options.map((option) => option.value) ?? pending.options;
13696
+ if (!offeredChoices) {
13697
+ throw new Error("Estado inconsistente: falta la pregunta adaptativa con sus opciones verificables. Vuelve a llamar get_task_status.");
13698
+ }
13699
+ if (!offeredChoices.includes(userChoice)) {
13700
+ throw new Error(`La opci\xF3n "${userChoice}" no fue ofrecida en la pregunta pendiente. Elige una opci\xF3n mostrada por get_task_status.`);
13701
+ }
13702
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
13703
+ productReferenceConfirmation: {
13704
+ candidateGrouping,
13705
+ userChoice,
13706
+ // Server overwrites with its own server-side computation;
13707
+ // we send the matching MCP-mirror value so the wire body
13708
+ // is internally coherent if the server skips overwrite.
13709
+ selectedImageUrlsHash: buildSelectedImageUrlsHash(selectedImageUrls)
13710
+ },
13711
+ ...input.action.confirmedProductNames ? {
13712
+ confirmedProductNames: input.action.confirmedProductNames
13713
+ } : {},
13714
+ ...pending.decisionKey && pending.referenceEvidenceVersion && pending.expectedEvidenceHash ? {
13715
+ decisionKey: pending.decisionKey,
13716
+ referenceEvidenceVersion: pending.referenceEvidenceVersion,
13717
+ expectedEvidenceHash: pending.expectedEvidenceHash
13718
+ } : {},
13719
+ // Multi-scene color showcase → upgrade to STANDARD. Gated to the
13720
+ // variants choice client-side so the wire body stays coherent; the
13721
+ // backend re-gates on variant_set grouping.
13722
+ ...resolveVariantConfirmVideoTier(input.action) === "standard" ? {
13723
+ videoTier: "standard"
13724
+ } : {}
13725
+ });
13726
+ return asText(r);
13522
13727
  }
13523
- if (input.action.gapDurationSeconds === 30) {
13524
- const choice = pending.gapDurationChoice;
13525
- if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
13526
- if (choice.recommendedSec !== 30) {
13527
- throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
13728
+ case "angle_review_approval": {
13729
+ const body = {};
13730
+ if (pending.customAngleDraft && pending.customAngleDraft.length > 0) {
13731
+ body.customAngle = pending.customAngleDraft;
13528
13732
  }
13529
- body.angleVideoOptions = [
13530
- {
13531
- angleIndex: choice.angleIndex,
13532
- ugcFormat: "double_30s"
13533
- }
13733
+ if (pending.painPointsDraft != null) body.painPoints = pending.painPointsDraft;
13734
+ if (pending.benefitsDraft != null) body.benefits = pending.benefitsDraft;
13735
+ if (pending.featuresDraft != null) body.features = [
13736
+ ...pending.featuresDraft
13534
13737
  ];
13535
- }
13536
- const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
13537
- const resolvedTaskKind = resolveStatusTaskKind(status);
13538
- const isVideoTask = resolvedTaskKind === "video";
13539
- const isAutopilotTask = resolvedTaskKind === "autopilot";
13540
- if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
13541
- const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
13542
- const optionsByIndex = new Map(existingOptions.map((option) => [
13543
- option.angleIndex,
13544
- {
13545
- ...option
13738
+ if (input.action.customAngle != null) body.customAngle = input.action.customAngle;
13739
+ if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
13740
+ if (input.action.benefits != null) body.benefits = input.action.benefits;
13741
+ if (input.action.features != null) body.features = input.action.features;
13742
+ if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
13743
+ throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
13744
+ }
13745
+ if (input.action.gapDurationSeconds === 30) {
13746
+ const choice = pending.gapDurationChoice;
13747
+ if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
13748
+ if (choice.recommendedSec !== 30) {
13749
+ throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
13546
13750
  }
13547
- ]));
13548
- for (const angleIndex of selectedAngleIndices) {
13549
- optionsByIndex.set(angleIndex, {
13550
- ...optionsByIndex.get(angleIndex) ?? {
13551
- angleIndex
13751
+ body.angleVideoOptions = [
13752
+ {
13753
+ angleIndex: choice.angleIndex,
13754
+ ugcFormat: "double_30s"
13755
+ }
13756
+ ];
13757
+ }
13758
+ const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
13759
+ const resolvedTaskKind = resolveStatusTaskKind(status);
13760
+ const isVideoTask = resolvedTaskKind === "video";
13761
+ const isAutopilotTask = resolvedTaskKind === "autopilot";
13762
+ if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
13763
+ const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
13764
+ const optionsByIndex = new Map(existingOptions.map((option) => [
13765
+ option.angleIndex,
13766
+ {
13767
+ ...option
13768
+ }
13769
+ ]));
13770
+ for (const angleIndex of selectedAngleIndices) {
13771
+ optionsByIndex.set(angleIndex, {
13772
+ ...optionsByIndex.get(angleIndex) ?? {
13773
+ angleIndex
13774
+ },
13775
+ angle: body.customAngle
13776
+ });
13777
+ }
13778
+ body.angleVideoOptions = [
13779
+ ...optionsByIndex.values()
13780
+ ].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
13781
+ }
13782
+ if (isVideoTask) {
13783
+ body.selectedAngleIndices = selectedAngleIndices;
13784
+ const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
13785
+ return asText(r2);
13786
+ }
13787
+ if (isAutopilotTask) {
13788
+ const resumeBody = {
13789
+ selectedAngleIndices,
13790
+ ...status.ugcFormat ? {
13791
+ ugcFormat: status.ugcFormat
13792
+ } : {
13793
+ ugcFormat: "single_15s"
13552
13794
  },
13553
- angle: body.customAngle
13554
- });
13795
+ ...status.videoStyle ? {
13796
+ videoStyle: status.videoStyle
13797
+ } : {},
13798
+ ...status.videoModel ? {
13799
+ videoModel: status.videoModel
13800
+ } : {},
13801
+ ...status.videoTier ? {
13802
+ videoTier: status.videoTier
13803
+ } : {},
13804
+ ...status.videoResolution ? {
13805
+ videoResolution: status.videoResolution
13806
+ } : {},
13807
+ ...status.narrativeStyle ? {
13808
+ narrativeStyle: status.narrativeStyle
13809
+ } : {},
13810
+ ...body.customAngle != null ? {
13811
+ customAngleOverride: body.customAngle
13812
+ } : {},
13813
+ ...body.painPoints != null ? {
13814
+ painPoints: body.painPoints
13815
+ } : {},
13816
+ ...body.benefits != null ? {
13817
+ benefits: body.benefits
13818
+ } : {},
13819
+ ...body.angleVideoOptions ? {
13820
+ angleVideoOptions: body.angleVideoOptions
13821
+ } : {}
13822
+ };
13823
+ if (body.features !== void 0) {
13824
+ await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
13825
+ features: body.features
13826
+ });
13827
+ }
13828
+ const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
13829
+ if (r2?.success === false) {
13830
+ throw new Error(approveRefusalMessage(r2.reason));
13831
+ }
13832
+ return asText(r2);
13555
13833
  }
13556
- body.angleVideoOptions = [
13557
- ...optionsByIndex.values()
13558
- ].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
13559
- }
13560
- if (isVideoTask) {
13561
- body.selectedAngleIndices = selectedAngleIndices;
13562
- const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
13563
- return asText(r2);
13564
- }
13565
- if (isAutopilotTask) {
13566
- const resumeBody = {
13567
- selectedAngleIndices,
13568
- ...status.ugcFormat ? {
13569
- ugcFormat: status.ugcFormat
13570
- } : {
13571
- ugcFormat: "single_15s"
13572
- },
13573
- ...status.videoStyle ? {
13574
- videoStyle: status.videoStyle
13575
- } : {},
13576
- ...status.videoModel ? {
13577
- videoModel: status.videoModel
13578
- } : {},
13579
- ...status.videoTier ? {
13580
- videoTier: status.videoTier
13581
- } : {},
13582
- ...status.videoResolution ? {
13583
- videoResolution: status.videoResolution
13584
- } : {},
13585
- ...status.narrativeStyle ? {
13586
- narrativeStyle: status.narrativeStyle
13587
- } : {},
13834
+ const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
13835
+ const imageBody = status.kind === "image" ? {
13588
13836
  ...body.customAngle != null ? {
13589
- customAngleOverride: body.customAngle
13837
+ customAngle: body.customAngle
13590
13838
  } : {},
13591
13839
  ...body.painPoints != null ? {
13592
13840
  painPoints: body.painPoints
13593
13841
  } : {},
13594
- ...body.benefits != null ? {
13595
- benefits: body.benefits
13596
- } : {},
13597
- ...body.angleVideoOptions ? {
13598
- angleVideoOptions: body.angleVideoOptions
13599
- } : {}
13600
- };
13601
- if (body.features !== void 0) {
13602
- await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
13842
+ ...body.features != null ? {
13603
13843
  features: body.features
13604
- });
13844
+ } : {}
13845
+ } : body;
13846
+ const r = await api.postJson(route, imageBody);
13847
+ if (r?.success === false) {
13848
+ throw new Error(approveRefusalMessage(r.reason));
13605
13849
  }
13606
- const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
13607
- return asText(r2);
13850
+ return asText(r);
13608
13851
  }
13609
- const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
13610
- const imageBody = status.kind === "image" ? {
13611
- ...body.customAngle != null ? {
13612
- customAngle: body.customAngle
13613
- } : {},
13614
- ...body.painPoints != null ? {
13615
- painPoints: body.painPoints
13616
- } : {},
13617
- ...body.features != null ? {
13618
- features: body.features
13619
- } : {}
13620
- } : body;
13621
- const r = await api.postJson(route, imageBody);
13622
- return asText(r);
13623
- }
13624
- case "service_angle_approval": {
13625
- const action = input.action;
13626
- const candidate = pending.angles?.find((angle) => angle.id === action.angleId);
13627
- if (!candidate) {
13628
- throw new Error("El angleId no pertenece a los conceptos pendientes de esta tarea.");
13852
+ case "service_angle_approval": {
13853
+ const action = input.action;
13854
+ const candidate = pending.angles?.find((angle) => angle.id === action.angleId);
13855
+ if (!candidate) {
13856
+ throw new Error("El angleId no pertenece a los conceptos pendientes de esta tarea.");
13857
+ }
13858
+ const edits = {
13859
+ content: action.content ?? candidate.content,
13860
+ creativeDirection: action.creativeDirection ?? candidate.creativeDirection,
13861
+ actor: action.actor ?? candidate.actor,
13862
+ // Echo the persisted saved actor. Omitted when absent so the PATCH
13863
+ // never writes a null over a selection made elsewhere — the
13864
+ // backend only updates the keys it receives.
13865
+ ...candidate.actorId ? {
13866
+ actorId: candidate.actorId
13867
+ } : {}
13868
+ };
13869
+ await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/${encodeURIComponent(candidate.id)}`, edits);
13870
+ const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/select`, {
13871
+ angleIds: [
13872
+ candidate.id
13873
+ ]
13874
+ });
13875
+ return asText(result);
13629
13876
  }
13630
- const edits = {
13631
- content: action.content ?? candidate.content,
13632
- creativeDirection: action.creativeDirection ?? candidate.creativeDirection,
13633
- actor: action.actor ?? candidate.actor,
13634
- // Echo the persisted saved actor. Omitted when absent so the PATCH
13635
- // never writes a null over a selection made elsewhere — the
13636
- // backend only updates the keys it receives.
13637
- ...candidate.actorId ? {
13638
- actorId: candidate.actorId
13639
- } : {}
13640
- };
13641
- await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/${encodeURIComponent(candidate.id)}`, edits);
13642
- const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/select`, {
13643
- angleIds: [
13644
- candidate.id
13645
- ]
13646
- });
13647
- return asText(result);
13648
- }
13649
- case "service_script_approval": {
13650
- const panels = pending.panels ?? [];
13651
- if (panels.length === 0) {
13652
- throw new Error("El storyboard pendiente no contiene paneles aprobables.");
13877
+ case "service_script_approval": {
13878
+ const panels = pending.panels ?? [];
13879
+ if (panels.length === 0) {
13880
+ throw new Error("El storyboard pendiente no contiene paneles aprobables.");
13881
+ }
13882
+ const voiceovers = {};
13883
+ const visualDirections = {};
13884
+ for (const panel of panels) {
13885
+ voiceovers[panel.beatId] = input.action.voiceovers?.[panel.beatId] ?? panel.voiceover;
13886
+ visualDirections[panel.beatId] = input.action.visualDirections?.[panel.beatId] ?? panel.visualDirection;
13887
+ }
13888
+ const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-script/approve`, {
13889
+ voiceovers,
13890
+ visualDirections
13891
+ });
13892
+ return asText(result);
13653
13893
  }
13654
- const voiceovers = {};
13655
- const visualDirections = {};
13656
- for (const panel of panels) {
13657
- voiceovers[panel.beatId] = input.action.voiceovers?.[panel.beatId] ?? panel.voiceover;
13658
- visualDirections[panel.beatId] = input.action.visualDirections?.[panel.beatId] ?? panel.visualDirection;
13894
+ default: {
13895
+ const _exhaustive = input.action;
13896
+ throw new Error(`Unsupported pending action kind: ${String(_exhaustive.kind)}`);
13659
13897
  }
13660
- const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-script/approve`, {
13661
- voiceovers,
13662
- visualDirections
13663
- });
13664
- return asText(result);
13665
- }
13666
- default: {
13667
- const _exhaustive = input.action;
13668
- throw new Error(`Unsupported pending action kind: ${String(_exhaustive.kind)}`);
13669
13898
  }
13899
+ } catch (e) {
13900
+ throw translateError(e);
13670
13901
  }
13671
- } catch (e) {
13672
- throw translateError(e);
13673
13902
  }
13674
- });
13903
+ );
13675
13904
  }
13676
13905
  __name(registerTaskTools, "registerTaskTools");
13677
13906
 
@@ -13825,7 +14054,7 @@ var LandingInput = z50.object({
13825
14054
  subheadline: CreativeRefs.subheadline
13826
14055
  });
13827
14056
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
13828
- server.tool("generate_free_video", 'Genera UN video libre con prompt/guion exacto, opcionalmente con imagen de referencia. Si el usuario adjunta o menciona una imagen local, llama primero request_media_upload con mediaKind=image y usa el assetId devuelto; no pidas path ni URL. Devuelve un taskId y procesa el job de forma as\xEDncrona.\n\nREQUERIDOS: rawPrompt, quality, durationSeconds y videoResolution.\n\nCALIDAD: "economical" = Econ\xF3mico y "high_detail" = Alto detalle. La duraci\xF3n debe pertenecer a la lista admitida por la calidad escogida; si el usuario pide otra, preg\xFAntale una duraci\xF3n v\xE1lida.\n\nSUBT\xCDTULOS: si addSubtitles=true, el rawPrompt completo se usa como texto exacto de subt\xEDtulos y debe tener m\xE1ximo 500 caracteres; no existe una pregunta adicional de transcripci\xF3n.\n\nPara videos normales con narrativa/\xE1ngulo usa generate_video_creative. Para tareas combinadas de producto usa create_product_task.', FreeVideoInput.shape, async (input) => {
14057
+ server.tool("generate_free_video", 'Genera UN video libre con prompt/guion exacto, opcionalmente con imagen de referencia. Si el usuario adjunta o menciona una imagen local, llama primero request_media_upload con mediaKind=image y usa el assetId devuelto; no pidas path ni URL. Devuelve un taskId y procesa el job de forma as\xEDncrona.\n\nREQUERIDOS: rawPrompt, quality, durationSeconds y videoResolution.\n\nCALIDAD: "economical" = Econ\xF3mico y "high_detail" = Alto detalle. La duraci\xF3n debe pertenecer a la lista admitida por la calidad escogida; si el usuario pide otra, preg\xFAntale una duraci\xF3n v\xE1lida.\n\nSUBT\xCDTULOS: si addSubtitles=true, el rawPrompt completo se usa como texto exacto de subt\xEDtulos y debe tener m\xE1ximo 500 caracteres; no existe una pregunta adicional de transcripci\xF3n.\n\nPara videos normales con narrativa/\xE1ngulo usa generate_video_creative. Para tareas combinadas de producto usa create_product_task.', FreeVideoInput.shape, CREATES_EXTERNAL, async (input) => {
13829
14058
  try {
13830
14059
  const allowedDurations = FREE_VIDEO_DURATIONS_BY_QUALITY[input.quality];
13831
14060
  if (!allowedDurations.includes(input.durationSeconds)) {
@@ -13855,7 +14084,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
13855
14084
  throw translateError(e);
13856
14085
  }
13857
14086
  });
13858
- server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio. Si el usuario adjunta o menciona fotos locales y a\xFAn no hay assetId, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nCOMPETENCIA Y GAP REPLICATION: con videoPreset="auto" (default) y competitorScanEnabled=true (default), el sistema analiza anuncios de competencia. Si encuentra un winner de video coherente, ancla el primaryAngle y genera autom\xE1ticamente la narrativa `gap_replication` para copiar su estructura audiovisual. No la selecciones manualmente: es un resultado del an\xE1lisis. Si competitorScanEnabled=false, esta b\xFAsqueda se omite.\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, async (input) => {
14087
+ server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio. Si el usuario adjunta o menciona fotos locales y a\xFAn no hay assetId, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nCOMPETENCIA Y GAP REPLICATION: con videoPreset="auto" (default) y competitorScanEnabled=true (default), el sistema analiza anuncios de competencia. Si encuentra un winner de video coherente, ancla el primaryAngle y genera autom\xE1ticamente la narrativa `gap_replication` para copiar su estructura audiovisual. No la selecciones manualmente: es un resultado del an\xE1lisis. Si competitorScanEnabled=false, esta b\xFAsqueda se omite.\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, CREATES_EXTERNAL, async (input) => {
13859
14088
  try {
13860
14089
  const resolved = resolveVideoPreset(input.videoPreset);
13861
14090
  const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
@@ -13880,7 +14109,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
13880
14109
  throw translateError(e);
13881
14110
  }
13882
14111
  });
13883
- server.tool("generate_image_creatives", 'Genera N variantes de imagen creativa (ads est\xE1ticos). Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), productTitle, primaryAngle, language, country. Si el usuario adjunta o menciona fotos locales y a\xFAn no hay assetId, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario:\n- price y offer (se renderizan en la imagen \u2014 nunca los asumas; si dice "t\xFA elige", om\xEDtelos).\n- compareAtPrice si hay descuento (ej. "$129.000" para tachar).\n- creativeArchetype y visualTreatment si quiere un look espec\xEDfico (performance vs premium DTC). Si no le importa, om\xEDtelos.\n\nNO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente.\n\nOTROS OPCIONALES: buyerPersona, nVariants (default 3), imageModel, imageStyle, styleMix, aspectRatio (default 4:5), generateAdCopy, pauseForAngleReview, features, brandColors, isCombo, creativeDirection.', ImageInput.shape, async (input) => {
14112
+ server.tool("generate_image_creatives", 'Genera N variantes de imagen creativa (ads est\xE1ticos). Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), productTitle, primaryAngle, language, country. Si el usuario adjunta o menciona fotos locales y a\xFAn no hay assetId, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario:\n- price y offer (se renderizan en la imagen \u2014 nunca los asumas; si dice "t\xFA elige", om\xEDtelos).\n- compareAtPrice si hay descuento (ej. "$129.000" para tachar).\n- creativeArchetype y visualTreatment si quiere un look espec\xEDfico (performance vs premium DTC). Si no le importa, om\xEDtelos.\n\nNO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente.\n\nOTROS OPCIONALES: buyerPersona, nVariants (default 3), imageModel, imageStyle, styleMix, aspectRatio (default 4:5), generateAdCopy, pauseForAngleReview, features, brandColors, isCombo, creativeDirection.', ImageInput.shape, CREATES_EXTERNAL, async (input) => {
13884
14113
  try {
13885
14114
  const { projectId, ...rest } = input;
13886
14115
  return await submitCreative(api, {
@@ -13898,7 +14127,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
13898
14127
  throw translateError(e);
13899
14128
  }
13900
14129
  });
13901
- server.tool("edit_image_creative", "Corrige UNA imagen ya generada de una tarea de creativos, sin regenerar el set completo.\n\nCUESTA CR\xC9DITOS: cobra una sola imagen (sin brief ni extracci\xF3n), mucho menos que volver a generar todo. ANTES de invocar, PREG\xDANTALE al usuario si quiere pagar esa correcci\xF3n.\n\nSolo funciona con la tarea en estado `done` \u2014 si a\xFAn est\xE1 procesando, espera con get_task_status. Una tarea en error terminal no se puede corregir.\n\nUsa variantIndex para elegir cu\xE1l de las im\xE1genes corregir (0 = la primera) e instruction para describir el cambio en lenguaje natural.", EditImageCreativeInput.shape, async (input) => {
14130
+ server.tool("edit_image_creative", "Corrige UNA imagen ya generada de una tarea de creativos, sin regenerar el set completo.\n\nCUESTA CR\xC9DITOS: cobra una sola imagen (sin brief ni extracci\xF3n), mucho menos que volver a generar todo. ANTES de invocar, PREG\xDANTALE al usuario si quiere pagar esa correcci\xF3n.\n\nSolo funciona con la tarea en estado `done` \u2014 si a\xFAn est\xE1 procesando, espera con get_task_status. Una tarea en error terminal no se puede corregir.\n\nUsa variantIndex para elegir cu\xE1l de las im\xE1genes corregir (0 = la primera) e instruction para describir el cambio en lenguaje natural.", EditImageCreativeInput.shape, OVERWRITES_EXTERNAL, async (input) => {
13902
14131
  try {
13903
14132
  const r = await api.postJson(`/v1/creatives/images/${encodeURIComponent(input.taskId)}/regenerate`, {
13904
14133
  comment: input.instruction,
@@ -13909,7 +14138,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
13909
14138
  throw translateError(e);
13910
14139
  }
13911
14140
  });
13912
- server.tool("generate_landing", 'Genera una landing page cl\xE1sica basada en im\xE1genes, lista para Shopify. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), language, country. Si el usuario dice algo natural como \u201Ccrea una landing con este producto\u201D y adjunta o menciona fotos locales, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para esos campos, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults): salesAngle, buyerPersona, isCombo, creativeDirection, landingStrategy.', LandingInput.shape, async (input) => {
14141
+ server.tool("generate_landing", 'Genera una landing page cl\xE1sica basada en im\xE1genes, lista para Shopify. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), language, country. Si el usuario dice algo natural como \u201Ccrea una landing con este producto\u201D y adjunta o menciona fotos locales, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para esos campos, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults): salesAngle, buyerPersona, isCombo, creativeDirection, landingStrategy.', LandingInput.shape, CREATES_EXTERNAL, async (input) => {
13913
14142
  try {
13914
14143
  const { projectId, ...rest } = input;
13915
14144
  return await submitCreative(api, {
@@ -14159,7 +14388,7 @@ function estimatePayload(input) {
14159
14388
  }
14160
14389
  __name(estimatePayload, "estimatePayload");
14161
14390
  function registerCreativeVariantTools(server, api) {
14162
- server.tool("estimate_creative_variant", "Calcula el costo de crear una variante desde una tarea existente, sin consumir cr\xE9ditos. Usa el task origen para resolver el modelo/tier real y devuelve m\xEDnimo, m\xE1ximo, saldo y si alcanza. Para landing env\xEDa las mismas instructions/sectionKey que usar\xE1s al crear: si hay instructions sin sectionKey, el rango incluye la clasificaci\xF3n pagada con IA. Llama esto antes de create_creative_variant y muestra el rango al usuario.", EstimateCreativeVariantInputBase.shape, async (input) => {
14391
+ server.tool("estimate_creative_variant", "Calcula el costo de crear una variante desde una tarea existente, sin consumir cr\xE9ditos. Usa el task origen para resolver el modelo/tier real y devuelve m\xEDnimo, m\xE1ximo, saldo y si alcanza. Para landing env\xEDa las mismas instructions/sectionKey que usar\xE1s al crear: si hay instructions sin sectionKey, el rango incluye la clasificaci\xF3n pagada con IA. Llama esto antes de create_creative_variant y muestra el rango al usuario.", EstimateCreativeVariantInputBase.shape, READS_INTERNAL, async (input) => {
14163
14392
  try {
14164
14393
  const validatedInput = parseVariantInput(EstimateCreativeVariantInput, input);
14165
14394
  const estimate = await api.postJson(`/v1/tasks/${encodeURIComponent(validatedInput.sourceTaskId)}/creative-variants/estimate`, estimatePayload(validatedInput));
@@ -14172,7 +14401,7 @@ function registerCreativeVariantTools(server, api) {
14172
14401
  throw translateError(error);
14173
14402
  }
14174
14403
  });
14175
- server.tool("create_creative_variant", "Crea un video, imagen o secci\xF3n de landing adicional dentro de una tarea existente. ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio: genera un UUID por intenci\xF3n y reutiliza el mismo ante timeouts o transporte ambiguo. Si recibes status=error, pide confirmaci\xF3n para un intento nuevo y usa otro UUID. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, async (input) => {
14404
+ server.tool("create_creative_variant", "Crea un video, imagen o secci\xF3n de landing adicional dentro de una tarea existente. ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio: genera un UUID por intenci\xF3n y reutiliza el mismo ante timeouts o transporte ambiguo. Si recibes status=error, pide confirmaci\xF3n para un intento nuevo y usa otro UUID. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, CREATES_EXTERNAL, async (input) => {
14176
14405
  try {
14177
14406
  const validatedInput = parseVariantInput(CreateCreativeVariantInput, input);
14178
14407
  const estimateInput = EstimateCreativeVariantInput.parse({
@@ -14266,7 +14495,7 @@ var RegenerateVideoScriptInput = z52.object({
14266
14495
  narrativeStyle: NarrativeStyleEnum.describe('Narrativa para el nuevo guion. Incluye valores hist\xF3ricos porque esta herramienta tambi\xE9n regenera tareas antiguas. REQUERIDO \u2014 el backend NO acepta "mantener la actual"; siempre tienes que elegir una. Si el usuario quiere re-rolear con la misma narrativa, pasa la que ya tiene el \xE1ngulo (la ves en get_video_script o en get_task_status).')
14267
14496
  }).strict();
14268
14497
  function registerScriptReviewTools(server, api) {
14269
- server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId?, persuasiveFunction?, winnerVisual?, expectedVisual? }], error? }. En escenas copiadas de un winner, `persuasiveFunction` explica qu\xE9 logra ese beat, `winnerVisual` conserva la observaci\xF3n fuente y `expectedVisual` describe nuestra adaptaci\xF3n localizada; muestra los tres junto al di\xE1logo para que una edici\xF3n no desaline\xE9 guion y visual. El campo `error` (cuando viene con status="ready") es el motivo legible por el que un intento previo de aprobaci\xF3n fue rechazado (p.ej. el guion no cab\xEDa en el tiempo y el \xE1ngulo volvi\xF3 a "ready"); mu\xE9straselo al usuario.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, async (input) => {
14498
+ server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId?, persuasiveFunction?, winnerVisual?, expectedVisual? }], error? }. En escenas copiadas de un winner, `persuasiveFunction` explica qu\xE9 logra ese beat, `winnerVisual` conserva la observaci\xF3n fuente y `expectedVisual` describe nuestra adaptaci\xF3n localizada; muestra los tres junto al di\xE1logo para que una edici\xF3n no desaline\xE9 guion y visual. El campo `error` (cuando viene con status="ready") es el motivo legible por el que un intento previo de aprobaci\xF3n fue rechazado (p.ej. el guion no cab\xEDa en el tiempo y el \xE1ngulo volvi\xF3 a "ready"); mu\xE9straselo al usuario.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, READS_INTERNAL, async (input) => {
14270
14499
  try {
14271
14500
  const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
14272
14501
  const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
@@ -14275,7 +14504,7 @@ function registerScriptReviewTools(server, api) {
14275
14504
  throw translateError(e);
14276
14505
  }
14277
14506
  });
14278
- server.tool("regenerate_video_script", 'Re-rolea (regenera) el guion del video pendiente de revisi\xF3n. \xDAsalo cuando:\n- get_video_script devuelve status="error" (el LLM fall\xF3 al redactar).\n- El usuario rechaza el guion entero y prefiere reintentar (no editar l\xEDnea por l\xEDnea).\n- Quieres probar otra narrativa (pasa `narrativeStyle`).\n\nFlujo: llama este tool \u2192 polea get_video_script hasta que vuelva a status="ready" \u2192 mu\xE9strale el nuevo guion al usuario \u2192 approve_video_script.\n\nIMPORTANTE: regenerar CUESTA cr\xE9ditos de LLM. No lo invoques sin confirmaci\xF3n del usuario salvo que el status sea "error" (sin retry el usuario no tiene salida).\n\nMULTI-ANGLE: pasa el mismo `angleKey` que usaste con get_video_script.', RegenerateVideoScriptInput.shape, async (input) => {
14507
+ server.tool("regenerate_video_script", 'Re-rolea (regenera) el guion del video pendiente de revisi\xF3n. \xDAsalo cuando:\n- get_video_script devuelve status="error" (el LLM fall\xF3 al redactar).\n- El usuario rechaza el guion entero y prefiere reintentar (no editar l\xEDnea por l\xEDnea).\n- Quieres probar otra narrativa (pasa `narrativeStyle`).\n\nFlujo: llama este tool \u2192 polea get_video_script hasta que vuelva a status="ready" \u2192 mu\xE9strale el nuevo guion al usuario \u2192 approve_video_script.\n\nIMPORTANTE: regenerar CUESTA cr\xE9ditos de LLM. No lo invoques sin confirmaci\xF3n del usuario salvo que el status sea "error" (sin retry el usuario no tiene salida).\n\nMULTI-ANGLE: pasa el mismo `angleKey` que usaste con get_video_script.', RegenerateVideoScriptInput.shape, OVERWRITES_EXTERNAL, async (input) => {
14279
14508
  try {
14280
14509
  const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/regenerate`, {
14281
14510
  angleKey: input.angleKey,
@@ -14286,25 +14515,36 @@ function registerScriptReviewTools(server, api) {
14286
14515
  throw translateError(e);
14287
14516
  }
14288
14517
  });
14289
- server.tool("approve_video_script", 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO valida el rango POR-ESCENA si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas).\n4. TOPE GLOBAL: adem\xE1s del rango por-escena, el backend rechaza con 400 ("El guion no cabe en el tiempo del video. Ac\xF3rtalo y vuelve a aprobar.") cuando el TOTAL de palabras de TODO el guion (incluidas las escenas que no editaste) supera el cupo de la voz \u2014 porque ese guion se pasar\xEDa del tope de duraci\xF3n al renderizar. Si lo ves, acorta las escenas m\xE1s largas (la suma de `maxWords` de get_video_script es el techo total) y reintenta; o usa regenerate_video_script. Este 400 es s\xEDncrono y NO consume render.\n5. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n6. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.', ApproveVideoScriptInput.shape, async (input) => {
14290
- try {
14291
- const body = {
14292
- scenes: input.scenes
14293
- };
14294
- if (input.angleKey) body.angleKey = input.angleKey;
14295
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
14296
- return asText(r);
14297
- } catch (e) {
14298
- throw translateError(e);
14518
+ server.tool(
14519
+ "approve_video_script",
14520
+ 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO valida el rango POR-ESCENA si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas).\n4. TOPE GLOBAL: adem\xE1s del rango por-escena, el backend rechaza con 400 ("El guion no cabe en el tiempo del video. Ac\xF3rtalo y vuelve a aprobar.") cuando el TOTAL de palabras de TODO el guion (incluidas las escenas que no editaste) supera el cupo de la voz \u2014 porque ese guion se pasar\xEDa del tope de duraci\xF3n al renderizar. Si lo ves, acorta las escenas m\xE1s largas (la suma de `maxWords` de get_video_script es el techo total) y reintenta; o usa regenerate_video_script. Este 400 es s\xEDncrono y NO consume render.\n5. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n6. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.',
14521
+ ApproveVideoScriptInput.shape,
14522
+ // Not CREATES_INTERNAL: approving the script is what STARTS the render, so
14523
+ // this call dispatches jobs to the video providers and burns credits. Its own
14524
+ // description says so ("Tras llamarlo, el render arranca y consume créditos").
14525
+ // A host that decides when to interrupt the user by `destructiveHint ||
14526
+ // openWorldHint` would otherwise auto-invoke the one call that spends money.
14527
+ CREATES_EXTERNAL,
14528
+ async (input) => {
14529
+ try {
14530
+ const body = {
14531
+ scenes: input.scenes
14532
+ };
14533
+ if (input.angleKey) body.angleKey = input.angleKey;
14534
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
14535
+ return asText(r);
14536
+ } catch (e) {
14537
+ throw translateError(e);
14538
+ }
14299
14539
  }
14300
- });
14540
+ );
14301
14541
  }
14302
14542
  __name(registerScriptReviewTools, "registerScriptReviewTools");
14303
14543
 
14304
14544
  // src/tools/connections.ts
14305
14545
  import { z as z53 } from "zod";
14306
14546
  function registerConnectionTools(server, api) {
14307
- server.tool("list_shopify_shops", "Lista las tiendas Shopify conectadas por este usuario. Si la lista est\xE1 vac\xEDa, dile al usuario que conecte una tienda en productmaker.app/integrations antes de intentar publicar.", z53.object({}).shape, async () => {
14547
+ server.tool("list_shopify_shops", "Lista las tiendas Shopify conectadas por este usuario. Si la lista est\xE1 vac\xEDa, dile al usuario que conecte una tienda en productmaker.app/integrations antes de intentar publicar.", z53.object({}).shape, READS_INTERNAL, async () => {
14308
14548
  try {
14309
14549
  const r = await api.getJson("/v1/shopify/shops");
14310
14550
  return asText(r);
@@ -14316,7 +14556,7 @@ function registerConnectionTools(server, api) {
14316
14556
  shopDomain: z53.string().min(3).max(120),
14317
14557
  query: z53.string().trim().max(120).optional(),
14318
14558
  taskId: z53.string().uuid().optional()
14319
- }).shape, async ({ shopDomain, query, taskId }) => {
14559
+ }).shape, READS_EXTERNAL, async ({ shopDomain, query, taskId }) => {
14320
14560
  try {
14321
14561
  const params = new URLSearchParams({
14322
14562
  shopDomain
@@ -14329,7 +14569,7 @@ function registerConnectionTools(server, api) {
14329
14569
  throw translateError(e);
14330
14570
  }
14331
14571
  });
14332
- server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Para publicar, prefiere `list_meta_destinations`, que tambi\xE9n devuelve el portfolio y las p\xE1ginas disponibles. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z53.object({}).shape, async () => {
14572
+ server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Para publicar, prefiere `list_meta_destinations`, que tambi\xE9n devuelve el portfolio y las p\xE1ginas disponibles. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z53.object({}).shape, READS_EXTERNAL, async () => {
14333
14573
  try {
14334
14574
  const r = await api.getJson("/v1/facebook/ad-accounts");
14335
14575
  return asText(r);
@@ -14340,6 +14580,7 @@ function registerConnectionTools(server, api) {
14340
14580
  server.registerTool("list_meta_destinations", {
14341
14581
  description: "Lista los destinos de publicaci\xF3n de Meta del usuario: Business portfolios, cuentas publicitarias dentro de cada portfolio y Facebook Pages disponibles. Es obligatorio llamar esta herramienta antes de publicar, mostrar las opciones y enviar al publicador el `portfolioId`, `adAccountId` y `pageId` elegidos; nunca elijas silenciosamente la primera p\xE1gina o cuenta. No modifica Meta.",
14342
14582
  inputSchema: z53.object({}).shape,
14583
+ annotations: READS_EXTERNAL,
14343
14584
  _meta: {
14344
14585
  "openai/widgetAccessible": true
14345
14586
  }
@@ -14351,7 +14592,7 @@ function registerConnectionTools(server, api) {
14351
14592
  throw translateError(e);
14352
14593
  }
14353
14594
  });
14354
- server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z53.object({}).shape, async () => {
14595
+ server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z53.object({}).shape, READS_EXTERNAL, async () => {
14355
14596
  try {
14356
14597
  const r = await api.getJson("/v1/tiktok/advertisers");
14357
14598
  return asText(r);
@@ -14365,7 +14606,7 @@ __name(registerConnectionTools, "registerConnectionTools");
14365
14606
  // src/tools/actors.ts
14366
14607
  import { z as z54 } from "zod";
14367
14608
  function registerActorTools(server, api) {
14368
- server.tool("list_actors", "Lista los personajes guardados del usuario (personas creadas con IA que pueden protagonizar los videos e im\xE1genes). Devuelve id, nombre, descripci\xF3n, g\xE9nero y voz. Usa el `id` como `actorId` al crear o editar una tarea. Solo aparecen los personajes listos para usar; si la lista est\xE1 vac\xEDa, dile al usuario que cree uno en productmaker.app antes de intentar asignarlo. No modifica nada.", z54.object({}).shape, async () => {
14609
+ server.tool("list_actors", "Lista los personajes guardados del usuario (personas creadas con IA que pueden protagonizar los videos e im\xE1genes). Devuelve id, nombre, descripci\xF3n, g\xE9nero y voz. Usa el `id` como `actorId` al crear o editar una tarea. Solo aparecen los personajes listos para usar; si la lista est\xE1 vac\xEDa, dile al usuario que cree uno en productmaker.app antes de intentar asignarlo. No modifica nada.", z54.object({}).shape, READS_INTERNAL, async () => {
14369
14610
  try {
14370
14611
  const rows = await api.getJson("/v1/actors");
14371
14612
  const actors = (Array.isArray(rows) ? rows : []).filter((actor) => actor.status === "ready").map(({ id, name, description, gender, voiceId }) => ({
@@ -14410,8 +14651,37 @@ var ShopifyInput = z55.object({
14410
14651
  ]).optional(),
14411
14652
  /** Shopify product GID, numeric id, or admin product URL. Required when
14412
14653
  * publicationMode is `existing`. */
14413
- targetProductId: z55.string().trim().min(1).max(300).optional()
14654
+ targetProductId: z55.string().trim().min(1).max(300).optional(),
14655
+ /** Add an announcement bar text above add-to-cart (optional). */
14656
+ announcementBarEnabled: z55.boolean().optional(),
14657
+ announcementBarText: z55.string().trim().min(1).max(SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH).optional()
14414
14658
  });
14659
+ var ShopifyInputWithAnnouncementBarValidation = ShopifyInput.superRefine((value, ctx) => {
14660
+ if (value.announcementBarEnabled && !value.announcementBarText) {
14661
+ ctx.addIssue({
14662
+ code: z55.ZodIssueCode.custom,
14663
+ path: [
14664
+ "announcementBarText"
14665
+ ],
14666
+ message: "announcementBarText is required when announcementBarEnabled is true"
14667
+ });
14668
+ }
14669
+ if (!value.announcementBarEnabled && value.announcementBarText) {
14670
+ ctx.addIssue({
14671
+ code: z55.ZodIssueCode.custom,
14672
+ path: [
14673
+ "announcementBarText"
14674
+ ],
14675
+ message: "announcementBarText requires announcementBarEnabled to be true"
14676
+ });
14677
+ }
14678
+ });
14679
+ function parseShopifyInput(rawInput) {
14680
+ const parsed = ShopifyInputWithAnnouncementBarValidation.safeParse(rawInput);
14681
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
14682
+ return parsed.data;
14683
+ }
14684
+ __name(parseShopifyInput, "parseShopifyInput");
14415
14685
  var McpCampaignConfig = z55.object({
14416
14686
  /** ALWAYS in US dollars, whatever the ad account's currency is. ProductMaker
14417
14687
  * converts to the account currency server-side. Do NOT pre-convert: on a COP
@@ -14647,9 +14917,10 @@ var TikTokInput = z55.object({
14647
14917
  });
14648
14918
  function registerPublishTools(server, api, opts = {}) {
14649
14919
  const transport = opts.transport ?? "http";
14650
- server.tool("publish_to_shopify", "Publica o actualiza la landing en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. Por defecto `publicationMode=auto`: si esta tarea ya fue publicada en esa tienda, actualiza ESE MISMO producto y conserva URL, precio, variantes, inventario y SKU; si no, crea uno. Usa `existing` + `targetProductId` cuando el usuario elija otro producto y `new` s\xF3lo si pide crear uno nuevo. ANTES de llamar preg\xFAntale \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED. Por defecto publica TODAS las im\xE1genes generadas; pasa `numLandingImages` s\xF3lo si el usuario pide menos. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
14920
+ server.tool("publish_to_shopify", "Publica o actualiza la landing en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. Por defecto `publicationMode=auto`: si esta tarea ya fue publicada en esa tienda, actualiza ESE MISMO producto y conserva URL, precio, variantes, inventario y SKU; si no, crea uno. Usa `existing` + `targetProductId` cuando el usuario elija otro producto y `new` s\xF3lo si pide crear uno nuevo. ANTES de llamar preg\xFAntale \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED. Por defecto publica TODAS las im\xE1genes generadas; pasa `numLandingImages` s\xF3lo si el usuario pide menos. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, OVERWRITES_EXTERNAL, async (input) => {
14651
14921
  try {
14652
- if (input.publicationMode === "existing" && !input.targetProductId) {
14922
+ const parsed = parseShopifyInput(input);
14923
+ if (parsed.publicationMode === "existing" && !parsed.targetProductId) {
14653
14924
  return {
14654
14925
  isError: true,
14655
14926
  content: [
@@ -14660,29 +14931,31 @@ function registerPublishTools(server, api, opts = {}) {
14660
14931
  ]
14661
14932
  };
14662
14933
  }
14663
- const variantIndex = input.variantIndex ?? 0;
14664
- const availableImages = await fetchLandingImageCount(api, input.taskId);
14934
+ const variantIndex = parsed.variantIndex ?? 0;
14935
+ const availableImages = await fetchLandingImageCount(api, parsed.taskId);
14665
14936
  const effectiveAvailable = availableImages > 0 ? availableImages : 3;
14666
- const numImages = Math.min(input.numLandingImages ?? effectiveAvailable, effectiveAvailable);
14937
+ const numImages = Math.min(parsed.numLandingImages ?? effectiveAvailable, effectiveAvailable);
14667
14938
  const selectedLandingImages = Array.from({
14668
14939
  length: numImages
14669
14940
  }, (_, i) => ({
14670
14941
  imageIndex: i,
14671
14942
  variantIndex
14672
14943
  }));
14673
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/publish/shopify`, {
14674
- shopDomain: input.shopDomain,
14944
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(parsed.taskId)}/publish/shopify`, {
14945
+ shopDomain: parsed.shopDomain,
14675
14946
  selectedLandingImages,
14676
- showVariantPicker: input.showVariantPicker,
14677
- publicationMode: input.publicationMode,
14678
- targetProductId: input.targetProductId
14947
+ showVariantPicker: parsed.showVariantPicker,
14948
+ publicationMode: parsed.publicationMode,
14949
+ targetProductId: parsed.targetProductId,
14950
+ announcementBarEnabled: parsed.announcementBarEnabled,
14951
+ announcementBarText: parsed.announcementBarText
14679
14952
  });
14680
14953
  return renderShopifyPublishMarkdown(r);
14681
14954
  } catch (e) {
14682
14955
  throw translateError(e);
14683
14956
  }
14684
14957
  });
14685
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde UNA sola tarea. Antes llama `list_meta_destinations`, deja que el usuario elija el `portfolioId` (Business portfolio), el `adAccountId` y el `pageId` exactos, y env\xEDa esos tres valores; no adivines ni uses la primera p\xE1gina. Si quieres juntar creativos de VARIAS tareas usa `publish_creatives_to_meta`. La campa\xF1a se crea SIEMPRE PAUSED y el usuario la activa manualmente. `pixelId` es opcional. `angleIndexes` permite seleccionar \xE1ngulos. `campaignConfig` admite presupuesto en USD, pa\xEDses ISO-2 y nombre. `primaryText` permite reemplazar el copy y `phoneNumber` configura WhatsApp. Despu\xE9s usa `get_meta_campaign_status`.", MetaInput.shape, async ({ taskId, campaignConfig, primaryText, phoneNumber, ...rest }) => {
14958
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde UNA sola tarea. Antes llama `list_meta_destinations`, deja que el usuario elija el `portfolioId` (Business portfolio), el `adAccountId` y el `pageId` exactos, y env\xEDa esos tres valores; no adivines ni uses la primera p\xE1gina. Si quieres juntar creativos de VARIAS tareas usa `publish_creatives_to_meta`. La campa\xF1a se crea SIEMPRE PAUSED y el usuario la activa manualmente. `pixelId` es opcional. `angleIndexes` permite seleccionar \xE1ngulos. `campaignConfig` admite presupuesto en USD, pa\xEDses ISO-2 y nombre. `primaryText` permite reemplazar el copy y `phoneNumber` configura WhatsApp. Despu\xE9s usa `get_meta_campaign_status`.", MetaInput.shape, CREATES_EXTERNAL, async ({ taskId, campaignConfig, primaryText, phoneNumber, ...rest }) => {
14686
14959
  try {
14687
14960
  const body = {
14688
14961
  ...rest
@@ -14710,7 +14983,7 @@ function registerPublishTools(server, api, opts = {}) {
14710
14983
  throw translateError(e);
14711
14984
  }
14712
14985
  });
14713
- server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Si la petici\xF3n natural adjunta o menciona archivos locales, llama primero `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL. Despu\xE9s llama `list_meta_destinations` y pregunta en el chat por el `portfolioId`, `adAccountId` y `pageId` exactos; el App es solo para archivos. Si la carga fue parcial, publica s\xF3lo los assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla, quedan durante el TTL para reintento. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, async (rawInput) => {
14986
+ server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Si la petici\xF3n natural adjunta o menciona archivos locales, llama primero `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL. Despu\xE9s llama `list_meta_destinations` y pregunta en el chat por el `portfolioId`, `adAccountId` y `pageId` exactos; el App es solo para archivos. Si la carga fue parcial, publica s\xF3lo los assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla, quedan durante el TTL para reintento. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, CREATES_EXTERNAL, async (rawInput) => {
14714
14987
  try {
14715
14988
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
14716
14989
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -14800,7 +15073,7 @@ function registerPublishTools(server, api, opts = {}) {
14800
15073
  throw translateError(e);
14801
15074
  }
14802
15075
  });
14803
- server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario SIEMPRE en d\xF3lares, ej. 25 \u2014 nunca el n\xFAmero en moneda local), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
15076
+ server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario SIEMPRE en d\xF3lares, ej. 25 \u2014 nunca el n\xFAmero en moneda local), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, CREATES_EXTERNAL, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
14804
15077
  try {
14805
15078
  const body = {
14806
15079
  ...rest
@@ -15026,6 +15299,7 @@ function registerMediaUploadTools(server, api) {
15026
15299
  title: "Subir archivos",
15027
15300
  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.",
15028
15301
  inputSchema: UploadSessionToolInputShape,
15302
+ annotations: CREATES_INTERNAL,
15029
15303
  outputSchema: UniversalUploadStructuredResponse.shape,
15030
15304
  _meta: {
15031
15305
  ui: {
@@ -15066,6 +15340,7 @@ function registerMediaUploadTools(server, api) {
15066
15340
  server.registerTool("check_media_upload", {
15067
15341
  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.",
15068
15342
  inputSchema: UploadStatusInput.shape,
15343
+ annotations: READS_INTERNAL,
15069
15344
  _meta: {
15070
15345
  "openai/widgetAccessible": true
15071
15346
  }
@@ -15090,7 +15365,7 @@ var TikTokStatusInput = z57.object({
15090
15365
  advertiserId: z57.string().min(1).describe("The same advertiserId used to publish the campaign.")
15091
15366
  });
15092
15367
  function registerCampaignTools(server, api) {
15093
- server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, async (input) => {
15368
+ server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, READS_EXTERNAL, async (input) => {
15094
15369
  try {
15095
15370
  const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
15096
15371
  return renderMetaStatusMarkdown(r);
@@ -15098,7 +15373,7 @@ function registerCampaignTools(server, api) {
15098
15373
  throw translateError(e);
15099
15374
  }
15100
15375
  });
15101
- server.tool("get_tiktok_campaign_status", "Consulta el estado actual + reporte lifetime (spend, impressions, clicks, reach) de una campa\xF1a TikTok publicada. Requiere `advertiserId` (el mismo con el que se cre\xF3). Llama esto DESPU\xC9S de `publish_to_tiktok` para confirmar creaci\xF3n correcta y reportar performance. Es read-only.", TikTokStatusInput.shape, async (input) => {
15376
+ server.tool("get_tiktok_campaign_status", "Consulta el estado actual + reporte lifetime (spend, impressions, clicks, reach) de una campa\xF1a TikTok publicada. Requiere `advertiserId` (el mismo con el que se cre\xF3). Llama esto DESPU\xC9S de `publish_to_tiktok` para confirmar creaci\xF3n correcta y reportar performance. Es read-only.", TikTokStatusInput.shape, READS_EXTERNAL, async (input) => {
15102
15377
  try {
15103
15378
  const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
15104
15379
  return renderTikTokStatusMarkdown(r);
@@ -15551,7 +15826,7 @@ var ProductCostingToolInput = z60.object({
15551
15826
  desiredMarginPercent: optionalPercent("Margen de ganancia deseado. Ejemplo: 10 significa 10%. Omite para usar el valor gu\xEDa editable del pa\xEDs.")
15552
15827
  });
15553
15828
  function registerProductCostingTools(server) {
15554
- server.tool("cost_product", "Calcula GRATIS y sin cr\xE9ditos cu\xE1nto cobrar por un producto de dropshipping en cualquiera de los pa\xEDses disponibles en ProductMaker. Es READ-ONLY: no crea tareas ni publica nada. Pide country y el costo real del proveedor; los dem\xE1s campos pueden usar valores gu\xEDa editables del pa\xEDs. Devuelve `price` y `compareAtPrice` listos para `create_product_task`. No inventes un costo de proveedor.", ProductCostingToolInput.shape, async (input) => {
15829
+ server.tool("cost_product", "Calcula GRATIS y sin cr\xE9ditos cu\xE1nto cobrar por un producto de dropshipping en cualquiera de los pa\xEDses disponibles en ProductMaker. Es READ-ONLY: no crea tareas ni publica nada. Pide country y el costo real del proveedor; los dem\xE1s campos pueden usar valores gu\xEDa editables del pa\xEDs. Devuelve `price` y `compareAtPrice` listos para `create_product_task`. No inventes un costo de proveedor.", ProductCostingToolInput.shape, READS_INTERNAL, async (input) => {
15555
15830
  if (input.country !== void 0 && input.country !== "CO" && [
15556
15831
  input.supplierCostCop,
15557
15832
  input.shippingCostCop,
@@ -15823,7 +16098,7 @@ async function pollUntilTerminal(api, runId, waitSeconds) {
15823
16098
  }
15824
16099
  __name(pollUntilTerminal, "pollUntilTerminal");
15825
16100
  function registerProductResearchTools(server, api) {
15826
- server.tool("find_winning_products", `Busca productos ganadores de dropshipping para un pa\xEDs y categor\xEDa, y explica POR QU\xC9 la IA eligi\xF3 cada uno (razones, riesgos y evidencia real de anuncios en Meta Ads). CUESTA ${PRICE_CREDITS} CR\xC9DITOS por ejecuci\xF3n. ANTES de invocar, dile al usuario el costo y espera su aceptaci\xF3n expl\xEDcita; s\xF3lo entonces pasa confirmed: true. Consulta primero list_product_research_runs (GRATIS) \u2014 puede que el usuario ya tenga un reporte pagado para ese pa\xEDs/categor\xEDa y no necesites cobrar de nuevo. Pa\xEDses disponibles: CO, MX, PE, EC, CL. Tarda varios minutos: usa waitSeconds para esperar, y si expira consulta el resultado con get_product_research_run \u2014 el reporte NO se pierde.`, FindInput.shape, async (input) => {
16101
+ server.tool("find_winning_products", `Busca productos ganadores de dropshipping para un pa\xEDs y categor\xEDa, y explica POR QU\xC9 la IA eligi\xF3 cada uno (razones, riesgos y evidencia real de anuncios en Meta Ads). CUESTA ${PRICE_CREDITS} CR\xC9DITOS por ejecuci\xF3n. ANTES de invocar, dile al usuario el costo y espera su aceptaci\xF3n expl\xEDcita; s\xF3lo entonces pasa confirmed: true. Consulta primero list_product_research_runs (GRATIS) \u2014 puede que el usuario ya tenga un reporte pagado para ese pa\xEDs/categor\xEDa y no necesites cobrar de nuevo. Pa\xEDses disponibles: CO, MX, PE, EC, CL. Tarda varios minutos: usa waitSeconds para esperar, y si expira consulta el resultado con get_product_research_run \u2014 el reporte NO se pierde.`, FindInput.shape, CREATES_EXTERNAL, async (input) => {
15827
16102
  const parsed = FindInput.parse(input);
15828
16103
  if (!parsed.confirmed) {
15829
16104
  throw mcp(McpErrorCode.INVALID_INPUT, `This research run costs ${PRICE_CREDITS} credits. Tell the user the cost, get an explicit yes, then call again with confirmed: true.`);
@@ -15857,7 +16132,7 @@ _Esta llamada no cobr\xF3 cr\xE9ditos: se reutiliz\xF3 una investigaci\xF3n reci
15857
16132
  throw translateError(error);
15858
16133
  }
15859
16134
  });
15860
- server.tool("get_product_research_run", "Devuelve el reporte completo de una investigaci\xF3n de productos ganadores: score, veredicto, confianza, razones, riesgos y evidencia de Meta Ads por producto. GRATIS y read-only. \xDAsalo cuando find_winning_products expir\xF3 la espera, o para releer un reporte anterior encontrado con list_product_research_runs.", GetInput.shape, async (input) => {
16135
+ server.tool("get_product_research_run", "Devuelve el reporte completo de una investigaci\xF3n de productos ganadores: score, veredicto, confianza, razones, riesgos y evidencia de Meta Ads por producto. GRATIS y read-only. \xDAsalo cuando find_winning_products expir\xF3 la espera, o para releer un reporte anterior encontrado con list_product_research_runs.", GetInput.shape, READS_INTERNAL, async (input) => {
15861
16136
  const parsed = GetInput.parse(input);
15862
16137
  try {
15863
16138
  const run = await api.getJson(`/v1/product-research/runs/${parsed.runId}`);
@@ -15877,7 +16152,7 @@ _Esta llamada no cobr\xF3 cr\xE9ditos: se reutiliz\xF3 una investigaci\xF3n reci
15877
16152
  throw translateError(error);
15878
16153
  }
15879
16154
  });
15880
- server.tool("list_product_research_runs", "Lista las investigaciones de productos ganadores ya pagadas por el usuario, de la m\xE1s reciente a la m\xE1s antigua. GRATIS y read-only. Cons\xFAltalo ANTES de lanzar una nueva: un reporte ya pagado se relee gratis con get_product_research_run.", ListInput.shape, async (input) => {
16155
+ server.tool("list_product_research_runs", "Lista las investigaciones de productos ganadores ya pagadas por el usuario, de la m\xE1s reciente a la m\xE1s antigua. GRATIS y read-only. Cons\xFAltalo ANTES de lanzar una nueva: un reporte ya pagado se relee gratis con get_product_research_run.", ListInput.shape, READS_INTERNAL, async (input) => {
15881
16156
  const parsed = ListInput.parse(input);
15882
16157
  try {
15883
16158
  const runs = await api.getJson(`/v1/product-research/runs?limit=${parsed.limit}`);
@@ -15984,7 +16259,7 @@ var CostProjectInput = z62.object({
15984
16259
  var GuideInput = z62.object({
15985
16260
  projectId: ProjectId.optional()
15986
16261
  }).strict();
15987
- var DropshippingProjectNextAction = z62.discriminatedUnion("kind", [
16262
+ var NEXT_ACTION_KINDS = [
15988
16263
  "cost_product",
15989
16264
  "provide_product_media",
15990
16265
  "generate_creatives",
@@ -15998,7 +16273,8 @@ var DropshippingProjectNextAction = z62.discriminatedUnion("kind", [
15998
16273
  "create_meta_campaign",
15999
16274
  "repair_meta_campaign",
16000
16275
  "review_paused_campaign"
16001
- ].map((kind) => z62.object({
16276
+ ];
16277
+ var DropshippingProjectNextAction = z62.discriminatedUnion("kind", NEXT_ACTION_KINDS.map((kind) => z62.object({
16002
16278
  kind: z62.literal(kind),
16003
16279
  href: z62.string().min(1)
16004
16280
  })));
@@ -16072,9 +16348,25 @@ var ProjectResponseSchema = z62.object({
16072
16348
  createdAt: z62.string().datetime(),
16073
16349
  updatedAt: z62.string().datetime()
16074
16350
  }).passthrough();
16351
+ var NEXT_ACTION_GUIDANCE = {
16352
+ cost_product: "Llama `cost_dropshipping_project` con el costo real del proveedor. Nunca lo inventes: p\xEDdeselo al usuario.",
16353
+ 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.",
16354
+ 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.",
16355
+ 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.",
16356
+ repair_creatives: "La tarea fall\xF3. Llama `get_task_status` para leer el error y expl\xEDcaselo al usuario antes de reintentar nada.",
16357
+ 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`.",
16358
+ connect_shopify: "Llama `list_shopify_shops`. Si no hay ninguna tienda conectada, el usuario debe conectarla en la web: d\xE1selo como paso manual.",
16359
+ publish_storefront: "Llama `publish_to_shopify` con el taskId y la tienda elegida.",
16360
+ wait_for_storefront: "La publicaci\xF3n en Shopify est\xE1 en curso. Llama `get_task_status` y espera; no publiques otra vez.",
16361
+ connect_meta: "Llama `list_meta_destinations`. Si no hay cuentas, el usuario debe conectar Meta en la web: d\xE1selo como paso manual.",
16362
+ 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.",
16363
+ 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.",
16364
+ 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."
16365
+ };
16075
16366
  function projectText(raw) {
16076
16367
  const project = ProjectResponseSchema.parse(raw);
16077
- const next = project.nextAction ? `${project.nextAction.kind} (${project.nextAction.href})` : "ninguna";
16368
+ const kind = project.nextAction?.kind;
16369
+ const guidance = kind ? NEXT_ACTION_GUIDANCE[kind] : void 0;
16078
16370
  return {
16079
16371
  content: [
16080
16372
  {
@@ -16084,6 +16376,9 @@ function projectText(raw) {
16084
16376
  productName: project.productName,
16085
16377
  phase: project.phase,
16086
16378
  nextAction: project.nextAction,
16379
+ // The href is a web anchor and is useless to an agent; the guidance
16380
+ // is the executable form of the same step.
16381
+ nextActionGuidance: guidance ?? null,
16087
16382
  hasReferenceImage: project.hasReferenceImage,
16088
16383
  costingSaved: Boolean(project.costingResult),
16089
16384
  rewardProgress: {
@@ -16092,7 +16387,7 @@ function projectText(raw) {
16092
16387
  milestones: project.rewards.milestones
16093
16388
  },
16094
16389
  version: project.version,
16095
- message: `Siguiente paso: ${next}. Relee este proyecto despu\xE9s de cada mutaci\xF3n.`
16390
+ message: guidance ? `Siguiente paso: ${guidance} Relee este proyecto despu\xE9s de cada mutaci\xF3n.` : "El proyecto no tiene ning\xFAn paso pendiente."
16096
16391
  })
16097
16392
  }
16098
16393
  ]
@@ -16100,28 +16395,38 @@ function projectText(raw) {
16100
16395
  }
16101
16396
  __name(projectText, "projectText");
16102
16397
  function registerDropshippingProjectTools(server, api) {
16103
- server.tool("start_dropshipping_project", "Inicia o reanuda un proyecto opcional de dropshipping. \xDAsalo cuando el usuario quiera seguir un producto hasta publicarlo; para una operaci\xF3n puntual usa el tool directo. La creaci\xF3n es idempotente y no cobra cr\xE9ditos.", StartProjectInput.shape, async (input) => {
16398
+ server.tool("start_dropshipping_project", "Inicia o reanuda un proyecto opcional de dropshipping. \xDAsalo cuando el usuario quiera seguir un producto hasta publicarlo; para una operaci\xF3n puntual usa el tool directo. La creaci\xF3n es idempotente y no cobra cr\xE9ditos.", StartProjectInput.shape, CREATES_INTERNAL, async (input) => {
16104
16399
  try {
16105
16400
  return projectText(await api.postJson("/v1/dropshipping-projects", input));
16106
16401
  } catch (error) {
16107
16402
  throw translateError(error);
16108
16403
  }
16109
16404
  });
16110
- server.tool("cost_dropshipping_project", "Guarda el costeo de un proyecto guiado. Escribe solo inputs; el backend recalcula el resultado can\xF3nico y controla la versi\xF3n. Pide al usuario el costo real del proveedor: nunca lo inventes. No crea tarea ni publica nada.", CostProjectInput.shape, async (input) => {
16111
- try {
16112
- if (input.returnRate + input.cancellationRate >= 1) {
16113
- throw new Error("returnRate + cancellationRate debe ser menor que 1.");
16405
+ server.tool(
16406
+ "cost_dropshipping_project",
16407
+ "Guarda el costeo de un proyecto guiado. Escribe solo inputs; el backend recalcula el resultado can\xF3nico y controla la versi\xF3n. Pide al usuario el costo real del proveedor: nunca lo inventes. No crea tarea ni publica nada.",
16408
+ CostProjectInput.shape,
16409
+ // Overwrites the costing inputs already saved on the project — the same
16410
+ // "previous values are gone" case as edit_task_draft, so destructive.
16411
+ // idempotentHint stays false despite the PUT: the `version` guard makes a
16412
+ // blind retry FAIL rather than replay, so a host should not auto-retry it.
16413
+ OVERWRITES_INTERNAL,
16414
+ async (input) => {
16415
+ try {
16416
+ if (input.returnRate + input.cancellationRate >= 1) {
16417
+ throw new Error("returnRate + cancellationRate debe ser menor que 1.");
16418
+ }
16419
+ const { projectId, version, ...costingInput } = input;
16420
+ return projectText(await api.putJson(`/v1/dropshipping-projects/${encodeURIComponent(projectId)}/costing`, {
16421
+ version,
16422
+ input: costingInput
16423
+ }));
16424
+ } catch (error) {
16425
+ throw translateError(error);
16114
16426
  }
16115
- const { projectId, version, ...costingInput } = input;
16116
- return projectText(await api.putJson(`/v1/dropshipping-projects/${encodeURIComponent(projectId)}/costing`, {
16117
- version,
16118
- input: costingInput
16119
- }));
16120
- } catch (error) {
16121
- throw translateError(error);
16122
16427
  }
16123
- });
16124
- server.tool("guide_dropshipping_launch", "Gu\xEDa read-only de un recorrido COMPLETO de dropshipping, desde producto/costeo hasta creativos, tienda y publicidad. No uses este tool para subir o publicar un creativo puntual en Meta: llama directamente `request_media_upload`, `list_meta_destinations` y `publish_creatives_to_meta`. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, async (input) => {
16428
+ );
16429
+ server.tool("guide_dropshipping_launch", "Gu\xEDa read-only de un recorrido COMPLETO de dropshipping, desde producto/costeo hasta creativos, tienda y publicidad. No uses este tool para subir o publicar un creativo puntual en Meta: llama directamente `request_media_upload`, `list_meta_destinations` y `publish_creatives_to_meta`. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, READS_INTERNAL, async (input) => {
16125
16430
  try {
16126
16431
  if (!input.projectId) {
16127
16432
  return asText({
@@ -16472,6 +16777,14 @@ function createApp(opts) {
16472
16777
  ok: true
16473
16778
  });
16474
16779
  });
16780
+ app.get("/.well-known/openai-apps-challenge", (_req, res) => {
16781
+ const token = opts.appsChallengeToken?.trim();
16782
+ if (!token) {
16783
+ res.status(404).type("text/plain").send("Not found");
16784
+ return;
16785
+ }
16786
+ res.status(200).type("text/plain").send(token);
16787
+ });
16475
16788
  app.get("/assets/:token/:filename?", rateLimit({
16476
16789
  windowMs: 6e4,
16477
16790
  max: 600,
@@ -16543,7 +16856,8 @@ async function startHttpServer(env = process.env) {
16543
16856
  const app = createApp({
16544
16857
  apiBaseUrl,
16545
16858
  proxyBaseUrl,
16546
- oauth
16859
+ oauth,
16860
+ appsChallengeToken: env.OPENAI_APPS_CHALLENGE_TOKEN ?? null
16547
16861
  });
16548
16862
  return await new Promise((resolve3, reject) => {
16549
16863
  const server = app.listen(port, () => {