ponch-mcp-server 1.0.74 → 1.0.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2726,7 +2726,7 @@ function registerCoreTools(server, session) {
2726
2726
  }
2727
2727
 
2728
2728
  // src/tools/marketing.ts
2729
- var import_zod37 = require("zod");
2729
+ var import_zod38 = require("zod");
2730
2730
 
2731
2731
  // ../packages/marketing-business-logic/dist/index.js
2732
2732
  var import_firebase_admin2 = require("firebase-admin");
@@ -2738,18 +2738,20 @@ var import_firebase_admin4 = require("firebase-admin");
2738
2738
  var import_firebase_admin5 = require("firebase-admin");
2739
2739
  var import_firebase_admin6 = require("firebase-admin");
2740
2740
  var import_zod30 = require("zod");
2741
- var import_zod31 = require("zod");
2742
2741
  var import_firebase_admin7 = require("firebase-admin");
2742
+ var import_zod31 = require("zod");
2743
2743
  var import_zod32 = require("zod");
2744
2744
  var import_firebase_admin8 = require("firebase-admin");
2745
+ var import_zod33 = require("zod");
2745
2746
  var import_firebase_admin9 = require("firebase-admin");
2747
+ var import_firebase_admin10 = require("firebase-admin");
2746
2748
  var import_firestore3 = require("firebase-admin/firestore");
2747
2749
  var import_firestore4 = require("firebase-admin/firestore");
2748
2750
  var import_firestore5 = require("firebase-admin/firestore");
2749
2751
  var import_firestore6 = require("firebase-admin/firestore");
2750
- var import_zod33 = require("zod");
2751
- var import_firestore7 = require("firebase-admin/firestore");
2752
2752
  var import_zod34 = require("zod");
2753
+ var import_firestore7 = require("firebase-admin/firestore");
2754
+ var import_zod35 = require("zod");
2753
2755
  var import_firestore8 = require("firebase-admin/firestore");
2754
2756
  var RULE_NEGATIVES = {
2755
2757
  allowFaces: "no people, no faces, no hands",
@@ -3506,6 +3508,95 @@ async function contenidoUpdater(input) {
3506
3508
  camposActualizados: Object.keys(update).filter((k) => k !== "updatedAt")
3507
3509
  };
3508
3510
  }
3511
+ async function addCalendarSlot(input) {
3512
+ const { db, tenantId, brandId, mes, semana, slot } = input;
3513
+ const calQuery = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).where("mes", "==", mes).limit(1).get();
3514
+ if (calQuery.empty) {
3515
+ return { ok: false, error: `Calendario ${mes} no encontrado para brand ${brandId}` };
3516
+ }
3517
+ const calDocRef = calQuery.docs[0].ref;
3518
+ let resultSlotIndex = -1;
3519
+ await db.runTransaction(async (tx) => {
3520
+ const freshSnap = await tx.get(calDocRef);
3521
+ if (!freshSnap.exists) {
3522
+ throw new Error(`addCalendarSlot: calendario ${calDocRef.id} desapareci\xF3`);
3523
+ }
3524
+ const freshCal = freshSnap.data();
3525
+ const freshSemanas = freshCal.semanas ?? [];
3526
+ const freshSemana = freshSemanas[semana - 1];
3527
+ if (!freshSemana) {
3528
+ throw new Error(`Semana ${semana} no existe en el calendario ${mes}`);
3529
+ }
3530
+ const freshItems = freshSemana.items ?? [];
3531
+ freshItems.push({
3532
+ ...slot,
3533
+ estado: slot.estado ?? ESTADO_CALENDARIO_ITEM.PLANIFICADO
3534
+ });
3535
+ resultSlotIndex = freshItems.length - 1;
3536
+ freshSemana.items = freshItems;
3537
+ freshSemanas[semana - 1] = freshSemana;
3538
+ tx.update(calDocRef, {
3539
+ semanas: freshSemanas,
3540
+ updatedAt: import_firebase_admin6.firestore.FieldValue.serverTimestamp()
3541
+ });
3542
+ });
3543
+ return { ok: true, slotIndex: resultSlotIndex };
3544
+ }
3545
+ var SlotSchema = import_zod30.z.object({
3546
+ dia: import_zod30.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Formato YYYY-MM-DD"),
3547
+ plataforma: import_zod30.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
3548
+ tipo: import_zod30.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]),
3549
+ keyword: import_zod30.z.string().min(1),
3550
+ tema: import_zod30.z.string().optional(),
3551
+ productoId: import_zod30.z.string().optional(),
3552
+ estado: import_zod30.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
3553
+ locationId: import_zod30.z.string().optional(),
3554
+ locationNombre: import_zod30.z.string().optional()
3555
+ });
3556
+ var ParamsSchema2 = import_zod30.z.object({
3557
+ tenantId: import_zod30.z.string().min(1),
3558
+ brandId: import_zod30.z.string().min(1),
3559
+ mes: import_zod30.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM"),
3560
+ semana: import_zod30.z.number().int().min(1).max(5),
3561
+ slot: SlotSchema
3562
+ });
3563
+ var OutputSchema2 = import_zod30.z.discriminatedUnion("ok", [
3564
+ import_zod30.z.object({ ok: import_zod30.z.literal(true), slotIndex: import_zod30.z.number().int() }),
3565
+ import_zod30.z.object({ ok: import_zod30.z.literal(false), error: import_zod30.z.string() })
3566
+ ]);
3567
+ var rawContract2 = {
3568
+ name: "add_calendar_slot",
3569
+ description: "Agrega un slot nuevo al calendario editorial. NO modifica slots existentes \u2014 para eso usa update_calendar_slot.",
3570
+ paramsSchema: ParamsSchema2,
3571
+ outputSchema: OutputSchema2,
3572
+ requiresConfirmation: false,
3573
+ destructive: false,
3574
+ affectsPublication: false,
3575
+ affectsExternal: false,
3576
+ martinSummaryTemplate: (input, output, locale) => {
3577
+ if (!output.ok) {
3578
+ return locale === "en" ? `I couldn't add the slot: ${output.error}` : `No pude agregar el slot: ${output.error}`;
3579
+ }
3580
+ return locale === "en" ? `Added a slot in week ${input.semana} of ${input.mes}.` : `Agregu\xE9 un slot en la semana ${input.semana} de ${input.mes}.`;
3581
+ },
3582
+ auditAction: "marketing.calendario.slot.agregar",
3583
+ extractTargetPath: (input, output) => {
3584
+ if (!output.ok) return "";
3585
+ return `tenants/${input.tenantId}/marketing_calendario/(brand=${input.brandId},mes=${input.mes}).semana=${input.semana}.slot=${output.slotIndex}`;
3586
+ },
3587
+ extractChanges: (input, output) => ({
3588
+ before: null,
3589
+ after: output.ok ? input.slot : null
3590
+ }),
3591
+ quotasConsumed: [],
3592
+ permissionScope: "module",
3593
+ permissionKey: "marketing",
3594
+ permissionAction: "editar",
3595
+ sideEffects: ["writes_firestore", "updates_calendar_slot"]
3596
+ };
3597
+ var addCalendarSlotContract = MartinContractSchema.parse(
3598
+ rawContract2
3599
+ );
3509
3600
  var CAMPOS_SEMANTICOS = ["keyword", "tema", "plataforma", "tipo"];
