@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/stdio.js CHANGED
@@ -264,7 +264,7 @@ var ApiClient = class {
264
264
  };
265
265
 
266
266
  // src/index.ts
267
- var MCP_VERSION = "3.2.0";
267
+ var MCP_VERSION = "3.4.0";
268
268
 
269
269
  // src/meta-upload-app.ts
270
270
  import { readFileSync } from "fs";
@@ -1517,6 +1517,42 @@ var VideoTier = z5.preprocess((v) => v === "grok" ? "single_call" : v, z5.enum([
1517
1517
  ]));
1518
1518
  var DEFAULT_VIDEO_TIER = "single_call";
1519
1519
 
1520
+ // ../../packages/types/src/image-provider.ts
1521
+ var GPT_IMAGE_2_RATIOS_BLOCKED_ABOVE_1K = [
1522
+ "4:5",
1523
+ "5:4"
1524
+ ];
1525
+ function gptImage2SupportsTier(aspectRatio, tier) {
1526
+ if (tier === "gpt-image-2:1k") return true;
1527
+ return !GPT_IMAGE_2_RATIOS_BLOCKED_ABOVE_1K.includes(aspectRatio);
1528
+ }
1529
+ __name(gptImage2SupportsTier, "gptImage2SupportsTier");
1530
+ var IMAGE_TIER_UNSUPPORTED_FOR_RATIO_RULE = "image_tier_unsupported_for_ratio";
1531
+ var IMAGE_TIER_LIST = [
1532
+ "gpt-image-2:1k",
1533
+ "gpt-image-2:2k",
1534
+ "gpt-image-2:4k"
1535
+ ];
1536
+ function parseImageTier(imageModel) {
1537
+ const normalized = imageModel.trim().toLowerCase();
1538
+ return IMAGE_TIER_LIST.includes(normalized) ? normalized : null;
1539
+ }
1540
+ __name(parseImageTier, "parseImageTier");
1541
+ function validateImageTierForRatio(aspectRatio, imageModel) {
1542
+ if (!aspectRatio || !imageModel) return null;
1543
+ const tier = parseImageTier(imageModel);
1544
+ if (!tier) return null;
1545
+ const ratio = aspectRatio.trim();
1546
+ if (!GPT_IMAGE_2_RATIOS_BLOCKED_ABOVE_1K.includes(ratio)) return null;
1547
+ if (gptImage2SupportsTier(ratio, tier)) return null;
1548
+ return buildImageTierUnsupportedMessage(ratio);
1549
+ }
1550
+ __name(validateImageTierForRatio, "validateImageTierForRatio");
1551
+ function buildImageTierUnsupportedMessage(aspectRatio) {
1552
+ return `El formato ${aspectRatio} solo est\xE1 disponible en calidad est\xE1ndar (1K). Elige 1K, o cambia el formato para usar Premium.`;
1553
+ }
1554
+ __name(buildImageTierUnsupportedMessage, "buildImageTierUnsupportedMessage");
1555
+
1520
1556
  // ../../packages/types/src/angle.ts
1521
1557
  import { z as z6 } from "zod";
1522
1558
  var ImageWinnerSignal = z6.enum([
@@ -2857,6 +2893,19 @@ var IngestOptions = z15.object({
2857
2893
  message
2858
2894
  });
2859
2895
  }
2896
+ const tierMessage = validateImageTierForRatio(data.aspectRatio, data.imageModel);
2897
+ if (tierMessage) {
2898
+ ctx.addIssue({
2899
+ code: z15.ZodIssueCode.custom,
2900
+ path: [
2901
+ "imageModel"
2902
+ ],
2903
+ params: {
2904
+ rule: IMAGE_TIER_UNSUPPORTED_FOR_RATIO_RULE
2905
+ },
2906
+ message: tierMessage
2907
+ });
2908
+ }
2860
2909
  });
