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