3510
3601
  function mapContenidoEstadoToSlotEstado(contenidoEstado) {
3511
3602
  switch (contenidoEstado) {
@@ -3542,13 +3633,16 @@ async function calendarSlotUpdater(input) {
3542
3633
  return { ok: false, error: `Semana ${semana} no existe` };
3543
3634
  }
3544
3635
  const items = semanaData.items ?? [];
3545
- const isNewSlot = slotIndex >= items.length;
3636
+ if (slotIndex >= items.length) {
3637
+ return {
3638
+ ok: false,
3639
+ error: `slotIndex ${slotIndex} no existe (semana ${semana} tiene ${items.length} slots). Para agregar un slot nuevo usa add_calendar_slot.`,
3640
+ code: "SLOT_NOT_FOUND"
3641
+ };
3642
+ }
3546
3643
  const tocaSemantica = CAMPOS_SEMANTICOS.some(
3547
3644
  (campo) => campo in cambios && cambios[campo] !== void 0
3548
3645
  );
3549
- if (isNewSlot) {
3550
- return handleAddSlot({ db, calDocRef, semana, cambios });
3551
- }
3552
3646
  const slotAnterior = items[slotIndex];
3553
3647
  const oldContenidoRef = slotAnterior.contenidoRef ?? null;
3554
3648
  if (oldContenidoRef && tocaSemantica && !accionContenidoExistente) {
@@ -3694,7 +3788,7 @@ async function calendarSlotUpdater(input) {
3694
3788
  freshSemanas[semana - 1] = freshSemana;
3695
3789
  tx.update(calDocRef, {
3696
3790
  semanas: freshSemanas,
3697
- updatedAt: import_firebase_admin6.firestore.FieldValue.serverTimestamp()
3791
+ updatedAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
3698
3792
  });
3699
3793
  });
3700
3794
  if (moveTarget && oldContenidoRef) {
@@ -3715,7 +3809,7 @@ async function calendarSlotUpdater(input) {
3715
3809
  await c.ref.update({
3716
3810
  estado: "descartado",
3717
3811
  rechazadoMotivo: "Slot del calendario fue modificado",
3718
- rechazadoAt: import_firebase_admin6.firestore.FieldValue.serverTimestamp()
3812
+ rechazadoAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
3719
3813
  });
3720
3814
  descartados++;
3721
3815
  } catch (err) {
@@ -3733,35 +3827,6 @@ async function calendarSlotUpdater(input) {
3733
3827
  ...movedTo ? { movedTo } : {}
3734
3828
  };
3735
3829
  }
3736
- async function handleAddSlot(args) {
3737
- const { db, calDocRef, semana, cambios } = args;
3738
- let resultSlotIndex = -1;
3739
- await db.runTransaction(async (tx) => {
3740
- const freshSnap = await tx.get(calDocRef);
3741
- if (!freshSnap.exists) {
3742
- throw new Error(`handleAddSlot: calendario ${calDocRef.id} desaparecio`);
3743
- }
3744
- const freshCal = freshSnap.data();
3745
- const freshSemanas = freshCal.semanas ?? [];
3746
- const freshSemana = freshSemanas[semana - 1];
3747
- if (!freshSemana) {
3748
- throw new Error(`handleAddSlot: semana ${semana} desaparecio`);
3749
- }
3750
- const freshItems = freshSemana.items ?? [];
3751
- freshItems.push({
3752
- ...cambios,
3753
- estado: cambios.estado ?? ESTADO_CALENDARIO_ITEM.PLANIFICADO
3754
- });
3755
- resultSlotIndex = freshItems.length - 1;
3756
- freshSemana.items = freshItems;
3757
- freshSemanas[semana - 1] = freshSemana;
3758
- tx.update(calDocRef, {
3759
- semanas: freshSemanas,
3760
- updatedAt: import_firebase_admin6.firestore.FieldValue.serverTimestamp()
3761
- });
3762
- });
3763
- return { ok: true, action: "added", slotIndex: resultSlotIndex, descartados: 0 };
3764
- }
3765
3830
  async function handleNuevoSlot(args) {
3766
3831
  const { db, calDocRef, semana, cambios, currentSlot } = args;
3767
3832
  const dia = cambios.dia ?? currentSlot.dia ?? null;
@@ -3788,7 +3853,7 @@ async function handleNuevoSlot(args) {
3788
3853
  freshSemanas[semana - 1] = freshSemana;
3789
3854
  tx.update(calDocRef, {
3790
3855
  semanas: freshSemanas,
3791
- updatedAt: import_firebase_admin6.firestore.FieldValue.serverTimestamp()
3856
+ updatedAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
3792
3857
  });
3793
3858
  });
3794
3859
  return {
@@ -3798,35 +3863,35 @@ async function handleNuevoSlot(args) {
3798
3863
  descartados: 0
3799
3864
  };
3800
3865
  }
3801
- var ParamsSchema2 = import_zod30.z.object({
3802
- tenantId: import_zod30.z.string().min(1),
3803
- brandId: import_zod30.z.string().min(1),
3804
- mes: import_zod30.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM"),
3805
- semana: import_zod30.z.number().int().min(1).max(5),
3806
- slotIndex: import_zod30.z.number().int().min(0),
3807
- cambios: import_zod30.z.record(import_zod30.z.string(), import_zod30.z.unknown()),
3808
- accionContenidoExistente: import_zod30.z.string().optional()
3866
+ var ParamsSchema3 = import_zod31.z.object({
3867
+ tenantId: import_zod31.z.string().min(1),
3868
+ brandId: import_zod31.z.string().min(1),
3869
+ mes: import_zod31.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM"),
3870
+ semana: import_zod31.z.number().int().min(1).max(5),
3871
+ slotIndex: import_zod31.z.number().int().min(0),
3872
+ cambios: import_zod31.z.record(import_zod31.z.string(), import_zod31.z.unknown()),
3873
+ accionContenidoExistente: import_zod31.z.string().optional()
3809
3874
  });
3810
- var OutputSchema2 = import_zod30.z.discriminatedUnion("ok", [
3811
- import_zod30.z.object({
3812
- ok: import_zod30.z.literal(true),
3813
- action: import_zod30.z.enum(["added", "updated", "nuevo_slot", "moved"]),
3814
- slotIndex: import_zod30.z.number().int(),
3815
- descartados: import_zod30.z.number().int(),
3816
- movedTo: import_zod30.z.string().optional()
3875
+ var OutputSchema3 = import_zod31.z.discriminatedUnion("ok", [
3876
+ import_zod31.z.object({
3877
+ ok: import_zod31.z.literal(true),
3878
+ action: import_zod31.z.enum(["updated", "nuevo_slot", "moved"]),
3879
+ slotIndex: import_zod31.z.number().int(),
3880
+ descartados: import_zod31.z.number().int(),
3881
+ movedTo: import_zod31.z.string().optional()
3817
3882
  }),
3818
- import_zod30.z.object({
3819
- ok: import_zod30.z.literal(false),
3820
- error: import_zod30.z.string(),
3821
- code: import_zod30.z.enum(["ACCION_CONTENIDO_EXISTENTE_REQUIRED", "MOVE_TARGET_OCCUPIED", "MOVE_TARGET_INVALID"]).optional(),
3822
- opciones: import_zod30.z.array(import_zod30.z.string()).optional()
3883
+ import_zod31.z.object({
3884
+ ok: import_zod31.z.literal(false),
3885
+ error: import_zod31.z.string(),
3886
+ code: import_zod31.z.enum(["SLOT_NOT_FOUND", "ACCION_CONTENIDO_EXISTENTE_REQUIRED", "MOVE_TARGET_OCCUPIED", "MOVE_TARGET_INVALID"]).optional(),
3887
+ opciones: import_zod31.z.array(import_zod31.z.string()).optional()
3823
3888
  })
3824
3889
  ]);
3825
- var rawContract2 = {
3890
+ var rawContract3 = {
3826
3891
  name: "update_calendar_slot",
3827
- description: "Modifica o agrega un slot del calendario editorial. Si slotIndex >= items.length agrega un slot nuevo. Si el slot ten\xEDa contenidoRef previo y los cambios tocan campos sem\xE1nticos (keyword/tema/plataforma/tipo), exige accionContenidoExistente con 4 opciones: descartar | mover:semana:N:slot:M | nuevo_slot | mantener.",
3828
- paramsSchema: ParamsSchema2,
3829
- outputSchema: OutputSchema2,
3892
+ description: "MODIFICA un slot existente del calendario editorial. NO agrega slots nuevos \u2014 para eso usa add_calendar_slot. Si el slot ten\xEDa contenidoRef previo y los cambios tocan campos sem\xE1nticos (keyword/tema/plataforma/tipo), exige accionContenidoExistente con 4 opciones: descartar | mover:semana:N:slot:M | nuevo_slot | mantener.",
3893
+ paramsSchema: ParamsSchema3,
3894
+ outputSchema: OutputSchema3,
3830
3895
  requiresConfirmation: false,
3831
3896
  destructive: false,
3832
3897
  // Mutación, pero reversible (puede deshacerse cambiando el slot).
@@ -3838,7 +3903,6 @@ var rawContract2 = {
3838
3903
  return `No pude actualizar el slot: ${output.error}`;
3839
3904
  }
3840
3905
  const verb = {
3841
- added: locale === "en" ? "added" : "agregu\xE9",
3842
3906
  updated: locale === "en" ? "updated" : "actualic\xE9",
3843
3907
  nuevo_slot: locale === "en" ? "created a new slot" : "cre\xE9 un slot nuevo",
3844
3908
  moved: locale === "en" ? "moved" : "mov\xED"
@@ -3868,7 +3932,7 @@ var rawContract2 = {
3868
3932
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
3869
3933
  };
3870
3934
  var calendarSlotUpdaterContract = MartinContractSchema.parse(
3871
- rawContract2
3935
+ rawContract3
3872
3936
  );
3873
3937
  async function getCalendar(input) {
3874
3938
  const { db, tenantId, brandId, mes } = input;
@@ -3890,23 +3954,23 @@ async function getCalendar(input) {
3890
3954
  calendario: { id: doc.id, ...doc.data() }
3891
3955
  };
3892
3956
  }
3893
- var ParamsSchema3 = import_zod31.z.object({
3894
- tenantId: import_zod31.z.string().min(1),
3895
- brandId: import_zod31.z.string().min(1),
3896
- mes: import_zod31.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM")
3957
+ var ParamsSchema4 = import_zod32.z.object({
3958
+ tenantId: import_zod32.z.string().min(1),
3959
+ brandId: import_zod32.z.string().min(1),
3960
+ mes: import_zod32.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM")
3897
3961
  });
3898
- var OutputSchema3 = import_zod31.z.object({
3899
- ok: import_zod31.z.literal(true),
3900
- mes: import_zod31.z.string(),
3901
- brandId: import_zod31.z.string(),
3902
- calendario: import_zod31.z.record(import_zod31.z.string(), import_zod31.z.unknown()).nullable(),
3903
- mensaje: import_zod31.z.string().optional()
3962
+ var OutputSchema4 = import_zod32.z.object({
3963
+ ok: import_zod32.z.literal(true),
3964
+ mes: import_zod32.z.string(),
3965
+ brandId: import_zod32.z.string(),
3966
+ calendario: import_zod32.z.record(import_zod32.z.string(), import_zod32.z.unknown()).nullable(),
3967
+ mensaje: import_zod32.z.string().optional()
3904
3968
  });
3905
- var rawContract3 = {
3969
+ var rawContract4 = {
3906
3970
  name: "get_calendar",
3907
3971
  description: "Lee el calendario editorial del mes para una brand. Retorna semanas con items planificados por plataforma.",
3908
- paramsSchema: ParamsSchema3,
3909
- outputSchema: OutputSchema3,
3972
+ paramsSchema: ParamsSchema4,
3973
+ outputSchema: OutputSchema4,
3910
3974
  requiresConfirmation: false,
3911
3975
  destructive: false,
3912
3976
  affectsPublication: false,
@@ -3928,7 +3992,7 @@ var rawContract3 = {
3928
3992
  sideEffects: ["reads_firestore"]
3929
3993
  };
3930
3994
  var getCalendarContract = MartinContractSchema.parse(
3931
- rawContract3
3995
+ rawContract4
3932
3996
  );
3933
3997
  var PLATAFORMA_A_FORMATO = {
3934
3998
  gbp: "gbp_4_3",
@@ -3966,12 +4030,12 @@ async function photoAssigner(input) {
3966
4030
  mediaVariante: {
3967
4031
  url: varianteUrl,
3968
4032
  formato,
3969
- linkedAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp(),
4033
+ linkedAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp(),
3970
4034
  permalink: null,
3971
4035
  // lo llenara publishToInstagram/etc en el futuro
3972
4036
  mediaExternalId: null
3973
4037
  },
3974
- editadoAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
4038
+ editadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
3975
4039
  };
3976
4040
  let slotResolved = null;
3977
4041
  if (calendarioItemRef) {
@@ -4021,7 +4085,7 @@ async function photoAssigner(input) {
4021
4085
  tx.update(contenidoRefDoc, contenidoUpdatePayload);
4022
4086
  tx.update(calDocRef, {
4023
4087
  semanas,
4024
- updatedAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
4088
+ updatedAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
4025
4089
  });
4026
4090
  });
4027
4091
  } else {
@@ -4223,26 +4287,26 @@ var BRAND_BRIEF_SCHEMA_HINT = {
4223
4287
  escenas: [{ id: string, nombre: string, promptHint: string }]
4224
4288
  }`
4225
4289
  };
4226
- var ParamsSchema4 = import_zod32.z.object({
4227
- tenantId: import_zod32.z.string().min(1),
4228
- brandId: import_zod32.z.string().min(1)
4290
+ var ParamsSchema5 = import_zod33.z.object({
4291
+ tenantId: import_zod33.z.string().min(1),
4292
+ brandId: import_zod33.z.string().min(1)
4229
4293
  });
4230
- var OutputSchema4 = import_zod32.z.discriminatedUnion("ok", [
4231
- import_zod32.z.object({
4232
- ok: import_zod32.z.literal(true),
4294
+ var OutputSchema5 = import_zod33.z.discriminatedUnion("ok", [
4295
+ import_zod33.z.object({
4296
+ ok: import_zod33.z.literal(true),
4233
4297
  /** Payload con instrucción + datos del negocio para que Claude genere el brief. */
4234
- payload: import_zod32.z.record(import_zod32.z.string(), import_zod32.z.unknown())
4298
+ payload: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown())
4235
4299
  }),
4236
- import_zod32.z.object({
4237
- ok: import_zod32.z.literal(false),
4238
- error: import_zod32.z.string()
4300
+ import_zod33.z.object({
4301
+ ok: import_zod33.z.literal(false),
4302
+ error: import_zod33.z.string()
4239
4303
  })
4240
4304
  ]);
4241
- var rawContract4 = {
4305
+ var rawContract5 = {
4242
4306
  name: "generate_brand_brief",
4243
4307
  description: "Prepara los datos del negocio (Shopify, SEO, GBP, site_content scrape, brand config, ubicaciones del tenant) para que el sistema genere un Brand Brief pre-llenado. NO escribe el brief \u2014 s\xF3lo arma el payload.",
4244
- paramsSchema: ParamsSchema4,
4245
- outputSchema: OutputSchema4,
4308
+ paramsSchema: ParamsSchema5,
4309
+ outputSchema: OutputSchema5,
4246
4310
  // Lectura pura, sin side effects de escritura ni publicación.
4247
4311
  requiresConfirmation: false,
4248
4312
  destructive: false,
@@ -4265,7 +4329,7 @@ var rawContract4 = {
4265
4329
  sideEffects: ["reads_firestore"]
4266
4330
  };
4267
4331
  var brandBriefBuilderContract = MartinContractSchema.parse(
4268
- rawContract4
4332
+ rawContract5
4269
4333
  );
4270
4334
  async function resolveLastImportId(db, tenantId, brandId) {
4271
4335
  const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
@@ -4509,7 +4573,7 @@ async function contenidoWriter(input) {
4509
4573
  await db.doc(`tenants/${tenantId}/marketing_contenido/${existente.id}`).update({
4510
4574
  estado: "descartado",
4511
4575
  rechazadoMotivo: "Reemplazado por contenido nuevo para el mismo slot",
4512
- rechazadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
4576
+ rechazadoAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp()
4513
4577
  });
4514
4578
  descartados++;
4515
4579
  }
@@ -4568,7 +4632,7 @@ async function contenidoWriter(input) {
4568
4632
  mediaUrl: null,
4569
4633
  calendarioItemRef: calendarioItemRef ?? null,
4570
4634
  datos,
4571
- creadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp(),
4635
+ creadoAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp(),
4572
4636
  creadoPorId: "mcp-cowork",
4573
4637
  origen: "ai_assisted"
4574
4638
  });
@@ -4626,7 +4690,7 @@ async function contenidoWriter(input) {
4626
4690
  tx.create(contenidoRef, contenido);
4627
4691
  tx.update(calDocRef, {
4628
4692
  semanas,
4629
- updatedAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
4693
+ updatedAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp()
4630
4694
  });
4631
4695
  });
4632
4696
  } else {
@@ -4744,7 +4808,7 @@ async function weeklyContentBuilder(input) {
4744
4808
  brandId,
4745
4809
  mes: targetMes,
4746
4810
  semanas,
4747
- creadoAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp(),
4811
+ creadoAt: import_firebase_admin10.firestore.FieldValue.serverTimestamp(),
4748
4812
  creadoPorId: "mcp-cowork",
4749
4813
  updatedAt: null
4750
4814
  };
@@ -6401,6 +6465,19 @@ function wrapWithContract(contract, helper, options = {}) {
6401
6465
  }
6402
6466
  const parseResult = contract.paramsSchema.safeParse(input);
6403
6467
  if (!parseResult.success) {
6468
+ const validationErrors = parseResult.error.issues.map((i) => {
6469
+ const issue = i;
6470
+ return {
6471
+ path: i.path,
6472
+ code: i.code,
6473
+ message: i.message,
6474
+ received: issue.received,
6475
+ expected: issue.expected
6476
+ };
6477
+ });
6478
+ const camposFallidos = validationErrors.map((v) => v.path.join(".")).filter(Boolean);
6479
+ const baseMsg = getWrapperMessage("input_invalido", locale);
6480
+ const text = camposFallidos.length ? locale === "en" ? `${baseMsg} Issues with: ${camposFallidos.join(", ")}.` : `${baseMsg} Hubo problemas con: ${camposFallidos.join(", ")}.` : baseMsg;
6404
6481
  await writeAuditLog({
6405
6482
  tenantId: ctx.tenantId,
6406
6483
  brandId: ctx.brandId ?? null,
@@ -6409,13 +6486,14 @@ function wrapWithContract(contract, helper, options = {}) {
6409
6486
  motivo: "Input inv\xE1lido \u2014 Zod parse failed",
6410
6487
  conversacionId: ctx.conversacionId ?? null,
6411
6488
  status: "error",
6412
- errorMessage: `Input shape inv\xE1lido: ${parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`,
6489
+ errorMessage: `Input shape inv\xE1lido: ${validationErrors.map((v) => `${v.path.join(".")}: ${v.code} (${v.message})`).join("; ")}`,
6413
6490
  durationMs: Date.now() - startMs
6414
6491
  });
6415
6492
  return {
6416
- text: getWrapperMessage("input_invalido", locale),
6493
+ text,
6417
6494
  structuredOutput: null,
6418
- state: "error"
6495
+ state: "error",
6496
+ validationErrors
6419
6497
  };
6420
6498
  }
6421
6499
  const parsedInput = parseResult.data;
@@ -6462,7 +6540,6 @@ function wrapWithContract(contract, helper, options = {}) {
6462
6540
  let output;
6463
6541
  try {
6464
6542
  output = await helper(parsedInput);
6465
- contract.outputSchema.parse(output);
6466
6543
  } catch (err) {
6467
6544
  await writeAuditLog({
6468
6545
  tenantId: ctx.tenantId,
@@ -6481,6 +6558,36 @@ function wrapWithContract(contract, helper, options = {}) {
6481
6558
  state: "error"
6482
6559
  };
6483
6560
  }
6561
+ const outputParse = contract.outputSchema.safeParse(output);
6562
+ if (!outputParse.success) {
6563
+ const validationErrors = outputParse.error.issues.map((i) => {
6564
+ const issue = i;
6565
+ return {
6566
+ path: i.path,
6567
+ code: i.code,
6568
+ message: i.message,
6569
+ received: issue.received,
6570
+ expected: issue.expected
6571
+ };
6572
+ });
6573
+ await writeAuditLog({
6574
+ tenantId: ctx.tenantId,
6575
+ brandId: ctx.brandId ?? null,
6576
+ actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
6577
+ action: contract.auditAction,
6578
+ motivo: `Output del helper inv\xE1lido \u2014 bug de "${contract.name}"`,
6579
+ conversacionId: ctx.conversacionId ?? null,
6580
+ status: "error",
6581
+ errorMessage: `Output shape inv\xE1lido: ${validationErrors.map((v) => `${v.path.join(".")}: ${v.code} (${v.message})`).join("; ")}`,
6582
+ durationMs: Date.now() - startMs
6583
+ });
6584
+ return {
6585
+ text: martinSafeError(new Error("output_invalid"), locale),
6586
+ structuredOutput: null,
6587
+ state: "error",
6588
+ validationErrors
6589
+ };
6590
+ }
6484
6591
  const maybeDisabled = output;
6485
6592
  if (maybeDisabled.disabled === true) {
6486
6593
  const code = maybeDisabled.code;
@@ -6535,63 +6642,63 @@ function wrapWithContract(contract, helper, options = {}) {
6535
6642
  };
6536
6643
  };
6537
6644
  }
6538
- var RecordarMemoriaParamsSchema = import_zod33.z.object({
6645
+ var RecordarMemoriaParamsSchema = import_zod34.z.object({
6539
6646
  tipo: TipoMemoriaEnum,
6540
6647
  categoria: CategoriaMemoriaEnum,
6541
- contenido: import_zod33.z.string().min(3).max(500)
6648
+ contenido: import_zod34.z.string().min(3).max(500)
6542
6649
  });
6543
- var RecordarMemoriaOutputSchema = import_zod33.z.object({
6544
- memoriaId: import_zod33.z.string(),
6545
- status: import_zod33.z.literal("creada")
6650
+ var RecordarMemoriaOutputSchema = import_zod34.z.object({
6651
+ memoriaId: import_zod34.z.string(),
6652
+ status: import_zod34.z.literal("creada")
6546
6653
  });
6547
- var OlvidarMemoriaParamsSchema = import_zod33.z.object({
6548
- memoriaId: import_zod33.z.string(),
6549
- motivo: import_zod33.z.string().optional()
6654
+ var OlvidarMemoriaParamsSchema = import_zod34.z.object({
6655
+ memoriaId: import_zod34.z.string(),
6656
+ motivo: import_zod34.z.string().optional()
6550
6657
  });
6551
- var OlvidarMemoriaOutputSchema = import_zod33.z.object({
6552
- status: import_zod33.z.literal("archivada")
6658
+ var OlvidarMemoriaOutputSchema = import_zod34.z.object({
6659
+ status: import_zod34.z.literal("archivada")
6553
6660
  });
6554
- var ConfigInputSchema = import_zod34.z.object({
6555
- diaSemana: import_zod34.z.number().int().min(0).max(6).nullable(),
6556
- diaMes: import_zod34.z.number().int().min(1).max(31).nullable(),
6557
- hora: import_zod34.z.string().regex(/^\d{2}:\d{2}$/),
6661
+ var ConfigInputSchema = import_zod35.z.object({
6662
+ diaSemana: import_zod35.z.number().int().min(0).max(6).nullable(),
6663
+ diaMes: import_zod35.z.number().int().min(1).max(31).nullable(),
6664
+ hora: import_zod35.z.string().regex(/^\d{2}:\d{2}$/),
6558
6665
  // Timezone NO viene en input — hereda de tenants/{tid}.zonaHoraria (audit fix 2).
6559
- fechaPuntual: import_zod34.z.string().datetime({ offset: true }).nullable()
6666
+ fechaPuntual: import_zod35.z.string().datetime({ offset: true }).nullable()
6560
6667
  }).strict();
6561
- var AccionInputSchema = import_zod34.z.object({
6562
- tool: import_zod34.z.string().min(1),
6563
- params: import_zod34.z.record(import_zod34.z.string(), import_zod34.z.unknown())
6668
+ var AccionInputSchema = import_zod35.z.object({
6669
+ tool: import_zod35.z.string().min(1),
6670
+ params: import_zod35.z.record(import_zod35.z.string(), import_zod35.z.unknown())
6564
6671
  }).strict();
6565
- var ProgramarRutinaParamsSchema = import_zod34.z.object({
6672
+ var ProgramarRutinaParamsSchema = import_zod35.z.object({
6566
6673
  tipo: TipoRutinaEnum,
6567
6674
  frecuencia: FrecuenciaRutinaEnum,
6568
6675
  config: ConfigInputSchema,
6569
6676
  accion: AccionInputSchema,
6570
- uidDestinatario: import_zod34.z.string()
6677
+ uidDestinatario: import_zod35.z.string()
6571
6678
  });
6572
- var ProgramarRutinaOutputSchema = import_zod34.z.object({
6573
- rutinaId: import_zod34.z.string(),
6574
- proximaEjecucionAt: import_zod34.z.string().datetime()
6679
+ var ProgramarRutinaOutputSchema = import_zod35.z.object({
6680
+ rutinaId: import_zod35.z.string(),
6681
+ proximaEjecucionAt: import_zod35.z.string().datetime()
6575
6682
  });
6576
- var PausarRutinaParamsSchema = import_zod34.z.object({
6577
- rutinaId: import_zod34.z.string(),
6578
- motivo: import_zod34.z.string().optional()
6683
+ var PausarRutinaParamsSchema = import_zod35.z.object({
6684
+ rutinaId: import_zod35.z.string(),
6685
+ motivo: import_zod35.z.string().optional()
6579
6686
  });
6580
- var PausarRutinaOutputSchema = import_zod34.z.object({
6581
- status: import_zod34.z.literal("pausada")
6687
+ var PausarRutinaOutputSchema = import_zod35.z.object({
6688
+ status: import_zod35.z.literal("pausada")
6582
6689
  });
6583
- var ArchivarRutinaParamsSchema = import_zod34.z.object({
6584
- rutinaId: import_zod34.z.string(),
6585
- motivo: import_zod34.z.string().optional()
6690
+ var ArchivarRutinaParamsSchema = import_zod35.z.object({
6691
+ rutinaId: import_zod35.z.string(),
6692
+ motivo: import_zod35.z.string().optional()
6586
6693
  });
6587
- var ArchivarRutinaOutputSchema = import_zod34.z.object({
6588
- status: import_zod34.z.literal("archivada")
6694
+ var ArchivarRutinaOutputSchema = import_zod35.z.object({
6695
+ status: import_zod35.z.literal("archivada")
6589
6696
  });
6590
- var ListarRutinasParamsSchema = import_zod34.z.object({
6591
- uid: import_zod34.z.string().optional()
6697
+ var ListarRutinasParamsSchema = import_zod35.z.object({
6698
+ uid: import_zod35.z.string().optional()
6592
6699
  });
6593
- var ListarRutinasOutputSchema = import_zod34.z.object({
6594
- rutinas: import_zod34.z.array(MartinRutinaSchema)
6700
+ var ListarRutinasOutputSchema = import_zod35.z.object({
6701
+ rutinas: import_zod35.z.array(MartinRutinaSchema)
6595
6702
  });
6596
6703
 
6597
6704
  // src/tools/martinContext.ts
@@ -6668,6 +6775,9 @@ async function callCF(name, input) {
6668
6775
  function callGetCalendar(input) {
6669
6776
  return callCF("marketingGetCalendarCallable", input);
6670
6777
  }
6778
+ function callAddCalendarSlot(input) {
6779
+ return callCF("marketingAddCalendarSlotCallable", input);
6780
+ }
6671
6781
  function callBrandBriefWriter(input) {
6672
6782
  return callCF("marketingBrandBriefWriterCallable", input);
6673
6783
  }
@@ -6718,7 +6828,7 @@ function callPhotoDirectorExecute(input) {
6718
6828
  }
6719
6829
 
6720
6830
  // src/tools/marketing/photos.ts
6721
- var import_zod36 = require("zod");
6831
+ var import_zod37 = require("zod");
6722
6832
 
6723
6833
  // src/services/marketingEmbeddings.ts
6724
6834
  var import_google_auth_library = require("google-auth-library");
@@ -6986,7 +7096,7 @@ async function findNearestInCollectionWithOverride(params) {
6986
7096
  }
6987
7097
 
6988
7098
  // src/tools/marketing/content.ts
6989
- var import_zod35 = require("zod");
7099
+ var import_zod36 = require("zod");
6990
7100
  var _logOverride = null;
6991
7101
  async function logToMcpLogs(entry) {
6992
7102
  if (_logOverride) return _logOverride(entry);
@@ -6999,22 +7109,22 @@ async function logToMcpLogs(entry) {
6999
7109
  } catch {
7000
7110
  }
7001
7111
  }
7002
- var IncludeSchema = import_zod35.z.object({
7003
- products: import_zod35.z.boolean().default(true),
7004
- collections: import_zod35.z.boolean().default(true),
7005
- articles: import_zod35.z.boolean().default(true),
7006
- pages: import_zod35.z.boolean().default(false)
7112
+ var IncludeSchema = import_zod36.z.object({
7113
+ products: import_zod36.z.boolean().default(true),
7114
+ collections: import_zod36.z.boolean().default(true),
7115
+ articles: import_zod36.z.boolean().default(true),
7116
+ pages: import_zod36.z.boolean().default(false)
7007
7117
  }).default({
7008
7118
  products: true,
7009
7119
  collections: true,
7010
7120
  articles: true,
7011
7121
  pages: false
7012
7122
  });
7013
- var LimitSchema = import_zod35.z.object({
7014
- products: import_zod35.z.number().int().min(0).max(20).default(5),
7015
- collections: import_zod35.z.number().int().min(0).max(10).default(3),
7016
- articles: import_zod35.z.number().int().min(0).max(20).default(5),
7017
- pages: import_zod35.z.number().int().min(0).max(10).default(2)
7123
+ var LimitSchema = import_zod36.z.object({
7124
+ products: import_zod36.z.number().int().min(0).max(20).default(5),
7125
+ collections: import_zod36.z.number().int().min(0).max(10).default(3),
7126
+ articles: import_zod36.z.number().int().min(0).max(20).default(5),
7127
+ pages: import_zod36.z.number().int().min(0).max(10).default(2)
7018
7128
  }).default({ products: 5, collections: 3, articles: 5, pages: 2 });
7019
7129
  async function findContentForTopicHandler(input, session) {
7020
7130
  const tenantId = session.requireTenant();
@@ -7100,13 +7210,13 @@ MODOS (parametro mode):
7100
7210
 
7101
7211
  Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = el doc aparecio en ambos modos (alta confianza). 1 = aparecio solo en text (problema visual) o solo en image (problema SEO). Esos "solo image" son candidatos perfectos para recomendar optimizacion al tenant.`,
7102
7212
  {
7103
- brandId: import_zod35.z.string().optional().describe("ID de la brand"),
7104
- contexto: import_zod35.z.string().min(1).describe("Parrafo, keyword o intencion"),
7105
- fecha: import_zod35.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
7213
+ brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7214
+ contexto: import_zod36.z.string().min(1).describe("Parrafo, keyword o intencion"),
7215
+ fecha: import_zod36.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
7106
7216
  include: IncludeSchema.optional(),
7107
7217
  limit: LimitSchema.optional(),
7108
- diversidad: import_zod35.z.boolean().default(true),
7109
- mode: import_zod35.z.enum(["text", "hybrid"]).default("text").describe("Modo de busqueda: 'text' (rapido, 1 query) o 'hybrid' (text+image via RRF, 2x queries, rescata SEO debil)")
7218
+ diversidad: import_zod36.z.boolean().default(true),
7219
+ mode: import_zod36.z.enum(["text", "hybrid"]).default("text").describe("Modo de busqueda: 'text' (rapido, 1 query) o 'hybrid' (text+image via RRF, 2x queries, rescata SEO debil)")
7110
7220
  },
7111
7221
  async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode: mode2 }) => {
7112
7222
  const brandId = inputBrandId ?? session.requireBrand();
@@ -7210,11 +7320,11 @@ REGLAS:
7210
7320
 
7211
7321
  USAR: antes de generar contenido de cualquier slot del calendario.`,
7212
7322
  {
7213
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7214
- keyword: import_zod36.z.string().describe("Keyword del slot"),
7215
- plataforma: import_zod36.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
7216
- fecha: import_zod36.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
7217
- limit: import_zod36.z.number().int().min(1).max(10).default(5)
7323
+ brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7324
+ keyword: import_zod37.z.string().describe("Keyword del slot"),
7325
+ plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
7326
+ fecha: import_zod37.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
7327
+ limit: import_zod37.z.number().int().min(1).max(10).default(5)
7218
7328
  },
7219
7329
  async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
7220
7330
  const tenantId = session.requireTenant();
@@ -7255,7 +7365,7 @@ DESPUES de ver la foto, decide:
7255
7365
 
7256
7366
  Luego llama execute_photo_edit con tu analisis y prompt.`,
7257
7367
  {
7258
- fotoId: import_zod36.z.string().describe("ID de la foto")
7368
+ fotoId: import_zod37.z.string().describe("ID de la foto")
7259
7369
  },
7260
7370
  async ({ fotoId }) => {
7261
7371
  const tenantId = session.requireTenant();
@@ -7288,14 +7398,14 @@ Retorna la foto editada para que la revises. Si no te gusta:
7288
7398
 
7289
7399
  Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
7290
7400
  {
7291
- fotoId: import_zod36.z.string(),
7292
- prompt: import_zod36.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
7293
- acciones: import_zod36.z.array(import_zod36.z.enum(["edit_background", "none"])),
7294
- descripcion: import_zod36.z.string().describe("Descripcion semantica en espanol"),
7295
- tipo: import_zod36.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
7296
- tagsPrimarios: import_zod36.z.array(import_zod36.z.string()),
7297
- tagsSecundarios: import_zod36.z.array(import_zod36.z.string()),
7298
- tagsContexto: import_zod36.z.array(import_zod36.z.string())
7401
+ fotoId: import_zod37.z.string(),
7402
+ prompt: import_zod37.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
7403
+ acciones: import_zod37.z.array(import_zod37.z.enum(["edit_background", "none"])),
7404
+ descripcion: import_zod37.z.string().describe("Descripcion semantica en espanol"),
7405
+ tipo: import_zod37.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
7406
+ tagsPrimarios: import_zod37.z.array(import_zod37.z.string()),
7407
+ tagsSecundarios: import_zod37.z.array(import_zod37.z.string()),
7408
+ tagsContexto: import_zod37.z.array(import_zod37.z.string())
7299
7409
  },
7300
7410
  async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
7301
7411
  const tenantId = session.requireTenant();
@@ -7372,7 +7482,7 @@ Retorna:
7372
7482
  - creditos: { balance, planId, periodEnd, costoPhotoEdit }
7373
7483
  - _instrucciones: que hacer segun el estado`,
7374
7484
  {
7375
- fotoId: import_zod36.z.string().describe("ID de la foto")
7485
+ fotoId: import_zod37.z.string().describe("ID de la foto")
7376
7486
  },
7377
7487
  async ({ fotoId }) => {
7378
7488
  const tenantId = session.requireTenant();
@@ -7472,11 +7582,11 @@ Retorna:
7472
7582
  "find_products_for_content",
7473
7583
  `[DEPRECATED 179.5] Wrapper interno de find_content_for_topic. Para nuevas integraciones, usa find_content_for_topic con include.products=true directamente. Este wrapper existe solo por compat hacia atras.`,
7474
7584
  {
7475
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7476
- contexto: import_zod36.z.string().describe("Parrafo, keyword o intencion del contenido"),
7477
- fecha: import_zod36.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
7478
- limit: import_zod36.z.number().int().min(1).max(10).default(5),
7479
- diversidad: import_zod36.z.boolean().default(true)
7585
+ brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7586
+ contexto: import_zod37.z.string().describe("Parrafo, keyword o intencion del contenido"),
7587
+ fecha: import_zod37.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
7588
+ limit: import_zod37.z.number().int().min(1).max(10).default(5),
7589
+ diversidad: import_zod37.z.boolean().default(true)
7480
7590
  },
7481
7591
  async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
7482
7592
  console.warn(
@@ -7529,10 +7639,10 @@ RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no
7529
7639
 
7530
7640
  USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
7531
7641
  {
7532
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7533
- plataforma: import_zod36.z.string().describe("gbp | instagram | shopify_blog"),
7534
- tipoContenido: import_zod36.z.string().describe("post | carousel | story | blog"),
7535
- keyword: import_zod36.z.string().describe("Keyword del slot")
7642
+ brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7643
+ plataforma: import_zod37.z.string().describe("gbp | instagram | shopify_blog"),
7644
+ tipoContenido: import_zod37.z.string().describe("post | carousel | story | blog"),
7645
+ keyword: import_zod37.z.string().describe("Keyword del slot")
7536
7646
  },
7537
7647
  async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
7538
7648
  const tenantId = session.requireTenant();
@@ -7562,15 +7672,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
7562
7672
 
7563
7673
  IMPORTANTE \u2014 campo 'razon': Este texto lo ve el tenant en la app. Escribe en lenguaje amigable y claro, NO tecnico. Ejemplo MALO: "get_photos_for_slot retorno 0 fotos para shopify_blog". Ejemplo BUENO: "No hay fotos de alcatraz para el blog del 8 de abril". Siempre en espanol si el tenant habla espanol.`,
7564
7674
  {
7565
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7566
- semana: import_zod36.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD").describe("Semana en ISO del lunes"),
7567
- necesidades: import_zod36.z.array(
7568
- import_zod36.z.object({
7569
- tema: import_zod36.z.string(),
7570
- keyword: import_zod36.z.string(),
7571
- cantidadSugerida: import_zod36.z.number().int().positive(),
7572
- razon: import_zod36.z.string(),
7573
- slotsAfectados: import_zod36.z.array(import_zod36.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
7675
+ brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7676
+ semana: import_zod37.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD").describe("Semana en ISO del lunes"),
7677
+ necesidades: import_zod37.z.array(
7678
+ import_zod37.z.object({
7679
+ tema: import_zod37.z.string(),
7680
+ keyword: import_zod37.z.string(),
7681
+ cantidadSugerida: import_zod37.z.number().int().positive(),
7682
+ razon: import_zod37.z.string(),
7683
+ slotsAfectados: import_zod37.z.array(import_zod37.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
7574
7684
  })
7575
7685
  ).min(1)
7576
7686
  },
@@ -7716,8 +7826,8 @@ function registerMarketingTools(server, session) {
7716
7826
  "get_calendar",
7717
7827
  "Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
7718
7828
  {
7719
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7720
- mes: import_zod37.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
7829
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
7830
+ mes: import_zod38.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
7721
7831
  },
7722
7832
  async ({ brandId: inputBrandId, mes }) => {
7723
7833
  const tenantId = session.requireTenant();
@@ -7750,7 +7860,7 @@ function registerMarketingTools(server, session) {
7750
7860
  "get_seo_snapshot",
7751
7861
  "Lee el snapshot SEO de la brand: rank, keywords, oportunidades, competidores.",
7752
7862
  {
7753
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
7863
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
7754
7864
  },
7755
7865
  async ({ brandId: inputBrandId }) => {
7756
7866
  const tenantId = session.requireTenant();
@@ -7769,8 +7879,8 @@ function registerMarketingTools(server, session) {
7769
7879
  "get_photo_gallery",
7770
7880
  "Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
7771
7881
  {
7772
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7773
- estado: import_zod37.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
7882
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
7883
+ estado: import_zod38.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
7774
7884
  },
7775
7885
  async ({ brandId: inputBrandId, estado }) => {
7776
7886
  session.requireTenant();
@@ -7810,7 +7920,7 @@ function registerMarketingTools(server, session) {
7810
7920
  "generate_marketing_plan",
7811
7921
  "Prepara los datos necesarios para generar un plan de marketing estrategico. Retorna snapshot SEO + productos para que Claude genere el plan con el system prompt.",
7812
7922
  {
7813
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
7923
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
7814
7924
  },
7815
7925
  async ({ brandId: inputBrandId }) => {
7816
7926
  const tenantId = session.requireTenant();
@@ -7824,8 +7934,8 @@ function registerMarketingTools(server, session) {
7824
7934
  "save_marketing_plan",
7825
7935
  "Guarda el plan de marketing generado por Claude en la configuracion de la brand. Escribe en tenants/{tenantId}/marketing_config/{brandId}.plan.",
7826
7936
  {
7827
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7828
- plan: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Plan de marketing generado (keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, etc.)")
7937
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
7938
+ plan: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Plan de marketing generado (keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, etc.)")
7829
7939
  },
7830
7940
  async ({ brandId: inputBrandId, plan }) => {
7831
7941
  const tenantId = session.requireTenant();
@@ -7838,7 +7948,14 @@ function registerMarketingTools(server, session) {
7838
7948
  input: { tenantId, brandId, plan },
7839
7949
  ctx
7840
7950
  });
7841
- const payload = result.state === "success" ? result.structuredOutput : { ok: false, state: result.state, mensaje: result.text };
7951
+ const payload = result.state === "success" ? result.structuredOutput : {
7952
+ ok: false,
7953
+ state: result.state,
7954
+ mensaje: result.text,
7955
+ // HITO 6 A6.7: incluir detalles Zod estructurados para que
7956
+ // Claude (LLM) pueda auto-recuperarse en el siguiente intento.
7957
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
7958
+ };
7842
7959
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
7843
7960
  }
7844
7961
  );
@@ -7846,9 +7963,9 @@ function registerMarketingTools(server, session) {
7846
7963
  "update_marketing_plan_field",
7847
7964
  "Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
7848
7965
  {
7849
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7850
- field: import_zod37.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
7851
- value: import_zod37.z.unknown().describe("Valor del campo")
7966
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
7967
+ field: import_zod38.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
7968
+ value: import_zod38.z.unknown().describe("Valor del campo")
7852
7969
  },
7853
7970
  async ({ brandId: inputBrandId, field, value }) => {
7854
7971
  const tenantId = session.requireTenant();
@@ -7861,7 +7978,7 @@ function registerMarketingTools(server, session) {
7861
7978
  "generate_brand_brief",
7862
7979
  "Prepara todos los datos del negocio para que Claude genere un Brand Brief pre-llenado. Retorna Shopify + SEO + GBP + tenant data. Claude genera el brief, luego usa save_brand_brief para guardar.",
7863
7980
  {
7864
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
7981
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
7865
7982
  },
7866
7983
  async ({ brandId: inputBrandId }) => {
7867
7984
  const tenantId = session.requireTenant();
@@ -7889,8 +8006,8 @@ function registerMarketingTools(server, session) {
7889
8006
  "save_brand_brief",
7890
8007
  "Guarda el Brand Brief generado por Claude en tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Escribe solo ese campo (merge parcial, no rebuild del doc).",
7891
8008
  {
7892
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7893
- brandBrief: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Brand Brief completo generado por Claude")
8009
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
8010
+ brandBrief: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Brand Brief completo generado por Claude")
7894
8011
  },
7895
8012
  async ({ brandId: inputBrandId, brandBrief }) => {
7896
8013
  const tenantId = session.requireTenant();
@@ -7903,9 +8020,9 @@ function registerMarketingTools(server, session) {
7903
8020
  "generate_weekly_content",
7904
8021
  "Prepara datos del calendario + fotos + plan para generar el contenido de la semana. Claude genera con el system prompt, luego usa save_generated_content para guardar.",
7905
8022
  {
7906
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7907
- semana: import_zod37.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
7908
- modo: import_zod37.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
8023
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
8024
+ semana: import_zod38.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
8025
+ modo: import_zod38.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
7909
8026
  },
7910
8027
  async ({ brandId: inputBrandId, semana, modo }) => {
7911
8028
  const tenantId = session.requireTenant();
@@ -7921,14 +8038,14 @@ function registerMarketingTools(server, session) {
7921
8038
 
7922
8039
  IMPORTANTE: Si seleccionaste una foto con get_photos_for_slot, SIEMPRE pasa fotoId aqu\xED. Sin fotoId el post se publica sin imagen.`,
7923
8040
  {
7924
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7925
- plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
7926
- tipo: import_zod37.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
7927
- keyword: import_zod37.z.string().optional().describe("Keyword target"),
7928
- languageCode: import_zod37.z.string().optional().describe("Idioma (es/en)"),
7929
- fotoId: import_zod37.z.string().optional().describe("ID de la foto a asociar"),
7930
- datos: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
7931
- calendarioItemRef: import_zod37.z.string().optional().describe("Referencia al item del calendario")
8041
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
8042
+ plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
8043
+ tipo: import_zod38.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
8044
+ keyword: import_zod38.z.string().optional().describe("Keyword target"),
8045
+ languageCode: import_zod38.z.string().optional().describe("Idioma (es/en)"),
8046
+ fotoId: import_zod38.z.string().optional().describe("ID de la foto a asociar"),
8047
+ datos: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
8048
+ calendarioItemRef: import_zod38.z.string().optional().describe("Referencia al item del calendario")
7932
8049
  },
7933
8050
  async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
7934
8051
  const tenantId = session.requireTenant();
@@ -7973,13 +8090,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
7973
8090
  NO puede cambiar: tenantId, brandId, id (inmutables).
7974
8091
  Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
7975
8092
  {
7976
- contenidoId: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
7977
- datos: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
7978
- fotoId: import_zod37.z.string().nullable().optional().describe("Actualizar foto asociada"),
7979
- keyword: import_zod37.z.string().nullable().optional().describe("Actualizar keyword"),
7980
- languageCode: import_zod37.z.string().optional().describe("Actualizar idioma"),
7981
- estado: import_zod37.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
7982
- calendarioItemRef: import_zod37.z.string().nullable().optional().describe("Vincular a un slot del calendario")
8093
+ contenidoId: import_zod38.z.string().describe("ID del doc en marketing_contenido"),
8094
+ datos: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
8095
+ fotoId: import_zod38.z.string().nullable().optional().describe("Actualizar foto asociada"),
8096
+ keyword: import_zod38.z.string().nullable().optional().describe("Actualizar keyword"),
8097
+ languageCode: import_zod38.z.string().optional().describe("Actualizar idioma"),
8098
+ estado: import_zod38.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
8099
+ calendarioItemRef: import_zod38.z.string().nullable().optional().describe("Vincular a un slot del calendario")
7983
8100
  },
7984
8101
  async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
7985
8102
  const tenantId = session.requireTenant();
@@ -8006,31 +8123,69 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
8006
8123
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
8007
8124
  }
8008
8125
  );
8126
+ server.tool(
8127
+ "add_calendar_slot",
8128
+ "Agrega un slot NUEVO al calendario editorial. Para modificar un slot existente usa update_calendar_slot.",
8129
+ {
8130
+ brandId: import_zod38.z.string().describe("ID de la brand"),
8131
+ mes: import_zod38.z.string().describe("Mes del calendario en formato YYYY-MM"),
8132
+ semana: import_zod38.z.number().describe("Numero de semana (1-5)"),
8133
+ slot: import_zod38.z.object({
8134
+ dia: import_zod38.z.string().describe("Fecha del slot en formato YYYY-MM-DD. Debe caer dentro del rango fechaInicio/fechaFin de la semana indicada."),
8135
+ plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
8136
+ tipo: import_zod38.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Tipo de contenido"),
8137
+ keyword: import_zod38.z.string().describe("Keyword principal del contenido"),
8138
+ tema: import_zod38.z.string().optional().describe("Tema del contenido. OMITE el campo si no aplica, no envies cadena vacia ni null."),
8139
+ productoId: import_zod38.z.string().optional().describe("ID de producto vinculado. OMITE el campo si no aplica."),
8140
+ estado: import_zod38.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("Estado inicial. OMITE si quieres default 'planificado'."),
8141
+ locationId: import_zod38.z.string().optional().describe("GBP location ID \u2014 solo si plataforma=gbp y multi-sucursal. OMITE si no aplica."),
8142
+ locationNombre: import_zod38.z.string().optional().describe("Nombre de la sucursal GBP. OMITE si no aplica.")
8143
+ }).describe("Datos del slot nuevo")
8144
+ },
8145
+ async ({ brandId, mes, semana, slot }) => {
8146
+ const tenantId = session.requireTenant();
8147
+ const ctx = buildMartinContext(session, brandId);
8148
+ const result = await dispatchWithContract({
8149
+ contract: addCalendarSlotContract,
8150
+ helper: addCalendarSlot,
8151
+ callable: callAddCalendarSlot,
8152
+ input: { tenantId, brandId, mes, semana, slot },
8153
+ ctx
8154
+ });
8155
+ const payload = result.state === "success" ? result.structuredOutput : {
8156
+ ok: false,
8157
+ state: result.state,
8158
+ mensaje: result.text,
8159
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
8160
+ };
8161
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
8162
+ }
8163
+ );
8009
8164
  server.tool(
8010
8165
  "update_calendar_slot",
8011
- "Modifica o agrega un slot del calendario editorial. Si slotIndex >= items.length, agrega un slot nuevo.",
8166
+ "MODIFICA un slot EXISTENTE del calendario editorial. Para crear un slot nuevo usa add_calendar_slot. Si slotIndex no existe, retorna error SLOT_NOT_FOUND.",
8012
8167
  {
8013
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
8014
- mes: import_zod37.z.string().describe("Mes del calendario (YYYY-MM)"),
8015
- semana: import_zod37.z.number().describe("Numero de semana (1-5)"),
8016
- slotIndex: import_zod37.z.number().describe("Indice del slot (0-based). Si >= items.length, agrega nuevo slot al final."),
8017
- cambios: import_zod37.z.object({
8018
- dia: import_zod37.z.string().nullable().optional(),
8019
- plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional(),
8020
- tipo: import_zod37.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional(),
8021
- keyword: import_zod37.z.string().nullable().optional(),
8022
- tema: import_zod37.z.string().nullable().optional(),
8023
- productoId: import_zod37.z.string().nullable().optional(),
8024
- estado: import_zod37.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
8025
- contenidoRef: import_zod37.z.string().nullable().optional(),
8026
- fotoIdAsignada: import_zod37.z.string().nullable().optional(),
8027
- notas: import_zod37.z.array(NotaCalendarioSchema).optional(),
8028
- locationId: import_zod37.z.string().nullable().optional().describe("GBP location ID \u2014 a qu\xE9 sucursal publicar el post"),
8029
- locationNombre: import_zod37.z.string().nullable().optional().describe("Nombre de la sucursal GBP (para UI)")
8168
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
8169
+ mes: import_zod38.z.string().describe("Mes del calendario (YYYY-MM)"),
8170
+ semana: import_zod38.z.number().describe("Numero de semana (1-5)"),
8171
+ slotIndex: import_zod38.z.number().describe("Indice del slot (0-based). Si >= items.length, agrega nuevo slot al final."),
8172
+ cambios: import_zod38.z.object({
8173
+ dia: import_zod38.z.string().nullable().optional(),
8174
+ plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional(),
8175
+ tipo: import_zod38.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional(),
8176
+ keyword: import_zod38.z.string().nullable().optional(),
8177
+ tema: import_zod38.z.string().nullable().optional(),
8178
+ productoId: import_zod38.z.string().nullable().optional(),
8179
+ estado: import_zod38.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
8180
+ contenidoRef: import_zod38.z.string().nullable().optional(),
8181
+ fotoIdAsignada: import_zod38.z.string().nullable().optional(),
8182
+ notas: import_zod38.z.array(NotaCalendarioSchema).optional(),
8183
+ locationId: import_zod38.z.string().nullable().optional().describe("GBP location ID \u2014 a qu\xE9 sucursal publicar el post"),
8184
+ locationNombre: import_zod38.z.string().nullable().optional().describe("Nombre de la sucursal GBP (para UI)")
8030
8185
  }).strict().describe("Campos del slot. Solo acepta campos validos del schema. Campos no reconocidos seran rechazados."),
8031
- accionContenidoExistente: import_zod37.z.union([
8032
- import_zod37.z.enum(["descartar", "nuevo_slot", "mantener"]),
8033
- import_zod37.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Formato: mover:semana:N:slot:M")
8186
+ accionContenidoExistente: import_zod38.z.union([
8187
+ import_zod38.z.enum(["descartar", "nuevo_slot", "mantener"]),
8188
+ import_zod38.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Formato: mover:semana:N:slot:M")
8034
8189
  ]).optional().describe(
8035
8190
  "Decision del tenant cuando el slot ya tiene un contenidoRef existente Y los cambios tocan keyword/tema/plataforma/tipo. Requerido en ese escenario (si no se pasa, el helper retorna ACCION_CONTENIDO_EXISTENTE_REQUIRED con las 4 opciones para que preguntes al tenant). Valores: 'descartar' (marca el contenido existente como descartado y aplica cambios al slot) | 'mover:semana:N:slot:M' (mueve el contenido existente al slot destino vacio y aplica cambios al slot origen) | 'nuevo_slot' (NO toca este slot ni su contenido; crea un slot nuevo el mismo dia con los cambios \u2014 util para multiples publicaciones/dia) | 'mantener' (conserva el contenido existente aqui y aplica los cambios igualmente \u2014 caso typo fix)."
8036
8191
  )
@@ -8046,7 +8201,14 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
8046
8201
  input: { tenantId, brandId, mes, semana, slotIndex, cambios, accionContenidoExistente },
8047
8202
  ctx
8048
8203
  });
8049
- const payload = result.state === "success" ? result.structuredOutput : { ok: false, state: result.state, mensaje: result.text };
8204
+ const payload = result.state === "success" ? result.structuredOutput : {
8205
+ ok: false,
8206
+ state: result.state,
8207
+ mensaje: result.text,
8208
+ // HITO 6 A6.7: incluir detalles Zod estructurados para que
8209
+ // Claude (LLM) pueda auto-recuperarse en el siguiente intento.
8210
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
8211
+ };
8050
8212
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
8051
8213
  }
8052
8214
  );
@@ -8060,9 +8222,9 @@ ESCRIBE EN DOS LUGARES:
8060
8222
 
8061
8223
  NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
8062
8224
  {
8063
- contenidoRef: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
8064
- fotoId: import_zod37.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
8065
- calendarioItemRef: import_zod37.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
8225
+ contenidoRef: import_zod38.z.string().describe("ID del doc en marketing_contenido"),
8226
+ fotoId: import_zod38.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
8227
+ calendarioItemRef: import_zod38.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
8066
8228
  },
8067
8229
  async ({ contenidoRef, fotoId, calendarioItemRef }) => {
8068
8230
  const tenantId = session.requireTenant();
@@ -8075,7 +8237,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8075
8237
  "approve_content",
8076
8238
  "Aprueba contenido para publicacion. Valida transicion de estado.",
8077
8239
  {
8078
- contenidoId: import_zod37.z.string().describe("ID del contenido a aprobar")
8240
+ contenidoId: import_zod38.z.string().describe("ID del contenido a aprobar")
8079
8241
  },
8080
8242
  async ({ contenidoId }) => {
8081
8243
  session.requireTenant();
@@ -8099,8 +8261,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8099
8261
  "reject_content",
8100
8262
  "Rechaza contenido con motivo. Valida transicion de estado.",
8101
8263
  {
8102
- contenidoId: import_zod37.z.string().describe("ID del contenido a rechazar"),
8103
- motivo: import_zod37.z.string().describe("Motivo del rechazo")
8264
+ contenidoId: import_zod38.z.string().describe("ID del contenido a rechazar"),
8265
+ motivo: import_zod38.z.string().describe("Motivo del rechazo")
8104
8266
  },
8105
8267
  async ({ contenidoId, motivo }) => {
8106
8268
  session.requireTenant();
@@ -8123,7 +8285,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8123
8285
  "get_collections",
8124
8286
  "Lee todas las colecciones canonicas (Shopify/WordPress/webonly) con sus 6 campos SEO actuales. Incluye best practices 2026.",
8125
8287
  {
8126
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
8288
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
8127
8289
  },
8128
8290
  async ({ brandId: inputBrandId }) => {
8129
8291
  const tenantId = session.requireTenant();
@@ -8210,7 +8372,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8210
8372
  "save_collection_suggestions",
8211
8373
  "Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
8212
8374
  {
8213
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
8375
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
8214
8376
  suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
8215
8377
  },
8216
8378
  async ({ brandId: inputBrandId, suggestions }) => {