2861
2910
  var IngestRequest = z15.object({
2862
2911
  url: z15.string().url(),
@@ -3221,7 +3270,13 @@ var VideoGenerator = z18.object({
3221
3270
  videoResolution: VideoResolution.optional(),
3222
3271
  /** Requested controls and the effective provider route that produced this result. */
3223
3272
  execution: VideoExecutionContract.optional(),
3224
- adCopy: z18.string().optional()
3273
+ adCopy: z18.string().optional(),
3274
+ /** TRUE when the seller APPROVED a script for this angle that the plan path
3275
+ * could not honor, so a fresh script was re-authored and rendered instead
3276
+ * (PR #1470). The angle still delivered — nothing was lost and nothing is
3277
+ * refundable — but the video is NOT the script they signed off on, and the
3278
+ * UI must say so. Absent (never `false`) on every honest delivery. */
3279
+ scriptRewritten: z18.boolean().optional()
3225
3280
  });
3226
3281
  var VideoScriptSceneForReview = z18.object({
3227
3282
  index: z18.number().int().nonnegative(),
@@ -3395,6 +3450,7 @@ var ShopifyPublicationMode = z20.enum([
3395
3450
  "existing",
3396
3451
  "new"
3397
3452
  ]);
3453
+ var SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH = 160;
3398
3454
  var ShopifyPublishRequest = z20.object({
3399
3455
  shopDomain: z20.string().min(3).max(120).optional(),
3400
3456
  /**
@@ -3413,7 +3469,9 @@ var ShopifyPublishRequest = z20.object({
3413
3469
  */
3414
3470
  publicationMode: ShopifyPublicationMode.default("auto"),
3415
3471
  /** Shopify product GID, numeric id, or admin product URL. Required for existing mode. */
3416
- targetProductId: z20.string().trim().min(1).max(300).optional()
3472
+ targetProductId: z20.string().trim().min(1).max(300).optional(),
3473
+ announcementBarEnabled: z20.boolean().optional(),
3474
+ announcementBarText: z20.string().trim().min(1).max(SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH).optional()
3417
3475
  }).superRefine((data, ctx) => {
3418
3476
  if (data.publicationMode === "existing" && !data.targetProductId) {
3419
3477
  ctx.addIssue({
@@ -3424,6 +3482,24 @@ var ShopifyPublishRequest = z20.object({
3424
3482
  message: "Selecciona un producto de Shopify."
3425
3483
  });
3426
3484
  }
3485
+ if (data.announcementBarEnabled && !data.announcementBarText) {
3486
+ ctx.addIssue({
3487
+ code: z20.ZodIssueCode.custom,
3488
+ path: [
3489
+ "announcementBarText"
3490
+ ],
3491
+ message: "Escribe el texto del anuncio."
3492
+ });
3493
+ }
3494
+ if (!data.announcementBarEnabled && data.announcementBarText) {
3495
+ ctx.addIssue({
3496
+ code: z20.ZodIssueCode.custom,
3497
+ path: [
3498
+ "announcementBarText"
3499
+ ],
3500
+ message: "Activa el anuncio para usar texto."
3501
+ });
3502
+ }
3427
3503
  });
3428
3504
  var ShopifyPublicationStatus = z20.enum([
3429
3505
  "pending",
@@ -3506,6 +3582,10 @@ var CreateShopifyBillingChargeRequest = z20.object({
3506
3582
  var CancelShopifySubscriptionRequest = z20.object({
3507
3583
  subscriptionId: z20.string().min(1)
3508
3584
  });
3585
+ var ShopifyAnnouncementBarState = z20.object({
3586
+ enabled: z20.boolean(),
3587
+ text: z20.string().max(SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH).optional()
3588
+ });
3509
3589
  var LandingHealthResponse = z20.object({
3510
3590
  taskId: z20.string(),
3511
3591
  state: z20.enum([
@@ -3547,9 +3627,20 @@ var ApproveAnglesRequest = z21.object({
3547
3627
  "mixed"
3548
3628
  ]).optional()
3549
3629
  });
3630
+ var ApproveAnglesRefusalReason = z21.enum([
3631
+ "photo_review_pending",
3632
+ "photos_not_ready",
3633
+ "reference_out_of_date"
3634
+ ]);
3550
3635
  var ApproveAnglesResponse = z21.object({
3551
3636
  success: z21.boolean(),
3552
- taskId: z21.string().uuid()
3637
+ taskId: z21.string().uuid(),
3638
+ reason: ApproveAnglesRefusalReason.optional()
3639
+ });
3640
+ var IngestResumeRefusalResponse = z21.object({
3641
+ success: z21.literal(false),
3642
+ taskId: z21.string().uuid(),
3643
+ reason: ApproveAnglesRefusalReason.optional()
3553
3644
  });
3554
3645
  var GenerateSuggestionsResponse = z21.object({
3555
3646
  painPoints: z21.array(z21.string()),
@@ -3665,7 +3756,8 @@ var ApproveVideoAnglesRequest = z21.object({
3665
3756
  });
3666
3757
  var ApproveVideoAnglesResponse = z21.object({
3667
3758
  success: z21.boolean(),
3668
- taskId: z21.string().uuid()
3759
+ taskId: z21.string().uuid(),
3760
+ reason: ApproveAnglesRefusalReason.optional()
3669
3761
  });
3670
3762
  var SaveVideoDraftRequest = z21.object({
3671
3763
  /** Global creative direction for all videos. Not spoken dialogue. */
@@ -3697,7 +3789,8 @@ var ApproveImageAnglesRequest = z21.object({
3697
3789
  });
3698
3790
  var ApproveImageAnglesResponse = z21.object({
3699
3791
  success: z21.boolean(),
3700
- taskId: z21.string().uuid()
3792
+ taskId: z21.string().uuid(),
3793
+ reason: ApproveAnglesRefusalReason.optional()
3701
3794
  });
3702
3795
  var SaveImageDraftRequest = z21.object({
3703
3796
  customAngle: z21.string().max(480).optional(),
@@ -4054,6 +4147,10 @@ var MVPResult = z24.object({
4054
4147
  videoAngleCount: z24.number().int().min(1).max(3).optional(),
4055
4148
  adCreative: AdCreative.optional(),
4056
4149
  shopifyPageUrl: z24.string().url().optional(),
4150
+ /** What the announcement bar looks like ON THE PUBLISHED landing right now.
4151
+ * The publish dialog prefills from this: without it the checkbox always opens
4152
+ * unchecked, and republishing anything would read as "remove the bar". */
4153
+ shopifyAnnouncementBar: ShopifyAnnouncementBarState.optional(),
4057
4154
  // Pain points and benefits for image wizard pre-filling
4058
4155
  painPoints: z24.string().optional(),
4059
4156
  benefits: z24.string().optional(),
@@ -4179,7 +4276,19 @@ var StatusResponse = z24.object({
4179
4276
  "done",
4180
4277
  "error"
4181
4278
  ]),
4279
+ /** ENGINEERING string (`Task.error`). Never render this to a seller. */
4182
4280
  error: z24.string().optional(),
4281
+ /**
4282
+ * SELLER-FACING failure copy, in Spanish, already decided by the backend —
4283
+ * render it VERBATIM. Present only for `status: 'error'`.
4284
+ *
4285
+ * Clients used to receive only `error` and guess at it with a local regex
4286
+ * table; measured 2026-08-18 over 187 failed tasks in 60 days, that guess
4287
+ * produced contentless copy ("Error desconocido." / "Error interno") for 123
4288
+ * of them. Populated from `Task.userMessage`, falling back to the same
4289
+ * resolver for rows written before that column existed.
4290
+ */
4291
+ userMessage: z24.string().optional(),
4183
4292
  startedAt: z24.string().datetime().optional(),
4184
4293
  updatedAt: z24.string().datetime().optional(),
4185
4294
  stageStatuses: StageStatusSnapshot,
@@ -4190,6 +4299,10 @@ var StatusResponse = z24.object({
4190
4299
  * campaign target geo when creating Meta ads from this task. */
4191
4300
  targetCountry: z24.string().optional(),
4192
4301
  shopifyPageUrl: z24.string().url().optional(),
4302
+ /** What the announcement bar looks like ON THE PUBLISHED landing right now.
4303
+ * The publish dialog prefills from this: without it the checkbox always opens
4304
+ * unchecked, and republishing anything would read as "remove the bar". */
4305
+ shopifyAnnouncementBar: ShopifyAnnouncementBarState.optional(),
4193
4306
  shopifyShops: z24.array(z24.object({
4194
4307
  shopDomain: z24.string(),
4195
4308
  primaryDomain: z24.string().nullable()
@@ -4200,6 +4313,10 @@ var StatusResponse = z24.object({
4200
4313
  requiresAction: z24.boolean().optional(),
4201
4314
  /** True when the task needs the user's attention rather than passive progress. */
4202
4315
  needsReview: z24.boolean().optional(),
4316
+ /** True when an open gate exists that somebody can actually ANSWER. A strictly
4317
+ * narrower fact than `needsReview` — see the identical field on `TaskSummary`
4318
+ * below for why conflating the two produced a call to action that led nowhere. */
4319
+ hasAnswerableGate: z24.boolean().optional(),
4203
4320
  /** Which landing pipeline produced this task's landing. 'code' tasks have no
4204
4321
  * classic LandingImage rows — the result lives in the inline code editor. */
4205
4322
  landingMode: LandingModeSchema.optional(),
@@ -4270,6 +4387,27 @@ var TaskSummary = z24.object({
4270
4387
  /** True when user action is ACTUALLY needed (angle review or script review ready).
4271
4388
  * Use this for status display to show "Revisar" vs "Progreso". */
4272
4389
  needsReview: z24.boolean().optional(),
4390
+ /**
4391
+ * True when an open gate exists that somebody can actually ANSWER.
4392
+ *
4393
+ * DIFFERENT FACT from `needsReview`, and the difference is the bug. `needsReview`
4394
+ * means "this task is waiting on the seller"; the list badge renders «Revisar»
4395
+ * from it. But a task whose dispatch was lost is also "waiting", with no question
4396
+ * open anywhere — and so is the autopilot angle wait, whose resume button is
4397
+ * measurably INERT (`resumeAfterAngles` CASes on a flag that is false there, so
4398
+ * it returns 200 and dispatches nothing). Both render «Revisar» today and both
4399
+ * lead the seller nowhere. The owner spent hours one night convinced the platform
4400
+ * was frozen, chasing a ghost the badge itself invented.
4401
+ *
4402
+ * Derived from the reconciler's gate registry, NOT from a hand-maintained flag:
4403
+ * being a registry entry is what "answerable" means, because every entry declares
4404
+ * a decider and a deadline at compile time. A gate added tomorrow reports itself
4405
+ * here on its own. See `answerableGateIdsFor` in
4406
+ * apps/api/src/modules/cron/gate-reconciler/gate-registry.ts.
4407
+ *
4408
+ * Render «Revisar» on THIS, not on `needsReview`.
4409
+ */
4410
+ hasAnswerableGate: z24.boolean().optional(),
4273
4411
  createdAt: z24.string().datetime().optional(),
4274
4412
  startedAt: z24.string().datetime().optional(),
4275
4413
  updatedAt: z24.string().datetime().optional(),
@@ -4757,8 +4895,20 @@ var AdminUserListResponse = z27.object({
4757
4895
  page: z27.number().int().positive(),
4758
4896
  pageSize: z27.number().int().positive()
4759
4897
  });
4898
+ var AdminCreditGrantReason = z27.enum([
4899
+ "compensation",
4900
+ "goodwill",
4901
+ "promo",
4902
+ "internal-testing",
4903
+ "correction"
4904
+ ]);
4760
4905
  var UpdateUserCreditsRequest = z27.object({
4761
- creditCents: z27.number().nonnegative()
4906
+ creditCents: z27.number().nonnegative(),
4907
+ reason: AdminCreditGrantReason,
4908
+ /** Free-text detail. Required when the enum alone does not explain the grant. */
4909
+ notes: z27.string().trim().min(1).max(500).optional(),
4910
+ /** The task the grant compensates, when there is one. */
4911
+ taskId: z27.string().uuid().optional()
4762
4912
  });
4763
4913
  var QualityReportStatus = z27.enum([
4764
4914
  "open",
@@ -9347,6 +9497,7 @@ function slot(name, required, mcp2, whatsapp = mcp2, source = "user") {
9347
9497
  }
9348
9498
  __name(slot, "slot");
9349
9499
  var SERVER_SELECTED_OR_ARTIFACT_FIELDS = /* @__PURE__ */ new Set([
9500
+ "actionKind",
9350
9501
  "actorId",
9351
9502
  "adAccountId",
9352
9503
  "advertiserId",
@@ -10171,7 +10322,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10171
10322
  ], [
10172
10323
  "meta_publication"
10173
10324
  ]),
10174
- defineCapability("publish_to_shopify", "ec37111aac872e6be22f34c8b547c1e4b58875f798c48e24ca9ca067a0a22d8b", "external_mutation", "publishing", [
10325
+ defineCapability("publish_to_shopify", "6a75ad9bfe04aed24ed41564011ac785657bab12caccd4d78c1de846ce798e82", "external_mutation", "publishing", [
10175
10326
  "external_mutation",
10176
10327
  "state_change"
10177
10328
  ], [
@@ -10191,7 +10342,13 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10191
10342
  slot("publication_target", false, [
10192
10343
  "publicationMode",
10193
10344
  "targetProductId"
10194
- ])
10345
+ ]),
10346
+ // MCP + web only: the WhatsApp publish tool exposes no announcement fields,
10347
+ // and claiming them here breaks the schema-parity invariant.
10348
+ slot("announcement_bar", false, [
10349
+ "announcementBarEnabled",
10350
+ "announcementBarText"
10351
+ ], [])
10195
10352
  ], [
10196
10353
  "product",
10197
10354
  "shop"
@@ -11092,6 +11249,42 @@ var ResearchCategoryEnum = z47.enum([
11092
11249
  "garden",
11093
11250
  "cleaning"
11094
11251
  ]);
11252
+ var READS_INTERNAL = {
11253
+ readOnlyHint: true,
11254
+ destructiveHint: false,
11255
+ idempotentHint: true,
11256
+ openWorldHint: false
11257
+ };
11258
+ var READS_EXTERNAL = {
11259
+ readOnlyHint: true,
11260
+ destructiveHint: false,
11261
+ idempotentHint: true,
11262
+ openWorldHint: true
11263
+ };
11264
+ var CREATES_INTERNAL = {
11265
+ readOnlyHint: false,
11266
+ destructiveHint: false,
11267
+ idempotentHint: false,
11268
+ openWorldHint: false
11269
+ };
11270
+ var CREATES_EXTERNAL = {
11271
+ readOnlyHint: false,
11272
+ destructiveHint: false,
11273
+ idempotentHint: false,
11274
+ openWorldHint: true
11275
+ };
11276
+ var OVERWRITES_INTERNAL = {
11277
+ readOnlyHint: false,
11278
+ destructiveHint: true,
11279
+ idempotentHint: false,
11280
+ openWorldHint: false
11281
+ };
11282
+ var OVERWRITES_EXTERNAL = {
11283
+ readOnlyHint: false,
11284
+ destructiveHint: true,
11285
+ idempotentHint: false,
11286
+ openWorldHint: true
11287
+ };
11095
11288
 
11096
11289
  // src/errors.ts
11097
11290
  var McpErrorCode = {
@@ -11549,6 +11742,18 @@ function translateError(err) {
11549
11742
  }
11550
11743
  __name(translateError, "translateError");
11551
11744
 
11745
+ // src/tools/approve-refusal-message.ts
11746
+ function approveRefusalMessage(reason) {
11747
+ if (reason === "photo_review_pending") {
11748
+ 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.";
11749
+ }
11750
+ if (reason === "photos_not_ready") {
11751
+ 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.";
11752
+ }
11753
+ return "No se inici\xF3 la generaci\xF3n y no se cobr\xF3 nada. Los cambios siguen guardados: vuelve a intentar.";
11754
+ }
11755
+ __name(approveRefusalMessage, "approveRefusalMessage");
11756
+
11552
11757
  // src/asset-proxy.ts
11553
11758
  import { createHmac, timingSafeEqual } from "crypto";
11554
11759
  var TOKEN_TTL_SECONDS = 3600;
@@ -11652,7 +11857,12 @@ async function fetchTaskOutputs(api, taskId, options) {
11652
11857
  return {
11653
11858
  angleKey: vg.angleKey ?? "",
11654
11859
  angleIndex: vg.angleIndex ?? 0,
11655
- variants: variants2.length ? variants2 : fallback
11860
+ variants: variants2.length ? variants2 : fallback,
11861
+ // Only ever true — an honest delivery leaves the key off entirely so no
11862
+ // consumer has to distinguish `false` from "the API is older than this".
11863
+ ...vg.scriptRewritten === true ? {
11864
+ scriptRewritten: true
11865
+ } : {}
11656
11866
  };
11657
11867
  }).filter((v) => v.angleKey && v.variants.length);
11658
11868
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -11715,6 +11925,9 @@ function renderOutputsMarkdown(outputs) {
11715
11925
  for (const variant of v.variants) {
11716
11926
  parts.push(`- [${escapeMdLabel(v.angleKey)} \xB7 ${escapeMdLabel(variant.kind)}](${variant.url})`);
11717
11927
  }
11928
+ if (v.scriptRewritten) {
11929
+ 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.`);
11930
+ }
11718
11931
  }
11719
11932
  }
11720
11933
  return parts.join("\n");
@@ -12021,7 +12234,7 @@ function resolveStatusTaskKind(status) {
12021
12234
  }
12022
12235
  __name(resolveStatusTaskKind, "resolveStatusTaskKind");
12023
12236
  function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
12024
- 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) => {
12237
+ 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) => {
12025
12238
  try {
12026
12239
  if (!input.images && !input.projectId) {
12027
12240
  throw new Error("images es requerido salvo que env\xEDes projectId.");
@@ -12089,7 +12302,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12089
12302
  throw translateError(e);
12090
12303
  }
12091
12304
  });
12092
- 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) => {
12305
+ 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) => {
12093
12306
  try {
12094
12307
  const created = await api.postJson("/v2/ingest", {
12095
12308
  taskType: "service",
@@ -12140,7 +12353,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12140
12353
  throw translateError(e);
12141
12354
  }
12142
12355
  });
12143
- server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSIGUE LLAM\xC1NDOLA hasta que el estado sea terminal (`done` o `error`) o aparezca `pendingUserAction`. Un retorno sin ninguna de esas dos cosas significa que la tarea sigue corriendo y que alcanzaste el timeout, NO que se colg\xF3 ni que termin\xF3: vuelve a llamarla con waitSeconds alto. Si dejas de llamarla, la tarea sigue avanzando sin que nadie la mire y el usuario se queda esperando en silencio.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO esperando una decisi\xF3n del USUARIO: muestra el draft, PREG\xDANTALE y espera su respuesta. Resp\xF3ndela t\xFA mismo con `resolve_pending_action` \xFAnicamente si el usuario ya te deleg\xF3 esa decisi\xF3n \u2014 porque te lo dijo en la conversaci\xF3n, o porque la tarea corre con `aiDecidesAll` / `reviewMode: automatic`. Nunca deduzcas la delegaci\xF3n del contenido del draft: que la propuesta te parezca obvia o correcta no es un permiso. Al responder, repite el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
12356
+ 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) => {
12144
12357
  try {
12145
12358
  const signal = extra?.signal;
12146
12359
  const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
@@ -12200,7 +12413,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12200
12413
  throw translateError(e);
12201
12414
  }
12202
12415
  });
12203
- 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) => {
12416
+ 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) => {
12204
12417
  try {
12205
12418
  const qs = new URLSearchParams();
12206
12419
  if (input.limit != null) qs.set("limit", String(input.limit));
@@ -12213,7 +12426,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12213
12426
  throw translateError(e);
12214
12427
  }
12215
12428
  });
12216
- 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) => {
12429
+ 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) => {
12217
12430
  try {
12218
12431
  const angle = await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/angles/${input.angleIndex}`, input.angle ?? {});
12219
12432
  return asText({
@@ -12223,257 +12436,273 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
12223
12436
  throw translateError(e);
12224
12437
  }
12225
12438
  });
12226
- server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Esta llamada habla EN NOMBRE DEL USUARIO: \xFAsala solo con una respuesta que \xE9l te haya dado, o cuando te haya delegado la decisi\xF3n de forma expl\xEDcita o v\xEDa `aiDecidesAll` / `reviewMode: automatic`. Si nadie te deleg\xF3 nada, pregunta y espera en vez de llamarla. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
12227
- try {
12228
- const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
12229
- const status = StatusForResolveSchema.parse(raw);
12230
- const pending = status.pendingUserAction;
12231
- if (!pending) {
12232
- throw new Error("No hay acci\xF3n pendiente para esta tarea \u2014 llama get_task_status primero para confirmar el estado.");
12233
- }
12234
- if (pending.kind !== input.action.kind) {
12235
- 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.`);
12236
- }
12237
- switch (input.action.kind) {
12238
- case "product_reference_set_approval": {
12239
- 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)) {
12240
- throw new Error("La aprobaci\xF3n de referencias no coincide con el snapshot mostrado; vuelve a llamar get_task_status.");
12241
- }
12242
- if (input.action.decision === "reject" && (!input.action.reason || !input.action.affectedRefKeys?.length || !input.action.recoveryAction)) {
12243
- throw new Error("Una referencia rechazada necesita motivo, referencias afectadas y acci\xF3n de recuperaci\xF3n.");
12244
- }
12245
- if (input.action.decision === "correct_view" && (!input.action.reason || !input.action.affectedRefKeys?.length)) {
12246
- throw new Error("Una correcci\xF3n de \xE1ngulo necesita motivo y referencias afectadas.");
12247
- }
12248
- if (input.action.decision === "correct_view" && input.action.affectedRefKeys?.length !== 1) {
12249
- throw new Error("Corrige una sola vista por intento.");
12250
- }
12251
- if (pending.phase === "preparation" && input.action.decision === "approve" && (!pending.selectedRefKeys || pending.selectedRefKeys.length === 0)) {
12252
- throw new Error("No hay fotos seleccionadas para preparar. Abre la tarea en ProductMaker Web y elige al menos una.");
12253
- }
12254
- const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/resolve-product-reference-set`, {
12255
- ...input.action,
12256
- ...pending.phase === "preparation" && input.action.decision === "approve" && pending.selectedRefKeys ? {
12257
- selectedRefKeys: pending.selectedRefKeys
12258
- } : {}
12259
- });
12260
- return asText(result);
12439
+ server.tool(
12440
+ "resolve_pending_action",
12441
+ "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.",
12442
+ ResolvePendingActionInput.shape,
12443
+ // Approving the gate RELEASES the pipeline into paid generation at the video
12444
+ // and image providers (registry effects: credit_consuming + state_change),
12445
+ // and decision=reject discards the approved reference set. Both halves, hence
12446
+ // destructive AND open-world.
12447
+ OVERWRITES_EXTERNAL,
12448
+ async (input) => {
12449
+ try {
12450
+ const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
12451
+ const status = StatusForResolveSchema.parse(raw);
12452
+ const pending = status.pendingUserAction;
12453
+ if (!pending) {
12454
+ throw new Error("No hay acci\xF3n pendiente para esta tarea \u2014 llama get_task_status primero para confirmar el estado.");
12261
12455
  }
12262
- case "product_reference_confirmation": {
12263
- const candidateGrouping = pending.candidateGrouping;
12264
- const selectedImageUrls = pending.selectedImageUrls ?? [];
12265
- if (pending.mode === "visual_recovery") {
12266
- throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
12267
- }
12268
- if (!candidateGrouping || selectedImageUrls.length === 0) {
12269
- throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
12270
- }
12271
- const userChoice = input.action.userChoice;
12272
- if (!userChoice) {
12273
- throw new Error("La pregunta adaptativa necesita userChoice.");
12274
- }
12275
- const offeredChoices = pending.question?.options.map((option) => option.value) ?? pending.options;
12276
- if (!offeredChoices) {
12277
- throw new Error("Estado inconsistente: falta la pregunta adaptativa con sus opciones verificables. Vuelve a llamar get_task_status.");
12278
- }
12279
- if (!offeredChoices.includes(userChoice)) {
12280
- throw new Error(`La opci\xF3n "${userChoice}" no fue ofrecida en la pregunta pendiente. Elige una opci\xF3n mostrada por get_task_status.`);
12281
- }
12282
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
12283
- productReferenceConfirmation: {
12284
- candidateGrouping,
12285
- userChoice,
12286
- // Server overwrites with its own server-side computation;
12287
- // we send the matching MCP-mirror value so the wire body
12288
- // is internally coherent if the server skips overwrite.
12289
- selectedImageUrlsHash: buildSelectedImageUrlsHash(selectedImageUrls)
12290
- },
12291
- ...input.action.confirmedProductNames ? {
12292
- confirmedProductNames: input.action.confirmedProductNames
12293
- } : {},
12294
- ...pending.decisionKey && pending.referenceEvidenceVersion && pending.expectedEvidenceHash ? {
12295
- decisionKey: pending.decisionKey,
12296
- referenceEvidenceVersion: pending.referenceEvidenceVersion,
12297
- expectedEvidenceHash: pending.expectedEvidenceHash
12298
- } : {},
12299
- // Multi-scene color showcase → upgrade to STANDARD. Gated to the
12300
- // variants choice client-side so the wire body stays coherent; the
12301
- // backend re-gates on variant_set grouping.
12302
- ...resolveVariantConfirmVideoTier(input.action) === "standard" ? {
12303
- videoTier: "standard"
12304
- } : {}
12305
- });
12306
- return asText(r);
12456
+ if (pending.kind !== input.action.kind) {
12457
+ 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.`);
12307
12458
  }
12308
- case "angle_review_approval": {
12309
- const body = {};
12310
- if (pending.customAngleDraft && pending.customAngleDraft.length > 0) {
12311
- body.customAngle = pending.customAngleDraft;
12459
+ switch (input.action.kind) {
12460
+ case "product_reference_set_approval": {
12461
+ 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)) {
12462
+ throw new Error("La aprobaci\xF3n de referencias no coincide con el snapshot mostrado; vuelve a llamar get_task_status.");
12463
+ }
12464
+ if (input.action.decision === "reject" && (!input.action.reason || !input.action.affectedRefKeys?.length || !input.action.recoveryAction)) {
12465
+ throw new Error("Una referencia rechazada necesita motivo, referencias afectadas y acci\xF3n de recuperaci\xF3n.");
12466
+ }
12467
+ if (input.action.decision === "correct_view" && (!input.action.reason || !input.action.affectedRefKeys?.length)) {
12468
+ throw new Error("Una correcci\xF3n de \xE1ngulo necesita motivo y referencias afectadas.");
12469
+ }
12470
+ if (input.action.decision === "correct_view" && input.action.affectedRefKeys?.length !== 1) {
12471
+ throw new Error("Corrige una sola vista por intento.");
12472
+ }
12473
+ if (pending.phase === "preparation" && input.action.decision === "approve" && (!pending.selectedRefKeys || pending.selectedRefKeys.length === 0)) {
12474
+ throw new Error("No hay fotos seleccionadas para preparar. Abre la tarea en ProductMaker Web y elige al menos una.");
12475
+ }
12476
+ const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/resolve-product-reference-set`, {
12477
+ ...input.action,
12478
+ ...pending.phase === "preparation" && input.action.decision === "approve" && pending.selectedRefKeys ? {
12479
+ selectedRefKeys: pending.selectedRefKeys
12480
+ } : {}
12481
+ });
12482
+ return asText(result);
12312
12483
  }
12313
- if (pending.painPointsDraft != null) body.painPoints = pending.painPointsDraft;
12314
- if (pending.benefitsDraft != null) body.benefits = pending.benefitsDraft;
12315
- if (pending.featuresDraft != null) body.features = [
12316
- ...pending.featuresDraft
12317
- ];
12318
- if (input.action.customAngle != null) body.customAngle = input.action.customAngle;
12319
- if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
12320
- if (input.action.benefits != null) body.benefits = input.action.benefits;
12321
- if (input.action.features != null) body.features = input.action.features;
12322
- if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
12323
- throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
12484
+ case "product_reference_confirmation": {
12485
+ const candidateGrouping = pending.candidateGrouping;
12486
+ const selectedImageUrls = pending.selectedImageUrls ?? [];
12487
+ if (pending.mode === "visual_recovery") {
12488
+ throw new Error("Esta recuperaci\xF3n necesita subir fotos con un ticket ligado al usuario. \xC1brela en ProductMaker Web; MCP no acepta URLs arbitrarias.");
12489
+ }
12490
+ if (!candidateGrouping || selectedImageUrls.length === 0) {
12491
+ throw new Error("Estado inconsistente: la tarea est\xE1 pausada pero falta candidateGrouping o selectedImageUrls.");
12492
+ }
12493
+ const userChoice = input.action.userChoice;
12494
+ if (!userChoice) {
12495
+ throw new Error("La pregunta adaptativa necesita userChoice.");
12496
+ }
12497
+ const offeredChoices = pending.question?.options.map((option) => option.value) ?? pending.options;
12498
+ if (!offeredChoices) {
12499
+ throw new Error("Estado inconsistente: falta la pregunta adaptativa con sus opciones verificables. Vuelve a llamar get_task_status.");
12500
+ }
12501
+ if (!offeredChoices.includes(userChoice)) {
12502
+ throw new Error(`La opci\xF3n "${userChoice}" no fue ofrecida en la pregunta pendiente. Elige una opci\xF3n mostrada por get_task_status.`);
12503
+ }
12504
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/confirm-product-references`, {
12505
+ productReferenceConfirmation: {
12506
+ candidateGrouping,
12507
+ userChoice,
12508
+ // Server overwrites with its own server-side computation;
12509
+ // we send the matching MCP-mirror value so the wire body
12510
+ // is internally coherent if the server skips overwrite.
12511
+ selectedImageUrlsHash: buildSelectedImageUrlsHash(selectedImageUrls)
12512
+ },
12513
+ ...input.action.confirmedProductNames ? {
12514
+ confirmedProductNames: input.action.confirmedProductNames
12515
+ } : {},
12516
+ ...pending.decisionKey && pending.referenceEvidenceVersion && pending.expectedEvidenceHash ? {
12517
+ decisionKey: pending.decisionKey,
12518
+ referenceEvidenceVersion: pending.referenceEvidenceVersion,
12519
+ expectedEvidenceHash: pending.expectedEvidenceHash
12520
+ } : {},
12521
+ // Multi-scene color showcase → upgrade to STANDARD. Gated to the
12522
+ // variants choice client-side so the wire body stays coherent; the
12523
+ // backend re-gates on variant_set grouping.
12524
+ ...resolveVariantConfirmVideoTier(input.action) === "standard" ? {
12525
+ videoTier: "standard"
12526
+ } : {}
12527
+ });
12528
+ return asText(r);
12324
12529
  }
12325
- if (input.action.gapDurationSeconds === 30) {
12326
- const choice = pending.gapDurationChoice;
12327
- if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
12328
- if (choice.recommendedSec !== 30) {
12329
- throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
12530
+ case "angle_review_approval": {
12531
+ const body = {};
12532
+ if (pending.customAngleDraft && pending.customAngleDraft.length > 0) {
12533
+ body.customAngle = pending.customAngleDraft;
12330
12534
  }
12331
- body.angleVideoOptions = [
12332
- {
12333
- angleIndex: choice.angleIndex,
12334
- ugcFormat: "double_30s"
12335
- }
12535
+ if (pending.painPointsDraft != null) body.painPoints = pending.painPointsDraft;
12536
+ if (pending.benefitsDraft != null) body.benefits = pending.benefitsDraft;
12537
+ if (pending.featuresDraft != null) body.features = [
12538
+ ...pending.featuresDraft
12336
12539
  ];
12337
- }
12338
- const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
12339
- const resolvedTaskKind = resolveStatusTaskKind(status);
12340
- const isVideoTask = resolvedTaskKind === "video";
12341
- const isAutopilotTask = resolvedTaskKind === "autopilot";
12342
- if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
12343
- const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
12344
- const optionsByIndex = new Map(existingOptions.map((option) => [
12345
- option.angleIndex,
12346
- {
12347
- ...option
12540
+ if (input.action.customAngle != null) body.customAngle = input.action.customAngle;
12541
+ if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
12542
+ if (input.action.benefits != null) body.benefits = input.action.benefits;
12543
+ if (input.action.features != null) body.features = input.action.features;
12544
+ if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
12545
+ throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
12546
+ }
12547
+ if (input.action.gapDurationSeconds === 30) {
12548
+ const choice = pending.gapDurationChoice;
12549
+ if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
12550
+ if (choice.recommendedSec !== 30) {
12551
+ throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
12348
12552
  }
12349
- ]));
12350
- for (const angleIndex of selectedAngleIndices) {
12351
- optionsByIndex.set(angleIndex, {
12352
- ...optionsByIndex.get(angleIndex) ?? {
12353
- angleIndex
12553
+ body.angleVideoOptions = [
12554
+ {
12555
+ angleIndex: choice.angleIndex,
12556
+ ugcFormat: "double_30s"
12557
+ }
12558
+ ];
12559
+ }
12560
+ const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
12561
+ const resolvedTaskKind = resolveStatusTaskKind(status);
12562
+ const isVideoTask = resolvedTaskKind === "video";
12563
+ const isAutopilotTask = resolvedTaskKind === "autopilot";
12564
+ if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
12565
+ const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
12566
+ const optionsByIndex = new Map(existingOptions.map((option) => [
12567
+ option.angleIndex,
12568
+ {
12569
+ ...option
12570
+ }
12571
+ ]));
12572
+ for (const angleIndex of selectedAngleIndices) {
12573
+ optionsByIndex.set(angleIndex, {
12574
+ ...optionsByIndex.get(angleIndex) ?? {
12575
+ angleIndex
12576
+ },
12577
+ angle: body.customAngle
12578
+ });
12579
+ }
12580
+ body.angleVideoOptions = [
12581
+ ...optionsByIndex.values()
12582
+ ].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
12583
+ }
12584
+ if (isVideoTask) {
12585
+ body.selectedAngleIndices = selectedAngleIndices;
12586
+ const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
12587
+ return asText(r2);
12588
+ }
12589
+ if (isAutopilotTask) {
12590
+ const resumeBody = {
12591
+ selectedAngleIndices,
12592
+ ...status.ugcFormat ? {
12593
+ ugcFormat: status.ugcFormat
12594
+ } : {
12595
+ ugcFormat: "single_15s"
12354
12596
  },
12355
- angle: body.customAngle
12356
- });
12597
+ ...status.videoStyle ? {
12598
+ videoStyle: status.videoStyle
12599
+ } : {},
12600
+ ...status.videoModel ? {
12601
+ videoModel: status.videoModel
12602
+ } : {},
12603
+ ...status.videoTier ? {
12604
+ videoTier: status.videoTier
12605
+ } : {},
12606
+ ...status.videoResolution ? {
12607
+ videoResolution: status.videoResolution
12608
+ } : {},
12609
+ ...status.narrativeStyle ? {
12610
+ narrativeStyle: status.narrativeStyle
12611
+ } : {},
12612
+ ...body.customAngle != null ? {
12613
+ customAngleOverride: body.customAngle
12614
+ } : {},
12615
+ ...body.painPoints != null ? {
12616
+ painPoints: body.painPoints
12617
+ } : {},
12618
+ ...body.benefits != null ? {
12619
+ benefits: body.benefits
12620
+ } : {},
12621
+ ...body.angleVideoOptions ? {
12622
+ angleVideoOptions: body.angleVideoOptions
12623
+ } : {}
12624
+ };
12625
+ if (body.features !== void 0) {
12626
+ await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
12627
+ features: body.features
12628
+ });
12629
+ }
12630
+ const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
12631
+ if (r2?.success === false) {
12632
+ throw new Error(approveRefusalMessage(r2.reason));
12633
+ }
12634
+ return asText(r2);
12357
12635
  }
12358
- body.angleVideoOptions = [
12359
- ...optionsByIndex.values()
12360
- ].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
12361
- }
12362
- if (isVideoTask) {
12363
- body.selectedAngleIndices = selectedAngleIndices;
12364
- const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
12365
- return asText(r2);
12366
- }
12367
- if (isAutopilotTask) {
12368
- const resumeBody = {
12369
- selectedAngleIndices,
12370
- ...status.ugcFormat ? {
12371
- ugcFormat: status.ugcFormat
12372
- } : {
12373
- ugcFormat: "single_15s"
12374
- },
12375
- ...status.videoStyle ? {
12376
- videoStyle: status.videoStyle
12377
- } : {},
12378
- ...status.videoModel ? {
12379
- videoModel: status.videoModel
12380
- } : {},
12381
- ...status.videoTier ? {
12382
- videoTier: status.videoTier
12383
- } : {},
12384
- ...status.videoResolution ? {
12385
- videoResolution: status.videoResolution
12386
- } : {},
12387
- ...status.narrativeStyle ? {
12388
- narrativeStyle: status.narrativeStyle
12389
- } : {},
12636
+ const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
12637
+ const imageBody = status.kind === "image" ? {
12390
12638
  ...body.customAngle != null ? {
12391
- customAngleOverride: body.customAngle
12639
+ customAngle: body.customAngle
12392
12640
  } : {},
12393
12641
  ...body.painPoints != null ? {
12394
12642
  painPoints: body.painPoints
12395
12643
  } : {},
12396
- ...body.benefits != null ? {
12397
- benefits: body.benefits
12398
- } : {},
12399
- ...body.angleVideoOptions ? {
12400
- angleVideoOptions: body.angleVideoOptions
12401
- } : {}
12402
- };
12403
- if (body.features !== void 0) {
12404
- await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
12644
+ ...body.features != null ? {
12405
12645
  features: body.features
12406
- });
12646
+ } : {}
12647
+ } : body;
12648
+ const r = await api.postJson(route, imageBody);
12649
+ if (r?.success === false) {
12650
+ throw new Error(approveRefusalMessage(r.reason));
12407
12651
  }
12408
- const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
12409
- return asText(r2);
12652
+ return asText(r);
12410
12653
  }
12411
- const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
12412
- const imageBody = status.kind === "image" ? {
12413
- ...body.customAngle != null ? {
12414
- customAngle: body.customAngle
12415
- } : {},
12416
- ...body.painPoints != null ? {
12417
- painPoints: body.painPoints
12418
- } : {},
12419
- ...body.features != null ? {
12420
- features: body.features
12421
- } : {}
12422
- } : body;
12423
- const r = await api.postJson(route, imageBody);
12424
- return asText(r);
12425
- }
12426
- case "service_angle_approval": {
12427
- const action = input.action;
12428
- const candidate = pending.angles?.find((angle) => angle.id === action.angleId);
12429
- if (!candidate) {
12430
- throw new Error("El angleId no pertenece a los conceptos pendientes de esta tarea.");
12654
+ case "service_angle_approval": {
12655
+ const action = input.action;
12656
+ const candidate = pending.angles?.find((angle) => angle.id === action.angleId);
12657
+ if (!candidate) {
12658
+ throw new Error("El angleId no pertenece a los conceptos pendientes de esta tarea.");
12659
+ }
12660
+ const edits = {
12661
+ content: action.content ?? candidate.content,
12662
+ creativeDirection: action.creativeDirection ?? candidate.creativeDirection,
12663
+ actor: action.actor ?? candidate.actor,
12664
+ // Echo the persisted saved actor. Omitted when absent so the PATCH
12665
+ // never writes a null over a selection made elsewhere — the
12666
+ // backend only updates the keys it receives.
12667
+ ...candidate.actorId ? {
12668
+ actorId: candidate.actorId
12669
+ } : {}
12670
+ };
12671
+ await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/${encodeURIComponent(candidate.id)}`, edits);
12672
+ const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/select`, {
12673
+ angleIds: [
12674
+ candidate.id
12675
+ ]
12676
+ });
12677
+ return asText(result);
12431
12678
  }
12432
- const edits = {
12433
- content: action.content ?? candidate.content,
12434
- creativeDirection: action.creativeDirection ?? candidate.creativeDirection,
12435
- actor: action.actor ?? candidate.actor,
12436
- // Echo the persisted saved actor. Omitted when absent so the PATCH
12437
- // never writes a null over a selection made elsewhere — the
12438
- // backend only updates the keys it receives.
12439
- ...candidate.actorId ? {
12440
- actorId: candidate.actorId
12441
- } : {}
12442
- };
12443
- await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/${encodeURIComponent(candidate.id)}`, edits);
12444
- const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-angles/select`, {
12445
- angleIds: [
12446
- candidate.id
12447
- ]
12448
- });
12449
- return asText(result);
12450
- }
12451
- case "service_script_approval": {
12452
- const panels = pending.panels ?? [];
12453
- if (panels.length === 0) {
12454
- throw new Error("El storyboard pendiente no contiene paneles aprobables.");
12679
+ case "service_script_approval": {
12680
+ const panels = pending.panels ?? [];
12681
+ if (panels.length === 0) {
12682
+ throw new Error("El storyboard pendiente no contiene paneles aprobables.");
12683
+ }
12684
+ const voiceovers = {};
12685
+ const visualDirections = {};
12686
+ for (const panel of panels) {
12687
+ voiceovers[panel.beatId] = input.action.voiceovers?.[panel.beatId] ?? panel.voiceover;
12688
+ visualDirections[panel.beatId] = input.action.visualDirections?.[panel.beatId] ?? panel.visualDirection;
12689
+ }
12690
+ const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-script/approve`, {
12691
+ voiceovers,
12692
+ visualDirections
12693
+ });
12694
+ return asText(result);
12455
12695
  }
12456
- const voiceovers = {};
12457
- const visualDirections = {};
12458
- for (const panel of panels) {
12459
- voiceovers[panel.beatId] = input.action.voiceovers?.[panel.beatId] ?? panel.voiceover;
12460
- visualDirections[panel.beatId] = input.action.visualDirections?.[panel.beatId] ?? panel.visualDirection;
12696
+ default: {
12697
+ const _exhaustive = input.action;
12698
+ throw new Error(`Unsupported pending action kind: ${String(_exhaustive.kind)}`);
12461
12699
  }
12462
- const result = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-script/approve`, {
12463
- voiceovers,
12464
- visualDirections
12465
- });
12466
- return asText(result);
12467
- }
12468
- default: {
12469
- const _exhaustive = input.action;
12470
- throw new Error(`Unsupported pending action kind: ${String(_exhaustive.kind)}`);
12471
12700
  }
12701
+ } catch (e) {
12702
+ throw translateError(e);
12472
12703
  }
12473
- } catch (e) {
12474
- throw translateError(e);
12475
12704
  }
12476
- });
12705
+ );
12477
12706
  }
12478
12707
  __name(registerTaskTools, "registerTaskTools");
12479
12708
 
@@ -12627,7 +12856,7 @@ var LandingInput = z49.object({
12627
12856
  subheadline: CreativeRefs.subheadline
12628
12857
  });
12629
12858
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
12630
- 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) => {
12859
+ 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) => {
12631
12860
  try {
12632
12861
  const allowedDurations = FREE_VIDEO_DURATIONS_BY_QUALITY[input.quality];
12633
12862
  if (!allowedDurations.includes(input.durationSeconds)) {
@@ -12657,7 +12886,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
12657
12886
  throw translateError(e);
12658
12887
  }
12659
12888
  });
12660
- 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) => {
12889
+ 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) => {
12661
12890
  try {
12662
12891
  const resolved = resolveVideoPreset(input.videoPreset);
12663
12892
  const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
@@ -12682,7 +12911,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
12682
12911
  throw translateError(e);
12683
12912
  }
12684
12913
  });
12685
- 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) => {
12914
+ 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) => {
12686
12915
  try {
12687
12916
  const { projectId, ...rest } = input;
12688
12917
  return await submitCreative(api, {
@@ -12700,7 +12929,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
12700
12929
  throw translateError(e);
12701
12930
  }
12702
12931
  });
12703
- 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) => {
12932
+ 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) => {
12704
12933
  try {
12705
12934
  const r = await api.postJson(`/v1/creatives/images/${encodeURIComponent(input.taskId)}/regenerate`, {
12706
12935
  comment: input.instruction,
@@ -12711,7 +12940,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
12711
12940
  throw translateError(e);
12712
12941
  }
12713
12942
  });
12714
- 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) => {
12943
+ 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) => {
12715
12944
  try {
12716
12945
  const { projectId, ...rest } = input;
12717
12946
  return await submitCreative(api, {
@@ -12961,7 +13190,7 @@ function estimatePayload(input) {
12961
13190
  }
12962
13191
  __name(estimatePayload, "estimatePayload");
12963
13192
  function registerCreativeVariantTools(server, api) {
12964
- 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) => {
13193
+ 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) => {
12965
13194
  try {
12966
13195
  const validatedInput = parseVariantInput(EstimateCreativeVariantInput, input);
12967
13196
  const estimate = await api.postJson(`/v1/tasks/${encodeURIComponent(validatedInput.sourceTaskId)}/creative-variants/estimate`, estimatePayload(validatedInput));
@@ -12974,7 +13203,7 @@ function registerCreativeVariantTools(server, api) {
12974
13203
  throw translateError(error);
12975
13204
  }
12976
13205
  });
12977
- 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) => {
13206
+ 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) => {
12978
13207
  try {
12979
13208
  const validatedInput = parseVariantInput(CreateCreativeVariantInput, input);
12980
13209
  const estimateInput = EstimateCreativeVariantInput.parse({
@@ -13068,7 +13297,7 @@ var RegenerateVideoScriptInput = z51.object({
13068
13297
  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).')
13069
13298
  }).strict();
13070
13299
  function registerScriptReviewTools(server, api) {
13071
- 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) => {
13300
+ 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) => {
13072
13301
  try {
13073
13302
  const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
13074
13303
  const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
@@ -13077,7 +13306,7 @@ function registerScriptReviewTools(server, api) {
13077
13306
  throw translateError(e);
13078
13307
  }
13079
13308
  });
13080
- 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) => {
13309
+ 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) => {
13081
13310
  try {
13082
13311
  const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/regenerate`, {
13083
13312
  angleKey: input.angleKey,
@@ -13088,25 +13317,36 @@ function registerScriptReviewTools(server, api) {
13088
13317
  throw translateError(e);
13089
13318
  }
13090
13319
  });
13091
- 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) => {
13092
- try {
13093
- const body = {
13094
- scenes: input.scenes
13095
- };
13096
- if (input.angleKey) body.angleKey = input.angleKey;
13097
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
13098
- return asText(r);
13099
- } catch (e) {
13100
- throw translateError(e);
13320
+ server.tool(
13321
+ "approve_video_script",
13322
+ '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.',
13323
+ ApproveVideoScriptInput.shape,
13324
+ // Not CREATES_INTERNAL: approving the script is what STARTS the render, so
13325
+ // this call dispatches jobs to the video providers and burns credits. Its own
13326
+ // description says so ("Tras llamarlo, el render arranca y consume créditos").
13327
+ // A host that decides when to interrupt the user by `destructiveHint ||
13328
+ // openWorldHint` would otherwise auto-invoke the one call that spends money.
13329
+ CREATES_EXTERNAL,
13330
+ async (input) => {
13331
+ try {
13332
+ const body = {
13333
+ scenes: input.scenes
13334
+ };
13335
+ if (input.angleKey) body.angleKey = input.angleKey;
13336
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script/approve`, body);
13337
+ return asText(r);
13338
+ } catch (e) {
13339
+ throw translateError(e);
13340
+ }
13101
13341
  }
13102
- });
13342
+ );
13103
13343
  }
13104
13344
  __name(registerScriptReviewTools, "registerScriptReviewTools");
13105
13345
 
13106
13346
  // src/tools/connections.ts
13107
13347
  import { z as z52 } from "zod";
13108
13348
  function registerConnectionTools(server, api) {
13109
- 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.", z52.object({}).shape, async () => {
13349
+ 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.", z52.object({}).shape, READS_INTERNAL, async () => {
13110
13350
  try {
13111
13351
  const r = await api.getJson("/v1/shopify/shops");
13112
13352
  return asText(r);
@@ -13118,7 +13358,7 @@ function registerConnectionTools(server, api) {
13118
13358
  shopDomain: z52.string().min(3).max(120),
13119
13359
  query: z52.string().trim().max(120).optional(),
13120
13360
  taskId: z52.string().uuid().optional()
13121
- }).shape, async ({ shopDomain, query, taskId }) => {
13361
+ }).shape, READS_EXTERNAL, async ({ shopDomain, query, taskId }) => {
13122
13362
  try {
13123
13363
  const params = new URLSearchParams({
13124
13364
  shopDomain
@@ -13131,7 +13371,7 @@ function registerConnectionTools(server, api) {
13131
13371
  throw translateError(e);
13132
13372
  }
13133
13373
  });
13134
- 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.", z52.object({}).shape, async () => {
13374
+ 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.", z52.object({}).shape, READS_EXTERNAL, async () => {
13135
13375
  try {
13136
13376
  const r = await api.getJson("/v1/facebook/ad-accounts");
13137
13377
  return asText(r);
@@ -13142,6 +13382,7 @@ function registerConnectionTools(server, api) {
13142
13382
  server.registerTool("list_meta_destinations", {
13143
13383
  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.",
13144
13384
  inputSchema: z52.object({}).shape,
13385
+ annotations: READS_EXTERNAL,
13145
13386
  _meta: {
13146
13387
  "openai/widgetAccessible": true
13147
13388
  }
@@ -13153,7 +13394,7 @@ function registerConnectionTools(server, api) {
13153
13394
  throw translateError(e);
13154
13395
  }
13155
13396
  });
13156
- 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.", z52.object({}).shape, async () => {
13397
+ 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.", z52.object({}).shape, READS_EXTERNAL, async () => {
13157
13398
  try {
13158
13399
  const r = await api.getJson("/v1/tiktok/advertisers");
13159
13400
  return asText(r);
@@ -13167,7 +13408,7 @@ __name(registerConnectionTools, "registerConnectionTools");
13167
13408
  // src/tools/actors.ts
13168
13409
  import { z as z53 } from "zod";
13169
13410
  function registerActorTools(server, api) {
13170
- 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.", z53.object({}).shape, async () => {
13411
+ 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.", z53.object({}).shape, READS_INTERNAL, async () => {
13171
13412
  try {
13172
13413
  const rows = await api.getJson("/v1/actors");
13173
13414
  const actors = (Array.isArray(rows) ? rows : []).filter((actor) => actor.status === "ready").map(({ id, name, description, gender, voiceId }) => ({
@@ -13212,8 +13453,37 @@ var ShopifyInput = z54.object({
13212
13453
  ]).optional(),
13213
13454
  /** Shopify product GID, numeric id, or admin product URL. Required when
13214
13455
  * publicationMode is `existing`. */
13215
- targetProductId: z54.string().trim().min(1).max(300).optional()
13456
+ targetProductId: z54.string().trim().min(1).max(300).optional(),
13457
+ /** Add an announcement bar text above add-to-cart (optional). */
13458
+ announcementBarEnabled: z54.boolean().optional(),
13459
+ announcementBarText: z54.string().trim().min(1).max(SHOPIFY_ANNOUNCEMENT_BAR_MAX_LENGTH).optional()
13216
13460
  });
13461
+ var ShopifyInputWithAnnouncementBarValidation = ShopifyInput.superRefine((value, ctx) => {
13462
+ if (value.announcementBarEnabled && !value.announcementBarText) {
13463
+ ctx.addIssue({
13464
+ code: z54.ZodIssueCode.custom,
13465
+ path: [
13466
+ "announcementBarText"
13467
+ ],
13468
+ message: "announcementBarText is required when announcementBarEnabled is true"
13469
+ });
13470
+ }
13471
+ if (!value.announcementBarEnabled && value.announcementBarText) {
13472
+ ctx.addIssue({
13473
+ code: z54.ZodIssueCode.custom,
13474
+ path: [
13475
+ "announcementBarText"
13476
+ ],
13477
+ message: "announcementBarText requires announcementBarEnabled to be true"
13478
+ });
13479
+ }
13480
+ });
13481
+ function parseShopifyInput(rawInput) {
13482
+ const parsed = ShopifyInputWithAnnouncementBarValidation.safeParse(rawInput);
13483
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
13484
+ return parsed.data;
13485
+ }
13486
+ __name(parseShopifyInput, "parseShopifyInput");
13217
13487
  var McpCampaignConfig = z54.object({
13218
13488
  /** ALWAYS in US dollars, whatever the ad account's currency is. ProductMaker
13219
13489
  * converts to the account currency server-side. Do NOT pre-convert: on a COP
@@ -13449,9 +13719,10 @@ var TikTokInput = z54.object({
13449
13719
  });
13450
13720
  function registerPublishTools(server, api, opts = {}) {
13451
13721
  const transport = opts.transport ?? "http";
13452
- 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) => {
13722
+ 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) => {
13453
13723
  try {
13454
- if (input.publicationMode === "existing" && !input.targetProductId) {
13724
+ const parsed = parseShopifyInput(input);
13725
+ if (parsed.publicationMode === "existing" && !parsed.targetProductId) {
13455
13726
  return {
13456
13727
  isError: true,
13457
13728
  content: [
@@ -13462,29 +13733,31 @@ function registerPublishTools(server, api, opts = {}) {
13462
13733
  ]
13463
13734
  };
13464
13735
  }
13465
- const variantIndex = input.variantIndex ?? 0;
13466
- const availableImages = await fetchLandingImageCount(api, input.taskId);
13736
+ const variantIndex = parsed.variantIndex ?? 0;
13737
+ const availableImages = await fetchLandingImageCount(api, parsed.taskId);
13467
13738
  const effectiveAvailable = availableImages > 0 ? availableImages : 3;
13468
- const numImages = Math.min(input.numLandingImages ?? effectiveAvailable, effectiveAvailable);
13739
+ const numImages = Math.min(parsed.numLandingImages ?? effectiveAvailable, effectiveAvailable);
13469
13740
  const selectedLandingImages = Array.from({
13470
13741
  length: numImages
13471
13742
  }, (_, i) => ({
13472
13743
  imageIndex: i,
13473
13744
  variantIndex
13474
13745
  }));
13475
- const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/publish/shopify`, {
13476
- shopDomain: input.shopDomain,
13746
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(parsed.taskId)}/publish/shopify`, {
13747
+ shopDomain: parsed.shopDomain,
13477
13748
  selectedLandingImages,
13478
- showVariantPicker: input.showVariantPicker,
13479
- publicationMode: input.publicationMode,
13480
- targetProductId: input.targetProductId
13749
+ showVariantPicker: parsed.showVariantPicker,
13750
+ publicationMode: parsed.publicationMode,
13751
+ targetProductId: parsed.targetProductId,
13752
+ announcementBarEnabled: parsed.announcementBarEnabled,
13753
+ announcementBarText: parsed.announcementBarText
13481
13754
  });
13482
13755
  return renderShopifyPublishMarkdown(r);
13483
13756
  } catch (e) {
13484
13757
  throw translateError(e);
13485
13758
  }
13486
13759
  });
13487
- 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 }) => {
13760
+ 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 }) => {
13488
13761
  try {
13489
13762
  const body = {
13490
13763
  ...rest
@@ -13512,7 +13785,7 @@ function registerPublishTools(server, api, opts = {}) {
13512
13785
  throw translateError(e);
13513
13786
  }
13514
13787
  });
13515
- 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) => {
13788
+ 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) => {
13516
13789
  try {
13517
13790
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
13518
13791
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -13602,7 +13875,7 @@ function registerPublishTools(server, api, opts = {}) {
13602
13875
  throw translateError(e);
13603
13876
  }
13604
13877
  });
13605
- 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 }) => {
13878
+ 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 }) => {
13606
13879
  try {
13607
13880
  const body = {
13608
13881
  ...rest
@@ -13828,6 +14101,7 @@ function registerMediaUploadTools(server, api) {
13828
14101
  title: "Subir archivos",
13829
14102
  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.",
13830
14103
  inputSchema: UploadSessionToolInputShape,
14104
+ annotations: CREATES_INTERNAL,
13831
14105
  outputSchema: UniversalUploadStructuredResponse.shape,
13832
14106
  _meta: {
13833
14107
  ui: {
@@ -13868,6 +14142,7 @@ function registerMediaUploadTools(server, api) {
13868
14142
  server.registerTool("check_media_upload", {
13869
14143
  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.",
13870
14144
  inputSchema: UploadStatusInput.shape,
14145
+ annotations: READS_INTERNAL,
13871
14146
  _meta: {
13872
14147
  "openai/widgetAccessible": true
13873
14148
  }
@@ -13892,7 +14167,7 @@ var TikTokStatusInput = z56.object({
13892
14167
  advertiserId: z56.string().min(1).describe("The same advertiserId used to publish the campaign.")
13893
14168
  });
13894
14169
  function registerCampaignTools(server, api) {
13895
- 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) => {
14170
+ 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) => {
13896
14171
  try {
13897
14172
  const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
13898
14173
  return renderMetaStatusMarkdown(r);
@@ -13900,7 +14175,7 @@ function registerCampaignTools(server, api) {
13900
14175
  throw translateError(e);
13901
14176
  }
13902
14177
  });
13903
- 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) => {
14178
+ 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) => {
13904
14179
  try {
13905
14180
  const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
13906
14181
  return renderTikTokStatusMarkdown(r);
@@ -14353,7 +14628,7 @@ var ProductCostingToolInput = z59.object({
14353
14628
  desiredMarginPercent: optionalPercent("Margen de ganancia deseado. Ejemplo: 10 significa 10%. Omite para usar el valor gu\xEDa editable del pa\xEDs.")
14354
14629
  });
14355
14630
  function registerProductCostingTools(server) {
14356
- 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) => {
14631
+ 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) => {
14357
14632
  if (input.country !== void 0 && input.country !== "CO" && [
14358
14633
  input.supplierCostCop,
14359
14634
  input.shippingCostCop,
@@ -14625,7 +14900,7 @@ async function pollUntilTerminal(api, runId, waitSeconds) {
14625
14900
  }
14626
14901
  __name(pollUntilTerminal, "pollUntilTerminal");
14627
14902
  function registerProductResearchTools(server, api) {
14628
- 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) => {
14903
+ 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) => {
14629
14904
  const parsed = FindInput.parse(input);
14630
14905
  if (!parsed.confirmed) {
14631
14906
  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.`);
@@ -14659,7 +14934,7 @@ _Esta llamada no cobr\xF3 cr\xE9ditos: se reutiliz\xF3 una investigaci\xF3n reci
14659
14934
  throw translateError(error);
14660
14935
  }
14661
14936
  });
14662
- 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) => {
14937
+ 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) => {
14663
14938
  const parsed = GetInput.parse(input);
14664
14939
  try {
14665
14940
  const run = await api.getJson(`/v1/product-research/runs/${parsed.runId}`);
@@ -14679,7 +14954,7 @@ _Esta llamada no cobr\xF3 cr\xE9ditos: se reutiliz\xF3 una investigaci\xF3n reci
14679
14954
  throw translateError(error);
14680
14955
  }
14681
14956
  });
14682
- 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) => {
14957
+ 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) => {
14683
14958
  const parsed = ListInput.parse(input);
14684
14959
  try {
14685
14960
  const runs = await api.getJson(`/v1/product-research/runs?limit=${parsed.limit}`);
@@ -14786,7 +15061,7 @@ var CostProjectInput = z61.object({
14786
15061
  var GuideInput = z61.object({
14787
15062
  projectId: ProjectId.optional()
14788
15063
  }).strict();
14789
- var DropshippingProjectNextAction = z61.discriminatedUnion("kind", [
15064
+ var NEXT_ACTION_KINDS = [
14790
15065
  "cost_product",
14791
15066
  "provide_product_media",
14792
15067
  "generate_creatives",
@@ -14800,7 +15075,8 @@ var DropshippingProjectNextAction = z61.discriminatedUnion("kind", [
14800
15075
  "create_meta_campaign",
14801
15076
  "repair_meta_campaign",
14802
15077
  "review_paused_campaign"
14803
- ].map((kind) => z61.object({
15078
+ ];
15079
+ var DropshippingProjectNextAction = z61.discriminatedUnion("kind", NEXT_ACTION_KINDS.map((kind) => z61.object({
14804
15080
  kind: z61.literal(kind),
14805
15081
  href: z61.string().min(1)
14806
15082
  })));
@@ -14874,9 +15150,25 @@ var ProjectResponseSchema = z61.object({
14874
15150
  createdAt: z61.string().datetime(),
14875
15151
  updatedAt: z61.string().datetime()
14876
15152
  }).passthrough();
15153
+ var NEXT_ACTION_GUIDANCE = {
15154
+ cost_product: "Llama `cost_dropshipping_project` con el costo real del proveedor. Nunca lo inventes: p\xEDdeselo al usuario.",
15155
+ 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.",
15156
+ 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.",
15157
+ 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.",
15158
+ repair_creatives: "La tarea fall\xF3. Llama `get_task_status` para leer el error y expl\xEDcaselo al usuario antes de reintentar nada.",
15159
+ 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`.",
15160
+ connect_shopify: "Llama `list_shopify_shops`. Si no hay ninguna tienda conectada, el usuario debe conectarla en la web: d\xE1selo como paso manual.",
15161
+ publish_storefront: "Llama `publish_to_shopify` con el taskId y la tienda elegida.",
15162
+ wait_for_storefront: "La publicaci\xF3n en Shopify est\xE1 en curso. Llama `get_task_status` y espera; no publiques otra vez.",
15163
+ connect_meta: "Llama `list_meta_destinations`. Si no hay cuentas, el usuario debe conectar Meta en la web: d\xE1selo como paso manual.",
15164
+ 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.",
15165
+ 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.",
15166
+ 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."
15167
+ };
14877
15168
  function projectText(raw) {
14878
15169
  const project = ProjectResponseSchema.parse(raw);
14879
- const next = project.nextAction ? `${project.nextAction.kind} (${project.nextAction.href})` : "ninguna";
15170
+ const kind = project.nextAction?.kind;
15171
+ const guidance = kind ? NEXT_ACTION_GUIDANCE[kind] : void 0;
14880
15172
  return {
14881
15173
  content: [
14882
15174
  {
@@ -14886,6 +15178,9 @@ function projectText(raw) {
14886
15178
  productName: project.productName,
14887
15179
  phase: project.phase,
14888
15180
  nextAction: project.nextAction,
15181
+ // The href is a web anchor and is useless to an agent; the guidance
15182
+ // is the executable form of the same step.
15183
+ nextActionGuidance: guidance ?? null,
14889
15184
  hasReferenceImage: project.hasReferenceImage,
14890
15185
  costingSaved: Boolean(project.costingResult),
14891
15186
  rewardProgress: {
@@ -14894,7 +15189,7 @@ function projectText(raw) {
14894
15189
  milestones: project.rewards.milestones
14895
15190
  },
14896
15191
  version: project.version,
14897
- message: `Siguiente paso: ${next}. Relee este proyecto despu\xE9s de cada mutaci\xF3n.`
15192
+ message: guidance ? `Siguiente paso: ${guidance} Relee este proyecto despu\xE9s de cada mutaci\xF3n.` : "El proyecto no tiene ning\xFAn paso pendiente."
14898
15193
  })
14899
15194
  }
14900
15195
  ]
@@ -14902,28 +15197,38 @@ function projectText(raw) {
14902
15197
  }
14903
15198
  __name(projectText, "projectText");
14904
15199
  function registerDropshippingProjectTools(server, api) {
14905
- 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) => {
15200
+ 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) => {
14906
15201
  try {
14907
15202
  return projectText(await api.postJson("/v1/dropshipping-projects", input));
14908
15203
  } catch (error) {
14909
15204
  throw translateError(error);
14910
15205
  }
14911
15206
  });
14912
- 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) => {
14913
- try {
14914
- if (input.returnRate + input.cancellationRate >= 1) {
14915
- throw new Error("returnRate + cancellationRate debe ser menor que 1.");
15207
+ server.tool(
15208
+ "cost_dropshipping_project",
15209
+ "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.",
15210
+ CostProjectInput.shape,
15211
+ // Overwrites the costing inputs already saved on the project — the same
15212
+ // "previous values are gone" case as edit_task_draft, so destructive.
15213
+ // idempotentHint stays false despite the PUT: the `version` guard makes a
15214
+ // blind retry FAIL rather than replay, so a host should not auto-retry it.
15215
+ OVERWRITES_INTERNAL,
15216
+ async (input) => {
15217
+ try {
15218
+ if (input.returnRate + input.cancellationRate >= 1) {
15219
+ throw new Error("returnRate + cancellationRate debe ser menor que 1.");
15220
+ }
15221
+ const { projectId, version, ...costingInput } = input;
15222
+ return projectText(await api.putJson(`/v1/dropshipping-projects/${encodeURIComponent(projectId)}/costing`, {
15223
+ version,
15224
+ input: costingInput
15225
+ }));
15226
+ } catch (error) {
15227
+ throw translateError(error);
14916
15228
  }
14917
- const { projectId, version, ...costingInput } = input;
14918
- return projectText(await api.putJson(`/v1/dropshipping-projects/${encodeURIComponent(projectId)}/costing`, {
14919
- version,
14920
- input: costingInput
14921
- }));
14922
- } catch (error) {
14923
- throw translateError(error);
14924
15229
  }
14925
- });
14926
- 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) => {
15230
+ );
15231
+ 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) => {
14927
15232
  try {
14928
15233
  if (!input.projectId) {
14929
15234
  return asText({