@productmaker/mcp 1.16.2 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stdio.js CHANGED
@@ -9,6 +9,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
 
11
11
  // src/api-client.ts
12
+ import { createHash } from "crypto";
12
13
  var ApiHttpError = class extends Error {
13
14
  static {
14
15
  __name(this, "ApiHttpError");
@@ -35,6 +36,15 @@ var ApiClient = class {
35
36
  Authorization: `Bearer ${this.apiKey}`
36
37
  };
37
38
  }
39
+ /**
40
+ * Stable per-key identifier. The MCP never sees the userId — the API key is
41
+ * the only per-user handle it holds — so this stands in as the user
42
+ * component of derived idempotency keys. Never log or return it raw to a
43
+ * caller: it is derived from a secret.
44
+ */
45
+ fingerprint() {
46
+ return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
47
+ }
38
48
  async getJson(path3) {
39
49
  const res = await fetch(`${this.baseUrl}${path3}`, {
40
50
  headers: this.authHeaders()
@@ -129,10 +139,10 @@ var ApiClient = class {
129
139
  };
130
140
 
131
141
  // src/index.ts
132
- var MCP_VERSION = "1.16.2";
142
+ var MCP_VERSION = "1.17.0";
133
143
 
134
144
  // src/tools/tasks.ts
135
- import { z as z38 } from "zod";
145
+ import { z as z39 } from "zod";
136
146
 
137
147
  // src/resolve-image.ts
138
148
  import { lookup as dnsLookup } from "dns/promises";
@@ -3146,6 +3156,125 @@ var AdminQualityReportListResponse = z24.object({
3146
3156
  var UpdateQualityReportStatusRequest = z24.object({
3147
3157
  status: QualityReportStatus
3148
3158
  });
3159
+ var AdminSupportConversationStatus = z24.enum([
3160
+ "bot_active",
3161
+ "human_pending",
3162
+ "human_active",
3163
+ "closed"
3164
+ ]);
3165
+ var AdminSupportDeliveryStatus = z24.enum([
3166
+ "draft",
3167
+ "pending",
3168
+ "sent",
3169
+ "delivered",
3170
+ "read",
3171
+ "failed",
3172
+ "blocked"
3173
+ ]);
3174
+ var AdminSupportMessage = z24.object({
3175
+ id: z24.string().uuid(),
3176
+ direction: z24.enum([
3177
+ "inbound",
3178
+ "outbound"
3179
+ ]),
3180
+ senderKind: z24.enum([
3181
+ "user",
3182
+ "bot",
3183
+ "admin",
3184
+ "system"
3185
+ ]),
3186
+ messageType: z24.enum([
3187
+ "text",
3188
+ "image",
3189
+ "audio",
3190
+ "document",
3191
+ "interactive",
3192
+ "status"
3193
+ ]),
3194
+ bodyRedacted: z24.string().nullable(),
3195
+ deliveryStatus: AdminSupportDeliveryStatus.nullable(),
3196
+ createdAt: z24.string().datetime()
3197
+ });
3198
+ var AdminSupportTicket = z24.object({
3199
+ id: z24.string().uuid(),
3200
+ category: z24.enum([
3201
+ "how_to",
3202
+ "credits",
3203
+ "billing",
3204
+ "task_status",
3205
+ "task_error",
3206
+ "video_quality",
3207
+ "landing_quality",
3208
+ "image_quality",
3209
+ "feature_request",
3210
+ "other"
3211
+ ]),
3212
+ failureMode: z24.string().nullable(),
3213
+ status: z24.enum([
3214
+ "open",
3215
+ "triaged",
3216
+ "resolved",
3217
+ "rejected"
3218
+ ]),
3219
+ summary: z24.string(),
3220
+ taskId: z24.string().nullable(),
3221
+ mixTaskId: z24.string().nullable(),
3222
+ qualityReportId: z24.string().nullable(),
3223
+ handoffReason: z24.string().nullable(),
3224
+ createdAt: z24.string().datetime(),
3225
+ updatedAt: z24.string().datetime()
3226
+ });
3227
+ var AdminSupportConversation = z24.object({
3228
+ id: z24.string().uuid(),
3229
+ status: AdminSupportConversationStatus,
3230
+ priority: z24.enum([
3231
+ "low",
3232
+ "normal",
3233
+ "high",
3234
+ "urgent"
3235
+ ]),
3236
+ currentIntent: z24.string().nullable(),
3237
+ assignedAdminId: z24.string().nullable(),
3238
+ customerServiceWindowEndsAt: z24.string().datetime().nullable(),
3239
+ lastInboundAt: z24.string().datetime().nullable(),
3240
+ createdAt: z24.string().datetime(),
3241
+ updatedAt: z24.string().datetime(),
3242
+ contact: z24.object({
3243
+ id: z24.string().uuid(),
3244
+ userId: z24.string().nullable(),
3245
+ verifiedAt: z24.string().datetime().nullable(),
3246
+ botMode: z24.enum([
3247
+ "enabled",
3248
+ "paused",
3249
+ "opted_out"
3250
+ ]),
3251
+ locale: z24.string()
3252
+ }),
3253
+ messages: z24.array(AdminSupportMessage),
3254
+ tickets: z24.array(AdminSupportTicket)
3255
+ });
3256
+ var AdminSupportListQuery = z24.object({
3257
+ status: AdminSupportConversationStatus.optional(),
3258
+ take: z24.coerce.number().int().min(1).max(100).default(50),
3259
+ cursorId: z24.string().uuid().optional()
3260
+ }).strict();
3261
+ var AdminSupportListResponse = z24.object({
3262
+ conversations: z24.array(AdminSupportConversation),
3263
+ nextCursor: z24.string().uuid().nullable()
3264
+ });
3265
+ var AdminSupportReplyRequest = z24.object({
3266
+ body: z24.string().trim().min(1).max(4096),
3267
+ idempotencyKey: z24.string().regex(/^[A-Za-z0-9_-]{8,128}$/)
3268
+ }).strict();
3269
+ var AdminSupportEmptyActionRequest = z24.object({}).strict();
3270
+ var AdminSupportActionResponse = z24.object({
3271
+ conversation: AdminSupportConversation
3272
+ });
3273
+ var AdminSupportReplyResponse = z24.object({
3274
+ messageId: z24.string().uuid(),
3275
+ deliveryStatus: AdminSupportDeliveryStatus,
3276
+ duplicate: z24.boolean()
3277
+ });
3149
3278
 
3150
3279
  // ../../packages/types/src/promo.ts
3151
3280
  import { z as z25 } from "zod";
@@ -3285,8 +3414,21 @@ import { z as z31 } from "zod";
3285
3414
  // ../../packages/types/src/campaigns.ts
3286
3415
  import { z as z30 } from "zod";
3287
3416
  var CAMPAIGN_LIMITS = {
3288
- /** Maximum number of creatives per campaign */
3289
- MAX_CREATIVES: 4,
3417
+ /**
3418
+ * Maximum number of creatives (= ads) per campaign.
3419
+ *
3420
+ * Not a Meta limit — Meta allows far more ads per ad set. This is our own
3421
+ * bound on how many uploads one publish call will push through the Graph API
3422
+ * in a single request. It was 4 with no stated reason, which forced callers
3423
+ * with a normal batch (3 videos + 3 images) to drop two by hand.
3424
+ */
3425
+ MAX_CREATIVES: 10,
3426
+ /**
3427
+ * Maximum ad sets per campaign. Distinct from MAX_CREATIVES, which it used
3428
+ * to share a constant with — that coupling meant raising the creative cap
3429
+ * would silently raise the ad-set cap too.
3430
+ */
3431
+ MAX_AD_SETS: 4,
3290
3432
  /** Maximum video duration in seconds */
3291
3433
  MAX_VIDEO_DURATION_SECONDS: 180,
3292
3434
  /** Maximum file size in bytes (350MB) */
@@ -3329,8 +3471,21 @@ var CampaignCreativeType = z30.enum([
3329
3471
  var CampaignCreative = z30.object({
3330
3472
  /** Unique identifier for the creative */
3331
3473
  id: z30.string(),
3332
- /** The uploaded file (null when loaded from server) */
3333
- file: z30.instanceof(File).nullable(),
3474
+ /**
3475
+ * The uploaded file (null when loaded from server).
3476
+ *
3477
+ * Deliberately NOT `z.instanceof(File)`: `File` is a browser global that Node
3478
+ * only exposes from v20. `z.instanceof` dereferences it while the schema is
3479
+ * being *constructed*, so merely importing this module threw
3480
+ * `ReferenceError: File is not defined` on Node 18 — which killed the whole
3481
+ * MCP CLI on startup, because its bundle pulls this barrel in for unrelated
3482
+ * types and never touches this schema. `z.custom` defers the check to
3483
+ * validation time and guards the global, so the module imports on any runtime
3484
+ * and only the browser upload path needs a real `File`.
3485
+ */
3486
+ file: z30.custom((value) => typeof File !== "undefined" && value instanceof File, {
3487
+ message: "Expected a File"
3488
+ }).nullable(),
3334
3489
  /** Type of media: image or video */
3335
3490
  type: CampaignCreativeType,
3336
3491
  /** Creative title (max 120 chars) - Maps to Meta's "Título" */
@@ -3439,6 +3594,9 @@ var HttpUrl = z30.string().url().refine((value) => {
3439
3594
  return false;
3440
3595
  }
3441
3596
  }, "URL must use http or https");
3597
+ var MetaAdAccountId = z30.string().trim().min(1, "Debes seleccionar una cuenta publicitaria").transform((value) => /^\d+$/.test(value) ? `act_${value}` : value).refine((value) => /^act_\d+$/.test(value), {
3598
+ message: 'adAccountId must look like "act_1234567890" (a bare numeric id is also accepted)'
3599
+ });
3442
3600
  var MetaCampaignDestination = z30.discriminatedUnion("type", [
3443
3601
  z30.object({
3444
3602
  type: z30.literal("landing"),
@@ -3471,7 +3629,7 @@ var MetaCampaignConfigBase = z30.object({
3471
3629
  campaignDailyBudget: z30.number().int().positive().optional(),
3472
3630
  /** Omitted preserves the historical recommended-floor behavior. */
3473
3631
  budgetPolicy: MetaBudgetPolicy.optional(),
3474
- adSets: z30.array(MetaAdSetConfig).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3632
+ adSets: z30.array(MetaAdSetConfig).min(1).max(CAMPAIGN_LIMITS.MAX_AD_SETS),
3475
3633
  destination: MetaCampaignDestination,
3476
3634
  saveAsDefault: z30.boolean().default(true)
3477
3635
  });
@@ -3504,7 +3662,7 @@ var MetaCampaignConfig = MetaCampaignConfigBase.superRefine((value, ctx) => {
3504
3662
  var MetaCampaignDefaults = MetaCampaignConfigBase.omit({
3505
3663
  saveAsDefault: true
3506
3664
  }).extend({
3507
- adSets: z30.array(MetaAdSetDefaults).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3665
+ adSets: z30.array(MetaAdSetDefaults).min(1).max(CAMPAIGN_LIMITS.MAX_AD_SETS),
3508
3666
  /**
3509
3667
  * Ad-account currency (ISO 4217: USD/COP/MXN/...) at the time the user saved
3510
3668
  * these defaults. The wizard re-uses the saved budget only when the current
@@ -3533,7 +3691,7 @@ var UploadToMetaRequest = z30.object({
3533
3691
  thumbnailUrl: HttpUrl.optional()
3534
3692
  })).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3535
3693
  /** Meta Ad Account ID */
3536
- adAccountId: z30.string().min(1),
3694
+ adAccountId: MetaAdAccountId,
3537
3695
  /** Optional Meta Pixel ID */
3538
3696
  pixelId: z30.string().optional(),
3539
3697
  /** Landing page URL (required for landing destinations; omitted for whatsapp) */
@@ -3615,7 +3773,7 @@ var MetaCampaignOverridesSchema = z31.object({
3615
3773
  destination: MetaCampaignDestination.optional()
3616
3774
  });
3617
3775
  var FacebookCreateCampaignBodySchema = z31.object({
3618
- adAccountId: z31.string().min(1, "Debes seleccionar una cuenta publicitaria"),
3776
+ adAccountId: MetaAdAccountId,
3619
3777
  pixelId: z31.string().optional(),
3620
3778
  landingUrl: HttpUrl2.optional(),
3621
3779
  campaignConfig: MetaCampaignConfig.optional(),
@@ -3691,8 +3849,8 @@ var FacebookCreateCampaignFromCreativesBodySchema = z31.object({
3691
3849
  mediaUrl: HttpUrl2,
3692
3850
  thumbnailUrl: HttpUrl2.optional(),
3693
3851
  creativeId: z31.string().min(1).optional()
3694
- })).min(1).max(4),
3695
- adAccountId: z31.string().min(1),
3852
+ })).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3853
+ adAccountId: MetaAdAccountId,
3696
3854
  pixelId: z31.string().optional(),
3697
3855
  /** Required unless campaignConfig.destination is 'whatsapp' (see superRefine below). */
3698
3856
  landingUrl: HttpUrl2.optional(),
@@ -3807,8 +3965,8 @@ var FacebookCreateCampaignFromTasksBodySchema = z31.object({
3807
3965
  description: z31.string().max(500).optional(),
3808
3966
  cta: MetaCtaType.optional(),
3809
3967
  creativeId: z31.string().min(1).optional()
3810
- })).min(1).max(4),
3811
- adAccountId: z31.string().min(1),
3968
+ })).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3969
+ adAccountId: MetaAdAccountId,
3812
3970
  pixelId: z31.string().optional(),
3813
3971
  landingUrl: HttpUrl2.optional(),
3814
3972
  targetCountry: z31.string().length(2).default("US"),
@@ -3952,6 +4110,23 @@ var DEFAULT_PRICING = {
3952
4110
  ourCostCents: 0.4,
3953
4111
  userPriceCents: 0.8
3954
4112
  },
4113
+ // WhatsApp ProductMaker intent proposal. Real OpenRouter usage.cost wins;
4114
+ // this is only the fallback when the relay omits cost.
4115
+ "openrouter:whatsapp-product-agent-intent": {
4116
+ ourCostCents: 0.4,
4117
+ userPriceCents: 0.8
4118
+ },
4119
+ // Internal support calls are cost-only: callers explicitly override user
4120
+ // price to zero. Static COGS estimates apply because OpenAI Responses exposes
4121
+ // token counts but not the provider's dollar charge.
4122
+ "openai:whatsapp-support-classifier": {
4123
+ ourCostCents: 0.1,
4124
+ userPriceCents: 0.2
4125
+ },
4126
+ "openai:whatsapp-support-draft": {
4127
+ ourCostCents: 0.4,
4128
+ userPriceCents: 0.8
4129
+ },
3955
4130
  // Task-derived creative recommendation (angle/persona/script/landing-section).
3956
4131
  // OpenRouter usage normally supplies the exact COGS and the call layer charges
3957
4132
  // 2x. This rule is the fail-closed fallback when usage.cost is unavailable.
@@ -6361,6 +6536,43 @@ var RegenerateEditRequestSchema = z36.object({
6361
6536
  comment: z36.string().min(1).max(2e3)
6362
6537
  });
6363
6538
 
6539
+ // ../../packages/types/src/scene-subject-contract.ts
6540
+ import { z as z37 } from "zod";
6541
+ var SceneSubjectContractSchema = z37.object({
6542
+ maxPeople: z37.number().int().min(1).max(10),
6543
+ minPeople: z37.number().int().min(0).max(10),
6544
+ minDistinctPeople: z37.number().int().min(0).max(10),
6545
+ maxHands: z37.number().int().min(0).max(20)
6546
+ }).superRefine((value, ctx) => {
6547
+ if (value.minPeople > value.maxPeople) {
6548
+ ctx.addIssue({
6549
+ code: z37.ZodIssueCode.custom,
6550
+ path: [
6551
+ "minPeople"
6552
+ ],
6553
+ message: "minPeople cannot exceed maxPeople"
6554
+ });
6555
+ }
6556
+ if (value.minDistinctPeople > value.maxPeople) {
6557
+ ctx.addIssue({
6558
+ code: z37.ZodIssueCode.custom,
6559
+ path: [
6560
+ "minDistinctPeople"
6561
+ ],
6562
+ message: "minDistinctPeople cannot exceed maxPeople"
6563
+ });
6564
+ }
6565
+ if (value.minDistinctPeople > value.minPeople) {
6566
+ ctx.addIssue({
6567
+ code: z37.ZodIssueCode.custom,
6568
+ path: [
6569
+ "minDistinctPeople"
6570
+ ],
6571
+ message: "minDistinctPeople cannot exceed minPeople"
6572
+ });
6573
+ }
6574
+ });
6575
+
6364
6576
  // src/resolve-media.ts
6365
6577
  var MAX_MEDIA_BYTES = CAMPAIGN_LIMITS.MAX_FILE_SIZE_BYTES;
6366
6578
  var MEDIA_FORMATS = [
@@ -6600,10 +6812,10 @@ async function resolveMediaRef(ref, opts) {
6600
6812
  __name(resolveMediaRef, "resolveMediaRef");
6601
6813
 
6602
6814
  // src/tools/shared.ts
6603
- import { z as z37 } from "zod";
6604
- var Lang = z37.string().min(2).max(10);
6605
- var Country = z37.string().min(2).max(3);
6606
- var LandingStrategyEnum = z37.enum([
6815
+ import { z as z38 } from "zod";
6816
+ var Lang = z38.string().min(2).max(10);
6817
+ var Country = z38.string().min(2).max(3);
6818
+ var LandingStrategyEnum = z38.enum([
6607
6819
  "auto",
6608
6820
  "classic",
6609
6821
  "pain_solution",
@@ -6612,12 +6824,12 @@ var LandingStrategyEnum = z37.enum([
6612
6824
  "offer_value",
6613
6825
  "lifestyle_identity"
6614
6826
  ]);
6615
- var LandingImageFormatEnum = z37.enum([
6827
+ var LandingImageFormatEnum = z38.enum([
6616
6828
  "9:16",
6617
6829
  "4:5",
6618
6830
  "1:1"
6619
6831
  ]);
6620
- var VideoPresetEnum = z37.enum([
6832
+ var VideoPresetEnum = z38.enum([
6621
6833
  "auto",
6622
6834
  "ugc_dynamic",
6623
6835
  "ugc_unboxing_chaos",
@@ -6628,7 +6840,7 @@ var VideoPresetEnum = z37.enum([
6628
6840
  "pixar_problem",
6629
6841
  "pixar_sassy"
6630
6842
  ]);
6631
- var TaskCreativeVariantVideoPresetEnum = z37.enum([
6843
+ var TaskCreativeVariantVideoPresetEnum = z38.enum([
6632
6844
  "auto",
6633
6845
  "ugc_dynamic",
6634
6846
  "problem_solution",
@@ -6687,7 +6899,7 @@ function resolveVideoPreset(preset) {
6687
6899
  return VIDEO_PRESET_RESOLUTION[preset] ?? null;
6688
6900
  }
6689
6901
  __name(resolveVideoPreset, "resolveVideoPreset");
6690
- var VideoTierEnum = z37.enum([
6902
+ var VideoTierEnum = z38.enum([
6691
6903
  "basic"
6692
6904
  ]);
6693
6905
  function resolveVideoTier(_tier) {
@@ -6702,17 +6914,17 @@ function resolveVideoFormatForDuration(durationSeconds) {
6702
6914
  return durationSeconds === 30 ? "double_30s" : "single_15s";
6703
6915
  }
6704
6916
  __name(resolveVideoFormatForDuration, "resolveVideoFormatForDuration");
6705
- var VideoResolutionEnum = z37.enum([
6917
+ var VideoResolutionEnum = z38.enum([
6706
6918
  "480p",
6707
6919
  "720p"
6708
6920
  ]);
6709
- var VideoStyleEnum = z37.enum([
6921
+ var VideoStyleEnum = z38.enum([
6710
6922
  "ugc",
6711
6923
  "cartoon_3d",
6712
6924
  "product_only",
6713
6925
  "ai_dynamic"
6714
6926
  ]);
6715
- var NarrativeStyleEnum = z37.enum([
6927
+ var NarrativeStyleEnum = z38.enum([
6716
6928
  "review",
6717
6929
  "problem_product_cta",
6718
6930
  "product_showcase",
@@ -6727,7 +6939,7 @@ var NarrativeStyleEnum = z37.enum([
6727
6939
  "ugc_skeptic_confession",
6728
6940
  "ugc_demo_review"
6729
6941
  ]);
6730
- var PublicNarrativeStyleEnum = z37.enum([
6942
+ var PublicNarrativeStyleEnum = z38.enum([
6731
6943
  "review",
6732
6944
  "ugc_unboxing_chaos",
6733
6945
  "problem_product_cta",
@@ -6737,7 +6949,7 @@ var PublicNarrativeStyleEnum = z37.enum([
6737
6949
  "animated_problem_pixar",
6738
6950
  "sassy_product_pixar"
6739
6951
  ]);
6740
- var VideoModelEnum = z37.enum([
6952
+ var VideoModelEnum = z38.enum([
6741
6953
  "veo",
6742
6954
  "wan",
6743
6955
  "bytedance",
@@ -6745,25 +6957,25 @@ var VideoModelEnum = z37.enum([
6745
6957
  "grok",
6746
6958
  "kling"
6747
6959
  ]);
6748
- var ProductReferenceUserChoiceEnum = z37.enum([
6960
+ var ProductReferenceUserChoiceEnum = z38.enum([
6749
6961
  "same_product",
6750
6962
  "variants",
6751
6963
  "different_products",
6752
6964
  "combo_distinct"
6753
6965
  ]);
6754
- var ProductReferenceCandidateGroupingEnum = z37.enum([
6966
+ var ProductReferenceCandidateGroupingEnum = z38.enum([
6755
6967
  "same_product_same_variant",
6756
6968
  "same_product_different_variants",
6757
6969
  "different_products",
6758
6970
  "unclear"
6759
6971
  ]);
6760
- var GapDurationChoiceMirror = z37.object({
6761
- angleIndex: z37.number().int().nonnegative(),
6762
- recommendedSec: z37.union([
6763
- z37.literal(15),
6764
- z37.literal(30)
6972
+ var GapDurationChoiceMirror = z38.object({
6973
+ angleIndex: z38.number().int().nonnegative(),
6974
+ recommendedSec: z38.union([
6975
+ z38.literal(15),
6976
+ z38.literal(30)
6765
6977
  ]),
6766
- estimatedDeltaCredits: z37.number().int().nonnegative().optional()
6978
+ estimatedDeltaCredits: z38.number().int().nonnegative().optional()
6767
6979
  });
6768
6980
  function buildSelectedImageUrlsHash(urls) {
6769
6981
  const input = [
@@ -6777,13 +6989,13 @@ function buildSelectedImageUrlsHash(urls) {
6777
6989
  return `fnv1a:${hash.toString(16).padStart(8, "0")}:${urls.length}`;
6778
6990
  }
6779
6991
  __name(buildSelectedImageUrlsHash, "buildSelectedImageUrlsHash");
6780
- var ImageCreativeStyleEnum = z37.enum([
6992
+ var ImageCreativeStyleEnum = z38.enum([
6781
6993
  "studio",
6782
6994
  "floating",
6783
6995
  "ingredients",
6784
6996
  "in_use"
6785
6997
  ]);
6786
- var ImageCreativeArchetypeEnum = z37.enum([
6998
+ var ImageCreativeArchetypeEnum = z38.enum([
6787
6999
  "direct_response_product_ad",
6788
7000
  "hero_outcome",
6789
7001
  "offer_value_prop",
@@ -6792,23 +7004,23 @@ var ImageCreativeArchetypeEnum = z37.enum([
6792
7004
  "problem_solution",
6793
7005
  "lifestyle_identity"
6794
7006
  ]);
6795
- var ImageCreativeVisualTreatmentEnum = z37.enum([
7007
+ var ImageCreativeVisualTreatmentEnum = z38.enum([
6796
7008
  "performance_product_ad",
6797
7009
  "premium_dtc_carousel"
6798
7010
  ]);
6799
- var ImageCreativeAspectRatioEnum = z37.enum([
7011
+ var ImageCreativeAspectRatioEnum = z38.enum([
6800
7012
  "1:1",
6801
7013
  "4:5",
6802
7014
  "9:16",
6803
7015
  "16:9"
6804
7016
  ]);
6805
- var ImageRefSchema = z37.object({
6806
- url: z37.string().url().optional().describe('URL p\xFAblica HTTPS de la foto. Ej: una URL de Shopify CDN, Cloudinary, Imgur. \xDAsala cuando el usuario te diga "est\xE1 en X.com/foto.jpg" o cuando la foto ya est\xE9 hospedada.'),
6807
- path: z37.string().min(1).optional().describe('Ruta del archivo en el computador del usuario. Ej: "/Users/maria/Desktop/foto.jpg", "~/Pictures/camiseta.png", "C:\\Users\\Maria\\Desktop\\foto.jpg". El MCP local la lee del disco y la sube. SOLO funciona cuando el MCP corre local (stdio). NO funciona en mcp.productmaker.app (HTTP) \u2014 ah\xED usa `url`.')
7017
+ var ImageRefSchema = z38.object({
7018
+ url: z38.string().url().optional().describe('URL p\xFAblica HTTPS de la foto. Ej: una URL de Shopify CDN, Cloudinary, Imgur. \xDAsala cuando el usuario te diga "est\xE1 en X.com/foto.jpg" o cuando la foto ya est\xE9 hospedada.'),
7019
+ path: z38.string().min(1).optional().describe('Ruta del archivo en el computador del usuario. Ej: "/Users/maria/Desktop/foto.jpg", "~/Pictures/camiseta.png", "C:\\Users\\Maria\\Desktop\\foto.jpg". El MCP local la lee del disco y la sube. SOLO funciona cuando el MCP corre local (stdio). NO funciona en mcp.productmaker.app (HTTP) \u2014 ah\xED usa `url`.')
6808
7020
  }).refine((v) => Boolean(v.url) !== Boolean(v.path), {
6809
7021
  message: "each image entry must have exactly one of `url` or `path`"
6810
7022
  });
6811
- var ImagesInput = z37.array(ImageRefSchema).min(1).max(5).describe('1 a 5 fotos del MISMO producto. La PRIMERA es la principal (hero); las dem\xE1s son \xE1ngulos adicionales. NO mezcles productos distintos en una sola llamada.\n\nCada entrada tiene EXACTAMENTE uno de:\n\u2022 `url` \u2014 URL p\xFAblica HTTPS (Shopify CDN, Cloudinary). Prefi\xE9rela si la foto ya est\xE1 hospedada.\n\u2022 `path` \u2014 Ruta del archivo en el computador del usuario (ej. "/Users/maria/Desktop/foto.jpg", "~/Downloads/camiseta.png"). \xDAsala cuando el usuario te dijo d\xF3nde guard\xF3 la foto. Solo MCP local \u2014 el cliente web rechaza paths.\n\nSi el usuario te peg\xF3 la foto en el chat pero no te dio ni URL ni ruta: PREG\xDANTALE d\xF3nde la tiene guardada (Escritorio, Descargas, etc.). Si el `path` falla con IMAGE_PATH_NOT_FOUND, prueba 1-2 ubicaciones obvias alternativas (~/Desktop, ~/Downloads) antes de molestar al usuario. Tras 3 intentos fallidos, p\xEDdele la ruta completa.');
7023
+ var ImagesInput = z38.array(ImageRefSchema).min(1).max(5).describe('1 a 5 fotos del MISMO producto. La PRIMERA es la principal (hero); las dem\xE1s son \xE1ngulos adicionales. NO mezcles productos distintos en una sola llamada.\n\nCada entrada tiene EXACTAMENTE uno de:\n\u2022 `url` \u2014 URL p\xFAblica HTTPS (Shopify CDN, Cloudinary). Prefi\xE9rela si la foto ya est\xE1 hospedada.\n\u2022 `path` \u2014 Ruta del archivo en el computador del usuario (ej. "/Users/maria/Desktop/foto.jpg", "~/Downloads/camiseta.png"). \xDAsala cuando el usuario te dijo d\xF3nde guard\xF3 la foto. Solo MCP local \u2014 el cliente web rechaza paths.\n\nSi el usuario te peg\xF3 la foto en el chat pero no te dio ni URL ni ruta: PREG\xDANTALE d\xF3nde la tiene guardada (Escritorio, Descargas, etc.). Si el `path` falla con IMAGE_PATH_NOT_FOUND, prueba 1-2 ubicaciones obvias alternativas (~/Desktop, ~/Downloads) antes de molestar al usuario. Tras 3 intentos fallidos, p\xEDdele la ruta completa.');
6812
7024
  function asText(value) {
6813
7025
  return {
6814
7026
  content: [
@@ -6861,6 +7073,28 @@ async function submitCreative(api, opts) {
6861
7073
  });
6862
7074
  }
6863
7075
  __name(submitCreative, "submitCreative");
7076
+ var ResearchTargetCountryEnum = z38.enum([
7077
+ "CO",
7078
+ "MX",
7079
+ "PE",
7080
+ "EC",
7081
+ "CL"
7082
+ ]);
7083
+ var ResearchCategoryEnum = z38.enum([
7084
+ "auto",
7085
+ "automotive",
7086
+ "kitchen",
7087
+ "pets",
7088
+ "home_organization",
7089
+ "beauty",
7090
+ "wellness",
7091
+ "fitness",
7092
+ "baby",
7093
+ "tech",
7094
+ "tools",
7095
+ "garden",
7096
+ "cleaning"
7097
+ ]);
6864
7098
 
6865
7099
  // src/errors.ts
6866
7100
  var McpErrorCode = {
@@ -6892,6 +7126,7 @@ var McpErrorCode = {
6892
7126
  TASK_NOT_READY: "TASK_NOT_READY",
6893
7127
  CREATIVE_VARIANT_FAILED: "CREATIVE_VARIANT_FAILED",
6894
7128
  CREATIVE_VARIANT_DISPATCH_FAILED: "CREATIVE_VARIANT_DISPATCH_FAILED",
7129
+ PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT: "PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT",
6895
7130
  // Connection presence
6896
7131
  NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
6897
7132
  NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
@@ -7013,12 +7248,13 @@ var CODE_MESSAGES = {
7013
7248
  TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
7014
7249
  CREATIVE_VARIANT_FAILED: "The previous creative variant attempt failed. Ask for confirmation before starting a new paid attempt with a new clientRequestId.",
7015
7250
  CREATIVE_VARIANT_DISPATCH_FAILED: "The creative variant could not be queued. Retry as a new paid attempt with a new clientRequestId.",
7251
+ PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT: "A product research run with the same country and category is already running. Check it with `list_product_research_runs` instead of starting a new paid run.",
7016
7252
  // API key
7017
7253
  INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
7018
7254
  REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
7019
7255
  // Rate
7020
7256
  RATE_LIMITED: "Too many requests. Wait a few seconds.",
7021
- INSUFFICIENT_CREDITS: "Insufficient credits to create this variant. Check your balance and retry after adding credits.",
7257
+ INSUFFICIENT_CREDITS: "Insufficient credits for this action. Check your balance and add credits at https://productmaker.app before retrying.",
7022
7258
  TASK_QUOTA_EXCEEDED: "You reached the active-task limit. Wait for a task to finish before creating another variant.",
7023
7259
  // Generic
7024
7260
  UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
@@ -7031,6 +7267,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
7031
7267
  McpErrorCode.INVALID_INPUT,
7032
7268
  McpErrorCode.INSUFFICIENT_CREDITS,
7033
7269
  McpErrorCode.TASK_QUOTA_EXCEEDED,
7270
+ McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
7034
7271
  McpErrorCode.INVALID_API_KEY,
7035
7272
  McpErrorCode.REVOKED_API_KEY,
7036
7273
  McpErrorCode.IMAGE_PATH_NOT_FOUND,
@@ -7093,6 +7330,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
7093
7330
  McpErrorCode.INSUFFICIENT_CREDITS,
7094
7331
  McpErrorCode.TASK_QUOTA_EXCEEDED,
7095
7332
  McpErrorCode.CREATIVE_VARIANT_DISPATCH_FAILED,
7333
+ McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
7096
7334
  // Shopify
7097
7335
  McpErrorCode.SHOPIFY_NOT_CONNECTED,
7098
7336
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
@@ -7186,6 +7424,11 @@ function resolveMessage(code, backendMessage) {
7186
7424
  return `The service returned an error (code: ${code}). Try again shortly or contact support if it persists.`;
7187
7425
  }
7188
7426
  __name(resolveMessage, "resolveMessage");
7427
+ function invalidInputFromZod(error) {
7428
+ const detail = error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
7429
+ return mcp(McpErrorCode.INVALID_INPUT, detail || resolveMessage(McpErrorCode.INVALID_INPUT, void 0));
7430
+ }
7431
+ __name(invalidInputFromZod, "invalidInputFromZod");
7189
7432
  function translateError(err) {
7190
7433
  if (err instanceof McpError) {
7191
7434
  return err;
@@ -7201,9 +7444,11 @@ function translateError(err) {
7201
7444
  const body = isRecord(err.body) ? err.body : {};
7202
7445
  if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
7203
7446
  const code = body.code;
7204
- return mcp(code, resolveMessage(code, body.message), body.helpUrl ? {
7205
- helpUrl: body.helpUrl
7206
- } : {});
7447
+ const extra = {};
7448
+ if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
7449
+ if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
7450
+ if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
7451
+ return mcp(code, resolveMessage(code, body.message), extra);
7207
7452
  }
7208
7453
  if (err.status === 401) {
7209
7454
  return mcp(McpErrorCode.INVALID_API_KEY, resolveMessage(McpErrorCode.INVALID_API_KEY, void 0));
@@ -7407,66 +7652,66 @@ function renderOutputsMarkdown(outputs) {
7407
7652
  __name(renderOutputsMarkdown, "renderOutputsMarkdown");
7408
7653
 
7409
7654
  // src/tools/tasks.ts
7410
- var CreateProductTaskInput = z38.object({
7655
+ var CreateProductTaskInput = z39.object({
7411
7656
  images: ImagesInput,
7412
- productUrl: z38.string().url().optional().describe("Opcional. URL de la p\xE1gina del producto (Shopify, AliExpress, Amazon, etc.) para enriquecer el contexto. Si se omite, la IA infiere todo desde la imagen."),
7657
+ productUrl: z39.string().url().optional().describe("Opcional. URL de la p\xE1gina del producto (Shopify, AliExpress, Amazon, etc.) para enriquecer el contexto. Si se omite, la IA infiere todo desde la imagen."),
7413
7658
  language: Lang.describe('C\xF3digo de idioma para el contenido generado. Ejemplos: "es", "en", "pt". REQUERIDO \u2014 si el usuario no lo especific\xF3, PREG\xDANTALE antes de invocar. NUNCA infieras desde el contexto del chat ni asumas "es" por default.'),
7414
7659
  country: Country.describe('C\xF3digo ISO del pa\xEDs objetivo. Ejemplos: "CO", "MX", "US", "AR". Define moneda, m\xE9todos de pago locales y tono. REQUERIDO \u2014 si el usuario no lo especific\xF3, PREG\xDANTALE antes de invocar. NUNCA infieras desde el contexto del chat.'),
7415
- price: z38.string().min(1).max(120).optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
7416
- compareAtPrice: z38.string().min(1).max(120).optional().describe('Opcional. Precio anterior para comunicar un descuento real en landing e im\xE1genes. Ej. "$129.000" cuando price="$89.900".'),
7417
- offer: z38.string().min(1).max(240).optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
7418
- paymentMethods: z38.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
7419
- productContext: z38.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
7420
- productLabel: z38.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
7660
+ price: z39.string().min(1).max(120).optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
7661
+ compareAtPrice: z39.string().min(1).max(120).optional().describe('Opcional. Precio anterior para comunicar un descuento real en landing e im\xE1genes. Ej. "$129.000" cuando price="$89.900".'),
7662
+ offer: z39.string().min(1).max(240).optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
7663
+ paymentMethods: z39.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
7664
+ productContext: z39.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
7665
+ productLabel: z39.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
7421
7666
  videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s; 30s usa el ritmo Multiescena. Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso), "problem_solution" (problema\u2192producto\u2192CTA), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla), "product_showcase" (solo producto, sin actor), "wearable_showcase" (ropa/accesorios puestos). Si el usuario pide algo fuera de esta lista (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.'),
7422
- durationSeconds: z38.union([
7423
- z38.literal(15),
7424
- z38.literal(30)
7667
+ durationSeconds: z39.union([
7668
+ z39.literal(15),
7669
+ z39.literal(30)
7425
7670
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
7426
- pauseForAngleReview: z38.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
7671
+ pauseForAngleReview: z39.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
7427
7672
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
7428
7673
  videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez (~el doble de precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
7429
- multiAngleVideos: z38.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
7674
+ multiAngleVideos: z39.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
7430
7675
  angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_unboxing_chaos", "problem_product_cta", "product_showcase", "wearable_showcase", "cinematic", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
7431
- outputs: z38.array(z38.enum([
7676
+ outputs: z39.array(z39.enum([
7432
7677
  "landing",
7433
7678
  "video",
7434
7679
  "image"
7435
7680
  ])).min(2).max(3).optional().describe('Opcional. Qu\xE9 outputs generar. Default: todos ["landing","video","image"]. Para 2 outputs: ["landing","image"] (sin video), ["video","image"] (sin landing), ["landing","video"] (sin imagen). Para 1 solo output, usa generate_landing / generate_video_creative / generate_image_creatives.'),
7436
- competitorScanEnabled: z38.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
7437
- brandId: z38.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer los outputs generados."),
7438
- creativeDirection: z38.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo) que aplica a los outputs de IMAGEN y LANDING (no al video \u2014 el estilo/ambiente del video se dirige con scriptNotes). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Ej. "amarillo y azul, minimalista". M\xE1x 500 caracteres.'),
7681
+ competitorScanEnabled: z39.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
7682
+ brandId: z39.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer los outputs generados."),
7683
+ creativeDirection: z39.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo) que aplica a los outputs de IMAGEN y LANDING (no al video \u2014 el estilo/ambiente del video se dirige con scriptNotes). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Ej. "amarillo y azul, minimalista". M\xE1x 500 caracteres.'),
7439
7684
  landingStrategy: LandingStrategyEnum.default("classic").describe('Opcional. Estructura de la landing cl\xE1sica cuando landing est\xE1 entre outputs: "classic" (estructura original probada y recomendada; default del primer rollout), "auto" (la IA decide; experimental), "pain_solution", "social_proof" (Landing Viral: UGC, WhatsApp, Instagram y evidencia social), "benefits_clarity", "offer_value" o "lifestyle_identity". Default "classic".'),
7440
7685
  landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica cuando landing est\xE1 entre outputs: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".')
7441
7686
  }).strict();
7442
- var GetTaskStatusInput = z38.object({
7443
- taskId: z38.string().min(1),
7444
- waitSeconds: z38.number().int().min(0).max(120).optional()
7445
- });
7446
- var ListTasksInput = z38.object({
7447
- limit: z38.number().int().min(1).max(50).optional(),
7448
- offset: z38.number().int().min(0).optional(),
7449
- search: z38.string().optional()
7450
- });
7451
- var EditTaskDraftInput = z38.object({
7452
- taskId: z38.string().min(1),
7453
- angleIndex: z38.number().int().min(0),
7454
- angle: z38.object({
7455
- angle: z38.string().min(1).max(480),
7456
- buyerPersona: z38.string().min(1).max(360),
7457
- actor: z38.string().min(1).max(360).optional(),
7458
- gender: z38.enum([
7687
+ var GetTaskStatusInput = z39.object({
7688
+ taskId: z39.string().min(1),
7689
+ waitSeconds: z39.number().int().min(0).max(120).optional()
7690
+ });
7691
+ var ListTasksInput = z39.object({
7692
+ limit: z39.number().int().min(1).max(50).optional(),
7693
+ offset: z39.number().int().min(0).optional(),
7694
+ search: z39.string().optional()
7695
+ });
7696
+ var EditTaskDraftInput = z39.object({
7697
+ taskId: z39.string().min(1),
7698
+ angleIndex: z39.number().int().min(0),
7699
+ angle: z39.object({
7700
+ angle: z39.string().min(1).max(480),
7701
+ buyerPersona: z39.string().min(1).max(360),
7702
+ actor: z39.string().min(1).max(360).optional(),
7703
+ gender: z39.enum([
7459
7704
  "female",
7460
7705
  "male"
7461
7706
  ]).optional(),
7462
- scriptNotes: z38.string().max(2e3).optional()
7707
+ scriptNotes: z39.string().max(2e3).optional()
7463
7708
  })
7464
7709
  });
7465
- var PendingActionPayload = z38.discriminatedUnion("kind", [
7466
- z38.object({
7467
- kind: z38.literal("product_reference_confirmation"),
7710
+ var PendingActionPayload = z39.discriminatedUnion("kind", [
7711
+ z39.object({
7712
+ kind: z39.literal("product_reference_confirmation"),
7468
7713
  userChoice: ProductReferenceUserChoiceEnum,
7469
- confirmedProductNames: z38.array(z38.string().min(1).max(120)).min(2).max(8).optional(),
7714
+ confirmedProductNames: z39.array(z39.string().min(1).max(120)).min(2).max(8).optional(),
7470
7715
  // SCOPED multi-scene opt-in — the ONLY way STANDARD ("Multiescena") is
7471
7716
  // reachable via MCP. Valid ONLY together with userChoice='variants' (same
7472
7717
  // product in several colors): renders one shot per color and stitches them,
@@ -7477,62 +7722,62 @@ var PendingActionPayload = z38.discriminatedUnion("kind", [
7477
7722
  // safe because multi-scene IS the intended render for a color variant set,
7478
7723
  // not a blind quality upsell. Maps to `videoTier: 'standard'` on the
7479
7724
  // confirm call; the backend re-gates it (variants + variant_set only).
7480
- useMultiSceneShowcase: z38.boolean().optional().describe('Opcional (SOLO con userChoice="variants"). true = render Multiescena: una toma por color, mostrando todas las variantes, hasta 30s. Se factura POR ESCENA (m\xE1s caro que el render normal de una sola toma) \u2014 AV\xCDSALE del mayor costo al usuario antes de activarlo. Omitir = render est\xE1ndar de una sola toma. Se ignora si userChoice no es "variants".')
7725
+ useMultiSceneShowcase: z39.boolean().optional().describe('Opcional (SOLO con userChoice="variants"). true = render Multiescena: una toma por color, mostrando todas las variantes, hasta 30s. Se factura POR ESCENA (m\xE1s caro que el render normal de una sola toma) \u2014 AV\xCDSALE del mayor costo al usuario antes de activarlo. Omitir = render est\xE1ndar de una sola toma. Se ignora si userChoice no es "variants".')
7481
7726
  }),
7482
- z38.object({
7483
- kind: z38.literal("angle_review_approval"),
7727
+ z39.object({
7728
+ kind: z39.literal("angle_review_approval"),
7484
7729
  // All edits are optional — empty action body just approves the
7485
7730
  // backend-generated draft as-is. Field shapes mirror the angle-review
7486
7731
  // request contracts in @packages/types/wizard.ts; the handler routes to
7487
7732
  // the dedicated video/image/autopilot endpoint based on StatusResponse.kind.
7488
- customAngle: z38.string().min(1).max(480).optional(),
7733
+ customAngle: z39.string().min(1).max(480).optional(),
7489
7734
  // Keep the shared MCP action within the strictest downstream contract:
7490
7735
  // autopilot/image resume endpoints cap these fields at 1500 characters.
7491
- painPoints: z38.string().max(1500).optional(),
7492
- benefits: z38.string().max(1500).optional(),
7493
- features: z38.array(z38.string().min(1).max(300)).max(10).optional(),
7494
- gapDurationSeconds: z38.union([
7495
- z38.literal(15),
7496
- z38.literal(30)
7736
+ painPoints: z39.string().max(1500).optional(),
7737
+ benefits: z39.string().max(1500).optional(),
7738
+ features: z39.array(z39.string().min(1).max(300)).max(10).optional(),
7739
+ gapDurationSeconds: z39.union([
7740
+ z39.literal(15),
7741
+ z39.literal(30)
7497
7742
  ]).optional().describe("Opcional. Solo para aceptar expl\xEDcitamente la recomendaci\xF3n gapDurationChoice del status. 30 env\xEDa el override double_30s para el \xE1ngulo recomendado; 15 u omitido conserva la duraci\xF3n cotizada.")
7498
7743
  })
7499
7744
  ]);
7500
- var ResolvePendingActionInput = z38.object({
7501
- taskId: z38.string().min(1),
7745
+ var ResolvePendingActionInput = z39.object({
7746
+ taskId: z39.string().min(1),
7502
7747
  action: PendingActionPayload
7503
7748
  }).strict();
7504
7749
  function resolveVariantConfirmVideoTier(action) {
7505
7750
  return action.useMultiSceneShowcase && action.userChoice === "variants" ? "standard" : void 0;
7506
7751
  }
7507
7752
  __name(resolveVariantConfirmVideoTier, "resolveVariantConfirmVideoTier");
7508
- var StatusForResolveSchema = z38.object({
7753
+ var StatusForResolveSchema = z39.object({
7509
7754
  // Metadata used to select the backend resume contract. Older API responses
7510
7755
  // may omit these fields, so the resolver keeps the generic fallback.
7511
- kind: z38.enum([
7756
+ kind: z39.enum([
7512
7757
  "autopilot",
7513
7758
  "video",
7514
7759
  "image",
7515
7760
  "landing",
7516
7761
  "mix"
7517
7762
  ]).optional(),
7518
- selectedOutputs: z38.array(z38.enum([
7763
+ selectedOutputs: z39.array(z39.enum([
7519
7764
  "landing",
7520
7765
  "video",
7521
7766
  "image"
7522
7767
  ])).optional(),
7523
- videoAngleCount: z38.number().int().min(1).max(3).optional(),
7524
- ugcFormat: z38.enum([
7768
+ videoAngleCount: z39.number().int().min(1).max(3).optional(),
7769
+ ugcFormat: z39.enum([
7525
7770
  "single_8s",
7526
7771
  "single_15s",
7527
7772
  "double_30s"
7528
7773
  ]).optional(),
7529
- videoStyle: z38.enum([
7774
+ videoStyle: z39.enum([
7530
7775
  "ugc",
7531
7776
  "cartoon_3d",
7532
7777
  "product_only",
7533
7778
  "ai_dynamic"
7534
7779
  ]).optional(),
7535
- videoModel: z38.enum([
7780
+ videoModel: z39.enum([
7536
7781
  "veo",
7537
7782
  "wan",
7538
7783
  "bytedance",
@@ -7540,26 +7785,26 @@ var StatusForResolveSchema = z38.object({
7540
7785
  "grok",
7541
7786
  "kling"
7542
7787
  ]).optional(),
7543
- videoTier: z38.enum([
7788
+ videoTier: z39.enum([
7544
7789
  "legacy",
7545
7790
  "standard",
7546
7791
  "pro",
7547
7792
  "single_call"
7548
7793
  ]).optional(),
7549
- videoResolution: z38.enum([
7794
+ videoResolution: z39.enum([
7550
7795
  "480p",
7551
7796
  "720p"
7552
7797
  ]).optional(),
7553
7798
  narrativeStyle: NarrativeStyleEnum.optional(),
7554
- pendingUserAction: z38.object({
7555
- kind: z38.string(),
7556
- candidateGrouping: z38.string().optional(),
7557
- selectedImageUrls: z38.array(z38.string()).optional(),
7558
- proposedProductNames: z38.array(z38.string()).optional(),
7559
- customAngleDraft: z38.string().optional(),
7560
- painPointsDraft: z38.string().optional(),
7561
- benefitsDraft: z38.string().optional(),
7562
- featuresDraft: z38.array(z38.string()).optional(),
7799
+ pendingUserAction: z39.object({
7800
+ kind: z39.string(),
7801
+ candidateGrouping: z39.string().optional(),
7802
+ selectedImageUrls: z39.array(z39.string()).optional(),
7803
+ proposedProductNames: z39.array(z39.string()).optional(),
7804
+ customAngleDraft: z39.string().optional(),
7805
+ painPointsDraft: z39.string().optional(),
7806
+ benefitsDraft: z39.string().optional(),
7807
+ featuresDraft: z39.array(z39.string()).optional(),
7563
7808
  // Recommendation consumed only when the caller explicitly sends
7564
7809
  // gapDurationSeconds: 30 in the angle approval action.
7565
7810
  gapDurationChoice: GapDurationChoiceMirror.optional()
@@ -7881,54 +8126,54 @@ __name(registerTaskTools, "registerTaskTools");
7881
8126
 
7882
8127
  // src/tools/creatives.ts
7883
8128
  import { randomUUID } from "crypto";
7884
- import { z as z39 } from "zod";
8129
+ import { z as z40 } from "zod";
7885
8130
  var CreativeRefs = {
7886
8131
  images: ImagesInput,
7887
8132
  language: Lang.describe('Idioma del contenido. Ej. "es", "en", "pt". REQUERIDO \u2014 si el usuario no lo dijo, PREG\xDANTALE antes de invocar. NUNCA infieras desde el contexto del chat ni asumas un default.'),
7888
8133
  country: Country.describe('Pa\xEDs objetivo ISO. Ej. "CO", "MX", "US", "AR". Define moneda, m\xE9todos de pago y tono. REQUERIDO \u2014 si el usuario no lo dijo, PREG\xDANTALE antes de invocar. NUNCA infieras desde el contexto del chat ni asumas un default.'),
7889
- price: z39.string().min(1).max(120).optional().describe('Precio en formato libre \u2014 ej. "$89.900", "USD 29.99". Opcional pero MUY IMPACTANTE para conversi\xF3n: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si el usuario te dice "t\xFA elige", omite el campo y la IA del backend sugiere uno.'),
7890
- offer: z39.string().min(1).max(240).optional().describe('Oferta a destacar \u2014 ej. "2x1", "Env\xEDo gratis", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si el usuario te dice "t\xFA elige", omite el campo.'),
7891
- compareAtPrice: z39.string().min(1).max(120).optional().describe('Opcional. Precio anterior o de referencia para mostrar descuento. Ej. "$129.000" cuando price="$89.900".'),
7892
- paymentMethods: z39.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago a mostrar. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
7893
- productContext: z39.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
7894
- productLabel: z39.string().trim().max(80).optional().describe('Opcional. Nombre o tipo del producto en pocas palabras (ej. "collar de oro", "quita pelusas para ropa"). Es AUTORITATIVO: la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA lo detecta desde la imagen. NO salta la extracci\xF3n.'),
7895
- buyerPersona: z39.string().optional().describe('Opcional. Descripci\xF3n del comprador objetivo. Ej. "Mujeres 35-50, profesionales urbanas". Si se omite, la IA infiere desde el \xE1ngulo y producto.'),
7896
- isCombo: z39.boolean().optional().describe("Opcional. Si true, el creativo presenta el producto como combo/pack. Default false."),
7897
- competitorScanEnabled: z39.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
7898
- brandId: z39.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer el creativo."),
7899
- creativeDirection: z39.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Aplica SOLO a im\xE1genes y landing. Ej. im\xE1genes: "amarillo, azul y rojo, minimalista"; landing: "elegante, paleta fr\xEDa". Para video usa scriptNotes (ambiente/escenario). M\xE1x 500 caracteres.')
8134
+ price: z40.string().min(1).max(120).optional().describe('Precio en formato libre \u2014 ej. "$89.900", "USD 29.99". Opcional pero MUY IMPACTANTE para conversi\xF3n: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si el usuario te dice "t\xFA elige", omite el campo y la IA del backend sugiere uno.'),
8135
+ offer: z40.string().min(1).max(240).optional().describe('Oferta a destacar \u2014 ej. "2x1", "Env\xEDo gratis", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si el usuario te dice "t\xFA elige", omite el campo.'),
8136
+ compareAtPrice: z40.string().min(1).max(120).optional().describe('Opcional. Precio anterior o de referencia para mostrar descuento. Ej. "$129.000" cuando price="$89.900".'),
8137
+ paymentMethods: z40.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago a mostrar. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
8138
+ productContext: z40.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
8139
+ productLabel: z40.string().trim().max(80).optional().describe('Opcional. Nombre o tipo del producto en pocas palabras (ej. "collar de oro", "quita pelusas para ropa"). Es AUTORITATIVO: la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA lo detecta desde la imagen. NO salta la extracci\xF3n.'),
8140
+ buyerPersona: z40.string().optional().describe('Opcional. Descripci\xF3n del comprador objetivo. Ej. "Mujeres 35-50, profesionales urbanas". Si se omite, la IA infiere desde el \xE1ngulo y producto.'),
8141
+ isCombo: z40.boolean().optional().describe("Opcional. Si true, el creativo presenta el producto como combo/pack. Default false."),
8142
+ competitorScanEnabled: z40.boolean().optional().describe("Opcional. Si true (default), el sistema analiza ads de competencia en Meta Ad Library y enriquece los \xE1ngulos generados con hooks y painPoints validados por el mercado (\u2248$0.20-0.48 extra por task \u2014 var\xEDa con cu\xE1ntas transcripciones de video se capturen). Si false, el pipeline genera \xE1ngulos sin esa se\xF1al \u2014 usa esto SOLO si el usuario ya tiene su propia investigaci\xF3n de mercado o quiere ahorrar costos en productos muy nicho sin presencia en Meta. Default true."),
8143
+ brandId: z40.string().uuid().optional().describe("Opcional. ID de una marca del usuario; si se env\xEDa, el sistema usa su voz, colores y persona para enriquecer el creativo."),
8144
+ creativeDirection: z40.string().max(500).optional().describe('Opcional. Direcci\xF3n creativa en texto libre (colores, mood, estilo). M\xC1XIMA PRIORIDAD: anula marca, colores y estilo por defecto. Aplica SOLO a im\xE1genes y landing. Ej. im\xE1genes: "amarillo, azul y rojo, minimalista"; landing: "elegante, paleta fr\xEDa". Para video usa scriptNotes (ambiente/escenario). M\xE1x 500 caracteres.')
7900
8145
  };
7901
- var VideoInput = z39.object({
8146
+ var VideoInput = z40.object({
7902
8147
  images: CreativeRefs.images,
7903
- title: z39.string().min(1).describe('T\xEDtulo corto del producto. Aparece en la primera vista del video. Ej. "Crema Anti-Edad Premium".'),
7904
- primaryAngle: z39.string().min(1).describe('\xC1ngulo de venta principal (frase del beneficio). Ej. "Reduce arrugas en 14 d\xEDas" o "Para quienes odian las cremas grasosas".'),
8148
+ title: z40.string().min(1).describe('T\xEDtulo corto del producto. Aparece en la primera vista del video. Ej. "Crema Anti-Edad Premium".'),
8149
+ primaryAngle: z40.string().min(1).describe('\xC1ngulo de venta principal (frase del beneficio). Ej. "Reduce arrugas en 14 d\xEDas" o "Para quienes odian las cremas grasosas".'),
7905
8150
  language: CreativeRefs.language,
7906
8151
  country: CreativeRefs.country,
7907
- aspectRatio: z39.enum([
8152
+ aspectRatio: z40.enum([
7908
8153
  "9:16",
7909
8154
  "16:9",
7910
8155
  "Auto"
7911
8156
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
7912
- durationSeconds: z39.union([
7913
- z39.literal(15),
7914
- z39.literal(30)
8157
+ durationSeconds: z40.union([
8158
+ z40.literal(15),
8159
+ z40.literal(30)
7915
8160
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
7916
8161
  videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan tanto a 15s como a 30s; el formato de 30s conserva el ritmo multi-escena. Opciones: "auto", "ugc_dynamic" (UGC cl\xE1sico testimonial multi-hook), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "cinematic" (historia visual premium), "pixar_problem" (problema animado 3D estilo Pixar), "pixar_sassy" (producto que habla con personalidad), "product_showcase" (sin actor, solo producto), "wearable_showcase" (ropa/accesorios puestos).'),
7917
8162
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista"; para durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
7918
8163
  videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "720p" = m\xE1xima nitidez (RECOMENDADO, default si se omite); "480p" = render m\xE1s econ\xF3mico. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p).'),
7919
- features: z39.array(z39.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
8164
+ features: z40.array(z40.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
7920
8165
  price: CreativeRefs.price,
7921
8166
  offer: CreativeRefs.offer,
7922
8167
  productContext: CreativeRefs.productContext,
7923
8168
  productLabel: CreativeRefs.productLabel,
7924
- cta: z39.string().optional().describe('Opcional. Call-to-action final del video. Ej. "Compra ahora", "Pide el tuyo hoy", "Link en bio". Si se omite, la IA escribe uno.'),
7925
- scriptNotes: z39.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
7926
- pauseForVideoScriptReview: z39.boolean().optional().describe("Opcional. Si true, la tarea pausa despu\xE9s de generar el guion y antes de renderizar. \xDAsalo cuando el usuario quiera editar el guion antes de gastar cr\xE9ditos en el video. Tras la pausa: get_video_script para leerlo, approve_video_script para aprobar (con o sin ediciones). Default false."),
7927
- pauseForAngleReview: z39.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
8169
+ cta: z40.string().optional().describe('Opcional. Call-to-action final del video. Ej. "Compra ahora", "Pide el tuyo hoy", "Link en bio". Si se omite, la IA escribe uno.'),
8170
+ scriptNotes: z40.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
8171
+ pauseForVideoScriptReview: z40.boolean().optional().describe("Opcional. Si true, la tarea pausa despu\xE9s de generar el guion y antes de renderizar. \xDAsalo cuando el usuario quiera editar el guion antes de gastar cr\xE9ditos en el video. Tras la pausa: get_video_script para leerlo, approve_video_script para aprobar (con o sin ediciones). Default false."),
8172
+ pauseForAngleReview: z40.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
7928
8173
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
7929
8174
  brandId: CreativeRefs.brandId
7930
8175
  }).strict();
7931
- var FreeVideoQualityEnum = z39.enum([
8176
+ var FreeVideoQualityEnum = z40.enum([
7932
8177
  "economical",
7933
8178
  "high_detail"
7934
8179
  ]);
@@ -7950,38 +8195,38 @@ var FREE_VIDEO_DURATIONS_BY_QUALITY = {
7950
8195
  15
7951
8196
  ]
7952
8197
  };
7953
- var FreeVideoInput = z39.object({
8198
+ var FreeVideoInput = z40.object({
7954
8199
  images: ImagesInput.optional().describe("Opcional. 1-5 fotos del producto. Om\xEDtelo para generar desde texto puro."),
7955
- rawPrompt: z39.string().trim().min(1).max(2e3).describe("Prompt o guion exacto del video. Si activas subt\xEDtulos, este mismo texto se usa como guion de subt\xEDtulos."),
8200
+ rawPrompt: z40.string().trim().min(1).max(2e3).describe("Prompt o guion exacto del video. Si activas subt\xEDtulos, este mismo texto se usa como guion de subt\xEDtulos."),
7956
8201
  quality: FreeVideoQualityEnum.describe('Calidad del video: "economical" = Econ\xF3mico o "high_detail" = Alto detalle.'),
7957
- durationSeconds: z39.number().int().min(1).max(15).describe("Duraci\xF3n exacta. Econ\xF3mico permite 6, 8, 10 o 15s; Alto detalle permite 4, 8, 12 o 15s."),
8202
+ durationSeconds: z40.number().int().min(1).max(15).describe("Duraci\xF3n exacta. Econ\xF3mico permite 6, 8, 10 o 15s; Alto detalle permite 4, 8, 12 o 15s."),
7958
8203
  videoResolution: VideoResolutionEnum.describe("Resoluci\xF3n del video: 480p o 720p."),
7959
- generateAiMusic: z39.boolean().default(false).describe("Opcional. Agrega m\xFAsica instrumental IA debajo del audio nativo. Default false."),
7960
- addSubtitles: z39.boolean().default(false).describe("Opcional. Quema subt\xEDtulos usando el rawPrompt exacto. Default false."),
7961
- idempotencyKey: z39.string().uuid().optional().describe("Opcional. UUID para reintentar exactamente esta misma solicitud sin crear ni cobrar otro video. Reutiliza el mismo valor si el cliente no recibi\xF3 la respuesta.")
8204
+ generateAiMusic: z40.boolean().default(false).describe("Opcional. Agrega m\xFAsica instrumental IA debajo del audio nativo. Default false."),
8205
+ addSubtitles: z40.boolean().default(false).describe("Opcional. Quema subt\xEDtulos usando el rawPrompt exacto. Default false."),
8206
+ idempotencyKey: z40.string().uuid().optional().describe("Opcional. UUID para reintentar exactamente esta misma solicitud sin crear ni cobrar otro video. Reutiliza el mismo valor si el cliente no recibi\xF3 la respuesta.")
7962
8207
  }).strict();
7963
8208
  var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
7964
8209
  var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
7965
8210
  var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
7966
8211
  var aspectRatioList = ImageCreativeAspectRatioEnum.options.join(" | ");
7967
- var ImageInput = z39.object({
8212
+ var ImageInput = z40.object({
7968
8213
  images: CreativeRefs.images,
7969
- productTitle: z39.string().min(1).describe('Nombre del producto. Ej. "Crema Anti-Edad Premium".'),
7970
- primaryAngle: z39.string().min(1).describe('\xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas".'),
8214
+ productTitle: z40.string().min(1).describe('Nombre del producto. Ej. "Crema Anti-Edad Premium".'),
8215
+ primaryAngle: z40.string().min(1).describe('\xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas".'),
7971
8216
  buyerPersona: CreativeRefs.buyerPersona,
7972
8217
  language: CreativeRefs.language,
7973
8218
  country: CreativeRefs.country,
7974
- nVariants: z39.number().int().min(1).max(6).optional().describe("Opcional. N\xFAmero de variantes a generar (1-6). Default 3."),
7975
- imageModel: z39.string().optional().describe('Opcional. Modelo de imagen. Ej. "gpt-image-2", "nano-banana-2". Om\xEDtelo para usar el default optimizado por costo/calidad.'),
8219
+ nVariants: z40.number().int().min(1).max(6).optional().describe("Opcional. N\xFAmero de variantes a generar (1-6). Default 3."),
8220
+ imageModel: z40.string().optional().describe('Opcional. Modelo de imagen. Ej. "gpt-image-2", "nano-banana-2". Om\xEDtelo para usar el default optimizado por costo/calidad.'),
7976
8221
  imageStyle: ImageCreativeStyleEnum.optional().describe(`Opcional. Estilo del shot del producto. Valores: ${imageStyleList}. Si se omite, la IA escoge seg\xFAn el archetype.`),
7977
8222
  creativeArchetype: ImageCreativeArchetypeEnum.optional().describe(`Opcional. Arquetipo CRO de la composici\xF3n. Valores: ${archetypeList}. Si se omite, la IA escoge seg\xFAn \xE1ngulo+oferta (ej. "offer_value_prop" cuando hay descuento, "social_proof_trust" cuando el \xE1ngulo es testimonial).`),
7978
8223
  visualTreatment: ImageCreativeVisualTreatmentEnum.optional().describe(`Opcional. Tratamiento gr\xE1fico. Valores: ${visualTreatmentList} ("performance_product_ad" = dealer-style alto CTR para Meta/TikTok ads; "premium_dtc_carousel" = fotograf\xEDa limpia estilo Allbirds/Glossier). Si se omite, la IA escoge.`),
7979
- styleMix: z39.boolean().optional().describe("Opcional. Si true, las N variantes mezclan distintos imageStyle para A/B test. Si false/omitido, todas comparten estilo. Default false."),
8224
+ styleMix: z40.boolean().optional().describe("Opcional. Si true, las N variantes mezclan distintos imageStyle para A/B test. Si false/omitido, todas comparten estilo. Default false."),
7980
8225
  aspectRatio: ImageCreativeAspectRatioEnum.optional().describe(`Opcional. Relaci\xF3n de aspecto. Valores: ${aspectRatioList}. Default "4:5" (vertical Instagram feed).`),
7981
- generateAdCopy: z39.boolean().optional().describe("Opcional. Si true, tambi\xE9n genera headline/body/CTA para el ad. Default false."),
7982
- pauseForAngleReview: z39.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo y antes de las im\xE1genes para que el usuario edite v\xEDa edit_task_draft. Default false."),
7983
- features: z39.array(z39.string()).optional().describe('Opcional. Features/beneficios a destacar visualmente. Ej. ["resistente al agua", "ultra liviano"].'),
7984
- brandColors: z39.array(z39.string()).optional().describe('Opcional. Paleta de marca en hex. Ej. ["#FF6B35", "#004E89"]. Si se omite, la IA elige seg\xFAn el producto.'),
8226
+ generateAdCopy: z40.boolean().optional().describe("Opcional. Si true, tambi\xE9n genera headline/body/CTA para el ad. Default false."),
8227
+ pauseForAngleReview: z40.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo y antes de las im\xE1genes para que el usuario edite v\xEDa edit_task_draft. Default false."),
8228
+ features: z40.array(z40.string()).optional().describe('Opcional. Features/beneficios a destacar visualmente. Ej. ["resistente al agua", "ultra liviano"].'),
8229
+ brandColors: z40.array(z40.string()).optional().describe('Opcional. Paleta de marca en hex. Ej. ["#FF6B35", "#004E89"]. Si se omite, la IA elige seg\xFAn el producto.'),
7985
8230
  price: CreativeRefs.price,
7986
8231
  compareAtPrice: CreativeRefs.compareAtPrice,
7987
8232
  offer: CreativeRefs.offer,
@@ -7992,11 +8237,11 @@ var ImageInput = z39.object({
7992
8237
  brandId: CreativeRefs.brandId,
7993
8238
  creativeDirection: CreativeRefs.creativeDirection
7994
8239
  });
7995
- var LandingInput = z39.object({
8240
+ var LandingInput = z40.object({
7996
8241
  images: CreativeRefs.images,
7997
8242
  language: CreativeRefs.language,
7998
8243
  country: CreativeRefs.country,
7999
- salesAngle: z39.string().optional().describe('Opcional. \xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas". Si se omite, la IA lo deduce de la imagen.'),
8244
+ salesAngle: z40.string().optional().describe('Opcional. \xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas". Si se omite, la IA lo deduce de la imagen.'),
8000
8245
  buyerPersona: CreativeRefs.buyerPersona,
8001
8246
  price: CreativeRefs.price,
8002
8247
  compareAtPrice: CreativeRefs.compareAtPrice,
@@ -8094,8 +8339,8 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8094
8339
  __name(registerCreativeTools, "registerCreativeTools");
8095
8340
 
8096
8341
  // src/tools/creative-variants.ts
8097
- import { z as z40 } from "zod";
8098
- var LandingSectionKeyEnum = z40.enum([
8342
+ import { z as z41 } from "zod";
8343
+ var LandingSectionKeyEnum = z41.enum([
8099
8344
  "problem_hook",
8100
8345
  "problem_cost",
8101
8346
  "solution_mechanism",
@@ -8135,24 +8380,24 @@ var LandingSectionKeyEnum = z40.enum([
8135
8380
  "lifestyle_fit",
8136
8381
  "join_lifestyle"
8137
8382
  ]);
8138
- var CreativeVariantKindEnum = z40.enum([
8383
+ var CreativeVariantKindEnum = z41.enum([
8139
8384
  "video",
8140
8385
  "image",
8141
8386
  "landing"
8142
8387
  ]);
8143
- var EstimateCreativeVariantInputBase = z40.object({
8144
- sourceTaskId: z40.string().uuid().describe("ID de la tarea original completada."),
8388
+ var EstimateCreativeVariantInputBase = z41.object({
8389
+ sourceTaskId: z41.string().uuid().describe("ID de la tarea original completada."),
8145
8390
  kind: CreativeVariantKindEnum,
8146
8391
  videoPreset: TaskCreativeVariantVideoPresetEnum.optional().describe("Opcional para video. Solo cambia el preset p\xFAblico; el servidor conserva el modelo/tier del task origen."),
8147
8392
  videoResolution: VideoResolutionEnum.optional().describe("Opcional para video. Resoluci\xF3n del nuevo render."),
8148
- nVariants: z40.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8393
+ nVariants: z41.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8149
8394
  sectionKey: LandingSectionKeyEnum.optional().describe("Opcional para landing. Si ya conoces la secci\xF3n, evita cotizar la clasificaci\xF3n con IA."),
8150
- instructions: z40.string().trim().min(1).max(2e3).optional().describe("Opcional para landing. Si se env\xEDa sin sectionKey, el estimado suma la clasificaci\xF3n pagada con IA.")
8395
+ instructions: z41.string().trim().min(1).max(2e3).optional().describe("Opcional para landing. Si se env\xEDa sin sectionKey, el estimado suma la clasificaci\xF3n pagada con IA.")
8151
8396
  }).strict();
8152
8397
  var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine((input, ctx) => {
8153
8398
  if (input.kind === "video" && input.nVariants !== void 0) {
8154
8399
  ctx.addIssue({
8155
- code: z40.ZodIssueCode.custom,
8400
+ code: z41.ZodIssueCode.custom,
8156
8401
  path: [
8157
8402
  "nVariants"
8158
8403
  ],
@@ -8162,7 +8407,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8162
8407
  if (input.kind !== "video") {
8163
8408
  if (input.videoPreset !== void 0) {
8164
8409
  ctx.addIssue({
8165
- code: z40.ZodIssueCode.custom,
8410
+ code: z41.ZodIssueCode.custom,
8166
8411
  path: [
8167
8412
  "videoPreset"
8168
8413
  ],
@@ -8171,7 +8416,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8171
8416
  }
8172
8417
  if (input.videoResolution !== void 0) {
8173
8418
  ctx.addIssue({
8174
- code: z40.ZodIssueCode.custom,
8419
+ code: z41.ZodIssueCode.custom,
8175
8420
  path: [
8176
8421
  "videoResolution"
8177
8422
  ],
@@ -8181,7 +8426,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8181
8426
  }
8182
8427
  if (input.kind !== "image" && input.nVariants !== void 0) {
8183
8428
  ctx.addIssue({
8184
- code: z40.ZodIssueCode.custom,
8429
+ code: z41.ZodIssueCode.custom,
8185
8430
  path: [
8186
8431
  "nVariants"
8187
8432
  ],
@@ -8191,7 +8436,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8191
8436
  if (input.kind !== "landing") {
8192
8437
  if (input.sectionKey !== void 0) {
8193
8438
  ctx.addIssue({
8194
- code: z40.ZodIssueCode.custom,
8439
+ code: z41.ZodIssueCode.custom,
8195
8440
  path: [
8196
8441
  "sectionKey"
8197
8442
  ],
@@ -8200,7 +8445,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8200
8445
  }
8201
8446
  if (input.instructions !== void 0) {
8202
8447
  ctx.addIssue({
8203
- code: z40.ZodIssueCode.custom,
8448
+ code: z41.ZodIssueCode.custom,
8204
8449
  path: [
8205
8450
  "instructions"
8206
8451
  ],
@@ -8209,31 +8454,31 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8209
8454
  }
8210
8455
  }
8211
8456
  });
8212
- var CreateCreativeVariantInputBase = z40.object({
8213
- sourceTaskId: z40.string().uuid().describe("ID de la tarea original completada."),
8457
+ var CreateCreativeVariantInputBase = z41.object({
8458
+ sourceTaskId: z41.string().uuid().describe("ID de la tarea original completada."),
8214
8459
  kind: CreativeVariantKindEnum,
8215
- confirm: z40.literal(true).describe("Confirmaci\xF3n expl\xEDcita del usuario despu\xE9s de mostrarle el estimado de cr\xE9ditos."),
8216
- clientRequestId: z40.string().uuid().describe("UUID estable y obligatorio para esta intenci\xF3n. Reutil\xEDzalo ante timeouts o transporte ambiguo. Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n y un UUID nuevo."),
8217
- angle: z40.string().trim().min(1).max(500).optional().describe("\xC1ngulo nuevo para video o imagen."),
8218
- buyerPersona: z40.string().trim().min(1).max(360).optional().describe("Buyer persona coherente con el \xE1ngulo."),
8219
- scriptNotes: z40.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n breve del guion para una variante de video."),
8220
- creativeDirection: z40.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n creativa para una variante de imagen."),
8460
+ confirm: z41.literal(true).describe("Confirmaci\xF3n expl\xEDcita del usuario despu\xE9s de mostrarle el estimado de cr\xE9ditos."),
8461
+ clientRequestId: z41.string().uuid().describe("UUID estable y obligatorio para esta intenci\xF3n. Reutil\xEDzalo ante timeouts o transporte ambiguo. Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n y un UUID nuevo."),
8462
+ angle: z41.string().trim().min(1).max(500).optional().describe("\xC1ngulo nuevo para video o imagen."),
8463
+ buyerPersona: z41.string().trim().min(1).max(360).optional().describe("Buyer persona coherente con el \xE1ngulo."),
8464
+ scriptNotes: z41.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n breve del guion para una variante de video."),
8465
+ creativeDirection: z41.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n creativa para una variante de imagen."),
8221
8466
  videoPreset: TaskCreativeVariantVideoPresetEnum.optional().describe("Opcional para video. Preset p\xFAblico; el servidor resuelve modelo/tier desde el task origen."),
8222
8467
  videoResolution: VideoResolutionEnum.optional().describe("Opcional para video."),
8223
- generateAiMusic: z40.boolean().optional().describe("Opcional para video."),
8224
- nVariants: z40.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8468
+ generateAiMusic: z41.boolean().optional().describe("Opcional para video."),
8469
+ nVariants: z41.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8225
8470
  imageStyle: ImageCreativeStyleEnum.optional().describe("Opcional para imagen."),
8226
8471
  creativeArchetype: ImageCreativeArchetypeEnum.optional().describe("Opcional para imagen."),
8227
8472
  visualTreatment: ImageCreativeVisualTreatmentEnum.optional().describe("Opcional para imagen."),
8228
8473
  aspectRatio: ImageCreativeAspectRatioEnum.optional().describe("Opcional para imagen."),
8229
- styleMix: z40.boolean().optional().describe("Opcional para imagen."),
8474
+ styleMix: z41.boolean().optional().describe("Opcional para imagen."),
8230
8475
  sectionKey: LandingSectionKeyEnum.optional().describe("Opcional para landing. Si se omite, se clasifica con IA a partir de instructions."),
8231
- instructions: z40.string().trim().min(1).max(2e3).optional().describe("Solicitud libre para una nueva secci\xF3n de landing. La IA la clasifica si no env\xEDas sectionKey.")
8476
+ instructions: z41.string().trim().min(1).max(2e3).optional().describe("Solicitud libre para una nueva secci\xF3n de landing. La IA la clasifica si no env\xEDas sectionKey.")
8232
8477
  }).strict();
8233
8478
  var CreateCreativeVariantInput = CreateCreativeVariantInputBase.superRefine((input, ctx) => {
8234
8479
  if ((input.kind === "video" || input.kind === "image") && !input.angle) {
8235
8480
  ctx.addIssue({
8236
- code: z40.ZodIssueCode.custom,
8481
+ code: z41.ZodIssueCode.custom,
8237
8482
  path: [
8238
8483
  "angle"
8239
8484
  ],
@@ -8242,7 +8487,7 @@ var CreateCreativeVariantInput = CreateCreativeVariantInputBase.superRefine((inp
8242
8487
  }
8243
8488
  if (input.kind === "landing" && !input.sectionKey && !input.instructions) {
8244
8489
  ctx.addIssue({
8245
- code: z40.ZodIssueCode.custom,
8490
+ code: z41.ZodIssueCode.custom,
8246
8491
  path: [
8247
8492
  "instructions"
8248
8493
  ],
@@ -8288,7 +8533,7 @@ var CreateCreativeVariantInput = CreateCreativeVariantInputBase.superRefine((inp
8288
8533
  for (const field of fieldsByKind[input.kind]) {
8289
8534
  if (input[field] !== void 0) {
8290
8535
  ctx.addIssue({
8291
- code: z40.ZodIssueCode.custom,
8536
+ code: z41.ZodIssueCode.custom,
8292
8537
  path: [
8293
8538
  field
8294
8539
  ],
@@ -8405,27 +8650,27 @@ function registerCreativeVariantTools(server, api) {
8405
8650
  __name(registerCreativeVariantTools, "registerCreativeVariantTools");
8406
8651
 
8407
8652
  // src/tools/script-review.ts
8408
- import { z as z41 } from "zod";
8409
- var GetVideoScriptInput = z41.object({
8410
- taskId: z41.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
8411
- angleKey: z41.string().min(1).optional().describe('Clave del \xE1ngulo cuyo guion se quiere leer (formato "angle_0", "angle_1", ...). REQUERIDO para tareas multi-angle: si se omite, el backend resuelve a "angle_0" LITERAL (no "el primer pendiente"), as\xED que en multi-angle donde angle_0 ya est\xE9 aprobado/saltado obtendr\xEDas status="approved" y nunca alcanzar\xEDas el \xE1ngulo realmente pendiente. Para single-angle puede omitirse.')
8653
+ import { z as z42 } from "zod";
8654
+ var GetVideoScriptInput = z42.object({
8655
+ taskId: z42.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
8656
+ angleKey: z42.string().min(1).optional().describe('Clave del \xE1ngulo cuyo guion se quiere leer (formato "angle_0", "angle_1", ...). REQUERIDO para tareas multi-angle: si se omite, el backend resuelve a "angle_0" LITERAL (no "el primer pendiente"), as\xED que en multi-angle donde angle_0 ya est\xE9 aprobado/saltado obtendr\xEDas status="approved" y nunca alcanzar\xEDas el \xE1ngulo realmente pendiente. Para single-angle puede omitirse.')
8412
8657
  }).strict();
8413
8658
  var VIDEO_SCRIPT_APPROVAL_MAX_SCENES2 = 50;
8414
8659
  var VIDEO_SCRIPT_APPROVAL_MAX_DIALOGUE_CHARS2 = 2e3;
8415
- var ApproveSceneEdit = z41.object({
8416
- index: z41.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
8417
- dialogue: z41.string().min(1).max(VIDEO_SCRIPT_APPROVAL_MAX_DIALOGUE_CHARS2).refine((v) => v.trim().length > 0, {
8660
+ var ApproveSceneEdit = z42.object({
8661
+ index: z42.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
8662
+ dialogue: z42.string().min(1).max(VIDEO_SCRIPT_APPROVAL_MAX_DIALOGUE_CHARS2).refine((v) => v.trim().length > 0, {
8418
8663
  message: "dialogue no puede ser s\xF3lo espacios"
8419
8664
  }).describe("Di\xE1logo final de la escena. NO puede estar vac\xEDo ni ser s\xF3lo espacios. Puede ser el original sin cambios o la edici\xF3n del usuario.")
8420
8665
  }).strict();
8421
- var ApproveVideoScriptInput = z41.object({
8422
- taskId: z41.string().min(1).describe("ID de la tarea."),
8423
- angleKey: z41.string().trim().min(1).max(120).optional().describe('Mismo angleKey usado en get_video_script. REQUERIDO en multi-angle (si se omite el backend resuelve a "angle_0" literal). En single-angle puede omitirse.'),
8424
- scenes: z41.array(ApproveSceneEdit).min(1).max(VIDEO_SCRIPT_APPROVAL_MAX_SCENES2).describe("Escenas con los di\xE1logos finales. El backend mergea por `index` \u2014 las escenas NO incluidas conservan el di\xE1logo original. Debe haber entre 1 y 50 entradas. Para cada entrada que mandes con un dialogue distinto del original, el backend valida minWords \u2264 words \u2264 maxWords y rechaza con 400 si te sales del rango.")
8666
+ var ApproveVideoScriptInput = z42.object({
8667
+ taskId: z42.string().min(1).describe("ID de la tarea."),
8668
+ angleKey: z42.string().trim().min(1).max(120).optional().describe('Mismo angleKey usado en get_video_script. REQUERIDO en multi-angle (si se omite el backend resuelve a "angle_0" literal). En single-angle puede omitirse.'),
8669
+ scenes: z42.array(ApproveSceneEdit).min(1).max(VIDEO_SCRIPT_APPROVAL_MAX_SCENES2).describe("Escenas con los di\xE1logos finales. El backend mergea por `index` \u2014 las escenas NO incluidas conservan el di\xE1logo original. Debe haber entre 1 y 50 entradas. Para cada entrada que mandes con un dialogue distinto del original, el backend valida minWords \u2264 words \u2264 maxWords y rechaza con 400 si te sales del rango.")
8425
8670
  }).strict();
8426
- var RegenerateVideoScriptInput = z41.object({
8427
- taskId: z41.string().min(1).describe("ID de la tarea."),
8428
- angleKey: z41.string().min(1).describe('Clave del \xE1ngulo a regenerar. Single-angle: pasa "angle_0". Multi-angle: el mismo que devuelve get_video_script. NO se puede omitir \u2014 el backend lo exige siempre.'),
8671
+ var RegenerateVideoScriptInput = z42.object({
8672
+ taskId: z42.string().min(1).describe("ID de la tarea."),
8673
+ angleKey: z42.string().min(1).describe('Clave del \xE1ngulo a regenerar. Single-angle: pasa "angle_0". Multi-angle: el mismo que devuelve get_video_script. NO se puede omitir \u2014 el backend lo exige siempre.'),
8429
8674
  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).')
8430
8675
  }).strict();
8431
8676
  function registerScriptReviewTools(server, api) {
@@ -8465,9 +8710,9 @@ function registerScriptReviewTools(server, api) {
8465
8710
  __name(registerScriptReviewTools, "registerScriptReviewTools");
8466
8711
 
8467
8712
  // src/tools/connections.ts
8468
- import { z as z42 } from "zod";
8713
+ import { z as z43 } from "zod";
8469
8714
  function registerConnectionTools(server, api) {
8470
- 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.", z42.object({}).shape, async () => {
8715
+ 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.", z43.object({}).shape, async () => {
8471
8716
  try {
8472
8717
  const r = await api.getJson("/v1/shopify/shops");
8473
8718
  return asText(r);
@@ -8475,10 +8720,10 @@ function registerConnectionTools(server, api) {
8475
8720
  throw translateError(e);
8476
8721
  }
8477
8722
  });
8478
- server.tool("list_shopify_products", 'Busca productos en una tienda Shopify conectada para elegir el target de una republicaci\xF3n. Usa el `id` devuelto junto con `publicationMode="existing"` en `publish_to_shopify`. Si `taskId` se proporciona, la respuesta tambi\xE9n indica el producto publicado actualmente para esa tarea y tienda. No modifica Shopify.', z42.object({
8479
- shopDomain: z42.string().min(3).max(120),
8480
- query: z42.string().trim().max(120).optional(),
8481
- taskId: z42.string().uuid().optional()
8723
+ server.tool("list_shopify_products", 'Busca productos en una tienda Shopify conectada para elegir el target de una republicaci\xF3n. Usa el `id` devuelto junto con `publicationMode="existing"` en `publish_to_shopify`. Si `taskId` se proporciona, la respuesta tambi\xE9n indica el producto publicado actualmente para esa tarea y tienda. No modifica Shopify.', z43.object({
8724
+ shopDomain: z43.string().min(3).max(120),
8725
+ query: z43.string().trim().max(120).optional(),
8726
+ taskId: z43.string().uuid().optional()
8482
8727
  }).shape, async ({ shopDomain, query, taskId }) => {
8483
8728
  try {
8484
8729
  const params = new URLSearchParams({
@@ -8492,7 +8737,7 @@ function registerConnectionTools(server, api) {
8492
8737
  throw translateError(e);
8493
8738
  }
8494
8739
  });
8495
- server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z42.object({}).shape, async () => {
8740
+ server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z43.object({}).shape, async () => {
8496
8741
  try {
8497
8742
  const r = await api.getJson("/v1/facebook/ad-accounts");
8498
8743
  return asText(r);
@@ -8500,7 +8745,7 @@ function registerConnectionTools(server, api) {
8500
8745
  throw translateError(e);
8501
8746
  }
8502
8747
  });
8503
- 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.", z42.object({}).shape, async () => {
8748
+ 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.", z43.object({}).shape, async () => {
8504
8749
  try {
8505
8750
  const r = await api.getJson("/v1/tiktok/advertisers");
8506
8751
  return asText(r);
@@ -8512,115 +8757,128 @@ function registerConnectionTools(server, api) {
8512
8757
  __name(registerConnectionTools, "registerConnectionTools");
8513
8758
 
8514
8759
  // src/tools/publish.ts
8515
- import { z as z43 } from "zod";
8516
- var ShopifyInput = z43.object({
8517
- taskId: z43.string().min(1),
8518
- shopDomain: z43.string().min(1),
8760
+ import { z as z44 } from "zod";
8761
+ var ShopifyInput = z44.object({
8762
+ taskId: z44.string().min(1),
8763
+ shopDomain: z44.string().min(1),
8519
8764
  /** Override: number of landing images to include. Default = ALL images the
8520
8765
  * task generated (the MCP fetches /v1/results first to count). Bounded
8521
8766
  * generously here; the backend validates each index against
8522
8767
  * landing.images.length and rejects out-of-range. */
8523
- numLandingImages: z43.number().int().min(1).max(20).optional(),
8524
- variantIndex: z43.number().int().min(0).optional(),
8768
+ numLandingImages: z44.number().int().min(1).max(20).optional(),
8769
+ variantIndex: z44.number().int().min(0).optional(),
8525
8770
  /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
8526
8771
  * When true, the landing renders variant-picker pills; when false, it ships
8527
8772
  * a single-option product page. Defaults to false. */
8528
- showVariantPicker: z43.boolean().optional(),
8773
+ showVariantPicker: z44.boolean().optional(),
8529
8774
  /** Default `auto` updates the product previously published for this task and
8530
8775
  * shop. Use `existing` with targetProductId to choose another product, or
8531
8776
  * `new` only when the user explicitly wants a duplicate/new listing. */
8532
- publicationMode: z43.enum([
8777
+ publicationMode: z44.enum([
8533
8778
  "auto",
8534
8779
  "existing",
8535
8780
  "new"
8536
8781
  ]).optional(),
8537
8782
  /** Shopify product GID, numeric id, or admin product URL. Required when
8538
8783
  * publicationMode is `existing`. */
8539
- targetProductId: z43.string().trim().min(1).max(300).optional()
8784
+ targetProductId: z44.string().trim().min(1).max(300).optional()
8540
8785
  });
8541
- var McpCampaignConfig = z43.object({
8786
+ var McpCampaignConfig = z44.object({
8542
8787
  /** ALWAYS in US dollars, whatever the ad account's currency is. ProductMaker
8543
8788
  * converts to the account currency server-side. Do NOT pre-convert: on a COP
8544
8789
  * account the user's "60.000 al día" is `dailyBudgetUsd: 15`, not 60000. */
8545
- dailyBudgetUsd: z43.number().positive().optional(),
8790
+ dailyBudgetUsd: z44.number().positive().optional(),
8546
8791
  /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
8547
8792
  * ad set). TikTok takes only the first entry. */
8548
- targetCountries: z43.array(z43.string().length(2)).max(1).optional(),
8549
- campaignName: z43.string().trim().min(1).max(100).optional()
8550
- });
8551
- var MetaInput = z43.object({
8552
- taskId: z43.string().min(1),
8553
- adAccountId: z43.string().min(1),
8554
- pixelId: z43.string().min(1).optional(),
8555
- landingUrl: z43.string().url().optional(),
8793
+ targetCountries: z44.array(z44.string().length(2)).max(1).optional(),
8794
+ campaignName: z44.string().trim().min(1).max(100).optional()
8795
+ });
8796
+ var McpAdAccountId = MetaAdAccountId.describe('Meta ad account id. Accepts "act_1234567890" or the bare numeric id "1234567890".');
8797
+ var MetaInput = z44.object({
8798
+ taskId: z44.string().min(1),
8799
+ adAccountId: McpAdAccountId,
8800
+ pixelId: z44.string().min(1).optional(),
8801
+ landingUrl: z44.string().url().optional(),
8556
8802
  /** Select which angles to publish by 0-based index. Omit to publish all
8557
8803
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
8558
- angleIndexes: z43.array(z43.number().int().nonnegative()).optional(),
8804
+ angleIndexes: z44.array(z44.number().int().nonnegative()).optional(),
8559
8805
  campaignConfig: McpCampaignConfig.optional(),
8560
8806
  /** Override the ad's primary text (body copy). If omitted, ProductMaker
8561
8807
  * uses the auto-generated copy attached to the task. Capped at 500 chars
8562
8808
  * (Meta's primary-text limit). */
8563
- primaryText: z43.string().trim().min(1).max(500).optional(),
8809
+ primaryText: z44.string().trim().min(1).max(500).optional(),
8564
8810
  /** Optional WhatsApp number. When set, the ad button opens a WhatsApp chat
8565
8811
  * (wa.me) instead of a landing page. */
8566
- phoneNumber: z43.string().min(8).max(32).optional()
8812
+ phoneNumber: z44.string().min(8).max(32).optional()
8567
8813
  });
8568
- var GroupedTaskSource = z43.object({
8569
- type: z43.literal("task"),
8570
- taskId: z43.string().min(1),
8571
- angleIndex: z43.number().int().min(0).optional(),
8572
- videoVariant: z43.enum([
8814
+ var GroupedTaskSource = z44.object({
8815
+ type: z44.literal("task"),
8816
+ taskId: z44.string().min(1),
8817
+ angleIndex: z44.number().int().min(0).optional(),
8818
+ videoVariant: z44.enum([
8573
8819
  "with_subtitles",
8574
8820
  "without_subtitles"
8575
8821
  ]).optional(),
8576
- imageVariantIndex: z43.number().int().min(0).optional()
8577
- });
8578
- var GroupedMediaSource = z43.object({
8579
- type: z43.literal("media"),
8580
- url: z43.string().url().optional(),
8581
- path: z43.string().min(1).optional(),
8582
- thumbnailUrl: z43.string().url().optional(),
8583
- thumbnailPath: z43.string().min(1).optional()
8822
+ imageVariantIndex: z44.number().int().min(0).optional()
8823
+ });
8824
+ var GroupedMediaSource = z44.object({
8825
+ type: z44.literal("media"),
8826
+ url: z44.string().url().optional(),
8827
+ path: z44.string().min(1).optional(),
8828
+ thumbnailUrl: z44.string().url().optional(),
8829
+ thumbnailPath: z44.string().min(1).optional()
8584
8830
  }).refine((value) => Boolean(value.url) !== Boolean(value.path), {
8585
8831
  message: "media source needs exactly one of `url` or `path`"
8586
8832
  }).refine((value) => !(value.thumbnailUrl && value.thumbnailPath), {
8587
8833
  message: "media source needs at most one of `thumbnailUrl` or `thumbnailPath`"
8588
8834
  });
8589
- var GroupedCreative = z43.object({
8590
- type: z43.enum([
8835
+ var GroupedCreative = z44.object({
8836
+ type: z44.enum([
8591
8837
  "image",
8592
8838
  "video"
8593
8839
  ]),
8594
- source: z43.union([
8840
+ source: z44.union([
8595
8841
  GroupedTaskSource,
8596
8842
  GroupedMediaSource
8597
8843
  ]),
8598
- title: z43.string().max(120).optional(),
8599
- description: z43.string().max(500).optional(),
8844
+ title: z44.string().max(120).optional(),
8845
+ description: z44.string().max(500).optional(),
8600
8846
  cta: MetaCtaType.optional(),
8601
- creativeId: z43.string().min(1).optional()
8847
+ creativeId: z44.string().min(1).optional()
8602
8848
  });
8603
- var GroupedCampaignConfig = z43.object({
8849
+ var GroupedCampaignConfig = z44.object({
8604
8850
  campaignType: MetaCampaignType.default("sales"),
8605
8851
  optimizationMode: MetaOptimizationMode.default("advantage_plus"),
8606
8852
  budgetMode: MetaBudgetMode.default("campaign"),
8607
- campaignDailyBudget: z43.number().int().positive().optional(),
8608
- adSets: z43.array(z43.object({
8609
- tempId: z43.string().min(1).max(80),
8610
- name: z43.string().min(1).max(100),
8611
- dailyBudget: z43.number().int().positive().optional(),
8612
- creativeIds: z43.array(z43.string().min(1)).min(1)
8613
- })).min(1).max(4),
8614
- destination: z43.object({
8615
- type: z43.literal("landing"),
8616
- landingUrl: z43.string().url()
8617
- }),
8618
- saveAsDefault: z43.boolean().default(false),
8853
+ campaignDailyBudget: z44.number().int().positive().optional(),
8854
+ adSets: z44.array(z44.object({
8855
+ tempId: z44.string().min(1).max(80),
8856
+ name: z44.string().min(1).max(100),
8857
+ dailyBudget: z44.number().int().positive().optional(),
8858
+ creativeIds: z44.array(z44.string().min(1)).min(1)
8859
+ })).min(1).max(CAMPAIGN_LIMITS.MAX_AD_SETS),
8860
+ /**
8861
+ * Valid values are 'landing' and 'whatsapp' — there is no 'website'. This
8862
+ * used to be hardcoded to `landing`, which both rejected `website` with an
8863
+ * opaque `invalid_literal` and made the WhatsApp destination — supported by
8864
+ * the API since #849 — unreachable from this tool.
8865
+ */
8866
+ destination: z44.discriminatedUnion("type", [
8867
+ z44.object({
8868
+ type: z44.literal("landing"),
8869
+ landingUrl: z44.string().url()
8870
+ }),
8871
+ z44.object({
8872
+ type: z44.literal("whatsapp"),
8873
+ phoneNumber: z44.string().min(8).max(32)
8874
+ })
8875
+ ]),
8876
+ saveAsDefault: z44.boolean().default(false),
8619
8877
  budgetPolicy: MetaBudgetPolicy.optional()
8620
8878
  }).superRefine((value, ctx) => {
8621
8879
  if (value.budgetMode === "campaign" && value.campaignDailyBudget === void 0) {
8622
8880
  ctx.addIssue({
8623
- code: z43.ZodIssueCode.custom,
8881
+ code: z44.ZodIssueCode.custom,
8624
8882
  path: [
8625
8883
  "campaignDailyBudget"
8626
8884
  ],
@@ -8631,7 +8889,7 @@ var GroupedCampaignConfig = z43.object({
8631
8889
  value.adSets.forEach((adSet, index) => {
8632
8890
  if (adSet.dailyBudget === void 0) {
8633
8891
  ctx.addIssue({
8634
- code: z43.ZodIssueCode.custom,
8892
+ code: z44.ZodIssueCode.custom,
8635
8893
  path: [
8636
8894
  "adSets",
8637
8895
  index,
@@ -8643,19 +8901,31 @@ var GroupedCampaignConfig = z43.object({
8643
8901
  });
8644
8902
  }
8645
8903
  });
8646
- var PublishCreativesToMetaInputBase = z43.object({
8647
- creatives: z43.array(GroupedCreative).min(1).max(4),
8648
- adAccountId: z43.string().min(1),
8649
- pixelId: z43.string().min(1).optional(),
8650
- landingUrl: z43.string().url(),
8651
- targetCountry: z43.string().length(2),
8652
- campaignName: z43.string().trim().min(1).max(100),
8904
+ var PublishCreativesToMetaInputBase = z44.object({
8905
+ creatives: z44.array(GroupedCreative).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
8906
+ adAccountId: McpAdAccountId,
8907
+ pixelId: z44.string().min(1).optional(),
8908
+ /** Required unless `campaignConfig.destination` is whatsapp — see the
8909
+ * superRefine below. Mirrors the API's `UploadToMetaRequest`. */
8910
+ landingUrl: z44.string().url().optional(),
8911
+ targetCountry: z44.string().length(2),
8912
+ campaignName: z44.string().trim().min(1).max(100),
8653
8913
  campaignConfig: GroupedCampaignConfig.optional()
8654
8914
  });
8655
8915
  var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((value, ctx) => {
8656
- if (value.campaignConfig && value.campaignConfig.destination.landingUrl !== value.landingUrl) {
8916
+ const destination = value.campaignConfig?.destination;
8917
+ if (destination?.type !== "whatsapp" && !value.landingUrl) {
8657
8918
  ctx.addIssue({
8658
- code: z43.ZodIssueCode.custom,
8919
+ code: z44.ZodIssueCode.custom,
8920
+ path: [
8921
+ "landingUrl"
8922
+ ],
8923
+ message: "landingUrl is required unless campaignConfig.destination.type is whatsapp"
8924
+ });
8925
+ }
8926
+ if (destination?.type === "landing" && destination.landingUrl !== value.landingUrl) {
8927
+ ctx.addIssue({
8928
+ code: z44.ZodIssueCode.custom,
8659
8929
  path: [
8660
8930
  "campaignConfig",
8661
8931
  "destination",
@@ -8665,12 +8935,12 @@ var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((v
8665
8935
  });
8666
8936
  }
8667
8937
  });
8668
- var PresignUploadResponseSchema = z43.object({
8669
- uploadUrl: z43.string().url().refine((value) => {
8938
+ var PresignUploadResponseSchema = z44.object({
8939
+ uploadUrl: z44.string().url().refine((value) => {
8670
8940
  const url = new URL(value);
8671
8941
  return !url.username && !url.password && url.protocol === "https:";
8672
8942
  }, "presigned upload URL must be HTTPS without embedded credentials"),
8673
- publicUrl: z43.string().url().refine((value) => {
8943
+ publicUrl: z44.string().url().refine((value) => {
8674
8944
  const url = new URL(value);
8675
8945
  return !url.username && !url.password;
8676
8946
  }, "public media URL must not contain embedded credentials")
@@ -8695,16 +8965,16 @@ async function resolveAndUploadMedia(api, ref, expectedKind, transport) {
8695
8965
  return uploadResolvedMedia(api, media);
8696
8966
  }
8697
8967
  __name(resolveAndUploadMedia, "resolveAndUploadMedia");
8698
- var TikTokInput = z43.object({
8699
- taskId: z43.string().min(1),
8700
- advertiserId: z43.string().min(1),
8701
- pixelId: z43.string().min(1).optional(),
8702
- landingUrl: z43.string().url().optional(),
8703
- angleIndexes: z43.array(z43.number().int().nonnegative()).optional(),
8968
+ var TikTokInput = z44.object({
8969
+ taskId: z44.string().min(1),
8970
+ advertiserId: z44.string().min(1),
8971
+ pixelId: z44.string().min(1).optional(),
8972
+ landingUrl: z44.string().url().optional(),
8973
+ angleIndexes: z44.array(z44.number().int().nonnegative()).optional(),
8704
8974
  campaignConfig: McpCampaignConfig.optional(),
8705
8975
  /** Override the ad's description (TikTok's body copy). If omitted,
8706
8976
  * ProductMaker uses the auto-generated copy attached to the task. */
8707
- primaryText: z43.string().trim().min(1).max(500).optional()
8977
+ primaryText: z44.string().trim().min(1).max(500).optional()
8708
8978
  });
8709
8979
  function registerPublishTools(server, api, opts = {}) {
8710
8980
  const transport = opts.transport ?? "http";
@@ -8743,7 +9013,7 @@ function registerPublishTools(server, api, opts = {}) {
8743
9013
  throw translateError(e);
8744
9014
  }
8745
9015
  });
8746
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario SIEMPRE en d\xF3lares, ej. 25 \u2014 ProductMaker lo convierte a la moneda de la cuenta; si el usuario te dice '60.000 COP al d\xEDa' pasa el equivalente en USD, NUNCA el n\xFAmero en moneda local), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `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. `phoneNumber` (opcional): si el usuario quiere que el bot\xF3n del anuncio abra un chat de WhatsApp en vez de una landing, pasa el n\xFAmero (con c\xF3digo de pa\xEDs, ej. +573001234567) \u2014 no hace falta `landingUrl` en ese caso. Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, primaryText, phoneNumber, ...rest }) => {
9016
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde UNA sola tarea. Si quieres juntar creativos de VARIAS tareas en una misma campa\xF1a o en un mismo ad set, usa `publish_creatives_to_meta` \u2014 esta herramienta crea una campa\xF1a por tarea. `adAccountId` acepta tanto 'act_1234567890' como el id num\xE9rico pelado '1234567890'. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario SIEMPRE en d\xF3lares, ej. 25 \u2014 ProductMaker lo convierte a la moneda de la cuenta; si el usuario te dice '60.000 COP al d\xEDa' pasa el equivalente en USD, NUNCA el n\xFAmero en moneda local), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `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. `phoneNumber` (opcional): si el usuario quiere que el bot\xF3n del anuncio abra un chat de WhatsApp en vez de una landing, pasa el n\xFAmero (con c\xF3digo de pa\xEDs, ej. +573001234567) \u2014 no hace falta `landingUrl` en ese caso. Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, primaryText, phoneNumber, ...rest }) => {
8747
9017
  try {
8748
9018
  const body = {
8749
9019
  ...rest
@@ -8771,8 +9041,11 @@ function registerPublishTools(server, api, opts = {}) {
8771
9041
  throw translateError(e);
8772
9042
  }
8773
9043
  });
8774
- server.tool("publish_creatives_to_meta", 'Crea UNA campa\xF1a de Meta Ads en estado PAUSED con 1\u20134 creativos agrupados en el mismo flujo. Cada creativo puede venir de una tarea (source.type="task") \u2014 usa angleIndex, videoVariant (with_subtitles/without_subtitles) o imageVariantIndex \u2014 o de media (source.type="media") con URL p\xFAblica o ruta local (stdio). Para videos media puedes dar thumbnailUrl o thumbnailPath; si el video es local, ProductMaker sube el archivo y genera la miniatura con FFmpeg server-side. La herramienta valida extensi\xF3n, MIME, magic bytes y tama\xF1o antes de crear la campa\xF1a. campaignConfig permite definir el ad set y budgetPolicy: exact respeta el presupuesto solicitado y falla expl\xEDcitamente si Meta exige un m\xEDnimo; recommended_floor aplica el piso recomendado. La campa\xF1a queda PAUSED; reporta campaignId, adSetId, IDs de ads, requestedBudget, appliedBudget y el enlace de Ads Manager. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, async (input) => {
9044
+ server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads en estado PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + " creativos (de tareas distintas si hace falta) en el mismo flujo, repartidos en hasta " + CAMPAIGN_LIMITS.MAX_AD_SETS + ' ad sets. Cada creativo puede venir de una tarea (source.type="task") \u2014 usa angleIndex, videoVariant (with_subtitles/without_subtitles) o imageVariantIndex \u2014 o de media (source.type="media") con URL p\xFAblica o ruta local (stdio). Para videos media, thumbnailUrl/thumbnailPath son OPCIONALES en ambos casos: ProductMaker sube el archivo y genera la miniatura con FFmpeg server-side, tanto si diste una URL como una ruta local. La herramienta valida extensi\xF3n, MIME, magic bytes y tama\xF1o antes de crear la campa\xF1a. `adAccountId` acepta tanto "act_1234567890" como el id num\xE9rico pelado "1234567890". `campaignConfig.destination` acepta exactamente dos valores: {type:"landing", landingUrl} o {type:"whatsapp", phoneNumber} \u2014 NO existe "website". campaignConfig permite definir el ad set y budgetPolicy: exact respeta el presupuesto solicitado y falla expl\xEDcitamente si Meta exige un m\xEDnimo; recommended_floor aplica el piso recomendado. La campa\xF1a queda PAUSED; reporta campaignId, adSetId, IDs de ads, requestedBudget, appliedBudget y el enlace de Ads Manager. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, async (rawInput) => {
8775
9045
  try {
9046
+ const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
9047
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
9048
+ const input = parsed.data;
8776
9049
  const creatives = [];
8777
9050
  for (let index = 0; index < input.creatives.length; index += 1) {
8778
9051
  const creative = input.creatives[index];
@@ -8800,9 +9073,6 @@ function registerPublishTools(server, api, opts = {}) {
8800
9073
  }, creative.type, transport);
8801
9074
  let thumbnailUrl;
8802
9075
  if (creative.type === "video") {
8803
- if (!creative.source.thumbnailUrl && !creative.source.thumbnailPath && !creative.source.path) {
8804
- throw new ResolveMediaError("META_NO_THUMBNAIL_SOURCE", "Video media requires thumbnailUrl or thumbnailPath unless the video is a local path; no campaign entities were created.");
8805
- }
8806
9076
  if (creative.source.thumbnailUrl || creative.source.thumbnailPath) {
8807
9077
  thumbnailUrl = await resolveAndUploadMedia(api, {
8808
9078
  url: creative.source.thumbnailUrl,
@@ -8915,13 +9185,13 @@ function toTikTokCampaignConfig(cfg) {
8915
9185
  __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
8916
9186
 
8917
9187
  // src/tools/campaigns.ts
8918
- import { z as z44 } from "zod";
8919
- var MetaStatusInput = z44.object({
8920
- campaignId: z44.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
9188
+ import { z as z45 } from "zod";
9189
+ var MetaStatusInput = z45.object({
9190
+ campaignId: z45.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
8921
9191
  });
8922
- var TikTokStatusInput = z44.object({
8923
- campaignId: z44.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
8924
- advertiserId: z44.string().min(1).describe("The same advertiserId used to publish the campaign.")
9192
+ var TikTokStatusInput = z45.object({
9193
+ campaignId: z45.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
9194
+ advertiserId: z45.string().min(1).describe("The same advertiserId used to publish the campaign.")
8925
9195
  });
8926
9196
  function registerCampaignTools(server, api) {
8927
9197
  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) => {
@@ -9018,10 +9288,10 @@ function renderTikTokStatusMarkdown(r) {
9018
9288
  __name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
9019
9289
 
9020
9290
  // ../../packages/product-economics/src/product-costing.ts
9021
- import { z as z46 } from "zod";
9291
+ import { z as z47 } from "zod";
9022
9292
 
9023
9293
  // ../../packages/product-economics/src/product-costing-countries.ts
9024
- import { z as z45 } from "zod";
9294
+ import { z as z46 } from "zod";
9025
9295
 
9026
9296
  // ../../packages/product-economics/src/product-costing-shipping-references.json
9027
9297
  var product_costing_shipping_references_default = {
@@ -9085,21 +9355,21 @@ var product_costing_shipping_references_default = {
9085
9355
  };
9086
9356
 
9087
9357
  // ../../packages/product-economics/src/product-costing-countries.ts
9088
- var ShippingReferenceSchema = z45.object({
9089
- reference: z45.number().positive(),
9090
- min: z45.number().nonnegative(),
9091
- max: z45.number().positive(),
9092
- updatedAt: z45.string().date(),
9093
- confidence: z45.enum([
9358
+ var ShippingReferenceSchema = z46.object({
9359
+ reference: z46.number().positive(),
9360
+ min: z46.number().nonnegative(),
9361
+ max: z46.number().positive(),
9362
+ updatedAt: z46.string().date(),
9363
+ confidence: z46.enum([
9094
9364
  "high",
9095
9365
  "medium",
9096
9366
  "medium-low"
9097
9367
  ]),
9098
- sources: z45.array(z45.string().url().startsWith("https://")).min(1).max(3)
9368
+ sources: z46.array(z46.string().url().startsWith("https://")).min(1).max(3)
9099
9369
  }).superRefine(({ reference, min, max }, context) => {
9100
9370
  if (min > reference || reference > max) {
9101
9371
  context.addIssue({
9102
- code: z45.ZodIssueCode.custom,
9372
+ code: z46.ZodIssueCode.custom,
9103
9373
  message: "shipping_reference_outside_range",
9104
9374
  path: [
9105
9375
  "reference"
@@ -9107,7 +9377,7 @@ var ShippingReferenceSchema = z45.object({
9107
9377
  });
9108
9378
  }
9109
9379
  });
9110
- var SHIPPING_REFERENCES = z45.record(ShippingReferenceSchema).parse(product_costing_shipping_references_default);
9380
+ var SHIPPING_REFERENCES = z46.record(ShippingReferenceSchema).parse(product_costing_shipping_references_default);
9111
9381
  var PRODUCT_COSTING_COUNTRY_CODES = Object.freeze(Object.keys(SHIPPING_REFERENCES));
9112
9382
  var SPANISH_LOCALE_COUNTRIES = /* @__PURE__ */ new Set([
9113
9383
  "MX",
@@ -9289,18 +9559,18 @@ function getProductCostingCountryName(country) {
9289
9559
  __name(getProductCostingCountryName, "getProductCostingCountryName");
9290
9560
 
9291
9561
  // ../../packages/product-economics/src/product-costing.ts
9292
- var ProductCostingInputSchema = z46.object({
9293
- country: z46.string().refine(isProductCostingCountryCode, "unsupported_country"),
9294
- supplierCost: z46.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").positive("supplier_cost_positive"),
9295
- shippingCost: z46.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("shipping_cost_nonnegative"),
9296
- returnRate: z46.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9297
- cancellationRate: z46.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9298
- adsManagerCpa: z46.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("cpa_nonnegative"),
9299
- desiredMargin: z46.number().finite().min(0, "margin_nonnegative").lt(1, "margin_below_one")
9562
+ var ProductCostingInputSchema = z47.object({
9563
+ country: z47.string().refine(isProductCostingCountryCode, "unsupported_country"),
9564
+ supplierCost: z47.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").positive("supplier_cost_positive"),
9565
+ shippingCost: z47.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("shipping_cost_nonnegative"),
9566
+ returnRate: z47.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9567
+ cancellationRate: z47.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9568
+ adsManagerCpa: z47.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("cpa_nonnegative"),
9569
+ desiredMargin: z47.number().finite().min(0, "margin_nonnegative").lt(1, "margin_below_one")
9300
9570
  }).superRefine(({ returnRate, cancellationRate }, context) => {
9301
9571
  if (returnRate + cancellationRate >= 1) {
9302
9572
  context.addIssue({
9303
- code: z46.ZodIssueCode.custom,
9573
+ code: z47.ZodIssueCode.custom,
9304
9574
  message: "delivery_rate_positive",
9305
9575
  path: [
9306
9576
  "cancellationRate"
@@ -9367,14 +9637,14 @@ function calculateProductCosting(rawInput) {
9367
9637
  __name(calculateProductCosting, "calculateProductCosting");
9368
9638
 
9369
9639
  // src/tools/product-costing.ts
9370
- import { z as z47 } from "zod";
9371
- var optionalAmount = /* @__PURE__ */ __name((description) => z47.number().finite().max(Number.MAX_SAFE_INTEGER).nonnegative().optional().describe(description), "optionalAmount");
9372
- var optionalPercent = /* @__PURE__ */ __name((description) => z47.number().finite().min(0).lt(100).optional().describe(description), "optionalPercent");
9373
- var ProductCostingToolInput = z47.object({
9374
- country: z47.string().trim().toUpperCase().refine(isProductCostingCountryCode, {
9640
+ import { z as z48 } from "zod";
9641
+ var optionalAmount = /* @__PURE__ */ __name((description) => z48.number().finite().max(Number.MAX_SAFE_INTEGER).nonnegative().optional().describe(description), "optionalAmount");
9642
+ var optionalPercent = /* @__PURE__ */ __name((description) => z48.number().finite().min(0).lt(100).optional().describe(description), "optionalPercent");
9643
+ var ProductCostingToolInput = z48.object({
9644
+ country: z48.string().trim().toUpperCase().refine(isProductCostingCountryCode, {
9375
9645
  message: "Pa\xEDs no soportado por ProductMaker."
9376
9646
  }).optional().describe("C\xF3digo ISO-2 de cualquiera de los pa\xEDses disponibles en ProductMaker. Omite para Colombia."),
9377
- supplierCost: z47.number().finite().max(Number.MAX_SAFE_INTEGER).positive().optional().describe("Costo real de una unidad con el proveedor, en la moneda del pa\xEDs. Requerido salvo que uses supplierCostCop para Colombia."),
9647
+ supplierCost: z48.number().finite().max(Number.MAX_SAFE_INTEGER).positive().optional().describe("Costo real de una unidad con el proveedor, en la moneda del pa\xEDs. Requerido salvo que uses supplierCostCop para Colombia."),
9378
9648
  supplierCostCop: optionalAmount("Alias anterior para costo del proveedor en Colombia. Prefiere supplierCost."),
9379
9649
  shippingCost: optionalAmount("Costo de un env\xEDo en la moneda del pa\xEDs. Omite para usar el valor gu\xEDa vigente."),
9380
9650
  shippingCostCop: optionalAmount("Alias anterior para flete en Colombia. Prefiere shippingCost."),
@@ -9505,6 +9775,224 @@ function formatPercent(value) {
9505
9775
  }
9506
9776
  __name(formatPercent, "formatPercent");
9507
9777
 
9778
+ // src/tools/product-research.ts
9779
+ import { z as z49 } from "zod";
9780
+
9781
+ // src/tools/product-research-idempotency.ts
9782
+ import { createHash as createHash2 } from "crypto";
9783
+ var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
9784
+ var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
9785
+ function uuidV5(name, namespace) {
9786
+ const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
9787
+ const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
9788
+ const bytes = Buffer.from(digest.subarray(0, 16));
9789
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
9790
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
9791
+ const hex = bytes.toString("hex");
9792
+ return [
9793
+ hex.slice(0, 8),
9794
+ hex.slice(8, 12),
9795
+ hex.slice(12, 16),
9796
+ hex.slice(16, 20),
9797
+ hex.slice(20)
9798
+ ].join("-");
9799
+ }
9800
+ __name(uuidV5, "uuidV5");
9801
+ function deriveIdempotencyKey(input) {
9802
+ const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
9803
+ const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
9804
+ return uuidV5(name, NAMESPACE);
9805
+ }
9806
+ __name(deriveIdempotencyKey, "deriveIdempotencyKey");
9807
+
9808
+ // src/tools/product-research-render.ts
9809
+ var num = /* @__PURE__ */ __name((value, suffix = "") => value === void 0 ? "\u2014" : `${Math.round(value * 100) / 100}${suffix}`, "num");
9810
+ function renderMeta(meta) {
9811
+ if (meta.status === "evidence_failed") {
9812
+ return [
9813
+ "- \u26A0\uFE0F **Sin evidencia publicitaria:** no se pudo consultar Meta Ads para este producto. La puntuaci\xF3n se apoya s\xF3lo en datos de venta."
9814
+ ];
9815
+ }
9816
+ if (meta.status === "evidence_empty") {
9817
+ return [
9818
+ "- \u26A0\uFE0F **Sin evidencia publicitaria:** no se encontraron anuncios activos en Meta. Puede ser un mercado virgen o una b\xFAsqueda sin cobertura; la puntuaci\xF3n se apoya s\xF3lo en datos de venta."
9819
+ ];
9820
+ }
9821
+ return [
9822
+ `- **Evidencia en Meta Ads:** ${meta.totalAds} anuncios de ${meta.distinctAdvertisers} anunciantes, m\xE1ximo ${meta.maxActiveDays} d\xEDas activos, oportunidad de \xE1ngulo ${meta.angleOpportunity}/100.`,
9823
+ meta.summary ? ` ${meta.summary}` : ""
9824
+ ].filter(Boolean);
9825
+ }
9826
+ __name(renderMeta, "renderMeta");
9827
+ function renderCandidate(candidate, index) {
9828
+ const name = candidate.nameLocal ?? candidate.nameOriginal;
9829
+ const lines = [
9830
+ `### ${index + 1}. ${name}`,
9831
+ `**${candidate.score}/100 \xB7 ${candidate.label} \xB7 confianza ${candidate.confidence}**`
9832
+ ];
9833
+ if (candidate.insight) lines.push("", candidate.insight);
9834
+ lines.push("", "**Por qu\xE9:**");
9835
+ lines.push(...candidate.reasons.map((reason) => `- ${reason}`));
9836
+ lines.push("", "**Riesgos:**");
9837
+ lines.push(...candidate.risks.length > 0 ? candidate.risks.map((risk) => `- ${risk}`) : [
9838
+ "- No se identificaron riesgos espec\xEDficos."
9839
+ ]);
9840
+ lines.push("", ...renderMeta(candidate.meta));
9841
+ lines.push("", `- **Venta (30d):** ${num(candidate.units30d)} unidades \xB7 ${num(candidate.revenueUsd30d, " USD")} \xB7 crecimiento ${num(candidate.growthPct30d, "%")} \xB7 precio ${num(candidate.priceUsd, " USD")}`);
9842
+ if (candidate.productUrl) lines.push(`- [Ver producto](${candidate.productUrl})`);
9843
+ return lines.join("\n");
9844
+ }
9845
+ __name(renderCandidate, "renderCandidate");
9846
+ function renderResearchReport(run) {
9847
+ if (run.status === "failed" || run.status === "refunded") {
9848
+ return [
9849
+ `\u274C **La investigaci\xF3n no pudo completarse** (estado: ${run.status}).`,
9850
+ run.errorCode ? `C\xF3digo: \`${run.errorCode}\`` : "",
9851
+ run.refunded ? `Los ${run.priceCredits} cr\xE9ditos fueron reembolsados.` : "\u26A0\uFE0F No consta reembolso \u2014 rev\xEDsalo antes de reintentar."
9852
+ ].filter(Boolean).join("\n");
9853
+ }
9854
+ if (run.status === "ready" && run.result === null) {
9855
+ return [
9856
+ `\u274C **No se pudo leer el reporte** (estado: ${run.status}).`,
9857
+ `El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3, pero el reporte qued\xF3 corrupto y no puede recuperarse.`,
9858
+ `Contacta a soporte con este runId para revisar un reembolso: \`${run.id}\`.`
9859
+ ].join("\n");
9860
+ }
9861
+ if (run.result === null) {
9862
+ return [
9863
+ `\u23F3 **La investigaci\xF3n sigue en curso** (estado: ${run.status}, etapa: ${run.progressStage}).`,
9864
+ `El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3; el reporte no se pierde.`,
9865
+ `Cons\xFAltalo con \`get_product_research_run\` usando runId \`${run.id}\`.`
9866
+ ].join("\n");
9867
+ }
9868
+ const { result } = run;
9869
+ const degraded = result.candidates.filter((candidate) => candidate.meta.status !== "evidence_available").length;
9870
+ const header = [
9871
+ `\u2705 **Productos ganadores \xB7 ${result.targetCountry} \xB7 ${result.categoryKey}**`,
9872
+ "",
9873
+ `Analizados ${result.methodology.kaloCandidateCount} candidatos del mercado ${result.sourceMarket}; ${result.methodology.metaCoveredCandidateCount} con cobertura de Meta Ads. Datos con ${num(result.freshnessHours, " h")} de antig\xFCedad.`
9874
+ ];
9875
+ if (degraded > 0) {
9876
+ header.push("", `\u26A0\uFE0F ${degraded} de ${result.candidates.length} productos no tienen evidencia publicitaria. Su puntuaci\xF3n se apoya s\xF3lo en datos de venta.`);
9877
+ }
9878
+ return [
9879
+ ...header,
9880
+ "",
9881
+ ...result.candidates.map((candidate, index) => renderCandidate(candidate, index))
9882
+ ].join("\n\n");
9883
+ }
9884
+ __name(renderResearchReport, "renderResearchReport");
9885
+
9886
+ // src/tools/product-research.ts
9887
+ var PRICE_CREDITS = 2e3;
9888
+ var TERMINAL2 = /* @__PURE__ */ new Set([
9889
+ "ready",
9890
+ "failed",
9891
+ "refunded"
9892
+ ]);
9893
+ var POLL_INTERVAL_MS = 5e3;
9894
+ var MAX_WAIT_SECONDS = 120;
9895
+ var FindInput = z49.object({
9896
+ targetCountry: ResearchTargetCountryEnum.describe("Pa\xEDs objetivo. S\xF3lo CO, MX, PE, EC y CL tienen cobertura de investigaci\xF3n."),
9897
+ categoryKey: ResearchCategoryEnum.default("auto").describe('Categor\xEDa a investigar. Usa "auto" para que la IA elija seg\xFAn el mercado.'),
9898
+ confirmed: z49.boolean().describe(`Ponlo en true SOLO despu\xE9s de que el usuario haya aceptado el cargo de ${PRICE_CREDITS} cr\xE9ditos. No lo asumas.`),
9899
+ waitSeconds: z49.number().int().min(0).max(MAX_WAIT_SECONDS).optional().describe("Segundos a esperar el reporte antes de devolver el runId. M\xE1ximo 120.")
9900
+ });
9901
+ var GetInput = z49.object({
9902
+ runId: z49.string().uuid().describe("ID del run devuelto por find_winning_products.")
9903
+ });
9904
+ var ListInput = z49.object({
9905
+ limit: z49.number().int().min(1).max(20).default(10)
9906
+ });
9907
+ function textResult(text) {
9908
+ return {
9909
+ content: [
9910
+ {
9911
+ type: "text",
9912
+ text
9913
+ }
9914
+ ]
9915
+ };
9916
+ }
9917
+ __name(textResult, "textResult");
9918
+ var sleep2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)), "sleep");
9919
+ async function pollUntilTerminal(api, runId, waitSeconds) {
9920
+ const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
9921
+ let run = await api.getJson(`/v1/product-research/runs/${runId}`);
9922
+ while (!TERMINAL2.has(run.status) && Date.now() < deadline) {
9923
+ await sleep2(POLL_INTERVAL_MS);
9924
+ run = await api.getJson(`/v1/product-research/runs/${runId}`);
9925
+ }
9926
+ return run;
9927
+ }
9928
+ __name(pollUntilTerminal, "pollUntilTerminal");
9929
+ function registerProductResearchTools(server, api) {
9930
+ 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) => {
9931
+ const parsed = FindInput.parse(input);
9932
+ if (!parsed.confirmed) {
9933
+ 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.`);
9934
+ }
9935
+ try {
9936
+ const idempotencyKey = deriveIdempotencyKey({
9937
+ fingerprint: api.fingerprint(),
9938
+ targetCountry: parsed.targetCountry,
9939
+ categoryKey: parsed.categoryKey,
9940
+ nowMs: Date.now()
9941
+ });
9942
+ const created = await api.postJson("/v1/product-research/runs", {
9943
+ targetCountry: parsed.targetCountry,
9944
+ categoryKey: parsed.categoryKey,
9945
+ idempotencyKey
9946
+ });
9947
+ const waitSeconds = parsed.waitSeconds ?? 0;
9948
+ let run = created;
9949
+ if (waitSeconds > 0 && !TERMINAL2.has(created.status)) {
9950
+ try {
9951
+ run = await pollUntilTerminal(api, created.id, waitSeconds);
9952
+ } catch {
9953
+ run = created;
9954
+ }
9955
+ }
9956
+ const notice = created.reused ? `
9957
+
9958
+ _Esta llamada no cobr\xF3 cr\xE9ditos: se reutiliz\xF3 una investigaci\xF3n reciente con los mismos par\xE1metros. Para investigar una categor\xEDa distinta, vuelve a llamar con un \`categoryKey\` expl\xEDcito \u2014 eso inicia una investigaci\xF3n NUEVA con un cargo NUEVO de ${PRICE_CREDITS} cr\xE9ditos, as\xED que pide confirmaci\xF3n expl\xEDcita al usuario antes de hacerlo._` : "";
9959
+ return textResult(`${renderResearchReport(run)}${notice}`);
9960
+ } catch (error) {
9961
+ throw translateError(error);
9962
+ }
9963
+ });
9964
+ 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) => {
9965
+ const parsed = GetInput.parse(input);
9966
+ try {
9967
+ const run = await api.getJson(`/v1/product-research/runs/${parsed.runId}`);
9968
+ return {
9969
+ content: [
9970
+ {
9971
+ type: "text",
9972
+ text: JSON.stringify(run)
9973
+ },
9974
+ {
9975
+ type: "text",
9976
+ text: renderResearchReport(run)
9977
+ }
9978
+ ]
9979
+ };
9980
+ } catch (error) {
9981
+ throw translateError(error);
9982
+ }
9983
+ });
9984
+ 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) => {
9985
+ const parsed = ListInput.parse(input);
9986
+ try {
9987
+ const runs = await api.getJson(`/v1/product-research/runs?limit=${parsed.limit}`);
9988
+ return textResult(JSON.stringify(runs));
9989
+ } catch (error) {
9990
+ throw translateError(error);
9991
+ }
9992
+ });
9993
+ }
9994
+ __name(registerProductResearchTools, "registerProductResearchTools");
9995
+
9508
9996
  // src/server.ts
9509
9997
  function createServer(opts) {
9510
9998
  const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
@@ -9521,6 +10009,7 @@ function createServer(opts) {
9521
10009
  });
9522
10010
  const api = new ApiClient(apiBaseUrl, apiKey);
9523
10011
  registerProductCostingTools(server);
10012
+ registerProductResearchTools(server, api);
9524
10013
  registerTaskTools(server, api, {
9525
10014
  proxyBaseUrl,
9526
10015
  transport