ponch-mcp-server 1.0.75 → 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
  };
@@ -6578,63 +6642,63 @@ function wrapWithContract(contract, helper, options = {}) {
6578
6642
  };
6579
6643
  };
6580
6644
  }
6581
- var RecordarMemoriaParamsSchema = import_zod33.z.object({
6645
+ var RecordarMemoriaParamsSchema = import_zod34.z.object({
6582
6646
  tipo: TipoMemoriaEnum,
6583
6647
  categoria: CategoriaMemoriaEnum,
6584
- contenido: import_zod33.z.string().min(3).max(500)
6648
+ contenido: import_zod34.z.string().min(3).max(500)
6585
6649
  });
6586
- var RecordarMemoriaOutputSchema = import_zod33.z.object({
6587
- memoriaId: import_zod33.z.string(),
6588
- 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")
6589
6653
  });
6590
- var OlvidarMemoriaParamsSchema = import_zod33.z.object({
6591
- memoriaId: import_zod33.z.string(),
6592
- 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()
6593
6657
  });
6594
- var OlvidarMemoriaOutputSchema = import_zod33.z.object({
6595
- status: import_zod33.z.literal("archivada")
6658
+ var OlvidarMemoriaOutputSchema = import_zod34.z.object({
6659
+ status: import_zod34.z.literal("archivada")
6596
6660
  });
6597
- var ConfigInputSchema = import_zod34.z.object({
6598
- diaSemana: import_zod34.z.number().int().min(0).max(6).nullable(),
6599
- diaMes: import_zod34.z.number().int().min(1).max(31).nullable(),
6600
- 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}$/),
6601
6665
  // Timezone NO viene en input — hereda de tenants/{tid}.zonaHoraria (audit fix 2).
6602
- fechaPuntual: import_zod34.z.string().datetime({ offset: true }).nullable()
6666
+ fechaPuntual: import_zod35.z.string().datetime({ offset: true }).nullable()
6603
6667
  }).strict();
6604
- var AccionInputSchema = import_zod34.z.object({
6605
- tool: import_zod34.z.string().min(1),
6606
- 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())
6607
6671
  }).strict();
6608
- var ProgramarRutinaParamsSchema = import_zod34.z.object({
6672
+ var ProgramarRutinaParamsSchema = import_zod35.z.object({
6609
6673
  tipo: TipoRutinaEnum,
6610
6674
  frecuencia: FrecuenciaRutinaEnum,
6611
6675
  config: ConfigInputSchema,
6612
6676
  accion: AccionInputSchema,
6613
- uidDestinatario: import_zod34.z.string()
6677
+ uidDestinatario: import_zod35.z.string()
6614
6678
  });
6615
- var ProgramarRutinaOutputSchema = import_zod34.z.object({
6616
- rutinaId: import_zod34.z.string(),
6617
- 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()
6618
6682
  });
6619
- var PausarRutinaParamsSchema = import_zod34.z.object({
6620
- rutinaId: import_zod34.z.string(),
6621
- 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()
6622
6686
  });
6623
- var PausarRutinaOutputSchema = import_zod34.z.object({
6624
- status: import_zod34.z.literal("pausada")
6687
+ var PausarRutinaOutputSchema = import_zod35.z.object({
6688
+ status: import_zod35.z.literal("pausada")
6625
6689
  });
6626
- var ArchivarRutinaParamsSchema = import_zod34.z.object({
6627
- rutinaId: import_zod34.z.string(),
6628
- 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()
6629
6693
  });
6630
- var ArchivarRutinaOutputSchema = import_zod34.z.object({
6631
- status: import_zod34.z.literal("archivada")
6694
+ var ArchivarRutinaOutputSchema = import_zod35.z.object({
6695
+ status: import_zod35.z.literal("archivada")
6632
6696
  });
6633
- var ListarRutinasParamsSchema = import_zod34.z.object({
6634
- uid: import_zod34.z.string().optional()
6697
+ var ListarRutinasParamsSchema = import_zod35.z.object({
6698
+ uid: import_zod35.z.string().optional()
6635
6699
  });
6636
- var ListarRutinasOutputSchema = import_zod34.z.object({
6637
- rutinas: import_zod34.z.array(MartinRutinaSchema)
6700
+ var ListarRutinasOutputSchema = import_zod35.z.object({
6701
+ rutinas: import_zod35.z.array(MartinRutinaSchema)
6638
6702
  });
6639
6703
 
6640
6704
  // src/tools/martinContext.ts
@@ -6711,6 +6775,9 @@ async function callCF(name, input) {
6711
6775
  function callGetCalendar(input) {
6712
6776
  return callCF("marketingGetCalendarCallable", input);
6713
6777
  }
6778
+ function callAddCalendarSlot(input) {
6779
+ return callCF("marketingAddCalendarSlotCallable", input);
6780
+ }
6714
6781
  function callBrandBriefWriter(input) {
6715
6782
  return callCF("marketingBrandBriefWriterCallable", input);
6716
6783
  }
@@ -6761,7 +6828,7 @@ function callPhotoDirectorExecute(input) {
6761
6828
  }
6762
6829
 
6763
6830
  // src/tools/marketing/photos.ts
6764
- var import_zod36 = require("zod");
6831
+ var import_zod37 = require("zod");
6765
6832
 
6766
6833
  // src/services/marketingEmbeddings.ts
6767
6834
  var import_google_auth_library = require("google-auth-library");
@@ -7029,7 +7096,7 @@ async function findNearestInCollectionWithOverride(params) {
7029
7096
  }
7030
7097
 
7031
7098
  // src/tools/marketing/content.ts
7032
- var import_zod35 = require("zod");
7099
+ var import_zod36 = require("zod");
7033
7100
  var _logOverride = null;
7034
7101
  async function logToMcpLogs(entry) {
7035
7102
  if (_logOverride) return _logOverride(entry);
@@ -7042,22 +7109,22 @@ async function logToMcpLogs(entry) {
7042
7109
  } catch {
7043
7110
  }
7044
7111
  }
7045
- var IncludeSchema = import_zod35.z.object({
7046
- products: import_zod35.z.boolean().default(true),
7047
- collections: import_zod35.z.boolean().default(true),
7048
- articles: import_zod35.z.boolean().default(true),
7049
- 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)
7050
7117
  }).default({
7051
7118
  products: true,
7052
7119
  collections: true,
7053
7120
  articles: true,
7054
7121
  pages: false
7055
7122
  });
7056
- var LimitSchema = import_zod35.z.object({
7057
- products: import_zod35.z.number().int().min(0).max(20).default(5),
7058
- collections: import_zod35.z.number().int().min(0).max(10).default(3),
7059
- articles: import_zod35.z.number().int().min(0).max(20).default(5),
7060
- 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)
7061
7128
  }).default({ products: 5, collections: 3, articles: 5, pages: 2 });
7062
7129
  async function findContentForTopicHandler(input, session) {
7063
7130
  const tenantId = session.requireTenant();
@@ -7143,13 +7210,13 @@ MODOS (parametro mode):
7143
7210
 
7144
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.`,
7145
7212
  {
7146
- brandId: import_zod35.z.string().optional().describe("ID de la brand"),
7147
- contexto: import_zod35.z.string().min(1).describe("Parrafo, keyword o intencion"),
7148
- 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"),
7149
7216
  include: IncludeSchema.optional(),
7150
7217
  limit: LimitSchema.optional(),
7151
- diversidad: import_zod35.z.boolean().default(true),
7152
- 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)")
7153
7220
  },
7154
7221
  async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode: mode2 }) => {
7155
7222
  const brandId = inputBrandId ?? session.requireBrand();
@@ -7253,11 +7320,11 @@ REGLAS:
7253
7320
 
7254
7321
  USAR: antes de generar contenido de cualquier slot del calendario.`,
7255
7322
  {
7256
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7257
- keyword: import_zod36.z.string().describe("Keyword del slot"),
7258
- plataforma: import_zod36.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
7259
- fecha: import_zod36.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
7260
- 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)
7261
7328
  },
7262
7329
  async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
7263
7330
  const tenantId = session.requireTenant();
@@ -7298,7 +7365,7 @@ DESPUES de ver la foto, decide:
7298
7365
 
7299
7366
  Luego llama execute_photo_edit con tu analisis y prompt.`,
7300
7367
  {
7301
- fotoId: import_zod36.z.string().describe("ID de la foto")
7368
+ fotoId: import_zod37.z.string().describe("ID de la foto")
7302
7369
  },
7303
7370
  async ({ fotoId }) => {
7304
7371
  const tenantId = session.requireTenant();
@@ -7331,14 +7398,14 @@ Retorna la foto editada para que la revises. Si no te gusta:
7331
7398
 
7332
7399
  Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
7333
7400
  {
7334
- fotoId: import_zod36.z.string(),
7335
- prompt: import_zod36.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
7336
- acciones: import_zod36.z.array(import_zod36.z.enum(["edit_background", "none"])),
7337
- descripcion: import_zod36.z.string().describe("Descripcion semantica en espanol"),
7338
- tipo: import_zod36.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
7339
- tagsPrimarios: import_zod36.z.array(import_zod36.z.string()),
7340
- tagsSecundarios: import_zod36.z.array(import_zod36.z.string()),
7341
- 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())
7342
7409
  },
7343
7410
  async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
7344
7411
  const tenantId = session.requireTenant();
@@ -7415,7 +7482,7 @@ Retorna:
7415
7482
  - creditos: { balance, planId, periodEnd, costoPhotoEdit }
7416
7483
  - _instrucciones: que hacer segun el estado`,
7417
7484
  {
7418
- fotoId: import_zod36.z.string().describe("ID de la foto")
7485
+ fotoId: import_zod37.z.string().describe("ID de la foto")
7419
7486
  },
7420
7487
  async ({ fotoId }) => {
7421
7488
  const tenantId = session.requireTenant();
@@ -7515,11 +7582,11 @@ Retorna:
7515
7582
  "find_products_for_content",
7516
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.`,
7517
7584
  {
7518
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7519
- contexto: import_zod36.z.string().describe("Parrafo, keyword o intencion del contenido"),
7520
- fecha: import_zod36.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
7521
- limit: import_zod36.z.number().int().min(1).max(10).default(5),
7522
- 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)
7523
7590
  },
7524
7591
  async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
7525
7592
  console.warn(
@@ -7572,10 +7639,10 @@ RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no
7572
7639
 
7573
7640
  USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
7574
7641
  {
7575
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7576
- plataforma: import_zod36.z.string().describe("gbp | instagram | shopify_blog"),
7577
- tipoContenido: import_zod36.z.string().describe("post | carousel | story | blog"),
7578
- 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")
7579
7646
  },
7580
7647
  async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
7581
7648
  const tenantId = session.requireTenant();
@@ -7605,15 +7672,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
7605
7672
 
7606
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.`,
7607
7674
  {
7608
- brandId: import_zod36.z.string().optional().describe("ID de la brand"),
7609
- 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"),
7610
- necesidades: import_zod36.z.array(
7611
- import_zod36.z.object({
7612
- tema: import_zod36.z.string(),
7613
- keyword: import_zod36.z.string(),
7614
- cantidadSugerida: import_zod36.z.number().int().positive(),
7615
- razon: import_zod36.z.string(),
7616
- 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")')
7617
7684
  })
7618
7685
  ).min(1)
7619
7686
  },
@@ -7759,8 +7826,8 @@ function registerMarketingTools(server, session) {
7759
7826
  "get_calendar",
7760
7827
  "Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
7761
7828
  {
7762
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7763
- 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)")
7764
7831
  },
7765
7832
  async ({ brandId: inputBrandId, mes }) => {
7766
7833
  const tenantId = session.requireTenant();
@@ -7793,7 +7860,7 @@ function registerMarketingTools(server, session) {
7793
7860
  "get_seo_snapshot",
7794
7861
  "Lee el snapshot SEO de la brand: rank, keywords, oportunidades, competidores.",
7795
7862
  {
7796
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
7863
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
7797
7864
  },
7798
7865
  async ({ brandId: inputBrandId }) => {
7799
7866
  const tenantId = session.requireTenant();
@@ -7812,8 +7879,8 @@ function registerMarketingTools(server, session) {
7812
7879
  "get_photo_gallery",
7813
7880
  "Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
7814
7881
  {
7815
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7816
- 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)")
7817
7884
  },
7818
7885
  async ({ brandId: inputBrandId, estado }) => {
7819
7886
  session.requireTenant();
@@ -7853,7 +7920,7 @@ function registerMarketingTools(server, session) {
7853
7920
  "generate_marketing_plan",
7854
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.",
7855
7922
  {
7856
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
7923
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
7857
7924
  },
7858
7925
  async ({ brandId: inputBrandId }) => {
7859
7926
  const tenantId = session.requireTenant();
@@ -7867,8 +7934,8 @@ function registerMarketingTools(server, session) {
7867
7934
  "save_marketing_plan",
7868
7935
  "Guarda el plan de marketing generado por Claude en la configuracion de la brand. Escribe en tenants/{tenantId}/marketing_config/{brandId}.plan.",
7869
7936
  {
7870
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7871
- 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.)")
7872
7939
  },
7873
7940
  async ({ brandId: inputBrandId, plan }) => {
7874
7941
  const tenantId = session.requireTenant();
@@ -7896,9 +7963,9 @@ function registerMarketingTools(server, session) {
7896
7963
  "update_marketing_plan_field",
7897
7964
  "Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
7898
7965
  {
7899
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7900
- field: import_zod37.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
7901
- 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")
7902
7969
  },
7903
7970
  async ({ brandId: inputBrandId, field, value }) => {
7904
7971
  const tenantId = session.requireTenant();
@@ -7911,7 +7978,7 @@ function registerMarketingTools(server, session) {
7911
7978
  "generate_brand_brief",
7912
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.",
7913
7980
  {
7914
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
7981
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
7915
7982
  },
7916
7983
  async ({ brandId: inputBrandId }) => {
7917
7984
  const tenantId = session.requireTenant();
@@ -7939,8 +8006,8 @@ function registerMarketingTools(server, session) {
7939
8006
  "save_brand_brief",
7940
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).",
7941
8008
  {
7942
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7943
- 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")
7944
8011
  },
7945
8012
  async ({ brandId: inputBrandId, brandBrief }) => {
7946
8013
  const tenantId = session.requireTenant();
@@ -7953,9 +8020,9 @@ function registerMarketingTools(server, session) {
7953
8020
  "generate_weekly_content",
7954
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.",
7955
8022
  {
7956
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7957
- semana: import_zod37.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
7958
- 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'.")
7959
8026
  },
7960
8027
  async ({ brandId: inputBrandId, semana, modo }) => {
7961
8028
  const tenantId = session.requireTenant();
@@ -7971,14 +8038,14 @@ function registerMarketingTools(server, session) {
7971
8038
 
7972
8039
  IMPORTANTE: Si seleccionaste una foto con get_photos_for_slot, SIEMPRE pasa fotoId aqu\xED. Sin fotoId el post se publica sin imagen.`,
7973
8040
  {
7974
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
7975
- plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
7976
- tipo: import_zod37.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
7977
- keyword: import_zod37.z.string().optional().describe("Keyword target"),
7978
- languageCode: import_zod37.z.string().optional().describe("Idioma (es/en)"),
7979
- fotoId: import_zod37.z.string().optional().describe("ID de la foto a asociar"),
7980
- datos: import_zod37.z.record(import_zod37.z.string(), import_zod37.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
7981
- 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")
7982
8049
  },
7983
8050
  async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
7984
8051
  const tenantId = session.requireTenant();
@@ -8023,13 +8090,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
8023
8090
  NO puede cambiar: tenantId, brandId, id (inmutables).
8024
8091
  Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
8025
8092
  {
8026
- contenidoId: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
8027
- 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: "..." }'),
8028
- fotoId: import_zod37.z.string().nullable().optional().describe("Actualizar foto asociada"),
8029
- keyword: import_zod37.z.string().nullable().optional().describe("Actualizar keyword"),
8030
- languageCode: import_zod37.z.string().optional().describe("Actualizar idioma"),
8031
- estado: import_zod37.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
8032
- 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")
8033
8100
  },
8034
8101
  async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
8035
8102
  const tenantId = session.requireTenant();
@@ -8056,31 +8123,69 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
8056
8123
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
8057
8124
  }
8058
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
+ );
8059
8164
  server.tool(
8060
8165
  "update_calendar_slot",
8061
- "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.",
8062
8167
  {
8063
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
8064
- mes: import_zod37.z.string().describe("Mes del calendario (YYYY-MM)"),
8065
- semana: import_zod37.z.number().describe("Numero de semana (1-5)"),
8066
- slotIndex: import_zod37.z.number().describe("Indice del slot (0-based). Si >= items.length, agrega nuevo slot al final."),
8067
- cambios: import_zod37.z.object({
8068
- dia: import_zod37.z.string().nullable().optional(),
8069
- plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional(),
8070
- tipo: import_zod37.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional(),
8071
- keyword: import_zod37.z.string().nullable().optional(),
8072
- tema: import_zod37.z.string().nullable().optional(),
8073
- productoId: import_zod37.z.string().nullable().optional(),
8074
- estado: import_zod37.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
8075
- contenidoRef: import_zod37.z.string().nullable().optional(),
8076
- fotoIdAsignada: import_zod37.z.string().nullable().optional(),
8077
- notas: import_zod37.z.array(NotaCalendarioSchema).optional(),
8078
- locationId: import_zod37.z.string().nullable().optional().describe("GBP location ID \u2014 a qu\xE9 sucursal publicar el post"),
8079
- 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)")
8080
8185
  }).strict().describe("Campos del slot. Solo acepta campos validos del schema. Campos no reconocidos seran rechazados."),
8081
- accionContenidoExistente: import_zod37.z.union([
8082
- import_zod37.z.enum(["descartar", "nuevo_slot", "mantener"]),
8083
- 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")
8084
8189
  ]).optional().describe(
8085
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)."
8086
8191
  )
@@ -8117,9 +8222,9 @@ ESCRIBE EN DOS LUGARES:
8117
8222
 
8118
8223
  NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
8119
8224
  {
8120
- contenidoRef: import_zod37.z.string().describe("ID del doc en marketing_contenido"),
8121
- fotoId: import_zod37.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
8122
- 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')
8123
8228
  },
8124
8229
  async ({ contenidoRef, fotoId, calendarioItemRef }) => {
8125
8230
  const tenantId = session.requireTenant();
@@ -8132,7 +8237,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8132
8237
  "approve_content",
8133
8238
  "Aprueba contenido para publicacion. Valida transicion de estado.",
8134
8239
  {
8135
- contenidoId: import_zod37.z.string().describe("ID del contenido a aprobar")
8240
+ contenidoId: import_zod38.z.string().describe("ID del contenido a aprobar")
8136
8241
  },
8137
8242
  async ({ contenidoId }) => {
8138
8243
  session.requireTenant();
@@ -8156,8 +8261,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8156
8261
  "reject_content",
8157
8262
  "Rechaza contenido con motivo. Valida transicion de estado.",
8158
8263
  {
8159
- contenidoId: import_zod37.z.string().describe("ID del contenido a rechazar"),
8160
- 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")
8161
8266
  },
8162
8267
  async ({ contenidoId, motivo }) => {
8163
8268
  session.requireTenant();
@@ -8180,7 +8285,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8180
8285
  "get_collections",
8181
8286
  "Lee todas las colecciones canonicas (Shopify/WordPress/webonly) con sus 6 campos SEO actuales. Incluye best practices 2026.",
8182
8287
  {
8183
- brandId: import_zod37.z.string().optional().describe("ID de la brand")
8288
+ brandId: import_zod38.z.string().optional().describe("ID de la brand")
8184
8289
  },
8185
8290
  async ({ brandId: inputBrandId }) => {
8186
8291
  const tenantId = session.requireTenant();
@@ -8267,7 +8372,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
8267
8372
  "save_collection_suggestions",
8268
8373
  "Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
8269
8374
  {
8270
- brandId: import_zod37.z.string().optional().describe("ID de la brand"),
8375
+ brandId: import_zod38.z.string().optional().describe("ID de la brand"),
8271
8376
  suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
8272
8377
  },
8273
8378
  async ({ brandId: inputBrandId, suggestions }) => {