@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/http.js CHANGED
@@ -12,6 +12,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
 
14
14
  // src/api-client.ts
15
+ import { createHash } from "crypto";
15
16
  var ApiHttpError = class extends Error {
16
17
  static {
17
18
  __name(this, "ApiHttpError");
@@ -38,6 +39,15 @@ var ApiClient = class {
38
39
  Authorization: `Bearer ${this.apiKey}`
39
40
  };
40
41
  }
42
+ /**
43
+ * Stable per-key identifier. The MCP never sees the userId — the API key is
44
+ * the only per-user handle it holds — so this stands in as the user
45
+ * component of derived idempotency keys. Never log or return it raw to a
46
+ * caller: it is derived from a secret.
47
+ */
48
+ fingerprint() {
49
+ return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
50
+ }
41
51
  async getJson(path3) {
42
52
  const res = await fetch(`${this.baseUrl}${path3}`, {
43
53
  headers: this.authHeaders()
@@ -132,10 +142,10 @@ var ApiClient = class {
132
142
  };
133
143
 
134
144
  // src/index.ts
135
- var MCP_VERSION = "1.16.2";
145
+ var MCP_VERSION = "1.17.0";
136
146
 
137
147
  // src/tools/tasks.ts
138
- import { z as z38 } from "zod";
148
+ import { z as z39 } from "zod";
139
149
 
140
150
  // src/resolve-image.ts
141
151
  import { lookup as dnsLookup } from "dns/promises";
@@ -3149,6 +3159,125 @@ var AdminQualityReportListResponse = z24.object({
3149
3159
  var UpdateQualityReportStatusRequest = z24.object({
3150
3160
  status: QualityReportStatus
3151
3161
  });
3162
+ var AdminSupportConversationStatus = z24.enum([
3163
+ "bot_active",
3164
+ "human_pending",
3165
+ "human_active",
3166
+ "closed"
3167
+ ]);
3168
+ var AdminSupportDeliveryStatus = z24.enum([
3169
+ "draft",
3170
+ "pending",
3171
+ "sent",
3172
+ "delivered",
3173
+ "read",
3174
+ "failed",
3175
+ "blocked"
3176
+ ]);
3177
+ var AdminSupportMessage = z24.object({
3178
+ id: z24.string().uuid(),
3179
+ direction: z24.enum([
3180
+ "inbound",
3181
+ "outbound"
3182
+ ]),
3183
+ senderKind: z24.enum([
3184
+ "user",
3185
+ "bot",
3186
+ "admin",
3187
+ "system"
3188
+ ]),
3189
+ messageType: z24.enum([
3190
+ "text",
3191
+ "image",
3192
+ "audio",
3193
+ "document",
3194
+ "interactive",
3195
+ "status"
3196
+ ]),
3197
+ bodyRedacted: z24.string().nullable(),
3198
+ deliveryStatus: AdminSupportDeliveryStatus.nullable(),
3199
+ createdAt: z24.string().datetime()
3200
+ });
3201
+ var AdminSupportTicket = z24.object({
3202
+ id: z24.string().uuid(),
3203
+ category: z24.enum([
3204
+ "how_to",
3205
+ "credits",
3206
+ "billing",
3207
+ "task_status",
3208
+ "task_error",
3209
+ "video_quality",
3210
+ "landing_quality",
3211
+ "image_quality",
3212
+ "feature_request",
3213
+ "other"
3214
+ ]),
3215
+ failureMode: z24.string().nullable(),
3216
+ status: z24.enum([
3217
+ "open",
3218
+ "triaged",
3219
+ "resolved",
3220
+ "rejected"
3221
+ ]),
3222
+ summary: z24.string(),
3223
+ taskId: z24.string().nullable(),
3224
+ mixTaskId: z24.string().nullable(),
3225
+ qualityReportId: z24.string().nullable(),
3226
+ handoffReason: z24.string().nullable(),
3227
+ createdAt: z24.string().datetime(),
3228
+ updatedAt: z24.string().datetime()
3229
+ });
3230
+ var AdminSupportConversation = z24.object({
3231
+ id: z24.string().uuid(),
3232
+ status: AdminSupportConversationStatus,
3233
+ priority: z24.enum([
3234
+ "low",
3235
+ "normal",
3236
+ "high",
3237
+ "urgent"
3238
+ ]),
3239
+ currentIntent: z24.string().nullable(),
3240
+ assignedAdminId: z24.string().nullable(),
3241
+ customerServiceWindowEndsAt: z24.string().datetime().nullable(),
3242
+ lastInboundAt: z24.string().datetime().nullable(),
3243
+ createdAt: z24.string().datetime(),
3244
+ updatedAt: z24.string().datetime(),
3245
+ contact: z24.object({
3246
+ id: z24.string().uuid(),
3247
+ userId: z24.string().nullable(),
3248
+ verifiedAt: z24.string().datetime().nullable(),
3249
+ botMode: z24.enum([
3250
+ "enabled",
3251
+ "paused",
3252
+ "opted_out"
3253
+ ]),
3254
+ locale: z24.string()
3255
+ }),
3256
+ messages: z24.array(AdminSupportMessage),
3257
+ tickets: z24.array(AdminSupportTicket)
3258
+ });
3259
+ var AdminSupportListQuery = z24.object({
3260
+ status: AdminSupportConversationStatus.optional(),
3261
+ take: z24.coerce.number().int().min(1).max(100).default(50),
3262
+ cursorId: z24.string().uuid().optional()
3263
+ }).strict();
3264
+ var AdminSupportListResponse = z24.object({
3265
+ conversations: z24.array(AdminSupportConversation),
3266
+ nextCursor: z24.string().uuid().nullable()
3267
+ });
3268
+ var AdminSupportReplyRequest = z24.object({
3269
+ body: z24.string().trim().min(1).max(4096),
3270
+ idempotencyKey: z24.string().regex(/^[A-Za-z0-9_-]{8,128}$/)
3271
+ }).strict();
3272
+ var AdminSupportEmptyActionRequest = z24.object({}).strict();
3273
+ var AdminSupportActionResponse = z24.object({
3274
+ conversation: AdminSupportConversation
3275
+ });
3276
+ var AdminSupportReplyResponse = z24.object({
3277
+ messageId: z24.string().uuid(),
3278
+ deliveryStatus: AdminSupportDeliveryStatus,
3279
+ duplicate: z24.boolean()
3280
+ });
3152
3281
 
3153
3282
  // ../../packages/types/src/promo.ts
3154
3283
  import { z as z25 } from "zod";
@@ -3288,8 +3417,21 @@ import { z as z31 } from "zod";
3288
3417
  // ../../packages/types/src/campaigns.ts
3289
3418
  import { z as z30 } from "zod";
3290
3419
  var CAMPAIGN_LIMITS = {
3291
- /** Maximum number of creatives per campaign */
3292
- MAX_CREATIVES: 4,
3420
+ /**
3421
+ * Maximum number of creatives (= ads) per campaign.
3422
+ *
3423
+ * Not a Meta limit — Meta allows far more ads per ad set. This is our own
3424
+ * bound on how many uploads one publish call will push through the Graph API
3425
+ * in a single request. It was 4 with no stated reason, which forced callers
3426
+ * with a normal batch (3 videos + 3 images) to drop two by hand.
3427
+ */
3428
+ MAX_CREATIVES: 10,
3429
+ /**
3430
+ * Maximum ad sets per campaign. Distinct from MAX_CREATIVES, which it used
3431
+ * to share a constant with — that coupling meant raising the creative cap
3432
+ * would silently raise the ad-set cap too.
3433
+ */
3434
+ MAX_AD_SETS: 4,
3293
3435
  /** Maximum video duration in seconds */
3294
3436
  MAX_VIDEO_DURATION_SECONDS: 180,
3295
3437
  /** Maximum file size in bytes (350MB) */
@@ -3332,8 +3474,21 @@ var CampaignCreativeType = z30.enum([
3332
3474
  var CampaignCreative = z30.object({
3333
3475
  /** Unique identifier for the creative */
3334
3476
  id: z30.string(),
3335
- /** The uploaded file (null when loaded from server) */
3336
- file: z30.instanceof(File).nullable(),
3477
+ /**
3478
+ * The uploaded file (null when loaded from server).
3479
+ *
3480
+ * Deliberately NOT `z.instanceof(File)`: `File` is a browser global that Node
3481
+ * only exposes from v20. `z.instanceof` dereferences it while the schema is
3482
+ * being *constructed*, so merely importing this module threw
3483
+ * `ReferenceError: File is not defined` on Node 18 — which killed the whole
3484
+ * MCP CLI on startup, because its bundle pulls this barrel in for unrelated
3485
+ * types and never touches this schema. `z.custom` defers the check to
3486
+ * validation time and guards the global, so the module imports on any runtime
3487
+ * and only the browser upload path needs a real `File`.
3488
+ */
3489
+ file: z30.custom((value) => typeof File !== "undefined" && value instanceof File, {
3490
+ message: "Expected a File"
3491
+ }).nullable(),
3337
3492
  /** Type of media: image or video */
3338
3493
  type: CampaignCreativeType,
3339
3494
  /** Creative title (max 120 chars) - Maps to Meta's "Título" */
@@ -3442,6 +3597,9 @@ var HttpUrl = z30.string().url().refine((value) => {
3442
3597
  return false;
3443
3598
  }
3444
3599
  }, "URL must use http or https");
3600
+ 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), {
3601
+ message: 'adAccountId must look like "act_1234567890" (a bare numeric id is also accepted)'
3602
+ });
3445
3603
  var MetaCampaignDestination = z30.discriminatedUnion("type", [
3446
3604
  z30.object({
3447
3605
  type: z30.literal("landing"),
@@ -3474,7 +3632,7 @@ var MetaCampaignConfigBase = z30.object({
3474
3632
  campaignDailyBudget: z30.number().int().positive().optional(),
3475
3633
  /** Omitted preserves the historical recommended-floor behavior. */
3476
3634
  budgetPolicy: MetaBudgetPolicy.optional(),
3477
- adSets: z30.array(MetaAdSetConfig).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3635
+ adSets: z30.array(MetaAdSetConfig).min(1).max(CAMPAIGN_LIMITS.MAX_AD_SETS),
3478
3636
  destination: MetaCampaignDestination,
3479
3637
  saveAsDefault: z30.boolean().default(true)
3480
3638
  });
@@ -3507,7 +3665,7 @@ var MetaCampaignConfig = MetaCampaignConfigBase.superRefine((value, ctx) => {
3507
3665
  var MetaCampaignDefaults = MetaCampaignConfigBase.omit({
3508
3666
  saveAsDefault: true
3509
3667
  }).extend({
3510
- adSets: z30.array(MetaAdSetDefaults).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3668
+ adSets: z30.array(MetaAdSetDefaults).min(1).max(CAMPAIGN_LIMITS.MAX_AD_SETS),
3511
3669
  /**
3512
3670
  * Ad-account currency (ISO 4217: USD/COP/MXN/...) at the time the user saved
3513
3671
  * these defaults. The wizard re-uses the saved budget only when the current
@@ -3536,7 +3694,7 @@ var UploadToMetaRequest = z30.object({
3536
3694
  thumbnailUrl: HttpUrl.optional()
3537
3695
  })).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3538
3696
  /** Meta Ad Account ID */
3539
- adAccountId: z30.string().min(1),
3697
+ adAccountId: MetaAdAccountId,
3540
3698
  /** Optional Meta Pixel ID */
3541
3699
  pixelId: z30.string().optional(),
3542
3700
  /** Landing page URL (required for landing destinations; omitted for whatsapp) */
@@ -3618,7 +3776,7 @@ var MetaCampaignOverridesSchema = z31.object({
3618
3776
  destination: MetaCampaignDestination.optional()
3619
3777
  });
3620
3778
  var FacebookCreateCampaignBodySchema = z31.object({
3621
- adAccountId: z31.string().min(1, "Debes seleccionar una cuenta publicitaria"),
3779
+ adAccountId: MetaAdAccountId,
3622
3780
  pixelId: z31.string().optional(),
3623
3781
  landingUrl: HttpUrl2.optional(),
3624
3782
  campaignConfig: MetaCampaignConfig.optional(),
@@ -3694,8 +3852,8 @@ var FacebookCreateCampaignFromCreativesBodySchema = z31.object({
3694
3852
  mediaUrl: HttpUrl2,
3695
3853
  thumbnailUrl: HttpUrl2.optional(),
3696
3854
  creativeId: z31.string().min(1).optional()
3697
- })).min(1).max(4),
3698
- adAccountId: z31.string().min(1),
3855
+ })).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3856
+ adAccountId: MetaAdAccountId,
3699
3857
  pixelId: z31.string().optional(),
3700
3858
  /** Required unless campaignConfig.destination is 'whatsapp' (see superRefine below). */
3701
3859
  landingUrl: HttpUrl2.optional(),
@@ -3810,8 +3968,8 @@ var FacebookCreateCampaignFromTasksBodySchema = z31.object({
3810
3968
  description: z31.string().max(500).optional(),
3811
3969
  cta: MetaCtaType.optional(),
3812
3970
  creativeId: z31.string().min(1).optional()
3813
- })).min(1).max(4),
3814
- adAccountId: z31.string().min(1),
3971
+ })).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
3972
+ adAccountId: MetaAdAccountId,
3815
3973
  pixelId: z31.string().optional(),
3816
3974
  landingUrl: HttpUrl2.optional(),
3817
3975
  targetCountry: z31.string().length(2).default("US"),
@@ -3955,6 +4113,23 @@ var DEFAULT_PRICING = {
3955
4113
  ourCostCents: 0.4,
3956
4114
  userPriceCents: 0.8
3957
4115
  },
4116
+ // WhatsApp ProductMaker intent proposal. Real OpenRouter usage.cost wins;
4117
+ // this is only the fallback when the relay omits cost.
4118
+ "openrouter:whatsapp-product-agent-intent": {
4119
+ ourCostCents: 0.4,
4120
+ userPriceCents: 0.8
4121
+ },
4122
+ // Internal support calls are cost-only: callers explicitly override user
4123
+ // price to zero. Static COGS estimates apply because OpenAI Responses exposes
4124
+ // token counts but not the provider's dollar charge.
4125
+ "openai:whatsapp-support-classifier": {
4126
+ ourCostCents: 0.1,
4127
+ userPriceCents: 0.2
4128
+ },
4129
+ "openai:whatsapp-support-draft": {
4130
+ ourCostCents: 0.4,
4131
+ userPriceCents: 0.8
4132
+ },
3958
4133
  // Task-derived creative recommendation (angle/persona/script/landing-section).
3959
4134
  // OpenRouter usage normally supplies the exact COGS and the call layer charges
3960
4135
  // 2x. This rule is the fail-closed fallback when usage.cost is unavailable.
@@ -6364,6 +6539,43 @@ var RegenerateEditRequestSchema = z36.object({
6364
6539
  comment: z36.string().min(1).max(2e3)
6365
6540
  });
6366
6541
 
6542
+ // ../../packages/types/src/scene-subject-contract.ts
6543
+ import { z as z37 } from "zod";
6544
+ var SceneSubjectContractSchema = z37.object({
6545
+ maxPeople: z37.number().int().min(1).max(10),
6546
+ minPeople: z37.number().int().min(0).max(10),
6547
+ minDistinctPeople: z37.number().int().min(0).max(10),
6548
+ maxHands: z37.number().int().min(0).max(20)
6549
+ }).superRefine((value, ctx) => {
6550
+ if (value.minPeople > value.maxPeople) {
6551
+ ctx.addIssue({
6552
+ code: z37.ZodIssueCode.custom,
6553
+ path: [
6554
+ "minPeople"
6555
+ ],
6556
+ message: "minPeople cannot exceed maxPeople"
6557
+ });
6558
+ }
6559
+ if (value.minDistinctPeople > value.maxPeople) {
6560
+ ctx.addIssue({
6561
+ code: z37.ZodIssueCode.custom,
6562
+ path: [
6563
+ "minDistinctPeople"
6564
+ ],
6565
+ message: "minDistinctPeople cannot exceed maxPeople"
6566
+ });
6567
+ }
6568
+ if (value.minDistinctPeople > value.minPeople) {
6569
+ ctx.addIssue({
6570
+ code: z37.ZodIssueCode.custom,
6571
+ path: [
6572
+ "minDistinctPeople"
6573
+ ],
6574
+ message: "minDistinctPeople cannot exceed minPeople"
6575
+ });
6576
+ }
6577
+ });
6578
+
6367
6579
  // src/resolve-media.ts
6368
6580
  var MAX_MEDIA_BYTES = CAMPAIGN_LIMITS.MAX_FILE_SIZE_BYTES;
6369
6581
  var MEDIA_FORMATS = [
@@ -6603,10 +6815,10 @@ async function resolveMediaRef(ref, opts) {
6603
6815
  __name(resolveMediaRef, "resolveMediaRef");
6604
6816
 
6605
6817
  // src/tools/shared.ts
6606
- import { z as z37 } from "zod";
6607
- var Lang = z37.string().min(2).max(10);
6608
- var Country = z37.string().min(2).max(3);
6609
- var LandingStrategyEnum = z37.enum([
6818
+ import { z as z38 } from "zod";
6819
+ var Lang = z38.string().min(2).max(10);
6820
+ var Country = z38.string().min(2).max(3);
6821
+ var LandingStrategyEnum = z38.enum([
6610
6822
  "auto",
6611
6823
  "classic",
6612
6824
  "pain_solution",
@@ -6615,12 +6827,12 @@ var LandingStrategyEnum = z37.enum([
6615
6827
  "offer_value",
6616
6828
  "lifestyle_identity"
6617
6829
  ]);
6618
- var LandingImageFormatEnum = z37.enum([
6830
+ var LandingImageFormatEnum = z38.enum([
6619
6831
  "9:16",
6620
6832
  "4:5",
6621
6833
  "1:1"
6622
6834
  ]);
6623
- var VideoPresetEnum = z37.enum([
6835
+ var VideoPresetEnum = z38.enum([
6624
6836
  "auto",
6625
6837
  "ugc_dynamic",
6626
6838
  "ugc_unboxing_chaos",
@@ -6631,7 +6843,7 @@ var VideoPresetEnum = z37.enum([
6631
6843
  "pixar_problem",
6632
6844
  "pixar_sassy"
6633
6845
  ]);
6634
- var TaskCreativeVariantVideoPresetEnum = z37.enum([
6846
+ var TaskCreativeVariantVideoPresetEnum = z38.enum([
6635
6847
  "auto",
6636
6848
  "ugc_dynamic",
6637
6849
  "problem_solution",
@@ -6690,7 +6902,7 @@ function resolveVideoPreset(preset) {
6690
6902
  return VIDEO_PRESET_RESOLUTION[preset] ?? null;
6691
6903
  }
6692
6904
  __name(resolveVideoPreset, "resolveVideoPreset");
6693
- var VideoTierEnum = z37.enum([
6905
+ var VideoTierEnum = z38.enum([
6694
6906
  "basic"
6695
6907
  ]);
6696
6908
  function resolveVideoTier(_tier) {
@@ -6705,17 +6917,17 @@ function resolveVideoFormatForDuration(durationSeconds) {
6705
6917
  return durationSeconds === 30 ? "double_30s" : "single_15s";
6706
6918
  }
6707
6919
  __name(resolveVideoFormatForDuration, "resolveVideoFormatForDuration");
6708
- var VideoResolutionEnum = z37.enum([
6920
+ var VideoResolutionEnum = z38.enum([
6709
6921
  "480p",
6710
6922
  "720p"
6711
6923
  ]);
6712
- var VideoStyleEnum = z37.enum([
6924
+ var VideoStyleEnum = z38.enum([
6713
6925
  "ugc",
6714
6926
  "cartoon_3d",
6715
6927
  "product_only",
6716
6928
  "ai_dynamic"
6717
6929
  ]);
6718
- var NarrativeStyleEnum = z37.enum([
6930
+ var NarrativeStyleEnum = z38.enum([
6719
6931
  "review",
6720
6932
  "problem_product_cta",
6721
6933
  "product_showcase",
@@ -6730,7 +6942,7 @@ var NarrativeStyleEnum = z37.enum([
6730
6942
  "ugc_skeptic_confession",
6731
6943
  "ugc_demo_review"
6732
6944
  ]);
6733
- var PublicNarrativeStyleEnum = z37.enum([
6945
+ var PublicNarrativeStyleEnum = z38.enum([
6734
6946
  "review",
6735
6947
  "ugc_unboxing_chaos",
6736
6948
  "problem_product_cta",
@@ -6740,7 +6952,7 @@ var PublicNarrativeStyleEnum = z37.enum([
6740
6952
  "animated_problem_pixar",
6741
6953
  "sassy_product_pixar"
6742
6954
  ]);
6743
- var VideoModelEnum = z37.enum([
6955
+ var VideoModelEnum = z38.enum([
6744
6956
  "veo",
6745
6957
  "wan",
6746
6958
  "bytedance",
@@ -6748,25 +6960,25 @@ var VideoModelEnum = z37.enum([
6748
6960
  "grok",
6749
6961
  "kling"
6750
6962
  ]);
6751
- var ProductReferenceUserChoiceEnum = z37.enum([
6963
+ var ProductReferenceUserChoiceEnum = z38.enum([
6752
6964
  "same_product",
6753
6965
  "variants",
6754
6966
  "different_products",
6755
6967
  "combo_distinct"
6756
6968
  ]);
6757
- var ProductReferenceCandidateGroupingEnum = z37.enum([
6969
+ var ProductReferenceCandidateGroupingEnum = z38.enum([
6758
6970
  "same_product_same_variant",
6759
6971
  "same_product_different_variants",
6760
6972
  "different_products",
6761
6973
  "unclear"
6762
6974
  ]);
6763
- var GapDurationChoiceMirror = z37.object({
6764
- angleIndex: z37.number().int().nonnegative(),
6765
- recommendedSec: z37.union([
6766
- z37.literal(15),
6767
- z37.literal(30)
6975
+ var GapDurationChoiceMirror = z38.object({
6976
+ angleIndex: z38.number().int().nonnegative(),
6977
+ recommendedSec: z38.union([
6978
+ z38.literal(15),
6979
+ z38.literal(30)
6768
6980
  ]),
6769
- estimatedDeltaCredits: z37.number().int().nonnegative().optional()
6981
+ estimatedDeltaCredits: z38.number().int().nonnegative().optional()
6770
6982
  });
6771
6983
  function buildSelectedImageUrlsHash(urls) {
6772
6984
  const input = [
@@ -6780,13 +6992,13 @@ function buildSelectedImageUrlsHash(urls) {
6780
6992
  return `fnv1a:${hash.toString(16).padStart(8, "0")}:${urls.length}`;
6781
6993
  }
6782
6994
  __name(buildSelectedImageUrlsHash, "buildSelectedImageUrlsHash");
6783
- var ImageCreativeStyleEnum = z37.enum([
6995
+ var ImageCreativeStyleEnum = z38.enum([
6784
6996
  "studio",
6785
6997
  "floating",
6786
6998
  "ingredients",
6787
6999
  "in_use"
6788
7000
  ]);
6789
- var ImageCreativeArchetypeEnum = z37.enum([
7001
+ var ImageCreativeArchetypeEnum = z38.enum([
6790
7002
  "direct_response_product_ad",
6791
7003
  "hero_outcome",
6792
7004
  "offer_value_prop",
@@ -6795,23 +7007,23 @@ var ImageCreativeArchetypeEnum = z37.enum([
6795
7007
  "problem_solution",
6796
7008
  "lifestyle_identity"
6797
7009
  ]);
6798
- var ImageCreativeVisualTreatmentEnum = z37.enum([
7010
+ var ImageCreativeVisualTreatmentEnum = z38.enum([
6799
7011
  "performance_product_ad",
6800
7012
  "premium_dtc_carousel"
6801
7013
  ]);
6802
- var ImageCreativeAspectRatioEnum = z37.enum([
7014
+ var ImageCreativeAspectRatioEnum = z38.enum([
6803
7015
  "1:1",
6804
7016
  "4:5",
6805
7017
  "9:16",
6806
7018
  "16:9"
6807
7019
  ]);
6808
- var ImageRefSchema = z37.object({
6809
- 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.'),
6810
- 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`.')
7020
+ var ImageRefSchema = z38.object({
7021
+ 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.'),
7022
+ 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`.')
6811
7023
  }).refine((v) => Boolean(v.url) !== Boolean(v.path), {
6812
7024
  message: "each image entry must have exactly one of `url` or `path`"
6813
7025
  });
6814
- 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.');
7026
+ 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.');
6815
7027
  function asText(value) {
6816
7028
  return {
6817
7029
  content: [
@@ -6864,6 +7076,28 @@ async function submitCreative(api, opts) {
6864
7076
  });
6865
7077
  }
6866
7078
  __name(submitCreative, "submitCreative");
7079
+ var ResearchTargetCountryEnum = z38.enum([
7080
+ "CO",
7081
+ "MX",
7082
+ "PE",
7083
+ "EC",
7084
+ "CL"
7085
+ ]);
7086
+ var ResearchCategoryEnum = z38.enum([
7087
+ "auto",
7088
+ "automotive",
7089
+ "kitchen",
7090
+ "pets",
7091
+ "home_organization",
7092
+ "beauty",
7093
+ "wellness",
7094
+ "fitness",
7095
+ "baby",
7096
+ "tech",
7097
+ "tools",
7098
+ "garden",
7099
+ "cleaning"
7100
+ ]);
6867
7101
 
6868
7102
  // src/errors.ts
6869
7103
  var McpErrorCode = {
@@ -6895,6 +7129,7 @@ var McpErrorCode = {
6895
7129
  TASK_NOT_READY: "TASK_NOT_READY",
6896
7130
  CREATIVE_VARIANT_FAILED: "CREATIVE_VARIANT_FAILED",
6897
7131
  CREATIVE_VARIANT_DISPATCH_FAILED: "CREATIVE_VARIANT_DISPATCH_FAILED",
7132
+ PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT: "PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT",
6898
7133
  // Connection presence
6899
7134
  NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
6900
7135
  NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
@@ -7016,12 +7251,13 @@ var CODE_MESSAGES = {
7016
7251
  TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
7017
7252
  CREATIVE_VARIANT_FAILED: "The previous creative variant attempt failed. Ask for confirmation before starting a new paid attempt with a new clientRequestId.",
7018
7253
  CREATIVE_VARIANT_DISPATCH_FAILED: "The creative variant could not be queued. Retry as a new paid attempt with a new clientRequestId.",
7254
+ 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.",
7019
7255
  // API key
7020
7256
  INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
7021
7257
  REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
7022
7258
  // Rate
7023
7259
  RATE_LIMITED: "Too many requests. Wait a few seconds.",
7024
- INSUFFICIENT_CREDITS: "Insufficient credits to create this variant. Check your balance and retry after adding credits.",
7260
+ INSUFFICIENT_CREDITS: "Insufficient credits for this action. Check your balance and add credits at https://productmaker.app before retrying.",
7025
7261
  TASK_QUOTA_EXCEEDED: "You reached the active-task limit. Wait for a task to finish before creating another variant.",
7026
7262
  // Generic
7027
7263
  UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
@@ -7034,6 +7270,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
7034
7270
  McpErrorCode.INVALID_INPUT,
7035
7271
  McpErrorCode.INSUFFICIENT_CREDITS,
7036
7272
  McpErrorCode.TASK_QUOTA_EXCEEDED,
7273
+ McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
7037
7274
  McpErrorCode.INVALID_API_KEY,
7038
7275
  McpErrorCode.REVOKED_API_KEY,
7039
7276
  McpErrorCode.IMAGE_PATH_NOT_FOUND,
@@ -7096,6 +7333,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
7096
7333
  McpErrorCode.INSUFFICIENT_CREDITS,
7097
7334
  McpErrorCode.TASK_QUOTA_EXCEEDED,
7098
7335
  McpErrorCode.CREATIVE_VARIANT_DISPATCH_FAILED,
7336
+ McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
7099
7337
  // Shopify
7100
7338
  McpErrorCode.SHOPIFY_NOT_CONNECTED,
7101
7339
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
@@ -7189,6 +7427,11 @@ function resolveMessage(code, backendMessage) {
7189
7427
  return `The service returned an error (code: ${code}). Try again shortly or contact support if it persists.`;
7190
7428
  }
7191
7429
  __name(resolveMessage, "resolveMessage");
7430
+ function invalidInputFromZod(error) {
7431
+ const detail = error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
7432
+ return mcp(McpErrorCode.INVALID_INPUT, detail || resolveMessage(McpErrorCode.INVALID_INPUT, void 0));
7433
+ }
7434
+ __name(invalidInputFromZod, "invalidInputFromZod");
7192
7435
  function translateError(err) {
7193
7436
  if (err instanceof McpError) {
7194
7437
  return err;
@@ -7204,9 +7447,11 @@ function translateError(err) {
7204
7447
  const body = isRecord(err.body) ? err.body : {};
7205
7448
  if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
7206
7449
  const code = body.code;
7207
- return mcp(code, resolveMessage(code, body.message), body.helpUrl ? {
7208
- helpUrl: body.helpUrl
7209
- } : {});
7450
+ const extra = {};
7451
+ if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
7452
+ if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
7453
+ if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
7454
+ return mcp(code, resolveMessage(code, body.message), extra);
7210
7455
  }
7211
7456
  if (err.status === 401) {
7212
7457
  return mcp(McpErrorCode.INVALID_API_KEY, resolveMessage(McpErrorCode.INVALID_API_KEY, void 0));
@@ -7451,66 +7696,66 @@ function renderOutputsMarkdown(outputs) {
7451
7696
  __name(renderOutputsMarkdown, "renderOutputsMarkdown");
7452
7697
 
7453
7698
  // src/tools/tasks.ts
7454
- var CreateProductTaskInput = z38.object({
7699
+ var CreateProductTaskInput = z39.object({
7455
7700
  images: ImagesInput,
7456
- 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."),
7701
+ 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."),
7457
7702
  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.'),
7458
7703
  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.'),
7459
- 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.'),
7460
- 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".'),
7461
- 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.'),
7462
- 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.'),
7463
- 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."),
7464
- 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.'),
7704
+ 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.'),
7705
+ 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".'),
7706
+ 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.'),
7707
+ 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.'),
7708
+ 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."),
7709
+ 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.'),
7465
7710
  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.'),
7466
- durationSeconds: z38.union([
7467
- z38.literal(15),
7468
- z38.literal(30)
7711
+ durationSeconds: z39.union([
7712
+ z39.literal(15),
7713
+ z39.literal(30)
7469
7714
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
7470
- 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."),
7715
+ 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."),
7471
7716
  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.'),
7472
7717
  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).'),
7473
- 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."),
7718
+ 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."),
7474
7719
  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.'),
7475
- outputs: z38.array(z38.enum([
7720
+ outputs: z39.array(z39.enum([
7476
7721
  "landing",
7477
7722
  "video",
7478
7723
  "image"
7479
7724
  ])).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.'),
7480
- 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."),
7481
- 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."),
7482
- 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.'),
7725
+ 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."),
7726
+ 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."),
7727
+ 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.'),
7483
7728
  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".'),
7484
7729
  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".')
7485
7730
  }).strict();
7486
- var GetTaskStatusInput = z38.object({
7487
- taskId: z38.string().min(1),
7488
- waitSeconds: z38.number().int().min(0).max(120).optional()
7489
- });
7490
- var ListTasksInput = z38.object({
7491
- limit: z38.number().int().min(1).max(50).optional(),
7492
- offset: z38.number().int().min(0).optional(),
7493
- search: z38.string().optional()
7494
- });
7495
- var EditTaskDraftInput = z38.object({
7496
- taskId: z38.string().min(1),
7497
- angleIndex: z38.number().int().min(0),
7498
- angle: z38.object({
7499
- angle: z38.string().min(1).max(480),
7500
- buyerPersona: z38.string().min(1).max(360),
7501
- actor: z38.string().min(1).max(360).optional(),
7502
- gender: z38.enum([
7731
+ var GetTaskStatusInput = z39.object({
7732
+ taskId: z39.string().min(1),
7733
+ waitSeconds: z39.number().int().min(0).max(120).optional()
7734
+ });
7735
+ var ListTasksInput = z39.object({
7736
+ limit: z39.number().int().min(1).max(50).optional(),
7737
+ offset: z39.number().int().min(0).optional(),
7738
+ search: z39.string().optional()
7739
+ });
7740
+ var EditTaskDraftInput = z39.object({
7741
+ taskId: z39.string().min(1),
7742
+ angleIndex: z39.number().int().min(0),
7743
+ angle: z39.object({
7744
+ angle: z39.string().min(1).max(480),
7745
+ buyerPersona: z39.string().min(1).max(360),
7746
+ actor: z39.string().min(1).max(360).optional(),
7747
+ gender: z39.enum([
7503
7748
  "female",
7504
7749
  "male"
7505
7750
  ]).optional(),
7506
- scriptNotes: z38.string().max(2e3).optional()
7751
+ scriptNotes: z39.string().max(2e3).optional()
7507
7752
  })
7508
7753
  });
7509
- var PendingActionPayload = z38.discriminatedUnion("kind", [
7510
- z38.object({
7511
- kind: z38.literal("product_reference_confirmation"),
7754
+ var PendingActionPayload = z39.discriminatedUnion("kind", [
7755
+ z39.object({
7756
+ kind: z39.literal("product_reference_confirmation"),
7512
7757
  userChoice: ProductReferenceUserChoiceEnum,
7513
- confirmedProductNames: z38.array(z38.string().min(1).max(120)).min(2).max(8).optional(),
7758
+ confirmedProductNames: z39.array(z39.string().min(1).max(120)).min(2).max(8).optional(),
7514
7759
  // SCOPED multi-scene opt-in — the ONLY way STANDARD ("Multiescena") is
7515
7760
  // reachable via MCP. Valid ONLY together with userChoice='variants' (same
7516
7761
  // product in several colors): renders one shot per color and stitches them,
@@ -7521,62 +7766,62 @@ var PendingActionPayload = z38.discriminatedUnion("kind", [
7521
7766
  // safe because multi-scene IS the intended render for a color variant set,
7522
7767
  // not a blind quality upsell. Maps to `videoTier: 'standard'` on the
7523
7768
  // confirm call; the backend re-gates it (variants + variant_set only).
7524
- 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".')
7769
+ 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".')
7525
7770
  }),
7526
- z38.object({
7527
- kind: z38.literal("angle_review_approval"),
7771
+ z39.object({
7772
+ kind: z39.literal("angle_review_approval"),
7528
7773
  // All edits are optional — empty action body just approves the
7529
7774
  // backend-generated draft as-is. Field shapes mirror the angle-review
7530
7775
  // request contracts in @packages/types/wizard.ts; the handler routes to
7531
7776
  // the dedicated video/image/autopilot endpoint based on StatusResponse.kind.
7532
- customAngle: z38.string().min(1).max(480).optional(),
7777
+ customAngle: z39.string().min(1).max(480).optional(),
7533
7778
  // Keep the shared MCP action within the strictest downstream contract:
7534
7779
  // autopilot/image resume endpoints cap these fields at 1500 characters.
7535
- painPoints: z38.string().max(1500).optional(),
7536
- benefits: z38.string().max(1500).optional(),
7537
- features: z38.array(z38.string().min(1).max(300)).max(10).optional(),
7538
- gapDurationSeconds: z38.union([
7539
- z38.literal(15),
7540
- z38.literal(30)
7780
+ painPoints: z39.string().max(1500).optional(),
7781
+ benefits: z39.string().max(1500).optional(),
7782
+ features: z39.array(z39.string().min(1).max(300)).max(10).optional(),
7783
+ gapDurationSeconds: z39.union([
7784
+ z39.literal(15),
7785
+ z39.literal(30)
7541
7786
  ]).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.")
7542
7787
  })
7543
7788
  ]);
7544
- var ResolvePendingActionInput = z38.object({
7545
- taskId: z38.string().min(1),
7789
+ var ResolvePendingActionInput = z39.object({
7790
+ taskId: z39.string().min(1),
7546
7791
  action: PendingActionPayload
7547
7792
  }).strict();
7548
7793
  function resolveVariantConfirmVideoTier(action) {
7549
7794
  return action.useMultiSceneShowcase && action.userChoice === "variants" ? "standard" : void 0;
7550
7795
  }
7551
7796
  __name(resolveVariantConfirmVideoTier, "resolveVariantConfirmVideoTier");
7552
- var StatusForResolveSchema = z38.object({
7797
+ var StatusForResolveSchema = z39.object({
7553
7798
  // Metadata used to select the backend resume contract. Older API responses
7554
7799
  // may omit these fields, so the resolver keeps the generic fallback.
7555
- kind: z38.enum([
7800
+ kind: z39.enum([
7556
7801
  "autopilot",
7557
7802
  "video",
7558
7803
  "image",
7559
7804
  "landing",
7560
7805
  "mix"
7561
7806
  ]).optional(),
7562
- selectedOutputs: z38.array(z38.enum([
7807
+ selectedOutputs: z39.array(z39.enum([
7563
7808
  "landing",
7564
7809
  "video",
7565
7810
  "image"
7566
7811
  ])).optional(),
7567
- videoAngleCount: z38.number().int().min(1).max(3).optional(),
7568
- ugcFormat: z38.enum([
7812
+ videoAngleCount: z39.number().int().min(1).max(3).optional(),
7813
+ ugcFormat: z39.enum([
7569
7814
  "single_8s",
7570
7815
  "single_15s",
7571
7816
  "double_30s"
7572
7817
  ]).optional(),
7573
- videoStyle: z38.enum([
7818
+ videoStyle: z39.enum([
7574
7819
  "ugc",
7575
7820
  "cartoon_3d",
7576
7821
  "product_only",
7577
7822
  "ai_dynamic"
7578
7823
  ]).optional(),
7579
- videoModel: z38.enum([
7824
+ videoModel: z39.enum([
7580
7825
  "veo",
7581
7826
  "wan",
7582
7827
  "bytedance",
@@ -7584,26 +7829,26 @@ var StatusForResolveSchema = z38.object({
7584
7829
  "grok",
7585
7830
  "kling"
7586
7831
  ]).optional(),
7587
- videoTier: z38.enum([
7832
+ videoTier: z39.enum([
7588
7833
  "legacy",
7589
7834
  "standard",
7590
7835
  "pro",
7591
7836
  "single_call"
7592
7837
  ]).optional(),
7593
- videoResolution: z38.enum([
7838
+ videoResolution: z39.enum([
7594
7839
  "480p",
7595
7840
  "720p"
7596
7841
  ]).optional(),
7597
7842
  narrativeStyle: NarrativeStyleEnum.optional(),
7598
- pendingUserAction: z38.object({
7599
- kind: z38.string(),
7600
- candidateGrouping: z38.string().optional(),
7601
- selectedImageUrls: z38.array(z38.string()).optional(),
7602
- proposedProductNames: z38.array(z38.string()).optional(),
7603
- customAngleDraft: z38.string().optional(),
7604
- painPointsDraft: z38.string().optional(),
7605
- benefitsDraft: z38.string().optional(),
7606
- featuresDraft: z38.array(z38.string()).optional(),
7843
+ pendingUserAction: z39.object({
7844
+ kind: z39.string(),
7845
+ candidateGrouping: z39.string().optional(),
7846
+ selectedImageUrls: z39.array(z39.string()).optional(),
7847
+ proposedProductNames: z39.array(z39.string()).optional(),
7848
+ customAngleDraft: z39.string().optional(),
7849
+ painPointsDraft: z39.string().optional(),
7850
+ benefitsDraft: z39.string().optional(),
7851
+ featuresDraft: z39.array(z39.string()).optional(),
7607
7852
  // Recommendation consumed only when the caller explicitly sends
7608
7853
  // gapDurationSeconds: 30 in the angle approval action.
7609
7854
  gapDurationChoice: GapDurationChoiceMirror.optional()
@@ -7925,54 +8170,54 @@ __name(registerTaskTools, "registerTaskTools");
7925
8170
 
7926
8171
  // src/tools/creatives.ts
7927
8172
  import { randomUUID } from "crypto";
7928
- import { z as z39 } from "zod";
8173
+ import { z as z40 } from "zod";
7929
8174
  var CreativeRefs = {
7930
8175
  images: ImagesInput,
7931
8176
  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.'),
7932
8177
  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.'),
7933
- 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.'),
7934
- 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.'),
7935
- 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".'),
7936
- 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.'),
7937
- 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."),
7938
- 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.'),
7939
- 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.'),
7940
- isCombo: z39.boolean().optional().describe("Opcional. Si true, el creativo presenta el producto como combo/pack. Default false."),
7941
- 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."),
7942
- 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."),
7943
- 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.')
8178
+ 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.'),
8179
+ 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.'),
8180
+ 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".'),
8181
+ 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.'),
8182
+ 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."),
8183
+ 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.'),
8184
+ 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.'),
8185
+ isCombo: z40.boolean().optional().describe("Opcional. Si true, el creativo presenta el producto como combo/pack. Default false."),
8186
+ 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."),
8187
+ 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."),
8188
+ 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.')
7944
8189
  };
7945
- var VideoInput = z39.object({
8190
+ var VideoInput = z40.object({
7946
8191
  images: CreativeRefs.images,
7947
- title: z39.string().min(1).describe('T\xEDtulo corto del producto. Aparece en la primera vista del video. Ej. "Crema Anti-Edad Premium".'),
7948
- 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".'),
8192
+ title: z40.string().min(1).describe('T\xEDtulo corto del producto. Aparece en la primera vista del video. Ej. "Crema Anti-Edad Premium".'),
8193
+ 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".'),
7949
8194
  language: CreativeRefs.language,
7950
8195
  country: CreativeRefs.country,
7951
- aspectRatio: z39.enum([
8196
+ aspectRatio: z40.enum([
7952
8197
  "9:16",
7953
8198
  "16:9",
7954
8199
  "Auto"
7955
8200
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
7956
- durationSeconds: z39.union([
7957
- z39.literal(15),
7958
- z39.literal(30)
8201
+ durationSeconds: z40.union([
8202
+ z40.literal(15),
8203
+ z40.literal(30)
7959
8204
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
7960
8205
  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).'),
7961
8206
  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.'),
7962
8207
  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).'),
7963
- 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"].'),
8208
+ 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"].'),
7964
8209
  price: CreativeRefs.price,
7965
8210
  offer: CreativeRefs.offer,
7966
8211
  productContext: CreativeRefs.productContext,
7967
8212
  productLabel: CreativeRefs.productLabel,
7968
- 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.'),
7969
- 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.'),
7970
- 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."),
7971
- 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."),
8213
+ 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.'),
8214
+ 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.'),
8215
+ 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."),
8216
+ 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."),
7972
8217
  competitorScanEnabled: CreativeRefs.competitorScanEnabled,
7973
8218
  brandId: CreativeRefs.brandId
7974
8219
  }).strict();
7975
- var FreeVideoQualityEnum = z39.enum([
8220
+ var FreeVideoQualityEnum = z40.enum([
7976
8221
  "economical",
7977
8222
  "high_detail"
7978
8223
  ]);
@@ -7994,38 +8239,38 @@ var FREE_VIDEO_DURATIONS_BY_QUALITY = {
7994
8239
  15
7995
8240
  ]
7996
8241
  };
7997
- var FreeVideoInput = z39.object({
8242
+ var FreeVideoInput = z40.object({
7998
8243
  images: ImagesInput.optional().describe("Opcional. 1-5 fotos del producto. Om\xEDtelo para generar desde texto puro."),
7999
- 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."),
8244
+ 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."),
8000
8245
  quality: FreeVideoQualityEnum.describe('Calidad del video: "economical" = Econ\xF3mico o "high_detail" = Alto detalle.'),
8001
- 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."),
8246
+ 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."),
8002
8247
  videoResolution: VideoResolutionEnum.describe("Resoluci\xF3n del video: 480p o 720p."),
8003
- generateAiMusic: z39.boolean().default(false).describe("Opcional. Agrega m\xFAsica instrumental IA debajo del audio nativo. Default false."),
8004
- addSubtitles: z39.boolean().default(false).describe("Opcional. Quema subt\xEDtulos usando el rawPrompt exacto. Default false."),
8005
- 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.")
8248
+ generateAiMusic: z40.boolean().default(false).describe("Opcional. Agrega m\xFAsica instrumental IA debajo del audio nativo. Default false."),
8249
+ addSubtitles: z40.boolean().default(false).describe("Opcional. Quema subt\xEDtulos usando el rawPrompt exacto. Default false."),
8250
+ 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.")
8006
8251
  }).strict();
8007
8252
  var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
8008
8253
  var archetypeList = ImageCreativeArchetypeEnum.options.join(" | ");
8009
8254
  var visualTreatmentList = ImageCreativeVisualTreatmentEnum.options.join(" | ");
8010
8255
  var aspectRatioList = ImageCreativeAspectRatioEnum.options.join(" | ");
8011
- var ImageInput = z39.object({
8256
+ var ImageInput = z40.object({
8012
8257
  images: CreativeRefs.images,
8013
- productTitle: z39.string().min(1).describe('Nombre del producto. Ej. "Crema Anti-Edad Premium".'),
8014
- primaryAngle: z39.string().min(1).describe('\xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas".'),
8258
+ productTitle: z40.string().min(1).describe('Nombre del producto. Ej. "Crema Anti-Edad Premium".'),
8259
+ primaryAngle: z40.string().min(1).describe('\xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas".'),
8015
8260
  buyerPersona: CreativeRefs.buyerPersona,
8016
8261
  language: CreativeRefs.language,
8017
8262
  country: CreativeRefs.country,
8018
- nVariants: z39.number().int().min(1).max(6).optional().describe("Opcional. N\xFAmero de variantes a generar (1-6). Default 3."),
8019
- 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.'),
8263
+ nVariants: z40.number().int().min(1).max(6).optional().describe("Opcional. N\xFAmero de variantes a generar (1-6). Default 3."),
8264
+ 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.'),
8020
8265
  imageStyle: ImageCreativeStyleEnum.optional().describe(`Opcional. Estilo del shot del producto. Valores: ${imageStyleList}. Si se omite, la IA escoge seg\xFAn el archetype.`),
8021
8266
  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).`),
8022
8267
  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.`),
8023
- 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."),
8268
+ 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."),
8024
8269
  aspectRatio: ImageCreativeAspectRatioEnum.optional().describe(`Opcional. Relaci\xF3n de aspecto. Valores: ${aspectRatioList}. Default "4:5" (vertical Instagram feed).`),
8025
- generateAdCopy: z39.boolean().optional().describe("Opcional. Si true, tambi\xE9n genera headline/body/CTA para el ad. Default false."),
8026
- 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."),
8027
- features: z39.array(z39.string()).optional().describe('Opcional. Features/beneficios a destacar visualmente. Ej. ["resistente al agua", "ultra liviano"].'),
8028
- 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.'),
8270
+ generateAdCopy: z40.boolean().optional().describe("Opcional. Si true, tambi\xE9n genera headline/body/CTA para el ad. Default false."),
8271
+ 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."),
8272
+ features: z40.array(z40.string()).optional().describe('Opcional. Features/beneficios a destacar visualmente. Ej. ["resistente al agua", "ultra liviano"].'),
8273
+ 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.'),
8029
8274
  price: CreativeRefs.price,
8030
8275
  compareAtPrice: CreativeRefs.compareAtPrice,
8031
8276
  offer: CreativeRefs.offer,
@@ -8036,11 +8281,11 @@ var ImageInput = z39.object({
8036
8281
  brandId: CreativeRefs.brandId,
8037
8282
  creativeDirection: CreativeRefs.creativeDirection
8038
8283
  });
8039
- var LandingInput = z39.object({
8284
+ var LandingInput = z40.object({
8040
8285
  images: CreativeRefs.images,
8041
8286
  language: CreativeRefs.language,
8042
8287
  country: CreativeRefs.country,
8043
- 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.'),
8288
+ 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.'),
8044
8289
  buyerPersona: CreativeRefs.buyerPersona,
8045
8290
  price: CreativeRefs.price,
8046
8291
  compareAtPrice: CreativeRefs.compareAtPrice,
@@ -8138,8 +8383,8 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
8138
8383
  __name(registerCreativeTools, "registerCreativeTools");
8139
8384
 
8140
8385
  // src/tools/creative-variants.ts
8141
- import { z as z40 } from "zod";
8142
- var LandingSectionKeyEnum = z40.enum([
8386
+ import { z as z41 } from "zod";
8387
+ var LandingSectionKeyEnum = z41.enum([
8143
8388
  "problem_hook",
8144
8389
  "problem_cost",
8145
8390
  "solution_mechanism",
@@ -8179,24 +8424,24 @@ var LandingSectionKeyEnum = z40.enum([
8179
8424
  "lifestyle_fit",
8180
8425
  "join_lifestyle"
8181
8426
  ]);
8182
- var CreativeVariantKindEnum = z40.enum([
8427
+ var CreativeVariantKindEnum = z41.enum([
8183
8428
  "video",
8184
8429
  "image",
8185
8430
  "landing"
8186
8431
  ]);
8187
- var EstimateCreativeVariantInputBase = z40.object({
8188
- sourceTaskId: z40.string().uuid().describe("ID de la tarea original completada."),
8432
+ var EstimateCreativeVariantInputBase = z41.object({
8433
+ sourceTaskId: z41.string().uuid().describe("ID de la tarea original completada."),
8189
8434
  kind: CreativeVariantKindEnum,
8190
8435
  videoPreset: TaskCreativeVariantVideoPresetEnum.optional().describe("Opcional para video. Solo cambia el preset p\xFAblico; el servidor conserva el modelo/tier del task origen."),
8191
8436
  videoResolution: VideoResolutionEnum.optional().describe("Opcional para video. Resoluci\xF3n del nuevo render."),
8192
- nVariants: z40.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8437
+ nVariants: z41.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8193
8438
  sectionKey: LandingSectionKeyEnum.optional().describe("Opcional para landing. Si ya conoces la secci\xF3n, evita cotizar la clasificaci\xF3n con IA."),
8194
- 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.")
8439
+ 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.")
8195
8440
  }).strict();
8196
8441
  var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine((input, ctx) => {
8197
8442
  if (input.kind === "video" && input.nVariants !== void 0) {
8198
8443
  ctx.addIssue({
8199
- code: z40.ZodIssueCode.custom,
8444
+ code: z41.ZodIssueCode.custom,
8200
8445
  path: [
8201
8446
  "nVariants"
8202
8447
  ],
@@ -8206,7 +8451,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8206
8451
  if (input.kind !== "video") {
8207
8452
  if (input.videoPreset !== void 0) {
8208
8453
  ctx.addIssue({
8209
- code: z40.ZodIssueCode.custom,
8454
+ code: z41.ZodIssueCode.custom,
8210
8455
  path: [
8211
8456
  "videoPreset"
8212
8457
  ],
@@ -8215,7 +8460,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8215
8460
  }
8216
8461
  if (input.videoResolution !== void 0) {
8217
8462
  ctx.addIssue({
8218
- code: z40.ZodIssueCode.custom,
8463
+ code: z41.ZodIssueCode.custom,
8219
8464
  path: [
8220
8465
  "videoResolution"
8221
8466
  ],
@@ -8225,7 +8470,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8225
8470
  }
8226
8471
  if (input.kind !== "image" && input.nVariants !== void 0) {
8227
8472
  ctx.addIssue({
8228
- code: z40.ZodIssueCode.custom,
8473
+ code: z41.ZodIssueCode.custom,
8229
8474
  path: [
8230
8475
  "nVariants"
8231
8476
  ],
@@ -8235,7 +8480,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8235
8480
  if (input.kind !== "landing") {
8236
8481
  if (input.sectionKey !== void 0) {
8237
8482
  ctx.addIssue({
8238
- code: z40.ZodIssueCode.custom,
8483
+ code: z41.ZodIssueCode.custom,
8239
8484
  path: [
8240
8485
  "sectionKey"
8241
8486
  ],
@@ -8244,7 +8489,7 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8244
8489
  }
8245
8490
  if (input.instructions !== void 0) {
8246
8491
  ctx.addIssue({
8247
- code: z40.ZodIssueCode.custom,
8492
+ code: z41.ZodIssueCode.custom,
8248
8493
  path: [
8249
8494
  "instructions"
8250
8495
  ],
@@ -8253,31 +8498,31 @@ var EstimateCreativeVariantInput = EstimateCreativeVariantInputBase.superRefine(
8253
8498
  }
8254
8499
  }
8255
8500
  });
8256
- var CreateCreativeVariantInputBase = z40.object({
8257
- sourceTaskId: z40.string().uuid().describe("ID de la tarea original completada."),
8501
+ var CreateCreativeVariantInputBase = z41.object({
8502
+ sourceTaskId: z41.string().uuid().describe("ID de la tarea original completada."),
8258
8503
  kind: CreativeVariantKindEnum,
8259
- confirm: z40.literal(true).describe("Confirmaci\xF3n expl\xEDcita del usuario despu\xE9s de mostrarle el estimado de cr\xE9ditos."),
8260
- 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."),
8261
- angle: z40.string().trim().min(1).max(500).optional().describe("\xC1ngulo nuevo para video o imagen."),
8262
- buyerPersona: z40.string().trim().min(1).max(360).optional().describe("Buyer persona coherente con el \xE1ngulo."),
8263
- scriptNotes: z40.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n breve del guion para una variante de video."),
8264
- creativeDirection: z40.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n creativa para una variante de imagen."),
8504
+ confirm: z41.literal(true).describe("Confirmaci\xF3n expl\xEDcita del usuario despu\xE9s de mostrarle el estimado de cr\xE9ditos."),
8505
+ 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."),
8506
+ angle: z41.string().trim().min(1).max(500).optional().describe("\xC1ngulo nuevo para video o imagen."),
8507
+ buyerPersona: z41.string().trim().min(1).max(360).optional().describe("Buyer persona coherente con el \xE1ngulo."),
8508
+ scriptNotes: z41.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n breve del guion para una variante de video."),
8509
+ creativeDirection: z41.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n creativa para una variante de imagen."),
8265
8510
  videoPreset: TaskCreativeVariantVideoPresetEnum.optional().describe("Opcional para video. Preset p\xFAblico; el servidor resuelve modelo/tier desde el task origen."),
8266
8511
  videoResolution: VideoResolutionEnum.optional().describe("Opcional para video."),
8267
- generateAiMusic: z40.boolean().optional().describe("Opcional para video."),
8268
- nVariants: z40.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8512
+ generateAiMusic: z41.boolean().optional().describe("Opcional para video."),
8513
+ nVariants: z41.number().int().min(1).max(3).optional().describe("Opcional para imagen. N\xFAmero de im\xE1genes adicionales (1-3)."),
8269
8514
  imageStyle: ImageCreativeStyleEnum.optional().describe("Opcional para imagen."),
8270
8515
  creativeArchetype: ImageCreativeArchetypeEnum.optional().describe("Opcional para imagen."),
8271
8516
  visualTreatment: ImageCreativeVisualTreatmentEnum.optional().describe("Opcional para imagen."),
8272
8517
  aspectRatio: ImageCreativeAspectRatioEnum.optional().describe("Opcional para imagen."),
8273
- styleMix: z40.boolean().optional().describe("Opcional para imagen."),
8518
+ styleMix: z41.boolean().optional().describe("Opcional para imagen."),
8274
8519
  sectionKey: LandingSectionKeyEnum.optional().describe("Opcional para landing. Si se omite, se clasifica con IA a partir de instructions."),
8275
- 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.")
8520
+ 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.")
8276
8521
  }).strict();
8277
8522
  var CreateCreativeVariantInput = CreateCreativeVariantInputBase.superRefine((input, ctx) => {
8278
8523
  if ((input.kind === "video" || input.kind === "image") && !input.angle) {
8279
8524
  ctx.addIssue({
8280
- code: z40.ZodIssueCode.custom,
8525
+ code: z41.ZodIssueCode.custom,
8281
8526
  path: [
8282
8527
  "angle"
8283
8528
  ],
@@ -8286,7 +8531,7 @@ var CreateCreativeVariantInput = CreateCreativeVariantInputBase.superRefine((inp
8286
8531
  }
8287
8532
  if (input.kind === "landing" && !input.sectionKey && !input.instructions) {
8288
8533
  ctx.addIssue({
8289
- code: z40.ZodIssueCode.custom,
8534
+ code: z41.ZodIssueCode.custom,
8290
8535
  path: [
8291
8536
  "instructions"
8292
8537
  ],
@@ -8332,7 +8577,7 @@ var CreateCreativeVariantInput = CreateCreativeVariantInputBase.superRefine((inp
8332
8577
  for (const field of fieldsByKind[input.kind]) {
8333
8578
  if (input[field] !== void 0) {
8334
8579
  ctx.addIssue({
8335
- code: z40.ZodIssueCode.custom,
8580
+ code: z41.ZodIssueCode.custom,
8336
8581
  path: [
8337
8582
  field
8338
8583
  ],
@@ -8449,27 +8694,27 @@ function registerCreativeVariantTools(server, api) {
8449
8694
  __name(registerCreativeVariantTools, "registerCreativeVariantTools");
8450
8695
 
8451
8696
  // src/tools/script-review.ts
8452
- import { z as z41 } from "zod";
8453
- var GetVideoScriptInput = z41.object({
8454
- taskId: z41.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
8455
- 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.')
8697
+ import { z as z42 } from "zod";
8698
+ var GetVideoScriptInput = z42.object({
8699
+ taskId: z42.string().min(1).describe("ID de la tarea (la misma que devolvi\xF3 create_product_task / generate_video_creative)."),
8700
+ 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.')
8456
8701
  }).strict();
8457
8702
  var VIDEO_SCRIPT_APPROVAL_MAX_SCENES2 = 50;
8458
8703
  var VIDEO_SCRIPT_APPROVAL_MAX_DIALOGUE_CHARS2 = 2e3;
8459
- var ApproveSceneEdit = z41.object({
8460
- index: z41.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
8461
- dialogue: z41.string().min(1).max(VIDEO_SCRIPT_APPROVAL_MAX_DIALOGUE_CHARS2).refine((v) => v.trim().length > 0, {
8704
+ var ApproveSceneEdit = z42.object({
8705
+ index: z42.number().int().nonnegative().describe("\xCDndice de la escena (0-based, mismo que devuelve get_video_script)."),
8706
+ dialogue: z42.string().min(1).max(VIDEO_SCRIPT_APPROVAL_MAX_DIALOGUE_CHARS2).refine((v) => v.trim().length > 0, {
8462
8707
  message: "dialogue no puede ser s\xF3lo espacios"
8463
8708
  }).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.")
8464
8709
  }).strict();
8465
- var ApproveVideoScriptInput = z41.object({
8466
- taskId: z41.string().min(1).describe("ID de la tarea."),
8467
- 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.'),
8468
- 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.")
8710
+ var ApproveVideoScriptInput = z42.object({
8711
+ taskId: z42.string().min(1).describe("ID de la tarea."),
8712
+ 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.'),
8713
+ 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.")
8469
8714
  }).strict();
8470
- var RegenerateVideoScriptInput = z41.object({
8471
- taskId: z41.string().min(1).describe("ID de la tarea."),
8472
- 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.'),
8715
+ var RegenerateVideoScriptInput = z42.object({
8716
+ taskId: z42.string().min(1).describe("ID de la tarea."),
8717
+ 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.'),
8473
8718
  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).')
8474
8719
  }).strict();
8475
8720
  function registerScriptReviewTools(server, api) {
@@ -8509,9 +8754,9 @@ function registerScriptReviewTools(server, api) {
8509
8754
  __name(registerScriptReviewTools, "registerScriptReviewTools");
8510
8755
 
8511
8756
  // src/tools/connections.ts
8512
- import { z as z42 } from "zod";
8757
+ import { z as z43 } from "zod";
8513
8758
  function registerConnectionTools(server, api) {
8514
- 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 () => {
8759
+ 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 () => {
8515
8760
  try {
8516
8761
  const r = await api.getJson("/v1/shopify/shops");
8517
8762
  return asText(r);
@@ -8519,10 +8764,10 @@ function registerConnectionTools(server, api) {
8519
8764
  throw translateError(e);
8520
8765
  }
8521
8766
  });
8522
- 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({
8523
- shopDomain: z42.string().min(3).max(120),
8524
- query: z42.string().trim().max(120).optional(),
8525
- taskId: z42.string().uuid().optional()
8767
+ 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({
8768
+ shopDomain: z43.string().min(3).max(120),
8769
+ query: z43.string().trim().max(120).optional(),
8770
+ taskId: z43.string().uuid().optional()
8526
8771
  }).shape, async ({ shopDomain, query, taskId }) => {
8527
8772
  try {
8528
8773
  const params = new URLSearchParams({
@@ -8536,7 +8781,7 @@ function registerConnectionTools(server, api) {
8536
8781
  throw translateError(e);
8537
8782
  }
8538
8783
  });
8539
- 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 () => {
8784
+ 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 () => {
8540
8785
  try {
8541
8786
  const r = await api.getJson("/v1/facebook/ad-accounts");
8542
8787
  return asText(r);
@@ -8544,7 +8789,7 @@ function registerConnectionTools(server, api) {
8544
8789
  throw translateError(e);
8545
8790
  }
8546
8791
  });
8547
- 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 () => {
8792
+ 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 () => {
8548
8793
  try {
8549
8794
  const r = await api.getJson("/v1/tiktok/advertisers");
8550
8795
  return asText(r);
@@ -8556,115 +8801,128 @@ function registerConnectionTools(server, api) {
8556
8801
  __name(registerConnectionTools, "registerConnectionTools");
8557
8802
 
8558
8803
  // src/tools/publish.ts
8559
- import { z as z43 } from "zod";
8560
- var ShopifyInput = z43.object({
8561
- taskId: z43.string().min(1),
8562
- shopDomain: z43.string().min(1),
8804
+ import { z as z44 } from "zod";
8805
+ var ShopifyInput = z44.object({
8806
+ taskId: z44.string().min(1),
8807
+ shopDomain: z44.string().min(1),
8563
8808
  /** Override: number of landing images to include. Default = ALL images the
8564
8809
  * task generated (the MCP fetches /v1/results first to count). Bounded
8565
8810
  * generously here; the backend validates each index against
8566
8811
  * landing.images.length and rejects out-of-range. */
8567
- numLandingImages: z43.number().int().min(1).max(20).optional(),
8568
- variantIndex: z43.number().int().min(0).optional(),
8812
+ numLandingImages: z44.number().int().min(1).max(20).optional(),
8813
+ variantIndex: z44.number().int().min(0).optional(),
8569
8814
  /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
8570
8815
  * When true, the landing renders variant-picker pills; when false, it ships
8571
8816
  * a single-option product page. Defaults to false. */
8572
- showVariantPicker: z43.boolean().optional(),
8817
+ showVariantPicker: z44.boolean().optional(),
8573
8818
  /** Default `auto` updates the product previously published for this task and
8574
8819
  * shop. Use `existing` with targetProductId to choose another product, or
8575
8820
  * `new` only when the user explicitly wants a duplicate/new listing. */
8576
- publicationMode: z43.enum([
8821
+ publicationMode: z44.enum([
8577
8822
  "auto",
8578
8823
  "existing",
8579
8824
  "new"
8580
8825
  ]).optional(),
8581
8826
  /** Shopify product GID, numeric id, or admin product URL. Required when
8582
8827
  * publicationMode is `existing`. */
8583
- targetProductId: z43.string().trim().min(1).max(300).optional()
8828
+ targetProductId: z44.string().trim().min(1).max(300).optional()
8584
8829
  });
8585
- var McpCampaignConfig = z43.object({
8830
+ var McpCampaignConfig = z44.object({
8586
8831
  /** ALWAYS in US dollars, whatever the ad account's currency is. ProductMaker
8587
8832
  * converts to the account currency server-side. Do NOT pre-convert: on a COP
8588
8833
  * account the user's "60.000 al día" is `dailyBudgetUsd: 15`, not 60000. */
8589
- dailyBudgetUsd: z43.number().positive().optional(),
8834
+ dailyBudgetUsd: z44.number().positive().optional(),
8590
8835
  /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
8591
8836
  * ad set). TikTok takes only the first entry. */
8592
- targetCountries: z43.array(z43.string().length(2)).max(1).optional(),
8593
- campaignName: z43.string().trim().min(1).max(100).optional()
8594
- });
8595
- var MetaInput = z43.object({
8596
- taskId: z43.string().min(1),
8597
- adAccountId: z43.string().min(1),
8598
- pixelId: z43.string().min(1).optional(),
8599
- landingUrl: z43.string().url().optional(),
8837
+ targetCountries: z44.array(z44.string().length(2)).max(1).optional(),
8838
+ campaignName: z44.string().trim().min(1).max(100).optional()
8839
+ });
8840
+ var McpAdAccountId = MetaAdAccountId.describe('Meta ad account id. Accepts "act_1234567890" or the bare numeric id "1234567890".');
8841
+ var MetaInput = z44.object({
8842
+ taskId: z44.string().min(1),
8843
+ adAccountId: McpAdAccountId,
8844
+ pixelId: z44.string().min(1).optional(),
8845
+ landingUrl: z44.string().url().optional(),
8600
8846
  /** Select which angles to publish by 0-based index. Omit to publish all
8601
8847
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
8602
- angleIndexes: z43.array(z43.number().int().nonnegative()).optional(),
8848
+ angleIndexes: z44.array(z44.number().int().nonnegative()).optional(),
8603
8849
  campaignConfig: McpCampaignConfig.optional(),
8604
8850
  /** Override the ad's primary text (body copy). If omitted, ProductMaker
8605
8851
  * uses the auto-generated copy attached to the task. Capped at 500 chars
8606
8852
  * (Meta's primary-text limit). */
8607
- primaryText: z43.string().trim().min(1).max(500).optional(),
8853
+ primaryText: z44.string().trim().min(1).max(500).optional(),
8608
8854
  /** Optional WhatsApp number. When set, the ad button opens a WhatsApp chat
8609
8855
  * (wa.me) instead of a landing page. */
8610
- phoneNumber: z43.string().min(8).max(32).optional()
8856
+ phoneNumber: z44.string().min(8).max(32).optional()
8611
8857
  });
8612
- var GroupedTaskSource = z43.object({
8613
- type: z43.literal("task"),
8614
- taskId: z43.string().min(1),
8615
- angleIndex: z43.number().int().min(0).optional(),
8616
- videoVariant: z43.enum([
8858
+ var GroupedTaskSource = z44.object({
8859
+ type: z44.literal("task"),
8860
+ taskId: z44.string().min(1),
8861
+ angleIndex: z44.number().int().min(0).optional(),
8862
+ videoVariant: z44.enum([
8617
8863
  "with_subtitles",
8618
8864
  "without_subtitles"
8619
8865
  ]).optional(),
8620
- imageVariantIndex: z43.number().int().min(0).optional()
8621
- });
8622
- var GroupedMediaSource = z43.object({
8623
- type: z43.literal("media"),
8624
- url: z43.string().url().optional(),
8625
- path: z43.string().min(1).optional(),
8626
- thumbnailUrl: z43.string().url().optional(),
8627
- thumbnailPath: z43.string().min(1).optional()
8866
+ imageVariantIndex: z44.number().int().min(0).optional()
8867
+ });
8868
+ var GroupedMediaSource = z44.object({
8869
+ type: z44.literal("media"),
8870
+ url: z44.string().url().optional(),
8871
+ path: z44.string().min(1).optional(),
8872
+ thumbnailUrl: z44.string().url().optional(),
8873
+ thumbnailPath: z44.string().min(1).optional()
8628
8874
  }).refine((value) => Boolean(value.url) !== Boolean(value.path), {
8629
8875
  message: "media source needs exactly one of `url` or `path`"
8630
8876
  }).refine((value) => !(value.thumbnailUrl && value.thumbnailPath), {
8631
8877
  message: "media source needs at most one of `thumbnailUrl` or `thumbnailPath`"
8632
8878
  });
8633
- var GroupedCreative = z43.object({
8634
- type: z43.enum([
8879
+ var GroupedCreative = z44.object({
8880
+ type: z44.enum([
8635
8881
  "image",
8636
8882
  "video"
8637
8883
  ]),
8638
- source: z43.union([
8884
+ source: z44.union([
8639
8885
  GroupedTaskSource,
8640
8886
  GroupedMediaSource
8641
8887
  ]),
8642
- title: z43.string().max(120).optional(),
8643
- description: z43.string().max(500).optional(),
8888
+ title: z44.string().max(120).optional(),
8889
+ description: z44.string().max(500).optional(),
8644
8890
  cta: MetaCtaType.optional(),
8645
- creativeId: z43.string().min(1).optional()
8891
+ creativeId: z44.string().min(1).optional()
8646
8892
  });
8647
- var GroupedCampaignConfig = z43.object({
8893
+ var GroupedCampaignConfig = z44.object({
8648
8894
  campaignType: MetaCampaignType.default("sales"),
8649
8895
  optimizationMode: MetaOptimizationMode.default("advantage_plus"),
8650
8896
  budgetMode: MetaBudgetMode.default("campaign"),
8651
- campaignDailyBudget: z43.number().int().positive().optional(),
8652
- adSets: z43.array(z43.object({
8653
- tempId: z43.string().min(1).max(80),
8654
- name: z43.string().min(1).max(100),
8655
- dailyBudget: z43.number().int().positive().optional(),
8656
- creativeIds: z43.array(z43.string().min(1)).min(1)
8657
- })).min(1).max(4),
8658
- destination: z43.object({
8659
- type: z43.literal("landing"),
8660
- landingUrl: z43.string().url()
8661
- }),
8662
- saveAsDefault: z43.boolean().default(false),
8897
+ campaignDailyBudget: z44.number().int().positive().optional(),
8898
+ adSets: z44.array(z44.object({
8899
+ tempId: z44.string().min(1).max(80),
8900
+ name: z44.string().min(1).max(100),
8901
+ dailyBudget: z44.number().int().positive().optional(),
8902
+ creativeIds: z44.array(z44.string().min(1)).min(1)
8903
+ })).min(1).max(CAMPAIGN_LIMITS.MAX_AD_SETS),
8904
+ /**
8905
+ * Valid values are 'landing' and 'whatsapp' — there is no 'website'. This
8906
+ * used to be hardcoded to `landing`, which both rejected `website` with an
8907
+ * opaque `invalid_literal` and made the WhatsApp destination — supported by
8908
+ * the API since #849 — unreachable from this tool.
8909
+ */
8910
+ destination: z44.discriminatedUnion("type", [
8911
+ z44.object({
8912
+ type: z44.literal("landing"),
8913
+ landingUrl: z44.string().url()
8914
+ }),
8915
+ z44.object({
8916
+ type: z44.literal("whatsapp"),
8917
+ phoneNumber: z44.string().min(8).max(32)
8918
+ })
8919
+ ]),
8920
+ saveAsDefault: z44.boolean().default(false),
8663
8921
  budgetPolicy: MetaBudgetPolicy.optional()
8664
8922
  }).superRefine((value, ctx) => {
8665
8923
  if (value.budgetMode === "campaign" && value.campaignDailyBudget === void 0) {
8666
8924
  ctx.addIssue({
8667
- code: z43.ZodIssueCode.custom,
8925
+ code: z44.ZodIssueCode.custom,
8668
8926
  path: [
8669
8927
  "campaignDailyBudget"
8670
8928
  ],
@@ -8675,7 +8933,7 @@ var GroupedCampaignConfig = z43.object({
8675
8933
  value.adSets.forEach((adSet, index) => {
8676
8934
  if (adSet.dailyBudget === void 0) {
8677
8935
  ctx.addIssue({
8678
- code: z43.ZodIssueCode.custom,
8936
+ code: z44.ZodIssueCode.custom,
8679
8937
  path: [
8680
8938
  "adSets",
8681
8939
  index,
@@ -8687,19 +8945,31 @@ var GroupedCampaignConfig = z43.object({
8687
8945
  });
8688
8946
  }
8689
8947
  });
8690
- var PublishCreativesToMetaInputBase = z43.object({
8691
- creatives: z43.array(GroupedCreative).min(1).max(4),
8692
- adAccountId: z43.string().min(1),
8693
- pixelId: z43.string().min(1).optional(),
8694
- landingUrl: z43.string().url(),
8695
- targetCountry: z43.string().length(2),
8696
- campaignName: z43.string().trim().min(1).max(100),
8948
+ var PublishCreativesToMetaInputBase = z44.object({
8949
+ creatives: z44.array(GroupedCreative).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
8950
+ adAccountId: McpAdAccountId,
8951
+ pixelId: z44.string().min(1).optional(),
8952
+ /** Required unless `campaignConfig.destination` is whatsapp — see the
8953
+ * superRefine below. Mirrors the API's `UploadToMetaRequest`. */
8954
+ landingUrl: z44.string().url().optional(),
8955
+ targetCountry: z44.string().length(2),
8956
+ campaignName: z44.string().trim().min(1).max(100),
8697
8957
  campaignConfig: GroupedCampaignConfig.optional()
8698
8958
  });
8699
8959
  var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((value, ctx) => {
8700
- if (value.campaignConfig && value.campaignConfig.destination.landingUrl !== value.landingUrl) {
8960
+ const destination = value.campaignConfig?.destination;
8961
+ if (destination?.type !== "whatsapp" && !value.landingUrl) {
8962
+ ctx.addIssue({
8963
+ code: z44.ZodIssueCode.custom,
8964
+ path: [
8965
+ "landingUrl"
8966
+ ],
8967
+ message: "landingUrl is required unless campaignConfig.destination.type is whatsapp"
8968
+ });
8969
+ }
8970
+ if (destination?.type === "landing" && destination.landingUrl !== value.landingUrl) {
8701
8971
  ctx.addIssue({
8702
- code: z43.ZodIssueCode.custom,
8972
+ code: z44.ZodIssueCode.custom,
8703
8973
  path: [
8704
8974
  "campaignConfig",
8705
8975
  "destination",
@@ -8709,12 +8979,12 @@ var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((v
8709
8979
  });
8710
8980
  }
8711
8981
  });
8712
- var PresignUploadResponseSchema = z43.object({
8713
- uploadUrl: z43.string().url().refine((value) => {
8982
+ var PresignUploadResponseSchema = z44.object({
8983
+ uploadUrl: z44.string().url().refine((value) => {
8714
8984
  const url = new URL(value);
8715
8985
  return !url.username && !url.password && url.protocol === "https:";
8716
8986
  }, "presigned upload URL must be HTTPS without embedded credentials"),
8717
- publicUrl: z43.string().url().refine((value) => {
8987
+ publicUrl: z44.string().url().refine((value) => {
8718
8988
  const url = new URL(value);
8719
8989
  return !url.username && !url.password;
8720
8990
  }, "public media URL must not contain embedded credentials")
@@ -8739,16 +9009,16 @@ async function resolveAndUploadMedia(api, ref, expectedKind, transport) {
8739
9009
  return uploadResolvedMedia(api, media);
8740
9010
  }
8741
9011
  __name(resolveAndUploadMedia, "resolveAndUploadMedia");
8742
- var TikTokInput = z43.object({
8743
- taskId: z43.string().min(1),
8744
- advertiserId: z43.string().min(1),
8745
- pixelId: z43.string().min(1).optional(),
8746
- landingUrl: z43.string().url().optional(),
8747
- angleIndexes: z43.array(z43.number().int().nonnegative()).optional(),
9012
+ var TikTokInput = z44.object({
9013
+ taskId: z44.string().min(1),
9014
+ advertiserId: z44.string().min(1),
9015
+ pixelId: z44.string().min(1).optional(),
9016
+ landingUrl: z44.string().url().optional(),
9017
+ angleIndexes: z44.array(z44.number().int().nonnegative()).optional(),
8748
9018
  campaignConfig: McpCampaignConfig.optional(),
8749
9019
  /** Override the ad's description (TikTok's body copy). If omitted,
8750
9020
  * ProductMaker uses the auto-generated copy attached to the task. */
8751
- primaryText: z43.string().trim().min(1).max(500).optional()
9021
+ primaryText: z44.string().trim().min(1).max(500).optional()
8752
9022
  });
8753
9023
  function registerPublishTools(server, api, opts = {}) {
8754
9024
  const transport = opts.transport ?? "http";
@@ -8787,7 +9057,7 @@ function registerPublishTools(server, api, opts = {}) {
8787
9057
  throw translateError(e);
8788
9058
  }
8789
9059
  });
8790
- 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 }) => {
9060
+ 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 }) => {
8791
9061
  try {
8792
9062
  const body = {
8793
9063
  ...rest
@@ -8815,8 +9085,11 @@ function registerPublishTools(server, api, opts = {}) {
8815
9085
  throw translateError(e);
8816
9086
  }
8817
9087
  });
8818
- 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) => {
9088
+ 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) => {
8819
9089
  try {
9090
+ const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
9091
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
9092
+ const input = parsed.data;
8820
9093
  const creatives = [];
8821
9094
  for (let index = 0; index < input.creatives.length; index += 1) {
8822
9095
  const creative = input.creatives[index];
@@ -8844,9 +9117,6 @@ function registerPublishTools(server, api, opts = {}) {
8844
9117
  }, creative.type, transport);
8845
9118
  let thumbnailUrl;
8846
9119
  if (creative.type === "video") {
8847
- if (!creative.source.thumbnailUrl && !creative.source.thumbnailPath && !creative.source.path) {
8848
- 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.");
8849
- }
8850
9120
  if (creative.source.thumbnailUrl || creative.source.thumbnailPath) {
8851
9121
  thumbnailUrl = await resolveAndUploadMedia(api, {
8852
9122
  url: creative.source.thumbnailUrl,
@@ -8959,13 +9229,13 @@ function toTikTokCampaignConfig(cfg) {
8959
9229
  __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
8960
9230
 
8961
9231
  // src/tools/campaigns.ts
8962
- import { z as z44 } from "zod";
8963
- var MetaStatusInput = z44.object({
8964
- campaignId: z44.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
9232
+ import { z as z45 } from "zod";
9233
+ var MetaStatusInput = z45.object({
9234
+ campaignId: z45.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
8965
9235
  });
8966
- var TikTokStatusInput = z44.object({
8967
- campaignId: z44.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
8968
- advertiserId: z44.string().min(1).describe("The same advertiserId used to publish the campaign.")
9236
+ var TikTokStatusInput = z45.object({
9237
+ campaignId: z45.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
9238
+ advertiserId: z45.string().min(1).describe("The same advertiserId used to publish the campaign.")
8969
9239
  });
8970
9240
  function registerCampaignTools(server, api) {
8971
9241
  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) => {
@@ -9062,10 +9332,10 @@ function renderTikTokStatusMarkdown(r) {
9062
9332
  __name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
9063
9333
 
9064
9334
  // ../../packages/product-economics/src/product-costing.ts
9065
- import { z as z46 } from "zod";
9335
+ import { z as z47 } from "zod";
9066
9336
 
9067
9337
  // ../../packages/product-economics/src/product-costing-countries.ts
9068
- import { z as z45 } from "zod";
9338
+ import { z as z46 } from "zod";
9069
9339
 
9070
9340
  // ../../packages/product-economics/src/product-costing-shipping-references.json
9071
9341
  var product_costing_shipping_references_default = {
@@ -9129,21 +9399,21 @@ var product_costing_shipping_references_default = {
9129
9399
  };
9130
9400
 
9131
9401
  // ../../packages/product-economics/src/product-costing-countries.ts
9132
- var ShippingReferenceSchema = z45.object({
9133
- reference: z45.number().positive(),
9134
- min: z45.number().nonnegative(),
9135
- max: z45.number().positive(),
9136
- updatedAt: z45.string().date(),
9137
- confidence: z45.enum([
9402
+ var ShippingReferenceSchema = z46.object({
9403
+ reference: z46.number().positive(),
9404
+ min: z46.number().nonnegative(),
9405
+ max: z46.number().positive(),
9406
+ updatedAt: z46.string().date(),
9407
+ confidence: z46.enum([
9138
9408
  "high",
9139
9409
  "medium",
9140
9410
  "medium-low"
9141
9411
  ]),
9142
- sources: z45.array(z45.string().url().startsWith("https://")).min(1).max(3)
9412
+ sources: z46.array(z46.string().url().startsWith("https://")).min(1).max(3)
9143
9413
  }).superRefine(({ reference, min, max }, context) => {
9144
9414
  if (min > reference || reference > max) {
9145
9415
  context.addIssue({
9146
- code: z45.ZodIssueCode.custom,
9416
+ code: z46.ZodIssueCode.custom,
9147
9417
  message: "shipping_reference_outside_range",
9148
9418
  path: [
9149
9419
  "reference"
@@ -9151,7 +9421,7 @@ var ShippingReferenceSchema = z45.object({
9151
9421
  });
9152
9422
  }
9153
9423
  });
9154
- var SHIPPING_REFERENCES = z45.record(ShippingReferenceSchema).parse(product_costing_shipping_references_default);
9424
+ var SHIPPING_REFERENCES = z46.record(ShippingReferenceSchema).parse(product_costing_shipping_references_default);
9155
9425
  var PRODUCT_COSTING_COUNTRY_CODES = Object.freeze(Object.keys(SHIPPING_REFERENCES));
9156
9426
  var SPANISH_LOCALE_COUNTRIES = /* @__PURE__ */ new Set([
9157
9427
  "MX",
@@ -9333,18 +9603,18 @@ function getProductCostingCountryName(country) {
9333
9603
  __name(getProductCostingCountryName, "getProductCostingCountryName");
9334
9604
 
9335
9605
  // ../../packages/product-economics/src/product-costing.ts
9336
- var ProductCostingInputSchema = z46.object({
9337
- country: z46.string().refine(isProductCostingCountryCode, "unsupported_country"),
9338
- supplierCost: z46.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").positive("supplier_cost_positive"),
9339
- shippingCost: z46.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("shipping_cost_nonnegative"),
9340
- returnRate: z46.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9341
- cancellationRate: z46.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9342
- adsManagerCpa: z46.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("cpa_nonnegative"),
9343
- desiredMargin: z46.number().finite().min(0, "margin_nonnegative").lt(1, "margin_below_one")
9606
+ var ProductCostingInputSchema = z47.object({
9607
+ country: z47.string().refine(isProductCostingCountryCode, "unsupported_country"),
9608
+ supplierCost: z47.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").positive("supplier_cost_positive"),
9609
+ shippingCost: z47.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("shipping_cost_nonnegative"),
9610
+ returnRate: z47.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9611
+ cancellationRate: z47.number().finite().min(0, "rate_nonnegative").lt(1, "rate_below_one"),
9612
+ adsManagerCpa: z47.number().finite().max(Number.MAX_SAFE_INTEGER, "amount_too_large").nonnegative("cpa_nonnegative"),
9613
+ desiredMargin: z47.number().finite().min(0, "margin_nonnegative").lt(1, "margin_below_one")
9344
9614
  }).superRefine(({ returnRate, cancellationRate }, context) => {
9345
9615
  if (returnRate + cancellationRate >= 1) {
9346
9616
  context.addIssue({
9347
- code: z46.ZodIssueCode.custom,
9617
+ code: z47.ZodIssueCode.custom,
9348
9618
  message: "delivery_rate_positive",
9349
9619
  path: [
9350
9620
  "cancellationRate"
@@ -9411,14 +9681,14 @@ function calculateProductCosting(rawInput) {
9411
9681
  __name(calculateProductCosting, "calculateProductCosting");
9412
9682
 
9413
9683
  // src/tools/product-costing.ts
9414
- import { z as z47 } from "zod";
9415
- var optionalAmount = /* @__PURE__ */ __name((description) => z47.number().finite().max(Number.MAX_SAFE_INTEGER).nonnegative().optional().describe(description), "optionalAmount");
9416
- var optionalPercent = /* @__PURE__ */ __name((description) => z47.number().finite().min(0).lt(100).optional().describe(description), "optionalPercent");
9417
- var ProductCostingToolInput = z47.object({
9418
- country: z47.string().trim().toUpperCase().refine(isProductCostingCountryCode, {
9684
+ import { z as z48 } from "zod";
9685
+ var optionalAmount = /* @__PURE__ */ __name((description) => z48.number().finite().max(Number.MAX_SAFE_INTEGER).nonnegative().optional().describe(description), "optionalAmount");
9686
+ var optionalPercent = /* @__PURE__ */ __name((description) => z48.number().finite().min(0).lt(100).optional().describe(description), "optionalPercent");
9687
+ var ProductCostingToolInput = z48.object({
9688
+ country: z48.string().trim().toUpperCase().refine(isProductCostingCountryCode, {
9419
9689
  message: "Pa\xEDs no soportado por ProductMaker."
9420
9690
  }).optional().describe("C\xF3digo ISO-2 de cualquiera de los pa\xEDses disponibles en ProductMaker. Omite para Colombia."),
9421
- 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."),
9691
+ 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."),
9422
9692
  supplierCostCop: optionalAmount("Alias anterior para costo del proveedor en Colombia. Prefiere supplierCost."),
9423
9693
  shippingCost: optionalAmount("Costo de un env\xEDo en la moneda del pa\xEDs. Omite para usar el valor gu\xEDa vigente."),
9424
9694
  shippingCostCop: optionalAmount("Alias anterior para flete en Colombia. Prefiere shippingCost."),
@@ -9549,6 +9819,224 @@ function formatPercent(value) {
9549
9819
  }
9550
9820
  __name(formatPercent, "formatPercent");
9551
9821
 
9822
+ // src/tools/product-research.ts
9823
+ import { z as z49 } from "zod";
9824
+
9825
+ // src/tools/product-research-idempotency.ts
9826
+ import { createHash as createHash2 } from "crypto";
9827
+ var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
9828
+ var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
9829
+ function uuidV5(name, namespace) {
9830
+ const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
9831
+ const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
9832
+ const bytes = Buffer.from(digest.subarray(0, 16));
9833
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
9834
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
9835
+ const hex = bytes.toString("hex");
9836
+ return [
9837
+ hex.slice(0, 8),
9838
+ hex.slice(8, 12),
9839
+ hex.slice(12, 16),
9840
+ hex.slice(16, 20),
9841
+ hex.slice(20)
9842
+ ].join("-");
9843
+ }
9844
+ __name(uuidV5, "uuidV5");
9845
+ function deriveIdempotencyKey(input) {
9846
+ const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
9847
+ const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
9848
+ return uuidV5(name, NAMESPACE);
9849
+ }
9850
+ __name(deriveIdempotencyKey, "deriveIdempotencyKey");
9851
+
9852
+ // src/tools/product-research-render.ts
9853
+ var num = /* @__PURE__ */ __name((value, suffix = "") => value === void 0 ? "\u2014" : `${Math.round(value * 100) / 100}${suffix}`, "num");
9854
+ function renderMeta(meta) {
9855
+ if (meta.status === "evidence_failed") {
9856
+ return [
9857
+ "- \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."
9858
+ ];
9859
+ }
9860
+ if (meta.status === "evidence_empty") {
9861
+ return [
9862
+ "- \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."
9863
+ ];
9864
+ }
9865
+ return [
9866
+ `- **Evidencia en Meta Ads:** ${meta.totalAds} anuncios de ${meta.distinctAdvertisers} anunciantes, m\xE1ximo ${meta.maxActiveDays} d\xEDas activos, oportunidad de \xE1ngulo ${meta.angleOpportunity}/100.`,
9867
+ meta.summary ? ` ${meta.summary}` : ""
9868
+ ].filter(Boolean);
9869
+ }
9870
+ __name(renderMeta, "renderMeta");
9871
+ function renderCandidate(candidate, index) {
9872
+ const name = candidate.nameLocal ?? candidate.nameOriginal;
9873
+ const lines = [
9874
+ `### ${index + 1}. ${name}`,
9875
+ `**${candidate.score}/100 \xB7 ${candidate.label} \xB7 confianza ${candidate.confidence}**`
9876
+ ];
9877
+ if (candidate.insight) lines.push("", candidate.insight);
9878
+ lines.push("", "**Por qu\xE9:**");
9879
+ lines.push(...candidate.reasons.map((reason) => `- ${reason}`));
9880
+ lines.push("", "**Riesgos:**");
9881
+ lines.push(...candidate.risks.length > 0 ? candidate.risks.map((risk) => `- ${risk}`) : [
9882
+ "- No se identificaron riesgos espec\xEDficos."
9883
+ ]);
9884
+ lines.push("", ...renderMeta(candidate.meta));
9885
+ lines.push("", `- **Venta (30d):** ${num(candidate.units30d)} unidades \xB7 ${num(candidate.revenueUsd30d, " USD")} \xB7 crecimiento ${num(candidate.growthPct30d, "%")} \xB7 precio ${num(candidate.priceUsd, " USD")}`);
9886
+ if (candidate.productUrl) lines.push(`- [Ver producto](${candidate.productUrl})`);
9887
+ return lines.join("\n");
9888
+ }
9889
+ __name(renderCandidate, "renderCandidate");
9890
+ function renderResearchReport(run) {
9891
+ if (run.status === "failed" || run.status === "refunded") {
9892
+ return [
9893
+ `\u274C **La investigaci\xF3n no pudo completarse** (estado: ${run.status}).`,
9894
+ run.errorCode ? `C\xF3digo: \`${run.errorCode}\`` : "",
9895
+ run.refunded ? `Los ${run.priceCredits} cr\xE9ditos fueron reembolsados.` : "\u26A0\uFE0F No consta reembolso \u2014 rev\xEDsalo antes de reintentar."
9896
+ ].filter(Boolean).join("\n");
9897
+ }
9898
+ if (run.status === "ready" && run.result === null) {
9899
+ return [
9900
+ `\u274C **No se pudo leer el reporte** (estado: ${run.status}).`,
9901
+ `El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3, pero el reporte qued\xF3 corrupto y no puede recuperarse.`,
9902
+ `Contacta a soporte con este runId para revisar un reembolso: \`${run.id}\`.`
9903
+ ].join("\n");
9904
+ }
9905
+ if (run.result === null) {
9906
+ return [
9907
+ `\u23F3 **La investigaci\xF3n sigue en curso** (estado: ${run.status}, etapa: ${run.progressStage}).`,
9908
+ `El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3; el reporte no se pierde.`,
9909
+ `Cons\xFAltalo con \`get_product_research_run\` usando runId \`${run.id}\`.`
9910
+ ].join("\n");
9911
+ }
9912
+ const { result } = run;
9913
+ const degraded = result.candidates.filter((candidate) => candidate.meta.status !== "evidence_available").length;
9914
+ const header = [
9915
+ `\u2705 **Productos ganadores \xB7 ${result.targetCountry} \xB7 ${result.categoryKey}**`,
9916
+ "",
9917
+ `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.`
9918
+ ];
9919
+ if (degraded > 0) {
9920
+ 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.`);
9921
+ }
9922
+ return [
9923
+ ...header,
9924
+ "",
9925
+ ...result.candidates.map((candidate, index) => renderCandidate(candidate, index))
9926
+ ].join("\n\n");
9927
+ }
9928
+ __name(renderResearchReport, "renderResearchReport");
9929
+
9930
+ // src/tools/product-research.ts
9931
+ var PRICE_CREDITS = 2e3;
9932
+ var TERMINAL2 = /* @__PURE__ */ new Set([
9933
+ "ready",
9934
+ "failed",
9935
+ "refunded"
9936
+ ]);
9937
+ var POLL_INTERVAL_MS = 5e3;
9938
+ var MAX_WAIT_SECONDS = 120;
9939
+ var FindInput = z49.object({
9940
+ targetCountry: ResearchTargetCountryEnum.describe("Pa\xEDs objetivo. S\xF3lo CO, MX, PE, EC y CL tienen cobertura de investigaci\xF3n."),
9941
+ categoryKey: ResearchCategoryEnum.default("auto").describe('Categor\xEDa a investigar. Usa "auto" para que la IA elija seg\xFAn el mercado.'),
9942
+ 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.`),
9943
+ 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.")
9944
+ });
9945
+ var GetInput = z49.object({
9946
+ runId: z49.string().uuid().describe("ID del run devuelto por find_winning_products.")
9947
+ });
9948
+ var ListInput = z49.object({
9949
+ limit: z49.number().int().min(1).max(20).default(10)
9950
+ });
9951
+ function textResult(text) {
9952
+ return {
9953
+ content: [
9954
+ {
9955
+ type: "text",
9956
+ text
9957
+ }
9958
+ ]
9959
+ };
9960
+ }
9961
+ __name(textResult, "textResult");
9962
+ var sleep2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)), "sleep");
9963
+ async function pollUntilTerminal(api, runId, waitSeconds) {
9964
+ const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
9965
+ let run = await api.getJson(`/v1/product-research/runs/${runId}`);
9966
+ while (!TERMINAL2.has(run.status) && Date.now() < deadline) {
9967
+ await sleep2(POLL_INTERVAL_MS);
9968
+ run = await api.getJson(`/v1/product-research/runs/${runId}`);
9969
+ }
9970
+ return run;
9971
+ }
9972
+ __name(pollUntilTerminal, "pollUntilTerminal");
9973
+ function registerProductResearchTools(server, api) {
9974
+ 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) => {
9975
+ const parsed = FindInput.parse(input);
9976
+ if (!parsed.confirmed) {
9977
+ 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.`);
9978
+ }
9979
+ try {
9980
+ const idempotencyKey = deriveIdempotencyKey({
9981
+ fingerprint: api.fingerprint(),
9982
+ targetCountry: parsed.targetCountry,
9983
+ categoryKey: parsed.categoryKey,
9984
+ nowMs: Date.now()
9985
+ });
9986
+ const created = await api.postJson("/v1/product-research/runs", {
9987
+ targetCountry: parsed.targetCountry,
9988
+ categoryKey: parsed.categoryKey,
9989
+ idempotencyKey
9990
+ });
9991
+ const waitSeconds = parsed.waitSeconds ?? 0;
9992
+ let run = created;
9993
+ if (waitSeconds > 0 && !TERMINAL2.has(created.status)) {
9994
+ try {
9995
+ run = await pollUntilTerminal(api, created.id, waitSeconds);
9996
+ } catch {
9997
+ run = created;
9998
+ }
9999
+ }
10000
+ const notice = created.reused ? `
10001
+
10002
+ _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._` : "";
10003
+ return textResult(`${renderResearchReport(run)}${notice}`);
10004
+ } catch (error) {
10005
+ throw translateError(error);
10006
+ }
10007
+ });
10008
+ 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) => {
10009
+ const parsed = GetInput.parse(input);
10010
+ try {
10011
+ const run = await api.getJson(`/v1/product-research/runs/${parsed.runId}`);
10012
+ return {
10013
+ content: [
10014
+ {
10015
+ type: "text",
10016
+ text: JSON.stringify(run)
10017
+ },
10018
+ {
10019
+ type: "text",
10020
+ text: renderResearchReport(run)
10021
+ }
10022
+ ]
10023
+ };
10024
+ } catch (error) {
10025
+ throw translateError(error);
10026
+ }
10027
+ });
10028
+ 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) => {
10029
+ const parsed = ListInput.parse(input);
10030
+ try {
10031
+ const runs = await api.getJson(`/v1/product-research/runs?limit=${parsed.limit}`);
10032
+ return textResult(JSON.stringify(runs));
10033
+ } catch (error) {
10034
+ throw translateError(error);
10035
+ }
10036
+ });
10037
+ }
10038
+ __name(registerProductResearchTools, "registerProductResearchTools");
10039
+
9552
10040
  // src/server.ts
9553
10041
  function createServer(opts) {
9554
10042
  const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
@@ -9565,6 +10053,7 @@ function createServer(opts) {
9565
10053
  });
9566
10054
  const api = new ApiClient(apiBaseUrl, apiKey);
9567
10055
  registerProductCostingTools(server);
10056
+ registerProductResearchTools(server, api);
9568
10057
  registerTaskTools(server, api, {
9569
10058
  proxyBaseUrl,
9570
10059
  transport