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