ponch-mcp-server 1.0.86 → 1.0.88

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
@@ -170,6 +170,34 @@ var Session = class {
170
170
  this.context.tenantId = tenantId;
171
171
  this.context.brandId = brandId;
172
172
  }
173
+ /**
174
+ * Cambia la brand activa SIN cambiar tenant. Disponible para cualquier
175
+ * user con multi-brand (agency mode). DEUDA 3 sesión 211.1 H/I — verifica
176
+ * que brandId esté en la lista de brands accesibles del user (cargada
177
+ * en connect_account).
178
+ *
179
+ * Lanza si:
180
+ * - El user no tiene multi-brand (brands.length <= 1).
181
+ * - El brandId solicitado NO está en la lista del user (defensa
182
+ * extra contra cross-brand attack — el helper también valida que
183
+ * exista en Firestore antes de invocar esto).
184
+ */
185
+ setBrand(brandId) {
186
+ const brands = this._authContext.brands ?? [];
187
+ if (brands.length <= 1) {
188
+ throw new Error(
189
+ `select_brand requires multi-brand access. This user has access to ${brands.length} brand(s).`
190
+ );
191
+ }
192
+ const allowed = brands.some((b) => b.id === brandId);
193
+ if (!allowed) {
194
+ throw new Error(
195
+ `Brand "${brandId}" is not in the user's accessible brands list [${brands.map((b) => b.id).join(", ")}].`
196
+ );
197
+ }
198
+ this.context.brandId = brandId;
199
+ this._authContext.brandId = brandId;
200
+ }
173
201
  /**
174
202
  * Revoca canSwitchTenant en runtime. Usado cuando el bootstrap detecta
175
203
  * token expirado — el usuario debe re-autenticar via connect_account
@@ -433,7 +461,7 @@ function serverTimestamp() {
433
461
  }
434
462
 
435
463
  // src/tools/context.ts
436
- var import_zod60 = require("zod");
464
+ var import_zod63 = require("zod");
437
465
  var import_functions2 = require("firebase/functions");
438
466
  var import_app3 = require("firebase/app");
439
467
 
@@ -1497,7 +1525,7 @@ var ORIGENES_CONTENIDO = [
1497
1525
  "imported"
1498
1526
  // sync Shopify/WordPress
1499
1527
  ];
1500
- var ESTADO_CONTENIDO = {
1528
+ var ESTADO_CONTENIDO2 = {
1501
1529
  BORRADOR: "borrador",
1502
1530
  GENERADO: "generado",
1503
1531
  PENDIENTE_APROBACION: "pendiente_aprobacion",
@@ -1632,12 +1660,25 @@ var ContenidoSchema = import_zod12.z.object({
1632
1660
  mediaVariante: MediaVarianteSchema.nullable().optional(),
1633
1661
  creadoAt: import_zod12.z.unknown().nullable().optional(),
1634
1662
  creadoPorId: import_zod12.z.string().nullable().optional(),
1663
+ // §9.2 PLAN_C2 — actor canónico de la creación. Auto-poblado desde
1664
+ // ctx.user.{nombre,clientType,clientMetadata} en server.tool.
1665
+ creadoPorNombre: import_zod12.z.string().nullable().optional(),
1666
+ creadoPorClient: import_zod12.z.string().nullable().optional(),
1667
+ creadoPorClientMetadata: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).nullable().optional(),
1635
1668
  origen: import_zod12.z.enum(ORIGENES_CONTENIDO).nullable().optional(),
1636
1669
  aprobadoAt: import_zod12.z.unknown().nullable().optional(),
1637
1670
  aprobadoPorId: import_zod12.z.string().nullable().optional(),
1638
1671
  aprobadoPorNombre: import_zod12.z.string().nullable().optional(),
1672
+ // §9.2 — extendido en Sub-A2.1; agregado al schema canónico aquí.
1673
+ aprobadoPorClient: import_zod12.z.string().nullable().optional(),
1674
+ aprobadoPorClientMetadata: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).nullable().optional(),
1639
1675
  rechazadoAt: import_zod12.z.unknown().nullable().optional(),
1640
1676
  rechazadoMotivo: import_zod12.z.string().nullable().optional(),
1677
+ // §9.2 — extendido en Sub-A2.2; agregado al schema canónico aquí.
1678
+ rechazadoPorId: import_zod12.z.string().nullable().optional(),
1679
+ rechazadoPorNombre: import_zod12.z.string().nullable().optional(),
1680
+ rechazadoPorClient: import_zod12.z.string().nullable().optional(),
1681
+ rechazadoPorClientMetadata: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).nullable().optional(),
1641
1682
  editadoAt: import_zod12.z.unknown().nullable().optional(),
1642
1683
  publicadoAt: import_zod12.z.unknown().nullable().optional(),
1643
1684
  errorPublicacion: import_zod12.z.string().nullable().optional(),
@@ -1665,6 +1706,9 @@ function buildContenido(input) {
1665
1706
  mediaVariante: input.mediaVariante ?? null,
1666
1707
  creadoAt: input.creadoAt ?? null,
1667
1708
  creadoPorId: input.creadoPorId ?? null,
1709
+ creadoPorNombre: input.creadoPorNombre ?? null,
1710
+ creadoPorClient: input.creadoPorClient ?? null,
1711
+ creadoPorClientMetadata: input.creadoPorClientMetadata ?? null,
1668
1712
  origen: input.origen ?? null
1669
1713
  });
1670
1714
  }
@@ -1679,7 +1723,7 @@ var ESTADOS_FOTO = [
1679
1723
  var ESTRATEGIAS_EDICION = ["gemini_edit", "tal_cual"];
1680
1724
  var FotoEstadoEnum = import_zod13.z.enum(ESTADOS_FOTO);
1681
1725
  var FotoEstrategiaEdicionEnum = import_zod13.z.enum(ESTRATEGIAS_EDICION);
1682
- var ESTADO_FOTO = {
1726
+ var ESTADO_FOTO2 = {
1683
1727
  NUEVA: "nueva",
1684
1728
  PROCESANDO: "procesando",
1685
1729
  EDITADA: "editada",
@@ -3510,17 +3554,17 @@ var PolyDateFormatter = class {
3510
3554
  constructor(dt, intl, opts) {
3511
3555
  this.opts = opts;
3512
3556
  this.originalZone = void 0;
3513
- let z36 = void 0;
3557
+ let z39 = void 0;
3514
3558
  if (this.opts.timeZone) {
3515
3559
  this.dt = dt;
3516
3560
  } else if (dt.zone.type === "fixed") {
3517
3561
  const gmtOffset = -1 * (dt.offset / 60);
3518
3562
  const offsetZ = gmtOffset >= 0 ? `Etc/GMT+${gmtOffset}` : `Etc/GMT${gmtOffset}`;
3519
3563
  if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) {
3520
- z36 = offsetZ;
3564
+ z39 = offsetZ;
3521
3565
  this.dt = dt;
3522
3566
  } else {
3523
- z36 = "UTC";
3567
+ z39 = "UTC";
3524
3568
  this.dt = dt.offset === 0 ? dt : dt.setZone("UTC").plus({ minutes: dt.offset });
3525
3569
  this.originalZone = dt.zone;
3526
3570
  }
@@ -3528,14 +3572,14 @@ var PolyDateFormatter = class {
3528
3572
  this.dt = dt;
3529
3573
  } else if (dt.zone.type === "iana") {
3530
3574
  this.dt = dt;
3531
- z36 = dt.zone.name;
3575
+ z39 = dt.zone.name;
3532
3576
  } else {
3533
- z36 = "UTC";
3577
+ z39 = "UTC";
3534
3578
  this.dt = dt.setZone("UTC").plus({ minutes: dt.offset });
3535
3579
  this.originalZone = dt.zone;
3536
3580
  }
3537
3581
  const intlOpts = { ...this.opts };
3538
- intlOpts.timeZone = intlOpts.timeZone || z36;
3582
+ intlOpts.timeZone = intlOpts.timeZone || z39;
3539
3583
  this.dtf = getCachedDTF(intl, intlOpts);
3540
3584
  }
3541
3585
  format() {
@@ -10837,16 +10881,21 @@ var import_zod49 = require("zod");
10837
10881
  var import_zod50 = require("zod");
10838
10882
  var import_firebase_admin11 = require("firebase-admin");
10839
10883
  var import_zod51 = require("zod");
10884
+ var import_firebase_admin12 = require("firebase-admin");
10840
10885
  var import_zod52 = require("zod");
10841
10886
  var import_zod53 = require("zod");
10842
- var import_firebase_admin12 = require("firebase-admin");
10843
- var import_zod54 = require("zod");
10844
10887
  var import_firebase_admin13 = require("firebase-admin");
10888
+ var import_zod54 = require("zod");
10845
10889
  var import_zod55 = require("zod");
10846
10890
  var import_zod56 = require("zod");
10891
+ var import_firebase_admin14 = require("firebase-admin");
10847
10892
  var import_zod57 = require("zod");
10893
+ var import_firebase_admin15 = require("firebase-admin");
10848
10894
  var import_zod58 = require("zod");
10849
10895
  var import_zod59 = require("zod");
10896
+ var import_zod60 = require("zod");
10897
+ var import_zod61 = require("zod");
10898
+ var import_zod62 = require("zod");
10850
10899
  var import_firestore4 = require("firebase-admin/firestore");
10851
10900
  var RULE_NEGATIVES = {
10852
10901
  allowFaces: "no people, no faces, no hands",
@@ -11088,7 +11137,7 @@ var brandBriefWriterContract = MartinContractSchema.parse(
11088
11137
  rawContract
11089
11138
  );
11090
11139
  async function save(input) {
11091
- const { db, tenantId, brandId, plan } = input;
11140
+ const { db, tenantId, brandId, plan, actor } = input;
11092
11141
  const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
11093
11142
  const brandSnap = await brandRef.get();
11094
11143
  if (!brandSnap.exists) {
@@ -11098,7 +11147,11 @@ async function save(input) {
11098
11147
  const planWithMeta = {
11099
11148
  ...planSinBlogStrategy,
11100
11149
  fechaCreacion: (/* @__PURE__ */ new Date()).toISOString(),
11101
- creadoPorId: "mcp-cowork"
11150
+ // §9.2 PLAN_C2 — actor real desde ctx.user (NO 'mcp-cowork' hardcoded).
11151
+ creadoPorId: actor.uid,
11152
+ creadoPorNombre: actor.nombre,
11153
+ creadoPorClient: actor.clientType,
11154
+ creadoPorClientMetadata: actor.clientMetadata ?? null
11102
11155
  };
11103
11156
  await brandRef.set({
11104
11157
  plan: planWithMeta,
@@ -11177,7 +11230,13 @@ var ParamsSchema2 = import_zod36.z.object({
11177
11230
  brandId: import_zod36.z.string().min(1).describe("Brand identifier within the tenant."),
11178
11231
  plan: import_zod36.z.record(import_zod36.z.string(), import_zod36.z.unknown()).describe(
11179
11232
  "Full marketing plan object generated by the LLM. May include a blogStrategy field \u2014 if present, it is extracted and stored at brand level (not nested inside plan)."
11180
- )
11233
+ ),
11234
+ actor: import_zod36.z.object({
11235
+ uid: import_zod36.z.string().min(1).describe('User id who is saving the plan. Example: "your-user-uid".'),
11236
+ nombre: import_zod36.z.string().min(1).describe('Human-readable user name. Example: "Daniel Gonz\xE1lez".'),
11237
+ clientType: import_zod36.z.string().min(1).describe('Client surface origin. Example: one of "mcp_client" | "martin" | "web" | "admin".'),
11238
+ clientMetadata: import_zod36.z.record(import_zod36.z.string(), import_zod36.z.unknown()).nullable().optional().describe("Optional metadata about the client.")
11239
+ }).describe("Actor (user) saving the plan \u2014 extracted by the server.tool from ctx.user (\xA79.2).")
11181
11240
  });
11182
11241
  var OutputSchema2 = import_zod36.z.discriminatedUnion("ok", [
11183
11242
  import_zod36.z.object({
@@ -11810,7 +11869,7 @@ async function calendarSlotUpdater(input) {
11810
11869
  const contenidoQuery = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("calendarioItemRef", "==", oldContenidoRef).limit(10).get();
11811
11870
  contenidosADescartar = contenidoQuery.docs.filter((c) => {
11812
11871
  const data = c.data();
11813
- return data.estado !== "descartado" && data.estado !== ESTADO_CONTENIDO.PUBLICADO;
11872
+ return data.estado !== "descartado" && data.estado !== ESTADO_CONTENIDO2.PUBLICADO;
11814
11873
  });
11815
11874
  }
11816
11875
  if (accionContenidoExistente === "nuevo_slot") {
@@ -12725,7 +12784,7 @@ async function contenidoApprover(input) {
12725
12784
  }
12726
12785
  const data = snap.data();
12727
12786
  const estadoActual = typeof data.estado === "string" ? data.estado : "";
12728
- if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO.APROBADO)) {
12787
+ if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO2.APROBADO)) {
12729
12788
  return {
12730
12789
  ok: false,
12731
12790
  code: "INVALID_STATE_TRANSITION",
@@ -12733,7 +12792,7 @@ async function contenidoApprover(input) {
12733
12792
  };
12734
12793
  }
12735
12794
  await ref.update({
12736
- estado: ESTADO_CONTENIDO.APROBADO,
12795
+ estado: ESTADO_CONTENIDO2.APROBADO,
12737
12796
  aprobadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp(),
12738
12797
  aprobadoPorId: actor.uid,
12739
12798
  aprobadoPorNombre: actor.nombre,
@@ -12744,7 +12803,7 @@ async function contenidoApprover(input) {
12744
12803
  ok: true,
12745
12804
  contenidoId,
12746
12805
  estadoAnterior: estadoActual,
12747
- nuevoEstado: ESTADO_CONTENIDO.APROBADO
12806
+ nuevoEstado: ESTADO_CONTENIDO2.APROBADO
12748
12807
  };
12749
12808
  }
12750
12809
  var ParamsSchema12 = import_zod47.z.object({
@@ -12814,7 +12873,7 @@ async function contenidoRejecter(input) {
12814
12873
  }
12815
12874
  const data = snap.data();
12816
12875
  const estadoActual = typeof data.estado === "string" ? data.estado : "";
12817
- if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO.RECHAZADO)) {
12876
+ if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO2.RECHAZADO)) {
12818
12877
  return {
12819
12878
  ok: false,
12820
12879
  code: "INVALID_STATE_TRANSITION",
@@ -12822,7 +12881,7 @@ async function contenidoRejecter(input) {
12822
12881
  };
12823
12882
  }
12824
12883
  await ref.update({
12825
- estado: ESTADO_CONTENIDO.RECHAZADO,
12884
+ estado: ESTADO_CONTENIDO2.RECHAZADO,
12826
12885
  rechazadoAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp(),
12827
12886
  rechazadoMotivo: motivo,
12828
12887
  rechazadoPorId: actor.uid,
@@ -12834,7 +12893,7 @@ async function contenidoRejecter(input) {
12834
12893
  ok: true,
12835
12894
  contenidoId,
12836
12895
  estadoAnterior: estadoActual,
12837
- nuevoEstado: ESTADO_CONTENIDO.RECHAZADO,
12896
+ nuevoEstado: ESTADO_CONTENIDO2.RECHAZADO,
12838
12897
  motivo
12839
12898
  };
12840
12899
  }
@@ -12900,7 +12959,7 @@ var contenidoRejecterContract = MartinContractSchema.parse(
12900
12959
  );
12901
12960
  async function photoBriefingWriter(input) {
12902
12961
  const { db, tenantId, brandId, semana, necesidades } = input;
12903
- const docId = `${brandId}_${semana}`;
12962
+ const docId = `${tenantId}_${brandId}_${semana}`;
12904
12963
  const ref = db.collection("tenants").doc(tenantId).collection("marketing_fotobriefings").doc(docId);
12905
12964
  const snap = await ref.get();
12906
12965
  const existing = snap.exists ? snap.data() : null;
@@ -12995,7 +13054,7 @@ var rawContract14 = {
12995
13054
  return `Agregu\xE9 ${output.necesidadesAgregadas} necesidad(es) al briefing semanal de fotos (semana ${input.semana}). Total activas: ${output.totalNecesidades - output.totalCubiertas}/${output.totalNecesidades}.`;
12996
13055
  },
12997
13056
  auditAction: "marketing.foto_briefing.agregar",
12998
- extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_fotobriefings/${input.brandId}_${input.semana}`,
13057
+ extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_fotobriefings/${input.tenantId}_${input.brandId}_${input.semana}`,
12999
13058
  extractChanges: (input, output) => ({
13000
13059
  before: null,
13001
13060
  // Helper no captura snapshot pre-merge.
@@ -13110,6 +13169,255 @@ var rawContract15 = {
13110
13169
  var tenantContextSetterContract = MartinContractSchema.parse(
13111
13170
  rawContract15
13112
13171
  );
13172
+ async function photoDescarter(input) {
13173
+ const { db, tenantId, fotoId, actor } = input;
13174
+ const ref = db.collection("tenants").doc(tenantId).collection("marketing_fotos").doc(fotoId);
13175
+ const snap = await ref.get();
13176
+ if (!snap.exists) {
13177
+ return { ok: false, code: "FOTO_NOT_FOUND" };
13178
+ }
13179
+ const data = snap.data();
13180
+ const estadoActual = typeof data.estado === "string" ? data.estado : "";
13181
+ if (!validarTransicionFoto(estadoActual, ESTADO_FOTO2.DESCARTADA)) {
13182
+ return {
13183
+ ok: false,
13184
+ code: "INVALID_STATE_TRANSITION",
13185
+ estadoActual
13186
+ };
13187
+ }
13188
+ await ref.update({
13189
+ estado: ESTADO_FOTO2.DESCARTADA,
13190
+ descartadaAt: import_firebase_admin11.firestore.FieldValue.serverTimestamp(),
13191
+ descartadaPorId: actor.uid,
13192
+ descartadaPorNombre: actor.nombre,
13193
+ descartadaPorClient: actor.clientType,
13194
+ descartadaPorClientMetadata: actor.clientMetadata ?? null
13195
+ });
13196
+ return {
13197
+ ok: true,
13198
+ fotoId,
13199
+ estadoAnterior: estadoActual,
13200
+ nuevoEstado: ESTADO_FOTO2.DESCARTADA
13201
+ };
13202
+ }
13203
+ var ParamsSchema16 = import_zod51.z.object({
13204
+ tenantId: import_zod51.z.string().min(1).describe('Tenant identifier (the business account). Example: "your-tenant-id".'),
13205
+ fotoId: import_zod51.z.string().min(1).describe('Photo identifier. Example: "your-tenant_your-brand_1234567890_abcdef".'),
13206
+ actor: import_zod51.z.object({
13207
+ uid: import_zod51.z.string().min(1).describe('User id who is discarding. Example: "your-user-uid".'),
13208
+ nombre: import_zod51.z.string().min(1).describe('Human-readable user name. Example: "Daniel Gonz\xE1lez".'),
13209
+ clientType: import_zod51.z.string().min(1).describe('Client surface origin. Example: one of "mcp_client" | "martin" | "web" | "admin".'),
13210
+ clientMetadata: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).nullable().optional().describe("Optional metadata about the client.")
13211
+ }).describe("Actor (user) performing the discard \u2014 extracted by the server.tool from ctx.user.")
13212
+ });
13213
+ var SuccessSchema7 = import_zod51.z.object({
13214
+ ok: import_zod51.z.literal(true),
13215
+ fotoId: import_zod51.z.string(),
13216
+ estadoAnterior: import_zod51.z.string(),
13217
+ nuevoEstado: import_zod51.z.literal("descartada")
13218
+ });
13219
+ var FailureSchema7 = import_zod51.z.object({
13220
+ ok: import_zod51.z.literal(false),
13221
+ code: import_zod51.z.enum(["FOTO_NOT_FOUND", "INVALID_STATE_TRANSITION"]),
13222
+ estadoActual: import_zod51.z.string().optional()
13223
+ });
13224
+ var OutputSchema16 = import_zod51.z.discriminatedUnion("ok", [SuccessSchema7, FailureSchema7]);
13225
+ var rawContract16 = {
13226
+ name: "discard_photo",
13227
+ description: "Mark a marketing photo as discarded. Validates state transition (nueva|editada|error \u2192 descartada). Use when the photo is no longer needed or has issues. Requires confirmation.",
13228
+ paramsSchema: ParamsSchema16,
13229
+ outputSchema: OutputSchema16,
13230
+ requiresConfirmation: true,
13231
+ destructive: false,
13232
+ affectsPublication: false,
13233
+ affectsExternal: false,
13234
+ martinConfirmationTemplate: (input, locale) => {
13235
+ return locale === "en" ? `Discard photo "${input.fotoId}"? The photo will be marked as descartada and won't be used in content.` : `\xBFDescartar la foto "${input.fotoId}"? La foto se marcar\xE1 como descartada y no se usar\xE1 en contenido.`;
13236
+ },
13237
+ martinSummaryTemplate: (input, output, locale) => {
13238
+ if (!output.ok) {
13239
+ if (output.code === "FOTO_NOT_FOUND") {
13240
+ return locale === "en" ? `Photo ${input.fotoId} not found.` : `No encontr\xE9 la foto ${input.fotoId}.`;
13241
+ }
13242
+ return locale === "en" ? `Cannot discard photo from state "${output.estadoActual ?? "unknown"}".` : `No se puede descartar la foto desde el estado "${output.estadoActual ?? "desconocido"}".`;
13243
+ }
13244
+ return locale === "en" ? `Photo ${output.fotoId} discarded (was ${output.estadoAnterior}).` : `Foto ${output.fotoId} descartada (estaba en ${output.estadoAnterior}).`;
13245
+ },
13246
+ auditAction: "marketing.foto.descartar",
13247
+ extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_fotos/${input.fotoId}`,
13248
+ extractChanges: (_input, output) => ({
13249
+ before: output.ok ? { estado: output.estadoAnterior } : null,
13250
+ after: output.ok ? { estado: output.nuevoEstado } : null
13251
+ }),
13252
+ quotasConsumed: [],
13253
+ permissionScope: "module",
13254
+ permissionKey: "marketing",
13255
+ permissionAction: "editar",
13256
+ sideEffects: ["writes_firestore"]
13257
+ };
13258
+ var photoDescarterContract = MartinContractSchema.parse(
13259
+ rawContract16
13260
+ );
13261
+ async function photoEditadaMarker(input) {
13262
+ const { db, tenantId, fotoId, actor } = input;
13263
+ const ref = db.collection("tenants").doc(tenantId).collection("marketing_fotos").doc(fotoId);
13264
+ const snap = await ref.get();
13265
+ if (!snap.exists) {
13266
+ return { ok: false, code: "FOTO_NOT_FOUND" };
13267
+ }
13268
+ const data = snap.data();
13269
+ const estadoActual = typeof data.estado === "string" ? data.estado : "";
13270
+ const archivoOriginal = typeof data.archivoOriginal === "string" ? data.archivoOriginal : null;
13271
+ if (!validarTransicionFoto(estadoActual, ESTADO_FOTO2.EDITADA)) {
13272
+ return {
13273
+ ok: false,
13274
+ code: "INVALID_STATE_TRANSITION",
13275
+ estadoActual
13276
+ };
13277
+ }
13278
+ if (!archivoOriginal) {
13279
+ return { ok: false, code: "FOTO_SIN_ARCHIVO_ORIGINAL", estadoActual };
13280
+ }
13281
+ await ref.update({
13282
+ estado: ESTADO_FOTO2.EDITADA,
13283
+ archivoEditado: archivoOriginal,
13284
+ fechaEdicion: import_firebase_admin12.firestore.FieldValue.serverTimestamp(),
13285
+ marcadaEditadaPorId: actor.uid,
13286
+ marcadaEditadaPorNombre: actor.nombre,
13287
+ marcadaEditadaPorClient: actor.clientType,
13288
+ marcadaEditadaPorClientMetadata: actor.clientMetadata ?? null
13289
+ });
13290
+ return {
13291
+ ok: true,
13292
+ fotoId,
13293
+ estadoAnterior: estadoActual,
13294
+ nuevoEstado: ESTADO_FOTO2.EDITADA,
13295
+ archivoEditado: archivoOriginal
13296
+ };
13297
+ }
13298
+ var ParamsSchema17 = import_zod52.z.object({
13299
+ tenantId: import_zod52.z.string().min(1).describe('Tenant identifier. Example: "your-tenant-id".'),
13300
+ fotoId: import_zod52.z.string().min(1).describe('Photo identifier. Example: "your-tenant_your-brand_1234567890_abcdef".'),
13301
+ actor: import_zod52.z.object({
13302
+ uid: import_zod52.z.string().min(1).describe('User id who marks the photo. Example: "your-user-uid".'),
13303
+ nombre: import_zod52.z.string().min(1).describe('Human-readable user name. Example: "Daniel Gonz\xE1lez".'),
13304
+ clientType: import_zod52.z.string().min(1).describe('Client surface origin. Example: one of "mcp_client" | "martin" | "web" | "admin".'),
13305
+ clientMetadata: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.unknown()).nullable().optional().describe("Optional metadata about the client.")
13306
+ }).describe("Actor (user) marking the photo \u2014 extracted by the server.tool from ctx.user.")
13307
+ });
13308
+ var SuccessSchema8 = import_zod52.z.object({
13309
+ ok: import_zod52.z.literal(true),
13310
+ fotoId: import_zod52.z.string(),
13311
+ estadoAnterior: import_zod52.z.string(),
13312
+ nuevoEstado: import_zod52.z.literal("editada"),
13313
+ archivoEditado: import_zod52.z.string()
13314
+ });
13315
+ var FailureSchema8 = import_zod52.z.object({
13316
+ ok: import_zod52.z.literal(false),
13317
+ code: import_zod52.z.enum(["FOTO_NOT_FOUND", "INVALID_STATE_TRANSITION", "FOTO_SIN_ARCHIVO_ORIGINAL"]),
13318
+ estadoActual: import_zod52.z.string().optional()
13319
+ });
13320
+ var OutputSchema17 = import_zod52.z.discriminatedUnion("ok", [SuccessSchema8, FailureSchema8]);
13321
+ var rawContract17 = {
13322
+ name: "use_photo_as_is",
13323
+ description: "Mark a photo as editada using the original file (no Gemini edit). Validates state transition (procesando \u2192 editada). Use when the original photo is already good enough for publication and no editing is required. archivoEditado is set to the original URL.",
13324
+ paramsSchema: ParamsSchema17,
13325
+ outputSchema: OutputSchema17,
13326
+ requiresConfirmation: false,
13327
+ destructive: false,
13328
+ affectsPublication: false,
13329
+ affectsExternal: false,
13330
+ martinSummaryTemplate: (input, output, locale) => {
13331
+ if (!output.ok) {
13332
+ if (output.code === "FOTO_NOT_FOUND") {
13333
+ return locale === "en" ? `Photo ${input.fotoId} not found.` : `No encontr\xE9 la foto ${input.fotoId}.`;
13334
+ }
13335
+ if (output.code === "FOTO_SIN_ARCHIVO_ORIGINAL") {
13336
+ return locale === "en" ? `Photo ${input.fotoId} has no original file to use.` : `La foto ${input.fotoId} no tiene archivo original que usar.`;
13337
+ }
13338
+ return locale === "en" ? `Cannot mark photo as edited from state "${output.estadoActual ?? "unknown"}".` : `No se puede marcar como editada desde el estado "${output.estadoActual ?? "desconocido"}".`;
13339
+ }
13340
+ return locale === "en" ? `Photo ${output.fotoId} marked as editada using the original (was ${output.estadoAnterior}).` : `Foto ${output.fotoId} marcada como editada usando la original (estaba en ${output.estadoAnterior}).`;
13341
+ },
13342
+ auditAction: "marketing.foto.usar_tal_cual",
13343
+ extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_fotos/${input.fotoId}`,
13344
+ extractChanges: (_input, output) => ({
13345
+ before: output.ok ? { estado: output.estadoAnterior } : null,
13346
+ after: output.ok ? { estado: output.nuevoEstado, archivoEditado: output.archivoEditado } : null
13347
+ }),
13348
+ quotasConsumed: [],
13349
+ permissionScope: "module",
13350
+ permissionKey: "marketing",
13351
+ permissionAction: "editar",
13352
+ sideEffects: ["writes_firestore"]
13353
+ };
13354
+ var photoEditadaMarkerContract = MartinContractSchema.parse(
13355
+ rawContract17
13356
+ );
13357
+ async function brandContextSetter(input) {
13358
+ const { db, tenantId, brandId } = input;
13359
+ const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
13360
+ const brandSnap = await brandRef.get();
13361
+ if (!brandSnap.exists) {
13362
+ return {
13363
+ ok: false,
13364
+ code: "BRAND_NOT_FOUND_IN_TENANT",
13365
+ tenantId,
13366
+ brandId
13367
+ };
13368
+ }
13369
+ const data = brandSnap.data();
13370
+ return {
13371
+ ok: true,
13372
+ tenantId,
13373
+ brandId,
13374
+ brandNombre: typeof data.nombre === "string" ? data.nombre : null,
13375
+ brandDominio: typeof data.dominio === "string" ? data.dominio : null
13376
+ };
13377
+ }
13378
+ var ParamsSchema18 = import_zod53.z.object({
13379
+ tenantId: import_zod53.z.string().min(1).describe(`Tenant identifier (always the user's own tenant). Example: "your-tenant-id".`),
13380
+ brandId: import_zod53.z.string().min(1).describe(`Target brand identifier within the user's tenant. Example: "your-brand-id".`)
13381
+ });
13382
+ var SuccessSchema9 = import_zod53.z.object({
13383
+ ok: import_zod53.z.literal(true),
13384
+ tenantId: import_zod53.z.string(),
13385
+ brandId: import_zod53.z.string(),
13386
+ brandNombre: import_zod53.z.string().nullable(),
13387
+ brandDominio: import_zod53.z.string().nullable()
13388
+ });
13389
+ var FailureSchema9 = import_zod53.z.object({
13390
+ ok: import_zod53.z.literal(false),
13391
+ code: import_zod53.z.literal("BRAND_NOT_FOUND_IN_TENANT"),
13392
+ tenantId: import_zod53.z.string(),
13393
+ brandId: import_zod53.z.string()
13394
+ });
13395
+ var OutputSchema18 = import_zod53.z.discriminatedUnion("ok", [SuccessSchema9, FailureSchema9]);
13396
+ var rawContract18 = {
13397
+ name: "select_brand",
13398
+ description: "Switch the active brand within the user's tenant. Validates that the target brand exists in the tenant. Helper does not mutate Firestore \u2014 caller (MCP server) updates session local state after success. Audit log writes who switched brand (forensics for agency mode). Use when the user has multiple brands and wants to work with a different one without passing brandId on every tool call.",
13399
+ paramsSchema: ParamsSchema18,
13400
+ outputSchema: OutputSchema18,
13401
+ requiresConfirmation: false,
13402
+ destructive: false,
13403
+ affectsPublication: false,
13404
+ affectsExternal: false,
13405
+ martinSummaryTemplate: (input, output, locale) => {
13406
+ if (!output.ok) {
13407
+ return locale === "en" ? `Brand "${input.brandId}" does not exist in your tenant.` : `La brand "${input.brandId}" no existe en tu tenant.`;
13408
+ }
13409
+ return locale === "en" ? `Active brand set to ${output.brandNombre ?? output.brandId}.` : `Brand activa establecida: ${output.brandNombre ?? output.brandId}.`;
13410
+ },
13411
+ auditAction: "self.brand_context.cambiar",
13412
+ quotasConsumed: [],
13413
+ // scope='self' — operación sobre la sesión del propio user. NO requiere
13414
+ // permissionKey/permissionAction (superRefine de MartinContractSchema).
13415
+ permissionScope: "self",
13416
+ sideEffects: ["reads_firestore"]
13417
+ };
13418
+ var brandContextSetterContract = MartinContractSchema.parse(
13419
+ rawContract18
13420
+ );
13113
13421
  var PLATAFORMA_A_FORMATO = {
13114
13422
  gbp: "gbp_4_3",
13115
13423
  shopify_blog: "blog_3_2",
@@ -13136,7 +13444,7 @@ async function photoAssigner(input) {
13136
13444
  return { ok: false, error: `Foto ${fotoId} no encontrada`, code: "PHOTO_NOT_FOUND" };
13137
13445
  }
13138
13446
  const foto = fotoQuery.docs[0].data();
13139
- if (foto.estado !== ESTADO_FOTO.EDITADA && foto.estado !== "usada") {
13447
+ if (foto.estado !== ESTADO_FOTO2.EDITADA && foto.estado !== "usada") {
13140
13448
  return {
13141
13449
  ok: false,
13142
13450
  error: `Foto en estado "${foto.estado}", debe ser "editada"`,
@@ -13151,12 +13459,12 @@ async function photoAssigner(input) {
13151
13459
  mediaVariante: {
13152
13460
  url: varianteUrl,
13153
13461
  formato,
13154
- linkedAt: import_firebase_admin11.firestore.FieldValue.serverTimestamp(),
13462
+ linkedAt: import_firebase_admin13.firestore.FieldValue.serverTimestamp(),
13155
13463
  permalink: null,
13156
13464
  // lo llenara publishToInstagram/etc en el futuro
13157
13465
  mediaExternalId: null
13158
13466
  },
13159
- editadoAt: import_firebase_admin11.firestore.FieldValue.serverTimestamp()
13467
+ editadoAt: import_firebase_admin13.firestore.FieldValue.serverTimestamp()
13160
13468
  };
13161
13469
  let slotResolved = null;
13162
13470
  if (calendarioItemRef) {
@@ -13206,7 +13514,7 @@ async function photoAssigner(input) {
13206
13514
  tx.update(contenidoRefDoc, contenidoUpdatePayload);
13207
13515
  tx.update(calDocRef, {
13208
13516
  semanas,
13209
- updatedAt: import_firebase_admin11.firestore.FieldValue.serverTimestamp()
13517
+ updatedAt: import_firebase_admin13.firestore.FieldValue.serverTimestamp()
13210
13518
  });
13211
13519
  });
13212
13520
  } else {
@@ -13221,35 +13529,35 @@ async function photoAssigner(input) {
13221
13529
  formato
13222
13530
  };
13223
13531
  }
13224
- var ParamsSchema16 = import_zod51.z.object({
13225
- tenantId: import_zod51.z.string().min(1).describe("Tenant identifier (the business account)."),
13226
- brandId: import_zod51.z.string().min(1).describe("Brand identifier within the tenant."),
13227
- contenidoRef: import_zod51.z.string().min(1).describe("Document ID in marketing_contenido (the post receiving the photo)."),
13228
- fotoId: import_zod51.z.string().min(1).describe("Photo ID in marketing_fotos. Photo must be in state='editada'."),
13229
- calendarioItemRef: import_zod51.z.string().regex(/^semana:\d+:slot:\d+$/).optional().describe(
13532
+ var ParamsSchema19 = import_zod54.z.object({
13533
+ tenantId: import_zod54.z.string().min(1).describe("Tenant identifier (the business account)."),
13534
+ brandId: import_zod54.z.string().min(1).describe("Brand identifier within the tenant."),
13535
+ contenidoRef: import_zod54.z.string().min(1).describe("Document ID in marketing_contenido (the post receiving the photo)."),
13536
+ fotoId: import_zod54.z.string().min(1).describe("Photo ID in marketing_fotos. Photo must be in state='editada'."),
13537
+ calendarioItemRef: import_zod54.z.string().regex(/^semana:\d+:slot:\d+$/).optional().describe(
13230
13538
  'Calendar slot reference in format "semana:N:slot:M". Optional \u2014 if provided, syncs the slot.fotoIdAsignada UI snapshot atomically with the content update. OMIT if there is no calendar slot to sync.'
13231
13539
  )
13232
13540
  });
13233
- var OutputSchema16 = import_zod51.z.discriminatedUnion("ok", [
13234
- import_zod51.z.object({
13235
- ok: import_zod51.z.literal(true),
13236
- fotoId: import_zod51.z.string(),
13237
- contenidoRef: import_zod51.z.string(),
13238
- plataforma: import_zod51.z.string(),
13239
- varianteUrl: import_zod51.z.string().nullable(),
13240
- formato: import_zod51.z.string()
13541
+ var OutputSchema19 = import_zod54.z.discriminatedUnion("ok", [
13542
+ import_zod54.z.object({
13543
+ ok: import_zod54.z.literal(true),
13544
+ fotoId: import_zod54.z.string(),
13545
+ contenidoRef: import_zod54.z.string(),
13546
+ plataforma: import_zod54.z.string(),
13547
+ varianteUrl: import_zod54.z.string().nullable(),
13548
+ formato: import_zod54.z.string()
13241
13549
  }),
13242
- import_zod51.z.object({
13243
- ok: import_zod51.z.literal(false),
13244
- error: import_zod51.z.string(),
13245
- code: import_zod51.z.enum(["CONTENT_NOT_FOUND", "CONTENT_TENANT_MISMATCH", "PHOTO_NOT_FOUND", "PHOTO_NOT_READY"]).optional()
13550
+ import_zod54.z.object({
13551
+ ok: import_zod54.z.literal(false),
13552
+ error: import_zod54.z.string(),
13553
+ code: import_zod54.z.enum(["CONTENT_NOT_FOUND", "CONTENT_TENANT_MISMATCH", "PHOTO_NOT_FOUND", "PHOTO_NOT_READY"]).optional()
13246
13554
  })
13247
13555
  ]);
13248
- var rawContract16 = {
13556
+ var rawContract19 = {
13249
13557
  name: "assign_photo_to_content",
13250
13558
  description: 'Assign an edited photo to an existing content. Writes atomically to BOTH marketing_contenido.fotoId+mediaVariante (canonical for publish) AND marketing_calendario slot.fotoIdAsignada (UI snapshot, only if calendarioItemRef provided). NEVER use update_calendar_slot for this \u2014 that one writes to the agenda, not the post. The photo must be in state="editada".',
13251
- paramsSchema: ParamsSchema16,
13252
- outputSchema: OutputSchema16,
13559
+ paramsSchema: ParamsSchema19,
13560
+ outputSchema: OutputSchema19,
13253
13561
  // Cambia la foto vinculada al contenido — reversible (volver a llamar con
13254
13562
  // otra fotoId), no publica nada externo. La foto editada queda intacta;
13255
13563
  // solo se actualiza la referencia.
@@ -13291,7 +13599,7 @@ var rawContract16 = {
13291
13599
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
13292
13600
  };
13293
13601
  var photoAssignerContract = MartinContractSchema.parse(
13294
- rawContract16
13602
+ rawContract19
13295
13603
  );
13296
13604
  function findPageByHeuristic(pages, pattern) {
13297
13605
  return pages.find((p) => pattern.test(p.title || "")) || pages.find((p) => pattern.test(p.handle || "")) || null;
@@ -13480,27 +13788,27 @@ var BRAND_BRIEF_SCHEMA_HINT = {
13480
13788
  escenas: [{ id: string, nombre: string, promptHint: string }]
13481
13789
  }`
13482
13790
  };
13483
- var ParamsSchema17 = import_zod52.z.object({
13484
- tenantId: import_zod52.z.string().min(1).describe("Tenant identifier (the business account)."),
13485
- brandId: import_zod52.z.string().min(1).describe("Brand identifier within the tenant.")
13791
+ var ParamsSchema20 = import_zod55.z.object({
13792
+ tenantId: import_zod55.z.string().min(1).describe("Tenant identifier (the business account)."),
13793
+ brandId: import_zod55.z.string().min(1).describe("Brand identifier within the tenant.")
13486
13794
  });
13487
- var OutputSchema17 = import_zod52.z.discriminatedUnion("ok", [
13488
- import_zod52.z.object({
13489
- ok: import_zod52.z.literal(true),
13795
+ var OutputSchema20 = import_zod55.z.discriminatedUnion("ok", [
13796
+ import_zod55.z.object({
13797
+ ok: import_zod55.z.literal(true),
13490
13798
  /** Payload con instrucción + datos del negocio para que Claude genere el brief. */
13491
- payload: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.unknown())
13799
+ payload: import_zod55.z.record(import_zod55.z.string(), import_zod55.z.unknown())
13492
13800
  }),
13493
- import_zod52.z.object({
13494
- ok: import_zod52.z.literal(false),
13495
- error: import_zod52.z.string(),
13496
- code: import_zod52.z.enum(["BRAND_NOT_FOUND"]).optional()
13801
+ import_zod55.z.object({
13802
+ ok: import_zod55.z.literal(false),
13803
+ error: import_zod55.z.string(),
13804
+ code: import_zod55.z.enum(["BRAND_NOT_FOUND"]).optional()
13497
13805
  })
13498
13806
  ]);
13499
- var rawContract17 = {
13807
+ var rawContract20 = {
13500
13808
  name: "generate_brand_brief",
13501
13809
  description: "Aggregate business data (Shopify, SEO, GBP, scraped site_content, brand config, tenant locations) into a payload to generate a pre-filled Brand Brief. Read-only \u2014 does NOT write the brief; the caller saves it via save_brand_brief.",
13502
- paramsSchema: ParamsSchema17,
13503
- outputSchema: OutputSchema17,
13810
+ paramsSchema: ParamsSchema20,
13811
+ outputSchema: OutputSchema20,
13504
13812
  // Lectura pura, sin side effects de escritura ni publicación.
13505
13813
  requiresConfirmation: false,
13506
13814
  destructive: false,
@@ -13525,7 +13833,7 @@ var rawContract17 = {
13525
13833
  sideEffects: ["reads_firestore"]
13526
13834
  };
13527
13835
  var brandBriefBuilderContract = MartinContractSchema.parse(
13528
- rawContract17
13836
+ rawContract20
13529
13837
  );
13530
13838
  async function resolveLastImportId(db, tenantId, brandId) {
13531
13839
  const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
@@ -13553,7 +13861,7 @@ async function marketingPlanBuilder(input) {
13553
13861
  }
13554
13862
  const productosQ = await db.collection("productos").where("tenantId", "==", tenantId).limit(100).get();
13555
13863
  const productos = productosQ.docs.map((d) => d.data());
13556
- const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(20).get();
13864
+ const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO2.PUBLICADO).limit(20).get();
13557
13865
  const historial = histQ.docs.map((d) => d.data());
13558
13866
  const lastImportId = await resolveLastImportId(db, tenantId, brandId);
13559
13867
  let colecciones = [];
@@ -13713,28 +14021,28 @@ var BLOG_STRATEGY_REGLAS = [
13713
14021
  "blogId, handle, title, ultimoPostFecha, ultimoPostKeyword, totalArticulos \u2014 copiar del shopifyBlogs (datos reales del import)",
13714
14022
  "defaultBlogId y defaultBlogHandle \u2014 usar el primer blog de la lista"
13715
14023
  ];
13716
- var ParamsSchema18 = import_zod53.z.object({
13717
- tenantId: import_zod53.z.string().min(1).describe("Tenant identifier (the business account)."),
13718
- brandId: import_zod53.z.string().min(1).describe("Brand identifier within the tenant.")
14024
+ var ParamsSchema21 = import_zod56.z.object({
14025
+ tenantId: import_zod56.z.string().min(1).describe("Tenant identifier (the business account)."),
14026
+ brandId: import_zod56.z.string().min(1).describe("Brand identifier within the tenant.")
13719
14027
  });
13720
- var OutputSchema18 = import_zod53.z.discriminatedUnion("ok", [
13721
- import_zod53.z.object({
13722
- ok: import_zod53.z.literal(true),
13723
- payload: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.unknown()).describe(
14028
+ var OutputSchema21 = import_zod56.z.discriminatedUnion("ok", [
14029
+ import_zod56.z.object({
14030
+ ok: import_zod56.z.literal(true),
14031
+ payload: import_zod56.z.record(import_zod56.z.string(), import_zod56.z.unknown()).describe(
13724
14032
  "Aggregated business data + instructions + schema hints for the LLM to generate a marketing plan. Includes seoSnapshot, productos, historialReciente, shopifyColecciones, shopifyBlogs, brandBrief, planActual, and schema/rules hints for coleccionesPriorizadas and blogStrategy."
13725
14033
  )
13726
14034
  }),
13727
- import_zod53.z.object({
13728
- ok: import_zod53.z.literal(false),
13729
- error: import_zod53.z.string(),
13730
- code: import_zod53.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"]).optional()
14035
+ import_zod56.z.object({
14036
+ ok: import_zod56.z.literal(false),
14037
+ error: import_zod56.z.string(),
14038
+ code: import_zod56.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"]).optional()
13731
14039
  })
13732
14040
  ]);
13733
- var rawContract18 = {
14041
+ var rawContract21 = {
13734
14042
  name: "generate_marketing_plan",
13735
14043
  description: "Aggregate business data (seoSnapshot, productos, brand config, brandBrief, historial, Shopify collections + blogs) into a payload for the LLM to generate a marketing plan. Read-only \u2014 does NOT save the plan. The caller saves via save_marketing_plan or update_marketing_plan_field after generating. Requires an existing seoSnapshot on the brand (returns SEO_SNAPSHOT_MISSING otherwise).",
13736
- paramsSchema: ParamsSchema18,
13737
- outputSchema: OutputSchema18,
14044
+ paramsSchema: ParamsSchema21,
14045
+ outputSchema: OutputSchema21,
13738
14046
  // Lectura pura, no muta nada.
13739
14047
  requiresConfirmation: false,
13740
14048
  destructive: false,
@@ -13759,7 +14067,7 @@ var rawContract18 = {
13759
14067
  sideEffects: ["reads_firestore"]
13760
14068
  };
13761
14069
  var marketingPlanBuilderContract = MartinContractSchema.parse(
13762
- rawContract18
14070
+ rawContract21
13763
14071
  );
13764
14072
  function buildGenId() {
13765
14073
  return `mkt_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
@@ -13776,6 +14084,7 @@ async function contenidoWriter(input) {
13776
14084
  fotoId,
13777
14085
  datos,
13778
14086
  calendarioItemRef,
14087
+ actor,
13779
14088
  linkPipeline
13780
14089
  } = input;
13781
14090
  const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
@@ -13819,7 +14128,7 @@ async function contenidoWriter(input) {
13819
14128
  await db.doc(`tenants/${tenantId}/marketing_contenido/${existente.id}`).update({
13820
14129
  estado: "descartado",
13821
14130
  rechazadoMotivo: "Reemplazado por contenido nuevo para el mismo slot",
13822
- rechazadoAt: import_firebase_admin12.firestore.FieldValue.serverTimestamp()
14131
+ rechazadoAt: import_firebase_admin14.firestore.FieldValue.serverTimestamp()
13823
14132
  });
13824
14133
  descartados++;
13825
14134
  }
@@ -13873,13 +14182,18 @@ async function contenidoWriter(input) {
13873
14182
  tipo: tipo ?? "post",
13874
14183
  keyword: keyword ?? null,
13875
14184
  languageCode,
13876
- estado: ESTADO_CONTENIDO.PENDIENTE_APROBACION,
14185
+ estado: ESTADO_CONTENIDO2.PENDIENTE_APROBACION,
13877
14186
  fotoId: fotoId ?? null,
13878
14187
  mediaUrl: null,
13879
14188
  calendarioItemRef: calendarioItemRef ?? null,
13880
14189
  datos,
13881
- creadoAt: import_firebase_admin12.firestore.FieldValue.serverTimestamp(),
13882
- creadoPorId: "mcp-cowork",
14190
+ creadoAt: import_firebase_admin14.firestore.FieldValue.serverTimestamp(),
14191
+ // Decisión canónica §9.2 PLAN_C2: actor real desde ctx.user (NO
14192
+ // 'mcp-cowork' hardcoded). Reportado en smoke 1.0.86 final.
14193
+ creadoPorId: actor.uid,
14194
+ creadoPorNombre: actor.nombre,
14195
+ creadoPorClient: actor.clientType,
14196
+ creadoPorClientMetadata: actor.clientMetadata ?? null,
13883
14197
  origen: "ai_assisted"
13884
14198
  });
13885
14199
  const contenidoRef = db.doc(`tenants/${tenantId}/marketing_contenido/${id}`);
@@ -13936,7 +14250,7 @@ async function contenidoWriter(input) {
13936
14250
  tx.create(contenidoRef, contenido);
13937
14251
  tx.update(calDocRef, {
13938
14252
  semanas,
13939
- updatedAt: import_firebase_admin12.firestore.FieldValue.serverTimestamp()
14253
+ updatedAt: import_firebase_admin14.firestore.FieldValue.serverTimestamp()
13940
14254
  });
13941
14255
  });
13942
14256
  } else {
@@ -13994,60 +14308,66 @@ async function contenidoWriter(input) {
13994
14308
  return {
13995
14309
  ok: true,
13996
14310
  contenidoId: id,
13997
- estado: ESTADO_CONTENIDO.PENDIENTE_APROBACION,
14311
+ estado: ESTADO_CONTENIDO2.PENDIENTE_APROBACION,
13998
14312
  plataforma,
13999
14313
  descartados,
14000
14314
  pipelineLinked
14001
14315
  };
14002
14316
  }
14003
- var ParamsSchema19 = import_zod54.z.object({
14004
- tenantId: import_zod54.z.string().min(1).describe("Tenant identifier (the business account)."),
14005
- brandId: import_zod54.z.string().min(1).describe("Brand identifier within the tenant."),
14006
- plataforma: import_zod54.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target platform for the content."),
14007
- tipo: import_zod54.z.string().optional().describe(
14317
+ var ParamsSchema22 = import_zod57.z.object({
14318
+ tenantId: import_zod57.z.string().min(1).describe("Tenant identifier (the business account)."),
14319
+ brandId: import_zod57.z.string().min(1).describe("Brand identifier within the tenant."),
14320
+ plataforma: import_zod57.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target platform for the content."),
14321
+ tipo: import_zod57.z.string().optional().describe(
14008
14322
  "Content type ('post', 'blog', 'carousel', 'reel', 'story', 'review_response'). Match to the platform."
14009
14323
  ),
14010
- keyword: import_zod54.z.string().optional().describe("Primary keyword for the content. OMIT if not applicable."),
14011
- languageCode: import_zod54.z.string().optional().describe(
14324
+ keyword: import_zod57.z.string().optional().describe("Primary keyword for the content. OMIT if not applicable."),
14325
+ languageCode: import_zod57.z.string().optional().describe(
14012
14326
  "Content language code (e.g. 'es', 'en'). For shopify_blog auto-injects to datos.languageCode if not present."
14013
14327
  ),
14014
- fotoId: import_zod54.z.string().optional().describe("Linked photo ID. OMIT if no photo associated yet (use assign_photo_to_content later)."),
14015
- datos: import_zod54.z.record(import_zod54.z.string(), import_zod54.z.unknown()).describe(
14328
+ fotoId: import_zod57.z.string().optional().describe("Linked photo ID. OMIT if no photo associated yet (use assign_photo_to_content later)."),
14329
+ datos: import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown()).describe(
14016
14330
  "Platform-specific content payload. Validated against Blog/GBP/IG/Review schemas. Use buildDatosBlog/GBP/IG/Review helpers to construct safely."
14017
14331
  ),
14018
- calendarioItemRef: import_zod54.z.string().optional().describe(
14332
+ calendarioItemRef: import_zod57.z.string().optional().describe(
14019
14333
  'Calendar slot reference (format "semana:N:slot:M"). If provided, links content to that slot AND discards prior non-discarded content of the same slot.'
14020
- )
14334
+ ),
14335
+ actor: import_zod57.z.object({
14336
+ uid: import_zod57.z.string().min(1).describe('User id who is creating the content. Example: "your-user-uid".'),
14337
+ nombre: import_zod57.z.string().min(1).describe('Human-readable user name. Example: "Daniel Gonz\xE1lez".'),
14338
+ clientType: import_zod57.z.string().min(1).describe('Client surface origin. Example: one of "mcp_client" | "martin" | "web" | "admin".'),
14339
+ clientMetadata: import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown()).nullable().optional().describe("Optional metadata about the client (session id, device, etc.).")
14340
+ }).describe("Actor (user) creating the content \u2014 extracted by the server.tool from ctx.user (\xA79.2).")
14021
14341
  });
14022
- var PipelineLinkResultSchema = import_zod54.z.object({
14023
- linked: import_zod54.z.boolean(),
14024
- paso: import_zod54.z.string().nullable(),
14025
- motivo: import_zod54.z.string().optional(),
14026
- code: import_zod54.z.string().optional(),
14027
- _instrucciones: import_zod54.z.string().optional()
14342
+ var PipelineLinkResultSchema = import_zod57.z.object({
14343
+ linked: import_zod57.z.boolean(),
14344
+ paso: import_zod57.z.string().nullable(),
14345
+ motivo: import_zod57.z.string().optional(),
14346
+ code: import_zod57.z.string().optional(),
14347
+ _instrucciones: import_zod57.z.string().optional()
14028
14348
  });
14029
- var OutputSchema19 = import_zod54.z.discriminatedUnion("ok", [
14030
- import_zod54.z.object({
14031
- ok: import_zod54.z.literal(true),
14032
- contenidoId: import_zod54.z.string(),
14033
- estado: import_zod54.z.string(),
14034
- plataforma: import_zod54.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
14035
- descartados: import_zod54.z.number().int().nonnegative(),
14349
+ var OutputSchema22 = import_zod57.z.discriminatedUnion("ok", [
14350
+ import_zod57.z.object({
14351
+ ok: import_zod57.z.literal(true),
14352
+ contenidoId: import_zod57.z.string(),
14353
+ estado: import_zod57.z.string(),
14354
+ plataforma: import_zod57.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
14355
+ descartados: import_zod57.z.number().int().nonnegative(),
14036
14356
  pipelineLinked: PipelineLinkResultSchema
14037
14357
  }),
14038
- import_zod54.z.object({
14039
- ok: import_zod54.z.literal(false),
14040
- error: import_zod54.z.string(),
14041
- code: import_zod54.z.enum(["CONTENT_FREQUENCY_LIMIT", "CONTENT_DATA_VALIDATION_FAILED"]).optional(),
14042
- activosEstaSemana: import_zod54.z.number().int().optional(),
14043
- limite: import_zod54.z.number().int().optional()
14358
+ import_zod57.z.object({
14359
+ ok: import_zod57.z.literal(false),
14360
+ error: import_zod57.z.string(),
14361
+ code: import_zod57.z.enum(["CONTENT_FREQUENCY_LIMIT", "CONTENT_DATA_VALIDATION_FAILED"]).optional(),
14362
+ activosEstaSemana: import_zod57.z.number().int().optional(),
14363
+ limite: import_zod57.z.number().int().optional()
14044
14364
  })
14045
14365
  ]);
14046
- var rawContract19 = {
14366
+ var rawContract22 = {
14047
14367
  name: "save_generated_content",
14048
14368
  description: "Save newly generated marketing content to marketing_contenido. The content is saved as pendiente_aprobacion \u2014 it is NOT published until the tenant approves it. Enforces weekly frequency limits per platform. If calendarioItemRef is provided, links to the slot and discards any prior content of the same slot. Validates datos against the platform schema.",
14049
- paramsSchema: ParamsSchema19,
14050
- outputSchema: OutputSchema19,
14369
+ paramsSchema: ParamsSchema22,
14370
+ outputSchema: OutputSchema22,
14051
14371
  // Crea borrador + descarta borrador previo del mismo slot. Reversible
14052
14372
  // (volver a llamar con datos corregidos crea un nuevo borrador y descarta
14053
14373
  // el actual). NO publica nada externo — la CF de publish se encarga
@@ -14092,7 +14412,7 @@ var rawContract19 = {
14092
14412
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
14093
14413
  };
14094
14414
  var contenidoWriterContract = MartinContractSchema.parse(
14095
- rawContract19
14415
+ rawContract22
14096
14416
  );
14097
14417
  function fmtDate(d) {
14098
14418
  const y = d.getFullYear();
@@ -14127,7 +14447,7 @@ function generarEstructuraCalendario(mes) {
14127
14447
  return semanas;
14128
14448
  }
14129
14449
  async function weeklyContentBuilder(input) {
14130
- const { db, tenantId, brandId, semana, modo } = input;
14450
+ const { db, tenantId, brandId, semana, modo, actor } = input;
14131
14451
  const targetModo = modo ?? "planificar";
14132
14452
  const now2 = /* @__PURE__ */ new Date();
14133
14453
  const targetMes = now2.toISOString().slice(0, 7);
@@ -14148,8 +14468,12 @@ async function weeklyContentBuilder(input) {
14148
14468
  brandId,
14149
14469
  mes: targetMes,
14150
14470
  semanas,
14151
- creadoAt: import_firebase_admin13.firestore.FieldValue.serverTimestamp(),
14152
- creadoPorId: "mcp-cowork",
14471
+ creadoAt: import_firebase_admin15.firestore.FieldValue.serverTimestamp(),
14472
+ // §9.2 PLAN_C2 — actor real desde ctx.user (NO 'mcp-cowork' hardcoded).
14473
+ creadoPorId: actor.uid,
14474
+ creadoPorNombre: actor.nombre,
14475
+ creadoPorClient: actor.clientType,
14476
+ creadoPorClientMetadata: actor.clientMetadata ?? null,
14153
14477
  updatedAt: null
14154
14478
  };
14155
14479
  await db.doc(`tenants/${tenantId}/marketing_calendario/${calId}`).set(calDoc);
@@ -14218,7 +14542,7 @@ async function weeklyContentBuilder(input) {
14218
14542
  }
14219
14543
  };
14220
14544
  }
14221
- const fotosQ = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO.EDITADA).limit(20).get();
14545
+ const fotosQ = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO2.EDITADA).limit(20).get();
14222
14546
  const fotosDisponibles = fotosQ.docs.map((d) => d.data());
14223
14547
  const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).limit(20).get();
14224
14548
  const historial = histQ.docs.map((d) => d.data());
@@ -14228,7 +14552,7 @@ async function weeklyContentBuilder(input) {
14228
14552
  const blogStrategy = brand.blogStrategy;
14229
14553
  const blogs = blogStrategy?.blogs ?? [];
14230
14554
  const idiomas = brand.idiomas;
14231
- const articulosQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("plataforma", "==", "shopify_blog").where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(20).get();
14555
+ const articulosQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("plataforma", "==", "shopify_blog").where("estado", "==", ESTADO_CONTENIDO2.PUBLICADO).limit(20).get();
14232
14556
  const articulosExistentes = articulosQ.docs.map(
14233
14557
  (d) => d.data()
14234
14558
  );
@@ -14367,34 +14691,40 @@ OBLIGATORIO: calendarioItemRef con formato EXACTO "semana:N:slot:M" (ej: "semana
14367
14691
  OBLIGATORIO: fotoId \u2014 SIEMPRE pasa el ID de la foto que elegiste con get_photos_for_slot.
14368
14692
  Si el slot tiene notas[], LEERLAS y usarlas como contexto. Si estado es revisar, regenerar adaptando a las notas.
14369
14693
  BLOG SLOTS: Usa _blogJIT para el contexto de cada slot shopify_blog \u2014 blogTarget, tono, author, articulosExistentes para interlinking.`;
14370
- var ParamsSchema20 = import_zod55.z.object({
14371
- tenantId: import_zod55.z.string().min(1).describe("Tenant identifier (the business account)."),
14372
- brandId: import_zod55.z.string().min(1).describe("Brand identifier within the tenant."),
14373
- semana: import_zod55.z.number().int().min(1).max(5).optional().describe(
14694
+ var ParamsSchema23 = import_zod58.z.object({
14695
+ tenantId: import_zod58.z.string().min(1).describe("Tenant identifier (the business account)."),
14696
+ brandId: import_zod58.z.string().min(1).describe("Brand identifier within the tenant."),
14697
+ semana: import_zod58.z.number().int().min(1).max(5).optional().describe(
14374
14698
  "Week number within the current month (1-5). OMIT to default to the current week inferred from today."
14375
14699
  ),
14376
- modo: import_zod55.z.enum(["planificar", "generar"]).optional().describe(
14700
+ modo: import_zod58.z.enum(["planificar", "generar"]).optional().describe(
14377
14701
  "Operation mode. 'planificar' (default): propose distribution platform+keyword+tipo per day. 'generar': requires slots in pre_aprobado/revisar \u2014 generate actual content. OMIT to default to 'planificar'."
14378
- )
14702
+ ),
14703
+ actor: import_zod58.z.object({
14704
+ uid: import_zod58.z.string().min(1).describe('User id who is generating the weekly content. Example: "your-user-uid".'),
14705
+ nombre: import_zod58.z.string().min(1).describe('Human-readable user name. Example: "Daniel Gonz\xE1lez".'),
14706
+ clientType: import_zod58.z.string().min(1).describe('Client surface origin. Example: one of "mcp_client" | "martin" | "web" | "admin".'),
14707
+ clientMetadata: import_zod58.z.record(import_zod58.z.string(), import_zod58.z.unknown()).nullable().optional().describe("Optional metadata about the client.")
14708
+ }).describe("Actor (user) generating the weekly content \u2014 extracted by the server.tool from ctx.user (\xA79.2). Used when auto-creating marketing_calendario doc.")
14379
14709
  });
14380
- var OutputSchema20 = import_zod55.z.discriminatedUnion("ok", [
14381
- import_zod55.z.object({
14382
- ok: import_zod55.z.literal(true),
14383
- payload: import_zod55.z.record(import_zod55.z.string(), import_zod55.z.unknown()).describe(
14710
+ var OutputSchema23 = import_zod58.z.discriminatedUnion("ok", [
14711
+ import_zod58.z.object({
14712
+ ok: import_zod58.z.literal(true),
14713
+ payload: import_zod58.z.record(import_zod58.z.string(), import_zod58.z.unknown()).describe(
14384
14714
  "Aggregated context for the LLM. Shape varies by modo: 'planificar' returns brand skeleton + slotsYaExistentes + historial; 'generar' returns slotsParaGenerar + fotosDisponibles + brand.blogStrategy + blogJIT (if blog slots present)."
14385
14715
  )
14386
14716
  }),
14387
- import_zod55.z.object({
14388
- ok: import_zod55.z.literal(false),
14389
- error: import_zod55.z.string(),
14390
- code: import_zod55.z.enum(["BRAND_NOT_FOUND", "WEEK_HAS_NO_SLOTS"]).optional()
14717
+ import_zod58.z.object({
14718
+ ok: import_zod58.z.literal(false),
14719
+ error: import_zod58.z.string(),
14720
+ code: import_zod58.z.enum(["BRAND_NOT_FOUND", "WEEK_HAS_NO_SLOTS"]).optional()
14391
14721
  })
14392
14722
  ]);
14393
- var rawContract20 = {
14723
+ var rawContract23 = {
14394
14724
  name: "generate_weekly_content",
14395
14725
  description: "Build the week's content. Two modes: 'planificar' aggregates context for the LLM to propose distribution (LLM then uses add_calendar_slot/update_calendar_slot per day); 'generar' returns pre-approved slots + photos + blog JIT context for the LLM to generate actual content (LLM then calls save_generated_content per slot). AUTO-CREATES the monthly marketing_calendario if it does not exist. NEVER generates content directly \u2014 always returns a payload for the LLM consumer.",
14396
- paramsSchema: ParamsSchema20,
14397
- outputSchema: OutputSchema20,
14726
+ paramsSchema: ParamsSchema23,
14727
+ outputSchema: OutputSchema23,
14398
14728
  // Auto-create de calendario es bootstrap reversible (volver a llamar usa el
14399
14729
  // calendario existente). NO publica nada externo. modo='generar' tampoco
14400
14730
  // publica — solo prepara contexto.
@@ -14439,7 +14769,7 @@ var rawContract20 = {
14439
14769
  sideEffects: ["reads_firestore", "writes_firestore"]
14440
14770
  };
14441
14771
  var weeklyContentBuilderContract = MartinContractSchema.parse(
14442
- rawContract20
14772
+ rawContract23
14443
14773
  );
14444
14774
  var DEFAULT_TEXT_THRESHOLD = 0.35;
14445
14775
  var DEFAULT_IMAGE_THRESHOLD = 0.08;
@@ -14628,69 +14958,69 @@ async function contentFinder(input) {
14628
14958
  }
14629
14959
  return result;
14630
14960
  }
14631
- var IncludeSchema = import_zod56.z.object({
14632
- products: import_zod56.z.boolean(),
14633
- collections: import_zod56.z.boolean(),
14634
- articles: import_zod56.z.boolean(),
14635
- pages: import_zod56.z.boolean()
14961
+ var IncludeSchema = import_zod59.z.object({
14962
+ products: import_zod59.z.boolean(),
14963
+ collections: import_zod59.z.boolean(),
14964
+ articles: import_zod59.z.boolean(),
14965
+ pages: import_zod59.z.boolean()
14636
14966
  }).describe(
14637
14967
  "Toggles for which Shopify content types to search. Default truthy for products/collections/articles, false for pages. Set false to skip a category for performance."
14638
14968
  );
14639
- var LimitSchema = import_zod56.z.object({
14640
- products: import_zod56.z.number().int().min(0).max(20),
14641
- collections: import_zod56.z.number().int().min(0).max(10),
14642
- articles: import_zod56.z.number().int().min(0).max(20),
14643
- pages: import_zod56.z.number().int().min(0).max(10)
14969
+ var LimitSchema = import_zod59.z.object({
14970
+ products: import_zod59.z.number().int().min(0).max(20),
14971
+ collections: import_zod59.z.number().int().min(0).max(10),
14972
+ articles: import_zod59.z.number().int().min(0).max(20),
14973
+ pages: import_zod59.z.number().int().min(0).max(10)
14644
14974
  }).describe(
14645
14975
  "Per-category result count caps. Defaults: products 5, collections 3, articles 5, pages 2."
14646
14976
  );
14647
- var ParamsSchema21 = import_zod56.z.object({
14648
- tenantId: import_zod56.z.string().min(1).describe("Tenant identifier (the business account)."),
14649
- brandId: import_zod56.z.string().min(1).describe("Brand identifier within the tenant."),
14650
- contexto: import_zod56.z.string().min(1).describe(
14977
+ var ParamsSchema24 = import_zod59.z.object({
14978
+ tenantId: import_zod59.z.string().min(1).describe("Tenant identifier (the business account)."),
14979
+ brandId: import_zod59.z.string().min(1).describe("Brand identifier within the tenant."),
14980
+ contexto: import_zod59.z.string().min(1).describe(
14651
14981
  "Search context: a paragraph, keyword, or intent string. Embedded for vector search."
14652
14982
  ),
14653
- fecha: import_zod56.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
14983
+ fecha: import_zod59.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
14654
14984
  "Content date in YYYY-MM-DD. Used to detect active season and prioritize matching collections."
14655
14985
  ),
14656
14986
  include: IncludeSchema,
14657
14987
  limit: LimitSchema,
14658
- diversidad: import_zod56.z.boolean().describe(
14988
+ diversidad: import_zod59.z.boolean().describe(
14659
14989
  "Whether to apply Jaccard title diversification + handle dedupe to results. Default true."
14660
14990
  ),
14661
- mode: import_zod56.z.enum(["text", "hybrid"]).optional().describe(
14991
+ mode: import_zod59.z.enum(["text", "hybrid"]).optional().describe(
14662
14992
  "Search mode. 'text' (default): single query per collection against embeddingText, fast. 'hybrid': parallel text + image queries with Reciprocal Rank Fusion, 2x queries \u2014 useful when text mode returns few results or to surface visually-similar items with weak SEO."
14663
14993
  )
14664
14994
  });
14665
- var VectorResultSchema = import_zod56.z.object({
14666
- id: import_zod56.z.string(),
14667
- similarity: import_zod56.z.number().optional()
14995
+ var VectorResultSchema = import_zod59.z.object({
14996
+ id: import_zod59.z.string(),
14997
+ similarity: import_zod59.z.number().optional()
14668
14998
  }).passthrough();
14669
- var TemporadaSchema2 = import_zod56.z.object({
14670
- coleccion: import_zod56.z.string().nullable(),
14671
- titulo: import_zod56.z.string().nullable(),
14672
- razon: import_zod56.z.string().nullable(),
14673
- fechaInicio: import_zod56.z.string().nullable(),
14674
- fechaFin: import_zod56.z.string().nullable()
14999
+ var TemporadaSchema2 = import_zod59.z.object({
15000
+ coleccion: import_zod59.z.string().nullable(),
15001
+ titulo: import_zod59.z.string().nullable(),
15002
+ razon: import_zod59.z.string().nullable(),
15003
+ fechaInicio: import_zod59.z.string().nullable(),
15004
+ fechaFin: import_zod59.z.string().nullable()
14675
15005
  });
14676
- var SuggestedActionSchema22 = import_zod56.z.record(import_zod56.z.string(), import_zod56.z.unknown());
14677
- var DetectedConflictSchema22 = import_zod56.z.record(import_zod56.z.string(), import_zod56.z.unknown());
14678
- var OutputSchema21 = import_zod56.z.object({
14679
- productos: import_zod56.z.array(VectorResultSchema),
14680
- colecciones: import_zod56.z.array(VectorResultSchema),
14681
- articles: import_zod56.z.array(VectorResultSchema),
14682
- pages: import_zod56.z.array(VectorResultSchema),
14683
- _instrucciones: import_zod56.z.string(),
14684
- _negativePrompt: import_zod56.z.string(),
15006
+ var SuggestedActionSchema22 = import_zod59.z.record(import_zod59.z.string(), import_zod59.z.unknown());
15007
+ var DetectedConflictSchema22 = import_zod59.z.record(import_zod59.z.string(), import_zod59.z.unknown());
15008
+ var OutputSchema24 = import_zod59.z.object({
15009
+ productos: import_zod59.z.array(VectorResultSchema),
15010
+ colecciones: import_zod59.z.array(VectorResultSchema),
15011
+ articles: import_zod59.z.array(VectorResultSchema),
15012
+ pages: import_zod59.z.array(VectorResultSchema),
15013
+ _instrucciones: import_zod59.z.string(),
15014
+ _negativePrompt: import_zod59.z.string(),
14685
15015
  _temporadaActiva: TemporadaSchema2.nullable(),
14686
15016
  _detectedConflict: DetectedConflictSchema22.optional(),
14687
- _suggestedActions: import_zod56.z.array(SuggestedActionSchema22)
15017
+ _suggestedActions: import_zod59.z.array(SuggestedActionSchema22)
14688
15018
  });
14689
- var rawContract21 = {
15019
+ var rawContract24 = {
14690
15020
  name: "find_content_for_topic",
14691
15021
  description: "Find Shopify content (products, collections, articles, pages) semantically related to a context/keyword + date. Uses multimodal vector search (1408d Vertex). Mode 'text' (default) queries embeddingText (fast); 'hybrid' merges text + image results via Reciprocal Rank Fusion (rescues items with weak text SEO). Auto-injects JIT context: linking instructions, brand visual negatives, active season collection, and conflict detection (>85% similar article).",
14692
- paramsSchema: ParamsSchema21,
14693
- outputSchema: OutputSchema21,
15022
+ paramsSchema: ParamsSchema24,
15023
+ outputSchema: OutputSchema24,
14694
15024
  // Lectura pura (vector search). Failures internas en search degradan a
14695
15025
  // arrays vacíos — el helper NO falla.
14696
15026
  requiresConfirmation: false,
@@ -14713,7 +15043,7 @@ var rawContract21 = {
14713
15043
  sideEffects: ["reads_firestore"]
14714
15044
  };
14715
15045
  var contentFinderContract = MartinContractSchema.parse(
14716
- rawContract21
15046
+ rawContract24
14717
15047
  );
14718
15048
  var DEFAULT_PHOTO_THRESHOLD = 0.7;
14719
15049
  var DEFAULT_SHOPIFY_THRESHOLD = 0.65;
@@ -14860,51 +15190,51 @@ async function slotAssetFinder(input) {
14860
15190
  _fuente: fuente
14861
15191
  };
14862
15192
  }
14863
- var ParamsSchema22 = import_zod57.z.object({
14864
- tenantId: import_zod57.z.string().min(1).describe("Tenant identifier (the business account)."),
14865
- brandId: import_zod57.z.string().min(1).describe("Brand identifier within the tenant."),
14866
- keyword: import_zod57.z.string().min(1).describe(
15193
+ var ParamsSchema25 = import_zod60.z.object({
15194
+ tenantId: import_zod60.z.string().min(1).describe("Tenant identifier (the business account)."),
15195
+ brandId: import_zod60.z.string().min(1).describe("Brand identifier within the tenant."),
15196
+ keyword: import_zod60.z.string().min(1).describe(
14867
15197
  "Slot keyword to search photos for. Used as embedding query for multimodal vector search."
14868
15198
  ),
14869
- plataforma: import_zod57.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe(
15199
+ plataforma: import_zod60.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe(
14870
15200
  "Target platform \u2014 determines the variant format to resolve (gbp_4_3, blog_3_2, ig_4_5, ig_1_1)."
14871
15201
  ),
14872
- fecha: import_zod57.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
15202
+ fecha: import_zod60.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
14873
15203
  "Slot date in YYYY-MM-DD. Used to detect active season and apply seasonal catalog overrides."
14874
15204
  ),
14875
- limit: import_zod57.z.number().int().min(1).max(10).optional().describe("Max number of photos to return. Default 5. Max 10.")
15205
+ limit: import_zod60.z.number().int().min(1).max(10).optional().describe("Max number of photos to return. Default 5. Max 10.")
14876
15206
  });
14877
- var TemporadaSchema22 = import_zod57.z.object({
14878
- coleccion: import_zod57.z.string().nullable(),
14879
- titulo: import_zod57.z.string().nullable(),
14880
- razon: import_zod57.z.string().nullable(),
14881
- fechaInicio: import_zod57.z.string().nullable(),
14882
- fechaFin: import_zod57.z.string().nullable()
15207
+ var TemporadaSchema22 = import_zod60.z.object({
15208
+ coleccion: import_zod60.z.string().nullable(),
15209
+ titulo: import_zod60.z.string().nullable(),
15210
+ razon: import_zod60.z.string().nullable(),
15211
+ fechaInicio: import_zod60.z.string().nullable(),
15212
+ fechaFin: import_zod60.z.string().nullable()
14883
15213
  });
14884
- var OutputSchema22 = import_zod57.z.discriminatedUnion("ok", [
15214
+ var OutputSchema25 = import_zod60.z.discriminatedUnion("ok", [
14885
15215
  // Note: success case does not include `ok: true` literal in helper return —
14886
15216
  // helper returns the success shape directly. Adapter to discriminated union
14887
15217
  // happens at wrapper level if needed; here we accept both shapes.
14888
- import_zod57.z.object({
14889
- ok: import_zod57.z.literal(true),
14890
- fotos: import_zod57.z.array(import_zod57.z.record(import_zod57.z.string(), import_zod57.z.unknown())),
14891
- _instrucciones: import_zod57.z.string(),
14892
- _negativePrompt: import_zod57.z.string(),
15218
+ import_zod60.z.object({
15219
+ ok: import_zod60.z.literal(true),
15220
+ fotos: import_zod60.z.array(import_zod60.z.record(import_zod60.z.string(), import_zod60.z.unknown())),
15221
+ _instrucciones: import_zod60.z.string(),
15222
+ _negativePrompt: import_zod60.z.string(),
14893
15223
  _temporadaActiva: TemporadaSchema22.nullable(),
14894
- _bloqueoProducto: import_zod57.z.boolean(),
14895
- _fuente: import_zod57.z.enum(["tenant", "shopify_product"])
15224
+ _bloqueoProducto: import_zod60.z.boolean(),
15225
+ _fuente: import_zod60.z.enum(["tenant", "shopify_product"])
14896
15226
  }),
14897
- import_zod57.z.object({
14898
- ok: import_zod57.z.literal(false),
14899
- error: import_zod57.z.string(),
14900
- code: import_zod57.z.enum(["BRAND_NOT_FOUND"]).optional()
15227
+ import_zod60.z.object({
15228
+ ok: import_zod60.z.literal(false),
15229
+ error: import_zod60.z.string(),
15230
+ code: import_zod60.z.enum(["BRAND_NOT_FOUND"]).optional()
14901
15231
  })
14902
15232
  ]);
14903
- var rawContract22 = {
15233
+ var rawContract25 = {
14904
15234
  name: "get_photos_for_slot",
14905
15235
  description: "Find tenant photos for a calendar slot (keyword + platform + date) via multimodal vector search (1408d Vertex). Resolves the platform-specific variant (gbp_4_3/blog_3_2/ig_4_5/ig_1_1). Auto-injects JIT context: how to choose, brand visual negatives, active season overrides, and source layer (tenant photos vs Shopify product fallback). If <3 photos returned, consider calling request_photo_shoot to alert the tenant.",
14906
- paramsSchema: ParamsSchema22,
14907
- outputSchema: OutputSchema22,
15236
+ paramsSchema: ParamsSchema25,
15237
+ outputSchema: OutputSchema25,
14908
15238
  requiresConfirmation: false,
14909
15239
  destructive: false,
14910
15240
  affectsPublication: false,
@@ -14931,7 +15261,7 @@ var rawContract22 = {
14931
15261
  sideEffects: ["reads_firestore"]
14932
15262
  };
14933
15263
  var slotAssetFinderContract = MartinContractSchema.parse(
14934
- rawContract22
15264
+ rawContract25
14935
15265
  );
14936
15266
  var DEFAULT_SIMILARITY_THRESHOLD = 0.6;
14937
15267
  function cosineSimilarity(a, b) {
@@ -15008,44 +15338,44 @@ async function canvaTemplateSelector(input) {
15008
15338
  _instrucciones: "Plantilla Canva seleccionada. Llama a marketingDesignWithCanva({contenidoId, plantillaId, fotoVariantePath, textos}) para renderizar la pieza final."
15009
15339
  };
15010
15340
  }
15011
- var ParamsSchema23 = import_zod58.z.object({
15012
- tenantId: import_zod58.z.string().min(1).describe("Tenant identifier (the business account)."),
15013
- brandId: import_zod58.z.string().min(1).describe("Brand identifier within the tenant."),
15014
- plataforma: import_zod58.z.string().min(1).describe(
15341
+ var ParamsSchema26 = import_zod61.z.object({
15342
+ tenantId: import_zod61.z.string().min(1).describe("Tenant identifier (the business account)."),
15343
+ brandId: import_zod61.z.string().min(1).describe("Brand identifier within the tenant."),
15344
+ plataforma: import_zod61.z.string().min(1).describe(
15015
15345
  "Target platform (e.g. 'gbp', 'shopify_blog', 'instagram'). Filters templates that declare this plataforma."
15016
15346
  ),
15017
- tipoContenido: import_zod58.z.string().min(1).describe(
15347
+ tipoContenido: import_zod61.z.string().min(1).describe(
15018
15348
  "Content type (e.g. 'post', 'carousel', 'story', 'blog'). Filters templates that declare this tipoContenido."
15019
15349
  ),
15020
- keyword: import_zod58.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
15350
+ keyword: import_zod61.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
15021
15351
  });
15022
- var OutputSchema23 = import_zod58.z.union([
15023
- import_zod58.z.object({
15024
- plantillaId: import_zod58.z.string(),
15025
- titulo: import_zod58.z.string().nullable(),
15026
- thumbnailUrl: import_zod58.z.string().nullable(),
15027
- similarity: import_zod58.z.number(),
15028
- _instrucciones: import_zod58.z.string()
15352
+ var OutputSchema26 = import_zod61.z.union([
15353
+ import_zod61.z.object({
15354
+ plantillaId: import_zod61.z.string(),
15355
+ titulo: import_zod61.z.string().nullable(),
15356
+ thumbnailUrl: import_zod61.z.string().nullable(),
15357
+ similarity: import_zod61.z.number(),
15358
+ _instrucciones: import_zod61.z.string()
15029
15359
  }),
15030
- import_zod58.z.object({
15031
- plantillaId: import_zod58.z.null(),
15032
- motivo: import_zod58.z.enum([
15360
+ import_zod61.z.object({
15361
+ plantillaId: import_zod61.z.null(),
15362
+ motivo: import_zod61.z.enum([
15033
15363
  "brand_no_encontrada",
15034
15364
  "no_canva",
15035
15365
  "no_match_plataforma",
15036
15366
  "modo_cliente_no_soportado",
15037
15367
  "similarity_baja"
15038
15368
  ]),
15039
- similarity: import_zod58.z.number().optional(),
15040
- _instrucciones: import_zod58.z.string().optional(),
15041
- _todo: import_zod58.z.string().optional()
15369
+ similarity: import_zod61.z.number().optional(),
15370
+ _instrucciones: import_zod61.z.string().optional(),
15371
+ _todo: import_zod61.z.string().optional()
15042
15372
  })
15043
15373
  ]);
15044
- var rawContract23 = {
15374
+ var rawContract26 = {
15045
15375
  name: "select_canva_template",
15046
15376
  description: "Select the best Canva template from the tenant for a slot. Filters templates by plataforma+tipoContenido, then cosine-similarity matches embeddings vs the keyword query. Returns plantillaId on match (similarity >= 0.60). On miss returns plantillaId=null with motivo enum (no_canva, no_match_plataforma, modo_cliente_no_soportado, similarity_baja, brand_no_encontrada). The LLM should degrade gracefully (generate without Canva template) on any miss.",
15047
- paramsSchema: ParamsSchema23,
15048
- outputSchema: OutputSchema23,
15377
+ paramsSchema: ParamsSchema26,
15378
+ outputSchema: OutputSchema26,
15049
15379
  // Lectura pura. NO falla — todo "no encontré" es resultado válido del helper.
15050
15380
  requiresConfirmation: false,
15051
15381
  destructive: false,
@@ -15072,7 +15402,7 @@ var rawContract23 = {
15072
15402
  sideEffects: ["reads_firestore"]
15073
15403
  };
15074
15404
  var canvaTemplateSelectorContract = MartinContractSchema.parse(
15075
- rawContract23
15405
+ rawContract26
15076
15406
  );
15077
15407
  function buildDirectorPlanInstrucciones(catalogoVisual) {
15078
15408
  const etiquetas = catalogoVisual.etiquetas || {};
@@ -15296,38 +15626,38 @@ async function photoDirectorExecute(input) {
15296
15626
  _instrucciones: buildDirectorExecuteSuccessInstrucciones(result.balanceAfter)
15297
15627
  };
15298
15628
  }
15299
- var PlanParamsSchema = import_zod59.z.object({
15300
- tenantId: import_zod59.z.string().min(1).describe("Tenant identifier (the business account)."),
15301
- fotoId: import_zod59.z.string().min(1).describe("Photo ID in marketing_fotos. The photo must have an archivoOriginal uploaded.")
15629
+ var PlanParamsSchema = import_zod62.z.object({
15630
+ tenantId: import_zod62.z.string().min(1).describe("Tenant identifier (the business account)."),
15631
+ fotoId: import_zod62.z.string().min(1).describe("Photo ID in marketing_fotos. The photo must have an archivoOriginal uploaded.")
15302
15632
  });
15303
- var PlanContextSchema = import_zod59.z.object({
15304
- fotoId: import_zod59.z.string(),
15305
- archivoOriginal: import_zod59.z.string(),
15306
- estrategia: import_zod59.z.string(),
15307
- brandBrief: import_zod59.z.object({
15308
- segmento: import_zod59.z.string().nullable(),
15309
- personalidad: import_zod59.z.unknown().nullable()
15633
+ var PlanContextSchema = import_zod62.z.object({
15634
+ fotoId: import_zod62.z.string(),
15635
+ archivoOriginal: import_zod62.z.string(),
15636
+ estrategia: import_zod62.z.string(),
15637
+ brandBrief: import_zod62.z.object({
15638
+ segmento: import_zod62.z.string().nullable(),
15639
+ personalidad: import_zod62.z.unknown().nullable()
15310
15640
  }),
15311
- visualRules: import_zod59.z.record(import_zod59.z.string(), import_zod59.z.unknown()),
15312
- catalogoVisual: import_zod59.z.record(import_zod59.z.string(), import_zod59.z.unknown()),
15313
- estiloVisual: import_zod59.z.unknown().nullable(),
15314
- notaTenant: import_zod59.z.unknown().nullable(),
15315
- productoLinkeadoManual: import_zod59.z.unknown().nullable(),
15316
- _instrucciones: import_zod59.z.string()
15641
+ visualRules: import_zod62.z.record(import_zod62.z.string(), import_zod62.z.unknown()),
15642
+ catalogoVisual: import_zod62.z.record(import_zod62.z.string(), import_zod62.z.unknown()),
15643
+ estiloVisual: import_zod62.z.unknown().nullable(),
15644
+ notaTenant: import_zod62.z.unknown().nullable(),
15645
+ productoLinkeadoManual: import_zod62.z.unknown().nullable(),
15646
+ _instrucciones: import_zod62.z.string()
15317
15647
  });
15318
- var PlanOutputSchema = import_zod59.z.discriminatedUnion("ok", [
15319
- import_zod59.z.object({
15320
- ok: import_zod59.z.literal(true),
15321
- imageBase64: import_zod59.z.string().describe("Compressed photo as base64 for transport to the LLM."),
15648
+ var PlanOutputSchema = import_zod62.z.discriminatedUnion("ok", [
15649
+ import_zod62.z.object({
15650
+ ok: import_zod62.z.literal(true),
15651
+ imageBase64: import_zod62.z.string().describe("Compressed photo as base64 for transport to the LLM."),
15322
15652
  context: PlanContextSchema
15323
15653
  }),
15324
- import_zod59.z.object({
15325
- ok: import_zod59.z.literal(false),
15326
- code: import_zod59.z.string(),
15327
- error: import_zod59.z.string(),
15328
- _instrucciones: import_zod59.z.string().optional(),
15329
- fix: import_zod59.z.string().optional(),
15330
- fotoId: import_zod59.z.string().optional()
15654
+ import_zod62.z.object({
15655
+ ok: import_zod62.z.literal(false),
15656
+ code: import_zod62.z.string(),
15657
+ error: import_zod62.z.string(),
15658
+ _instrucciones: import_zod62.z.string().optional(),
15659
+ fix: import_zod62.z.string().optional(),
15660
+ fotoId: import_zod62.z.string().optional()
15331
15661
  })
15332
15662
  ]);
15333
15663
  var planRawContract = {
@@ -15357,43 +15687,43 @@ var planRawContract = {
15357
15687
  var photoDirectorPlanContract = MartinContractSchema.parse(
15358
15688
  planRawContract
15359
15689
  );
15360
- var ExecuteParamsSchema = import_zod59.z.object({
15690
+ var ExecuteParamsSchema = import_zod62.z.object({
15361
15691
  // tenantId is injected by the wrapper from ctx (A7 module-scope pattern,
15362
15692
  // even though the helper function itself derives tenantId via the foto's
15363
15693
  // brandId). Needed here for extractTargetPath canonical path.
15364
- tenantId: import_zod59.z.string().min(1).describe("Tenant identifier (the business account)."),
15365
- fotoId: import_zod59.z.string().min(1).describe("Photo ID to edit (must have been planned via plan_photo_edit)."),
15366
- prompt: import_zod59.z.string().nullable().describe(
15694
+ tenantId: import_zod62.z.string().min(1).describe("Tenant identifier (the business account)."),
15695
+ fotoId: import_zod62.z.string().min(1).describe("Photo ID to edit (must have been planned via plan_photo_edit)."),
15696
+ prompt: import_zod62.z.string().nullable().describe(
15367
15697
  "English prompt for Gemini Image Edit when acciones includes edit_background. Pass null when strategy is 'tal_cual' (no editing \u2014 just regenerate thumbnail + embedding with new tags)."
15368
15698
  ),
15369
- acciones: import_zod59.z.array(import_zod59.z.enum(["edit_background", "none"])).describe(
15699
+ acciones: import_zod62.z.array(import_zod62.z.enum(["edit_background", "none"])).describe(
15370
15700
  "Edit operations to perform. Use ['edit_background'] for AI background edit, ['none'] when strategy is 'tal_cual'."
15371
15701
  ),
15372
- descripcion: import_zod59.z.string().describe("Semantic description in Spanish (saved as descripcionVision on the photo)."),
15373
- tipo: import_zod59.z.string().nullable().describe("Photo type from the brand catalogoVisual. null only if catalog has no matching type."),
15374
- tagsPrimarios: import_zod59.z.array(import_zod59.z.string()).describe("Primary tags from the catalogoVisual."),
15375
- tagsSecundarios: import_zod59.z.array(import_zod59.z.string()).describe("Secondary tags from the catalogoVisual."),
15376
- tagsContexto: import_zod59.z.array(import_zod59.z.string()).describe("Contextual tags (occasion, mood, style).")
15702
+ descripcion: import_zod62.z.string().describe("Semantic description in Spanish (saved as descripcionVision on the photo)."),
15703
+ tipo: import_zod62.z.string().nullable().describe("Photo type from the brand catalogoVisual. null only if catalog has no matching type."),
15704
+ tagsPrimarios: import_zod62.z.array(import_zod62.z.string()).describe("Primary tags from the catalogoVisual."),
15705
+ tagsSecundarios: import_zod62.z.array(import_zod62.z.string()).describe("Secondary tags from the catalogoVisual."),
15706
+ tagsContexto: import_zod62.z.array(import_zod62.z.string()).describe("Contextual tags (occasion, mood, style).")
15377
15707
  });
15378
- var ExecuteOutputSchema = import_zod59.z.discriminatedUnion("ok", [
15379
- import_zod59.z.object({
15380
- ok: import_zod59.z.literal(true),
15381
- fotoId: import_zod59.z.string(),
15382
- archivoEditado: import_zod59.z.string().optional(),
15383
- thumbnailUrl: import_zod59.z.string().optional(),
15384
- iteracion: import_zod59.z.number().optional(),
15385
- editCosto: import_zod59.z.number().optional(),
15386
- creditsConsumed: import_zod59.z.number().optional(),
15387
- balanceAfter: import_zod59.z.number().optional(),
15388
- imageBase64: import_zod59.z.string().nullable(),
15389
- _instrucciones: import_zod59.z.string()
15708
+ var ExecuteOutputSchema = import_zod62.z.discriminatedUnion("ok", [
15709
+ import_zod62.z.object({
15710
+ ok: import_zod62.z.literal(true),
15711
+ fotoId: import_zod62.z.string(),
15712
+ archivoEditado: import_zod62.z.string().optional(),
15713
+ thumbnailUrl: import_zod62.z.string().optional(),
15714
+ iteracion: import_zod62.z.number().optional(),
15715
+ editCosto: import_zod62.z.number().optional(),
15716
+ creditsConsumed: import_zod62.z.number().optional(),
15717
+ balanceAfter: import_zod62.z.number().optional(),
15718
+ imageBase64: import_zod62.z.string().nullable(),
15719
+ _instrucciones: import_zod62.z.string()
15390
15720
  }),
15391
- import_zod59.z.object({
15392
- ok: import_zod59.z.literal(false),
15393
- error: import_zod59.z.string(),
15394
- code: import_zod59.z.string(),
15395
- details: import_zod59.z.record(import_zod59.z.string(), import_zod59.z.unknown()).optional(),
15396
- _instrucciones: import_zod59.z.string()
15721
+ import_zod62.z.object({
15722
+ ok: import_zod62.z.literal(false),
15723
+ error: import_zod62.z.string(),
15724
+ code: import_zod62.z.string(),
15725
+ details: import_zod62.z.record(import_zod62.z.string(), import_zod62.z.unknown()).optional(),
15726
+ _instrucciones: import_zod62.z.string()
15397
15727
  })
15398
15728
  ]);
15399
15729
  var executeRawContract = {
@@ -15778,8 +16108,8 @@ async function buildTenantContext(db, tenantId, brandId) {
15778
16108
  if (!configSnap.exists) return "";
15779
16109
  const brand = configSnap.data();
15780
16110
  const [fotosQ, contenidoQ, productosQ] = await Promise.all([
15781
- db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO.EDITADA).limit(10).get(),
15782
- db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(10).get(),
16111
+ db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO2.EDITADA).limit(10).get(),
16112
+ db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO2.PUBLICADO).limit(10).get(),
15783
16113
  db.collection("productos").where("tenantId", "==", tenantId).limit(50).get()
15784
16114
  ]);
15785
16115
  const fotosEditadas = fotosQ.docs.map((d) => d.data());
@@ -16050,6 +16380,15 @@ function callPhotoBriefingWriter(input) {
16050
16380
  function callTenantContextSetter(input) {
16051
16381
  return callCF("marketingTenantContextSetterCallable", input);
16052
16382
  }
16383
+ function callPhotoDescarter(input) {
16384
+ return callCF("marketingPhotoDescarterCallable", input);
16385
+ }
16386
+ function callPhotoEditadaMarker(input) {
16387
+ return callCF("marketingPhotoEditadaMarkerCallable", input);
16388
+ }
16389
+ function callBrandContextSetter(input) {
16390
+ return callCF("marketingBrandContextSetterCallable", input);
16391
+ }
16053
16392
  function callBrandBriefWriter(input) {
16054
16393
  return callCF("marketingBrandBriefWriterCallable", input);
16055
16394
  }
@@ -16199,8 +16538,8 @@ function registerContextTools(server, session) {
16199
16538
  "set_context",
16200
16539
  "Set the active tenant + brand for the current super_admin MCP session. Validates that the target brand exists. Available only to super_admin users.",
16201
16540
  {
16202
- tenantId: import_zod60.z.string().describe("Target tenant identifier"),
16203
- brandId: import_zod60.z.string().describe("Target brand identifier within the tenant")
16541
+ tenantId: import_zod63.z.string().describe("Target tenant identifier"),
16542
+ brandId: import_zod63.z.string().describe("Target brand identifier within the tenant")
16204
16543
  },
16205
16544
  async ({ tenantId, brandId }) => {
16206
16545
  const ctx = await buildContext(session, brandId);
@@ -16237,12 +16576,60 @@ function registerContextTools(server, session) {
16237
16576
  }
16238
16577
  );
16239
16578
  }
16579
+ if (session.brands && session.brands.length > 1) {
16580
+ server.tool(
16581
+ "select_brand",
16582
+ "Switch the active brand within your tenant. Use when you have multi-brand access (agency mode) and want to work with a different brand without passing brandId on every tool call. Does NOT change tenant.",
16583
+ {
16584
+ brandId: import_zod63.z.string().describe("Target brand identifier (must be in your accessible brands list)")
16585
+ },
16586
+ async ({ brandId }) => {
16587
+ const tenantId = session.requireTenant();
16588
+ const ctx = await buildContext(session, brandId);
16589
+ const result = await dispatchWithContract({
16590
+ contract: brandContextSetterContract,
16591
+ helper: brandContextSetter,
16592
+ callable: callBrandContextSetter,
16593
+ input: { tenantId, brandId },
16594
+ ctx
16595
+ });
16596
+ let payload;
16597
+ if (result.state === "success" && result.structuredOutput) {
16598
+ const out = result.structuredOutput;
16599
+ if (out.ok) {
16600
+ try {
16601
+ session.setBrand(out.brandId);
16602
+ payload = {
16603
+ ok: true,
16604
+ tenant: out.tenantId,
16605
+ brand: out.brandNombre ?? out.brandId,
16606
+ dominio: out.brandDominio
16607
+ };
16608
+ } catch (err) {
16609
+ const msg = err instanceof Error ? err.message : String(err);
16610
+ payload = { ok: false, code: "BRAND_NOT_ACCESSIBLE_BY_USER", mensaje: msg };
16611
+ }
16612
+ } else {
16613
+ payload = out;
16614
+ }
16615
+ } else {
16616
+ payload = {
16617
+ ok: false,
16618
+ state: result.state,
16619
+ mensaje: result.text,
16620
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
16621
+ };
16622
+ }
16623
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
16624
+ }
16625
+ );
16626
+ }
16240
16627
  if (!session.serviceAccountPath) {
16241
16628
  server.tool(
16242
16629
  "connect_account",
16243
16630
  "Conecta tu cuenta de Ponch para acceder a tus datos. Primero visita https://atpops.vercel.app/auth/mcp-connect para obtener el codigo.",
16244
16631
  {
16245
- code: import_zod60.z.string().describe("Codigo de conexion de 8 caracteres (ej: ABCD-EFGH) obtenido de la pagina web")
16632
+ code: import_zod63.z.string().describe("Codigo de conexion de 8 caracteres (ej: ABCD-EFGH) obtenido de la pagina web")
16246
16633
  },
16247
16634
  async ({ code }) => {
16248
16635
  try {
@@ -16316,13 +16703,13 @@ function registerContextTools(server, session) {
16316
16703
  }
16317
16704
 
16318
16705
  // src/tools/core.ts
16319
- var import_zod61 = require("zod");
16706
+ var import_zod64 = require("zod");
16320
16707
  function registerCoreTools(server, session) {
16321
16708
  server.tool(
16322
16709
  "get_business_summary",
16323
16710
  "Resumen general del negocio: metricas clave, contenido pendiente, estado de conexiones, alertas.",
16324
16711
  {
16325
- brandId: import_zod61.z.string().optional().describe("ID de la brand. Si no se pasa, usa la brand del contexto.")
16712
+ brandId: import_zod64.z.string().optional().describe("ID de la brand. Si no se pasa, usa la brand del contexto.")
16326
16713
  },
16327
16714
  async ({ brandId: inputBrandId }) => {
16328
16715
  const tenantId = session.requireTenant();
@@ -16398,7 +16785,7 @@ function registerCoreTools(server, session) {
16398
16785
  "get_pending_actions",
16399
16786
  "Lista todo lo que necesita atencion: contenido por aprobar, fotos sin procesar.",
16400
16787
  {
16401
- brandId: import_zod61.z.string().optional().describe("ID de la brand")
16788
+ brandId: import_zod64.z.string().optional().describe("ID de la brand")
16402
16789
  },
16403
16790
  async ({ brandId: inputBrandId }) => {
16404
16791
  const tenantId = session.requireTenant();
@@ -16436,77 +16823,13 @@ function registerCoreTools(server, session) {
16436
16823
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
16437
16824
  }
16438
16825
  );
16439
- server.tool(
16440
- "execute_action",
16441
- "Ejecuta una accion sobre contenido o foto: aprobar, rechazar, descartar foto, usar foto tal cual.",
16442
- {
16443
- accion: import_zod61.z.enum(["aprobar", "rechazar", "descartar_foto", "usar_foto_tal_cual"]).describe("Accion a ejecutar"),
16444
- targetId: import_zod61.z.string().describe("ID del contenido o foto"),
16445
- motivo: import_zod61.z.string().optional().describe("Motivo del rechazo (requerido para rechazar)")
16446
- },
16447
- async ({ accion, targetId, motivo }) => {
16448
- session.requireTenant();
16449
- if (accion === "aprobar") {
16450
- const doc = await readDoc("marketing_contenido", targetId);
16451
- if (!doc) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Contenido no encontrado" }) }] };
16452
- if (!esTransicionValida(doc.estado, ESTADO_CONTENIDO.APROBADO)) {
16453
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `No se puede aprobar desde estado "${doc.estado}"` }) }] };
16454
- }
16455
- await updateDoc("marketing_contenido", targetId, {
16456
- estado: ESTADO_CONTENIDO.APROBADO,
16457
- aprobadoAt: serverTimestamp(),
16458
- aprobadoPorId: "mcp-cowork",
16459
- aprobadoPorNombre: "Cowork AI"
16460
- });
16461
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, nuevoEstado: ESTADO_CONTENIDO.APROBADO }) }] };
16462
- }
16463
- if (accion === "rechazar") {
16464
- if (!motivo) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Motivo es requerido para rechazar" }) }] };
16465
- const doc = await readDoc("marketing_contenido", targetId);
16466
- if (!doc) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Contenido no encontrado" }) }] };
16467
- if (!esTransicionValida(doc.estado, ESTADO_CONTENIDO.RECHAZADO)) {
16468
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `No se puede rechazar desde estado "${doc.estado}"` }) }] };
16469
- }
16470
- await updateDoc("marketing_contenido", targetId, {
16471
- estado: ESTADO_CONTENIDO.RECHAZADO,
16472
- rechazadoAt: serverTimestamp(),
16473
- rechazadoMotivo: motivo
16474
- });
16475
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, nuevoEstado: ESTADO_CONTENIDO.RECHAZADO }) }] };
16476
- }
16477
- if (accion === "descartar_foto") {
16478
- const doc = await readDoc("marketing_fotos", targetId);
16479
- if (!doc) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Foto no encontrada" }) }] };
16480
- if (!validarTransicionFoto(doc.estado, ESTADO_FOTO.DESCARTADA)) {
16481
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `No se puede descartar desde estado "${doc.estado}"` }) }] };
16482
- }
16483
- await updateDoc("marketing_fotos", targetId, { estado: ESTADO_FOTO.DESCARTADA });
16484
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, nuevoEstado: ESTADO_FOTO.DESCARTADA }) }] };
16485
- }
16486
- if (accion === "usar_foto_tal_cual") {
16487
- const doc = await readDoc("marketing_fotos", targetId);
16488
- if (!doc) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Foto no encontrada" }) }] };
16489
- if (!validarTransicionFoto(doc.estado, ESTADO_FOTO.EDITADA)) {
16490
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `No se puede marcar como editada desde estado "${doc.estado}"` }) }] };
16491
- }
16492
- await updateDoc("marketing_fotos", targetId, {
16493
- estado: ESTADO_FOTO.EDITADA,
16494
- archivoEditado: doc.archivoOriginal,
16495
- // usa la original como editada
16496
- fechaEdicion: serverTimestamp()
16497
- });
16498
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, nuevoEstado: ESTADO_FOTO.EDITADA }) }] };
16499
- }
16500
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Accion "${accion}" no reconocida` }) }] };
16501
- }
16502
- );
16503
16826
  }
16504
16827
 
16505
16828
  // src/tools/marketing.ts
16506
- var import_zod64 = require("zod");
16829
+ var import_zod67 = require("zod");
16507
16830
 
16508
16831
  // src/tools/marketing/photos.ts
16509
- var import_zod62 = require("zod");
16832
+ var import_zod65 = require("zod");
16510
16833
 
16511
16834
  // src/services/marketingEmbeddings.ts
16512
16835
  var import_google_auth_library = require("google-auth-library");
@@ -16802,11 +17125,11 @@ REGLAS:
16802
17125
 
16803
17126
  USAR: antes de generar contenido de cualquier slot del calendario.`,
16804
17127
  {
16805
- brandId: import_zod62.z.string().optional().describe("ID de la brand"),
16806
- keyword: import_zod62.z.string().describe("Keyword del slot"),
16807
- plataforma: import_zod62.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
16808
- fecha: import_zod62.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
16809
- limit: import_zod62.z.number().int().min(1).max(10).default(5)
17128
+ brandId: import_zod65.z.string().optional().describe("ID de la brand"),
17129
+ keyword: import_zod65.z.string().describe("Keyword del slot"),
17130
+ plataforma: import_zod65.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
17131
+ fecha: import_zod65.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
17132
+ limit: import_zod65.z.number().int().min(1).max(10).default(5)
16810
17133
  },
16811
17134
  async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
16812
17135
  const tenantId = session.requireTenant();
@@ -16854,7 +17177,7 @@ DESPUES de ver la foto, decide:
16854
17177
 
16855
17178
  Luego llama execute_photo_edit con tu analisis y prompt.`,
16856
17179
  {
16857
- fotoId: import_zod62.z.string().describe("ID de la foto")
17180
+ fotoId: import_zod65.z.string().describe("ID de la foto")
16858
17181
  },
16859
17182
  async ({ fotoId }) => {
16860
17183
  const tenantId = session.requireTenant();
@@ -16902,14 +17225,14 @@ Retorna la foto editada para que la revises. Si no te gusta:
16902
17225
 
16903
17226
  Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
16904
17227
  {
16905
- fotoId: import_zod62.z.string(),
16906
- prompt: import_zod62.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
16907
- acciones: import_zod62.z.array(import_zod62.z.enum(["edit_background", "none"])),
16908
- descripcion: import_zod62.z.string().describe("Descripcion semantica en espanol"),
16909
- tipo: import_zod62.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
16910
- tagsPrimarios: import_zod62.z.array(import_zod62.z.string()),
16911
- tagsSecundarios: import_zod62.z.array(import_zod62.z.string()),
16912
- tagsContexto: import_zod62.z.array(import_zod62.z.string())
17228
+ fotoId: import_zod65.z.string(),
17229
+ prompt: import_zod65.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
17230
+ acciones: import_zod65.z.array(import_zod65.z.enum(["edit_background", "none"])),
17231
+ descripcion: import_zod65.z.string().describe("Descripcion semantica en espanol"),
17232
+ tipo: import_zod65.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
17233
+ tagsPrimarios: import_zod65.z.array(import_zod65.z.string()),
17234
+ tagsSecundarios: import_zod65.z.array(import_zod65.z.string()),
17235
+ tagsContexto: import_zod65.z.array(import_zod65.z.string())
16913
17236
  },
16914
17237
  async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
16915
17238
  const tenantId = session.requireTenant();
@@ -16977,7 +17300,7 @@ Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si
16977
17300
  "get_photo_edit_status",
16978
17301
  "Return full photo edit status: photo state + iteration counter + active lock info + brand credits balance + actionable decision (puedeEditar) with reason if not. Use BEFORE retrying execute_photo_edit when you got a PHOTO_LOCKED error, or to show the tenant remaining iterations and credits.",
16979
17302
  {
16980
- fotoId: import_zod62.z.string().describe("Photo identifier")
17303
+ fotoId: import_zod65.z.string().describe("Photo identifier")
16981
17304
  },
16982
17305
  async ({ fotoId }) => {
16983
17306
  const tenantId = session.requireTenant();
@@ -17022,11 +17345,11 @@ Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si
17022
17345
  "find_products_for_content",
17023
17346
  `[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.`,
17024
17347
  {
17025
- brandId: import_zod62.z.string().optional().describe("ID de la brand"),
17026
- contexto: import_zod62.z.string().describe("Parrafo, keyword o intencion del contenido"),
17027
- fecha: import_zod62.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
17028
- limit: import_zod62.z.number().int().min(1).max(10).default(5),
17029
- diversidad: import_zod62.z.boolean().default(true)
17348
+ brandId: import_zod65.z.string().optional().describe("ID de la brand"),
17349
+ contexto: import_zod65.z.string().describe("Parrafo, keyword o intencion del contenido"),
17350
+ fecha: import_zod65.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
17351
+ limit: import_zod65.z.number().int().min(1).max(10).default(5),
17352
+ diversidad: import_zod65.z.boolean().default(true)
17030
17353
  },
17031
17354
  async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
17032
17355
  console.warn(
@@ -17103,10 +17426,10 @@ RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no
17103
17426
 
17104
17427
  USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
17105
17428
  {
17106
- brandId: import_zod62.z.string().optional().describe("ID de la brand"),
17107
- plataforma: import_zod62.z.string().describe("gbp | instagram | shopify_blog"),
17108
- tipoContenido: import_zod62.z.string().describe("post | carousel | story | blog"),
17109
- keyword: import_zod62.z.string().describe("Keyword del slot")
17429
+ brandId: import_zod65.z.string().optional().describe("ID de la brand"),
17430
+ plataforma: import_zod65.z.string().describe("gbp | instagram | shopify_blog"),
17431
+ tipoContenido: import_zod65.z.string().describe("post | carousel | story | blog"),
17432
+ keyword: import_zod65.z.string().describe("Keyword del slot")
17110
17433
  },
17111
17434
  async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
17112
17435
  const tenantId = session.requireTenant();
@@ -17151,15 +17474,15 @@ USE WHEN: get_photos_for_slot returns few photos and the slot is not yet covered
17151
17474
 
17152
17475
  IMPORTANT \u2014 'razon' field: the tenant sees this in the app. Write in friendly, clear language, NOT technical. BAD example: "get_photos_for_slot returned 0 photos for shopify_blog". GOOD example: "No hay fotos de alcatraz para el blog del 8 de abril". Use the tenant's preferred language.`,
17153
17476
  {
17154
- brandId: import_zod62.z.string().optional().describe("Brand identifier (defaults to session brand)"),
17155
- semana: import_zod62.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD del lunes").describe("Week identifier (ISO date of Monday)"),
17156
- necesidades: import_zod62.z.array(
17157
- import_zod62.z.object({
17158
- tema: import_zod62.z.string(),
17159
- keyword: import_zod62.z.string(),
17160
- cantidadSugerida: import_zod62.z.number().int().positive(),
17161
- razon: import_zod62.z.string(),
17162
- slotsAfectados: import_zod62.z.array(import_zod62.z.string()).optional().describe('Affected calendar slot refs (format "semana:N:slot:M")')
17477
+ brandId: import_zod65.z.string().optional().describe("Brand identifier (defaults to session brand)"),
17478
+ semana: import_zod65.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD del lunes").describe("Week identifier (ISO date of Monday)"),
17479
+ necesidades: import_zod65.z.array(
17480
+ import_zod65.z.object({
17481
+ tema: import_zod65.z.string(),
17482
+ keyword: import_zod65.z.string(),
17483
+ cantidadSugerida: import_zod65.z.number().int().positive(),
17484
+ razon: import_zod65.z.string(),
17485
+ slotsAfectados: import_zod65.z.array(import_zod65.z.string()).optional().describe('Affected calendar slot refs (format "semana:N:slot:M")')
17163
17486
  })
17164
17487
  ).min(1)
17165
17488
  },
@@ -17198,10 +17521,87 @@ IMPORTANT \u2014 'razon' field: the tenant sees this in the app. Write in friend
17198
17521
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
17199
17522
  }
17200
17523
  );
17524
+ server.tool(
17525
+ "discard_photo",
17526
+ "Discard a marketing photo (mark as descartada). Validates state transition. Use when the photo is no longer needed. Requires confirmation: first call returns state=pending_confirmation; pass confirm=true on the second call to execute.",
17527
+ {
17528
+ fotoId: import_zod65.z.string().describe("Photo identifier to discard"),
17529
+ brandId: import_zod65.z.string().optional().describe("Brand identifier (defaults to session brand)"),
17530
+ confirm: import_zod65.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
17531
+ },
17532
+ async ({ fotoId, brandId: inputBrandId, confirm }) => {
17533
+ const tenantId = session.requireTenant();
17534
+ const brandId = inputBrandId ?? session.requireBrand();
17535
+ const ctx = await buildContext(session, brandId, { confirmationGranted: confirm === true });
17536
+ const actor = {
17537
+ uid: ctx.user.uid,
17538
+ nombre: ctx.user.nombre ?? ctx.user.uid,
17539
+ clientType: ctx.user.clientType ?? "mcp_client",
17540
+ clientMetadata: ctx.user.clientMetadata ?? null
17541
+ };
17542
+ const result = await dispatchWithContract({
17543
+ contract: photoDescarterContract,
17544
+ helper: photoDescarter,
17545
+ callable: callPhotoDescarter,
17546
+ input: { tenantId, fotoId, actor },
17547
+ ctx
17548
+ });
17549
+ let payload;
17550
+ if (result.state === "success" && result.structuredOutput) {
17551
+ payload = result.structuredOutput;
17552
+ } else {
17553
+ payload = {
17554
+ ok: false,
17555
+ state: result.state,
17556
+ mensaje: result.text,
17557
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
17558
+ };
17559
+ }
17560
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
17561
+ }
17562
+ );
17563
+ server.tool(
17564
+ "use_photo_as_is",
17565
+ "Mark a photo as editada using the original file (no Gemini edit). Validates state transition (procesando \u2192 editada). Use when the original photo is already good enough for publication.",
17566
+ {
17567
+ fotoId: import_zod65.z.string().describe("Photo identifier to mark as edited"),
17568
+ brandId: import_zod65.z.string().optional().describe("Brand identifier (defaults to session brand)")
17569
+ },
17570
+ async ({ fotoId, brandId: inputBrandId }) => {
17571
+ const tenantId = session.requireTenant();
17572
+ const brandId = inputBrandId ?? session.requireBrand();
17573
+ const ctx = await buildContext(session, brandId);
17574
+ const actor = {
17575
+ uid: ctx.user.uid,
17576
+ nombre: ctx.user.nombre ?? ctx.user.uid,
17577
+ clientType: ctx.user.clientType ?? "mcp_client",
17578
+ clientMetadata: ctx.user.clientMetadata ?? null
17579
+ };
17580
+ const result = await dispatchWithContract({
17581
+ contract: photoEditadaMarkerContract,
17582
+ helper: photoEditadaMarker,
17583
+ callable: callPhotoEditadaMarker,
17584
+ input: { tenantId, fotoId, actor },
17585
+ ctx
17586
+ });
17587
+ let payload;
17588
+ if (result.state === "success" && result.structuredOutput) {
17589
+ payload = result.structuredOutput;
17590
+ } else {
17591
+ payload = {
17592
+ ok: false,
17593
+ state: result.state,
17594
+ mensaje: result.text,
17595
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
17596
+ };
17597
+ }
17598
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
17599
+ }
17600
+ );
17201
17601
  }
17202
17602
 
17203
17603
  // src/tools/marketing/content.ts
17204
- var import_zod63 = require("zod");
17604
+ var import_zod66 = require("zod");
17205
17605
  var _logOverride = null;
17206
17606
  async function logToMcpLogs(entry) {
17207
17607
  if (_logOverride) return _logOverride(entry);
@@ -17214,22 +17614,22 @@ async function logToMcpLogs(entry) {
17214
17614
  } catch {
17215
17615
  }
17216
17616
  }
17217
- var IncludeSchema2 = import_zod63.z.object({
17218
- products: import_zod63.z.boolean().default(true),
17219
- collections: import_zod63.z.boolean().default(true),
17220
- articles: import_zod63.z.boolean().default(true),
17221
- pages: import_zod63.z.boolean().default(false)
17617
+ var IncludeSchema2 = import_zod66.z.object({
17618
+ products: import_zod66.z.boolean().default(true),
17619
+ collections: import_zod66.z.boolean().default(true),
17620
+ articles: import_zod66.z.boolean().default(true),
17621
+ pages: import_zod66.z.boolean().default(false)
17222
17622
  }).default({
17223
17623
  products: true,
17224
17624
  collections: true,
17225
17625
  articles: true,
17226
17626
  pages: false
17227
17627
  });
17228
- var LimitSchema2 = import_zod63.z.object({
17229
- products: import_zod63.z.number().int().min(0).max(20).default(5),
17230
- collections: import_zod63.z.number().int().min(0).max(10).default(3),
17231
- articles: import_zod63.z.number().int().min(0).max(20).default(5),
17232
- pages: import_zod63.z.number().int().min(0).max(10).default(2)
17628
+ var LimitSchema2 = import_zod66.z.object({
17629
+ products: import_zod66.z.number().int().min(0).max(20).default(5),
17630
+ collections: import_zod66.z.number().int().min(0).max(10).default(3),
17631
+ articles: import_zod66.z.number().int().min(0).max(20).default(5),
17632
+ pages: import_zod66.z.number().int().min(0).max(10).default(2)
17233
17633
  }).default({ products: 5, collections: 3, articles: 5, pages: 2 });
17234
17634
  function registerContentTools(server, session) {
17235
17635
  server.tool(
@@ -17257,13 +17657,13 @@ MODOS (parametro mode):
17257
17657
 
17258
17658
  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.`,
17259
17659
  {
17260
- brandId: import_zod63.z.string().optional().describe("ID de la brand"),
17261
- contexto: import_zod63.z.string().min(1).describe("Parrafo, keyword o intencion"),
17262
- fecha: import_zod63.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
17660
+ brandId: import_zod66.z.string().optional().describe("ID de la brand"),
17661
+ contexto: import_zod66.z.string().min(1).describe("Parrafo, keyword o intencion"),
17662
+ fecha: import_zod66.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
17263
17663
  include: IncludeSchema2.optional(),
17264
17664
  limit: LimitSchema2.optional(),
17265
- diversidad: import_zod63.z.boolean().default(true),
17266
- mode: import_zod63.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)")
17665
+ diversidad: import_zod66.z.boolean().default(true),
17666
+ mode: import_zod66.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)")
17267
17667
  },
17268
17668
  async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode }) => {
17269
17669
  const tenantId = session.requireTenant();
@@ -17474,8 +17874,8 @@ function registerMarketingTools(server, session) {
17474
17874
  "get_calendar",
17475
17875
  "Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
17476
17876
  {
17477
- brandId: import_zod64.z.string().optional().describe("ID de la brand"),
17478
- mes: import_zod64.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
17877
+ brandId: import_zod67.z.string().optional().describe("ID de la brand"),
17878
+ mes: import_zod67.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
17479
17879
  },
17480
17880
  async ({ brandId: inputBrandId, mes }) => {
17481
17881
  const tenantId = session.requireTenant();
@@ -17513,7 +17913,7 @@ function registerMarketingTools(server, session) {
17513
17913
  "get_seo_snapshot",
17514
17914
  "Read the latest Semrush SEO snapshot for a brand: rank, top keywords, opportunities, competitors.",
17515
17915
  {
17516
- brandId: import_zod64.z.string().optional().describe("Brand identifier within the tenant")
17916
+ brandId: import_zod67.z.string().optional().describe("Brand identifier within the tenant")
17517
17917
  },
17518
17918
  async ({ brandId: inputBrandId }) => {
17519
17919
  const tenantId = session.requireTenant();
@@ -17549,8 +17949,8 @@ function registerMarketingTools(server, session) {
17549
17949
  "get_photo_gallery",
17550
17950
  "List marketing photos for a brand with per-state counts. Optionally filter by state.",
17551
17951
  {
17552
- brandId: import_zod64.z.string().optional().describe("Brand identifier within the tenant"),
17553
- estado: import_zod64.z.string().optional().describe("Optional photo state filter (nueva, procesando, editada, usada, descartada, error)")
17952
+ brandId: import_zod67.z.string().optional().describe("Brand identifier within the tenant"),
17953
+ estado: import_zod67.z.string().optional().describe("Optional photo state filter (nueva, procesando, editada, usada, descartada, error)")
17554
17954
  },
17555
17955
  async ({ brandId: inputBrandId, estado }) => {
17556
17956
  const tenantId = session.requireTenant();
@@ -17586,7 +17986,7 @@ function registerMarketingTools(server, session) {
17586
17986
  "generate_marketing_plan",
17587
17987
  "Aggregate the data needed to generate a strategic marketing plan: SEO snapshot + products. The LLM uses the system prompt to generate the plan from this payload.",
17588
17988
  {
17589
- brandId: import_zod64.z.string().optional().describe("ID de la brand")
17989
+ brandId: import_zod67.z.string().optional().describe("ID de la brand")
17590
17990
  },
17591
17991
  async ({ brandId: inputBrandId }) => {
17592
17992
  const tenantId = session.requireTenant();
@@ -17600,18 +18000,24 @@ function registerMarketingTools(server, session) {
17600
18000
  "save_marketing_plan",
17601
18001
  "Save a marketing plan to the brand configuration. Writes to tenants/{tenantId}/marketing_config/{brandId}.plan. If the plan object includes a blogStrategy field, it is extracted and stored at brand level (not nested inside plan).",
17602
18002
  {
17603
- brandId: import_zod64.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
17604
- plan: import_zod64.z.record(import_zod64.z.string(), import_zod64.z.unknown()).describe("Full marketing plan object. Common fields: keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, coleccionesPriorizadas, blogStrategy.")
18003
+ brandId: import_zod67.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
18004
+ plan: import_zod67.z.record(import_zod67.z.string(), import_zod67.z.unknown()).describe("Full marketing plan object. Common fields: keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, coleccionesPriorizadas, blogStrategy.")
17605
18005
  },
17606
18006
  async ({ brandId: inputBrandId, plan }) => {
17607
18007
  const tenantId = session.requireTenant();
17608
18008
  const brandId = inputBrandId ?? session.requireBrand();
17609
18009
  const ctx = await buildContext(session, brandId);
18010
+ const actor = {
18011
+ uid: ctx.user.uid,
18012
+ nombre: ctx.user.nombre ?? ctx.user.uid,
18013
+ clientType: ctx.user.clientType ?? "mcp_client",
18014
+ clientMetadata: ctx.user.clientMetadata ?? null
18015
+ };
17610
18016
  const result = await dispatchWithContract({
17611
18017
  contract: planWriterSaveContract,
17612
18018
  helper: ({ db, ...rest }) => planWriter.save({ db, ...rest }),
17613
18019
  callable: callPlanWriterSave,
17614
- input: { tenantId, brandId, plan },
18020
+ input: { tenantId, brandId, plan, actor },
17615
18021
  ctx
17616
18022
  });
17617
18023
  const payload = result.state === "success" ? result.structuredOutput : {
@@ -17629,9 +18035,9 @@ function registerMarketingTools(server, session) {
17629
18035
  "update_marketing_plan_field",
17630
18036
  "Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
17631
18037
  {
17632
- brandId: import_zod64.z.string().optional().describe("ID de la brand"),
17633
- field: import_zod64.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
17634
- value: import_zod64.z.unknown().describe("Valor del campo")
18038
+ brandId: import_zod67.z.string().optional().describe("ID de la brand"),
18039
+ field: import_zod67.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
18040
+ value: import_zod67.z.unknown().describe("Valor del campo")
17635
18041
  },
17636
18042
  async ({ brandId: inputBrandId, field, value }) => {
17637
18043
  const tenantId = session.requireTenant();
@@ -17644,7 +18050,7 @@ function registerMarketingTools(server, session) {
17644
18050
  "generate_brand_brief",
17645
18051
  "Aggregate all business data needed to generate a Brand Brief: Shopify (products, collections, orders, shop info), SEO snapshot, GBP profiles, scraped site_content, brand config, tenant locations. Read-only \u2014 does NOT write the brief. After receiving the payload, generate the brief content and save it via save_brand_brief.",
17646
18052
  {
17647
- brandId: import_zod64.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
18053
+ brandId: import_zod67.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
17648
18054
  },
17649
18055
  async ({ brandId: inputBrandId }) => {
17650
18056
  const tenantId = session.requireTenant();
@@ -17677,8 +18083,8 @@ function registerMarketingTools(server, session) {
17677
18083
  "save_brand_brief",
17678
18084
  "Save the Brand Brief at tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Partial merge \u2014 only the brandBrief field is overwritten.",
17679
18085
  {
17680
- brandId: import_zod64.z.string().optional().describe("ID de la brand"),
17681
- brandBrief: import_zod64.z.record(import_zod64.z.string(), import_zod64.z.unknown()).describe("Full Brand Brief object.")
18086
+ brandId: import_zod67.z.string().optional().describe("ID de la brand"),
18087
+ brandBrief: import_zod67.z.record(import_zod67.z.string(), import_zod67.z.unknown()).describe("Full Brand Brief object.")
17682
18088
  },
17683
18089
  async ({ brandId: inputBrandId, brandBrief }) => {
17684
18090
  const tenantId = session.requireTenant();
@@ -17704,19 +18110,25 @@ function registerMarketingTools(server, session) {
17704
18110
  "generate_weekly_content",
17705
18111
  "Aggregate calendar + photos + plan data to generate the week's content. The LLM generates with the system prompt, then uses save_generated_content to persist.",
17706
18112
  {
17707
- brandId: import_zod64.z.string().optional().describe("ID de la brand"),
17708
- semana: import_zod64.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
17709
- modo: import_zod64.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
18113
+ brandId: import_zod67.z.string().optional().describe("ID de la brand"),
18114
+ semana: import_zod67.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
18115
+ modo: import_zod67.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
17710
18116
  },
17711
18117
  async ({ brandId: inputBrandId, semana, modo }) => {
17712
18118
  const tenantId = session.requireTenant();
17713
18119
  const brandId = inputBrandId ?? session.requireBrand();
17714
18120
  const ctx = await buildContext(session, brandId);
18121
+ const actor = {
18122
+ uid: ctx.user.uid,
18123
+ nombre: ctx.user.nombre ?? ctx.user.uid,
18124
+ clientType: ctx.user.clientType ?? "mcp_client",
18125
+ clientMetadata: ctx.user.clientMetadata ?? null
18126
+ };
17715
18127
  const result = await dispatchWithContract({
17716
18128
  contract: weeklyContentBuilderContract,
17717
18129
  helper: weeklyContentBuilder,
17718
18130
  callable: callWeeklyContentBuilder,
17719
- input: { tenantId, brandId, semana, modo },
18131
+ input: { tenantId, brandId, semana, modo, actor },
17720
18132
  ctx
17721
18133
  });
17722
18134
  const payload = result.state === "success" ? result.structuredOutput?.payload ?? result.structuredOutput : {
@@ -17734,25 +18146,31 @@ function registerMarketingTools(server, session) {
17734
18146
 
17735
18147
  IMPORTANT: If you selected a photo with get_photos_for_slot, ALWAYS pass fotoId here. Without fotoId the post will be published without an image.`,
17736
18148
  {
17737
- brandId: import_zod64.z.string().optional().describe("ID de la brand"),
17738
- plataforma: import_zod64.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
17739
- tipo: import_zod64.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
17740
- keyword: import_zod64.z.string().optional().describe("Keyword target"),
17741
- languageCode: import_zod64.z.string().optional().describe("Idioma (es/en)"),
17742
- fotoId: import_zod64.z.string().optional().describe("ID de la foto a asociar"),
17743
- datos: import_zod64.z.record(import_zod64.z.string(), import_zod64.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
17744
- calendarioItemRef: import_zod64.z.string().optional().describe("Referencia al item del calendario")
18149
+ brandId: import_zod67.z.string().optional().describe("ID de la brand"),
18150
+ plataforma: import_zod67.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
18151
+ tipo: import_zod67.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
18152
+ keyword: import_zod67.z.string().optional().describe("Keyword target"),
18153
+ languageCode: import_zod67.z.string().optional().describe("Idioma (es/en)"),
18154
+ fotoId: import_zod67.z.string().optional().describe("ID de la foto a asociar"),
18155
+ datos: import_zod67.z.record(import_zod67.z.string(), import_zod67.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
18156
+ calendarioItemRef: import_zod67.z.string().optional().describe("Referencia al item del calendario")
17745
18157
  },
17746
18158
  async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
17747
18159
  const tenantId = session.requireTenant();
17748
18160
  const brandId = inputBrandId ?? session.requireBrand();
17749
18161
  try {
17750
18162
  const ctx = await buildContext(session, brandId);
18163
+ const actor = {
18164
+ uid: ctx.user.uid,
18165
+ nombre: ctx.user.nombre ?? ctx.user.uid,
18166
+ clientType: ctx.user.clientType ?? "mcp_client",
18167
+ clientMetadata: ctx.user.clientMetadata ?? null
18168
+ };
17751
18169
  const result = await dispatchWithContract({
17752
18170
  contract: contenidoWriterContract,
17753
18171
  helper: (input) => contenidoWriter({ ...input, linkPipeline: linkContenidoAPasoPipeline }),
17754
18172
  callable: callContenidoWriter,
17755
- input: { tenantId, brandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef },
18173
+ input: { tenantId, brandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef, actor },
17756
18174
  ctx
17757
18175
  });
17758
18176
  const payload = result.state === "success" ? result.structuredOutput : {
@@ -17777,13 +18195,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
17777
18195
  NO puede cambiar: tenantId, brandId, id (inmutables).
17778
18196
  Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
17779
18197
  {
17780
- contenidoId: import_zod64.z.string().describe("ID del doc en marketing_contenido"),
17781
- datos: import_zod64.z.record(import_zod64.z.string(), import_zod64.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
17782
- fotoId: import_zod64.z.string().nullable().optional().describe("Actualizar foto asociada"),
17783
- keyword: import_zod64.z.string().nullable().optional().describe("Actualizar keyword"),
17784
- languageCode: import_zod64.z.string().optional().describe("Actualizar idioma"),
17785
- estado: import_zod64.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
17786
- calendarioItemRef: import_zod64.z.string().nullable().optional().describe("Vincular a un slot del calendario")
18198
+ contenidoId: import_zod67.z.string().describe("ID del doc en marketing_contenido"),
18199
+ datos: import_zod67.z.record(import_zod67.z.string(), import_zod67.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
18200
+ fotoId: import_zod67.z.string().nullable().optional().describe("Actualizar foto asociada"),
18201
+ keyword: import_zod67.z.string().nullable().optional().describe("Actualizar keyword"),
18202
+ languageCode: import_zod67.z.string().optional().describe("Actualizar idioma"),
18203
+ estado: import_zod67.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
18204
+ calendarioItemRef: import_zod67.z.string().nullable().optional().describe("Vincular a un slot del calendario")
17787
18205
  },
17788
18206
  async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
17789
18207
  const tenantId = session.requireTenant();
@@ -17817,19 +18235,19 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
17817
18235
  "add_calendar_slot",
17818
18236
  "Add a NEW slot to the editorial calendar. To modify an existing slot use update_calendar_slot instead.",
17819
18237
  {
17820
- brandId: import_zod64.z.string().describe('Brand ID (e.g. "ponch", "teleglobos").'),
17821
- mes: import_zod64.z.string().describe('Calendar month in YYYY-MM format (e.g. "2026-05").'),
17822
- semana: import_zod64.z.number().describe("Week number within the month (1-5)."),
17823
- slot: import_zod64.z.object({
17824
- dia: import_zod64.z.string().describe("Slot date in YYYY-MM-DD format. Must fall within the week's fechaInicio/fechaFin range."),
17825
- plataforma: import_zod64.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target publishing platform."),
17826
- tipo: import_zod64.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Content type."),
17827
- keyword: import_zod64.z.string().describe("Primary keyword for the content."),
17828
- tema: import_zod64.z.string().optional().describe("Content topic/theme. OMIT this field if it does not apply. Do NOT send empty string or null."),
17829
- productoId: import_zod64.z.string().optional().describe("Linked product ID. OMIT this field if no product is linked."),
17830
- estado: import_zod64.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe('Initial status. OMIT to use default "planificado".'),
17831
- locationId: import_zod64.z.string().optional().describe("GBP location ID \u2014 only when plataforma=gbp AND tenant is multi-location. OMIT otherwise."),
17832
- locationNombre: import_zod64.z.string().optional().describe("Human-readable GBP location name. OMIT if locationId is not provided.")
18238
+ brandId: import_zod67.z.string().describe('Brand ID (e.g. "ponch", "teleglobos").'),
18239
+ mes: import_zod67.z.string().describe('Calendar month in YYYY-MM format (e.g. "2026-05").'),
18240
+ semana: import_zod67.z.number().describe("Week number within the month (1-5)."),
18241
+ slot: import_zod67.z.object({
18242
+ dia: import_zod67.z.string().describe("Slot date in YYYY-MM-DD format. Must fall within the week's fechaInicio/fechaFin range."),
18243
+ plataforma: import_zod67.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target publishing platform."),
18244
+ tipo: import_zod67.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Content type."),
18245
+ keyword: import_zod67.z.string().describe("Primary keyword for the content."),
18246
+ tema: import_zod67.z.string().optional().describe("Content topic/theme. OMIT this field if it does not apply. Do NOT send empty string or null."),
18247
+ productoId: import_zod67.z.string().optional().describe("Linked product ID. OMIT this field if no product is linked."),
18248
+ estado: import_zod67.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe('Initial status. OMIT to use default "planificado".'),
18249
+ locationId: import_zod67.z.string().optional().describe("GBP location ID \u2014 only when plataforma=gbp AND tenant is multi-location. OMIT otherwise."),
18250
+ locationNombre: import_zod67.z.string().optional().describe("Human-readable GBP location name. OMIT if locationId is not provided.")
17833
18251
  }).describe("New slot data.")
17834
18252
  },
17835
18253
  async ({ brandId, mes, semana, slot }) => {
@@ -17855,27 +18273,27 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
17855
18273
  "update_calendar_slot",
17856
18274
  "MODIFY an EXISTING slot in the editorial calendar. To create a new slot use add_calendar_slot. If slotIndex does not exist, returns error SLOT_NOT_FOUND.",
17857
18275
  {
17858
- brandId: import_zod64.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
17859
- mes: import_zod64.z.string().describe("Calendar month in YYYY-MM format."),
17860
- semana: import_zod64.z.number().describe("Week number within the month (1-5)."),
17861
- slotIndex: import_zod64.z.number().describe("Slot index (0-based). Must point to an existing slot \u2014 to add new slots use add_calendar_slot."),
17862
- cambios: import_zod64.z.object({
17863
- dia: import_zod64.z.string().nullable().optional().describe("Slot date in YYYY-MM-DD. OMIT if not changing date. Use null to explicitly clear."),
17864
- plataforma: import_zod64.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional().describe("OMIT if not changing platform."),
17865
- tipo: import_zod64.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional().describe("OMIT if not changing content type."),
17866
- keyword: import_zod64.z.string().nullable().optional().describe("OMIT if not changing keyword."),
17867
- tema: import_zod64.z.string().nullable().optional().describe("OMIT if not changing topic."),
17868
- productoId: import_zod64.z.string().nullable().optional().describe("OMIT if not changing linked product. Use null to unlink."),
17869
- estado: import_zod64.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("OMIT if not changing status manually."),
17870
- contenidoRef: import_zod64.z.string().nullable().optional().describe("OMIT \u2014 managed by the system, not by callers."),
17871
- fotoIdAsignada: import_zod64.z.string().nullable().optional().describe("Use assign_photo_to_content for photos. OMIT here."),
17872
- notas: import_zod64.z.array(NotaCalendarioSchema).optional().describe("Append-only notes for the slot."),
17873
- locationId: import_zod64.z.string().nullable().optional().describe("GBP location ID \u2014 only when plataforma=gbp."),
17874
- locationNombre: import_zod64.z.string().nullable().optional().describe("Human-readable GBP location name (for UI).")
18276
+ brandId: import_zod67.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
18277
+ mes: import_zod67.z.string().describe("Calendar month in YYYY-MM format."),
18278
+ semana: import_zod67.z.number().describe("Week number within the month (1-5)."),
18279
+ slotIndex: import_zod67.z.number().describe("Slot index (0-based). Must point to an existing slot \u2014 to add new slots use add_calendar_slot."),
18280
+ cambios: import_zod67.z.object({
18281
+ dia: import_zod67.z.string().nullable().optional().describe("Slot date in YYYY-MM-DD. OMIT if not changing date. Use null to explicitly clear."),
18282
+ plataforma: import_zod67.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional().describe("OMIT if not changing platform."),
18283
+ tipo: import_zod67.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional().describe("OMIT if not changing content type."),
18284
+ keyword: import_zod67.z.string().nullable().optional().describe("OMIT if not changing keyword."),
18285
+ tema: import_zod67.z.string().nullable().optional().describe("OMIT if not changing topic."),
18286
+ productoId: import_zod67.z.string().nullable().optional().describe("OMIT if not changing linked product. Use null to unlink."),
18287
+ estado: import_zod67.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("OMIT if not changing status manually."),
18288
+ contenidoRef: import_zod67.z.string().nullable().optional().describe("OMIT \u2014 managed by the system, not by callers."),
18289
+ fotoIdAsignada: import_zod67.z.string().nullable().optional().describe("Use assign_photo_to_content for photos. OMIT here."),
18290
+ notas: import_zod67.z.array(NotaCalendarioSchema).optional().describe("Append-only notes for the slot."),
18291
+ locationId: import_zod67.z.string().nullable().optional().describe("GBP location ID \u2014 only when plataforma=gbp."),
18292
+ locationNombre: import_zod67.z.string().nullable().optional().describe("Human-readable GBP location name (for UI).")
17875
18293
  }).strict().describe("Fields to update on the slot. Only declared fields accepted; unknown fields are rejected. OMIT any field you are not changing."),
17876
- accionContenidoExistente: import_zod64.z.union([
17877
- import_zod64.z.enum(["descartar", "nuevo_slot", "mantener"]),
17878
- import_zod64.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
18294
+ accionContenidoExistente: import_zod67.z.union([
18295
+ import_zod67.z.enum(["descartar", "nuevo_slot", "mantener"]),
18296
+ import_zod67.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
17879
18297
  ]).optional().describe(
17880
18298
  'Required ONLY when the slot already has a contenidoRef AND the cambios touch semantic fields (keyword/tema/plataforma/tipo). In that case, the helper returns ACCION_CONTENIDO_EXISTENTE_REQUIRED with the 4 options listed below \u2014 ask the tenant which one to apply. OMIT this field in any other case. Do NOT send null. Valid values: "descartar" (mark existing content as discarded and apply changes to this slot) | "mover:semana:N:slot:M" (move existing content to target empty slot N/M and apply changes to origin slot) | "nuevo_slot" (do NOT touch this slot or its content; create a new slot same day with the changes \u2014 useful for multiple posts per day) | "mantener" (keep existing content here and apply changes anyway \u2014 typo-fix case where old content stays valid).'
17881
18299
  )
@@ -17920,9 +18338,9 @@ ESCRIBE EN DOS LUGARES:
17920
18338
 
17921
18339
  NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
17922
18340
  {
17923
- contenidoRef: import_zod64.z.string().describe("ID del doc en marketing_contenido"),
17924
- fotoId: import_zod64.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
17925
- calendarioItemRef: import_zod64.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
18341
+ contenidoRef: import_zod67.z.string().describe("ID del doc en marketing_contenido"),
18342
+ fotoId: import_zod67.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
18343
+ calendarioItemRef: import_zod67.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
17926
18344
  },
17927
18345
  async ({ contenidoRef, fotoId, calendarioItemRef }) => {
17928
18346
  const tenantId = session.requireTenant();
@@ -17946,15 +18364,16 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
17946
18364
  );
17947
18365
  server.tool(
17948
18366
  "approve_content",
17949
- "Approve marketing content for publication. Validates state transition (pendiente_aprobacion \u2192 aprobado).",
18367
+ "Approve marketing content for publication. Validates state transition (pendiente_aprobacion \u2192 aprobado). Requires confirmation: first call returns state=pending_confirmation; pass confirm=true on the second call to execute.",
17950
18368
  {
17951
- contenidoId: import_zod64.z.string().describe("Marketing content document id"),
17952
- brandId: import_zod64.z.string().optional().describe("Brand identifier (defaults to session brand)")
18369
+ contenidoId: import_zod67.z.string().describe("Marketing content document id"),
18370
+ brandId: import_zod67.z.string().optional().describe("Brand identifier (defaults to session brand)"),
18371
+ confirm: import_zod67.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
17953
18372
  },
17954
- async ({ contenidoId, brandId: inputBrandId }) => {
18373
+ async ({ contenidoId, brandId: inputBrandId, confirm }) => {
17955
18374
  const tenantId = session.requireTenant();
17956
18375
  const brandId = inputBrandId ?? session.requireBrand();
17957
- const ctx = await buildContext(session, brandId);
18376
+ const ctx = await buildContext(session, brandId, { confirmationGranted: confirm === true });
17958
18377
  const actor = {
17959
18378
  uid: ctx.user.uid,
17960
18379
  nombre: ctx.user.nombre ?? ctx.user.uid,
@@ -17985,16 +18404,17 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
17985
18404
  );
17986
18405
  server.tool(
17987
18406
  "reject_content",
17988
- "Reject marketing content with a reason. Validates state transition (pendiente_aprobacion \u2192 rechazado). Reversible via rechazado \u2192 editado \u2192 pendiente_aprobacion \u2192 aprobado.",
18407
+ "Reject marketing content with a reason. Validates state transition (pendiente_aprobacion \u2192 rechazado). Reversible via rechazado \u2192 editado \u2192 pendiente_aprobacion \u2192 aprobado. Requires confirmation: first call returns state=pending_confirmation; pass confirm=true on the second call to execute.",
17989
18408
  {
17990
- contenidoId: import_zod64.z.string().describe("Marketing content document id"),
17991
- motivo: import_zod64.z.string().describe("Reason for rejection (required, surfaced to tenant)"),
17992
- brandId: import_zod64.z.string().optional().describe("Brand identifier (defaults to session brand)")
18409
+ contenidoId: import_zod67.z.string().describe("Marketing content document id"),
18410
+ motivo: import_zod67.z.string().describe("Reason for rejection (required, surfaced to tenant)"),
18411
+ brandId: import_zod67.z.string().optional().describe("Brand identifier (defaults to session brand)"),
18412
+ confirm: import_zod67.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
17993
18413
  },
17994
- async ({ contenidoId, motivo, brandId: inputBrandId }) => {
18414
+ async ({ contenidoId, motivo, brandId: inputBrandId, confirm }) => {
17995
18415
  const tenantId = session.requireTenant();
17996
18416
  const brandId = inputBrandId ?? session.requireBrand();
17997
- const ctx = await buildContext(session, brandId);
18417
+ const ctx = await buildContext(session, brandId, { confirmationGranted: confirm === true });
17998
18418
  const actor = {
17999
18419
  uid: ctx.user.uid,
18000
18420
  nombre: ctx.user.nombre ?? ctx.user.uid,
@@ -18027,7 +18447,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
18027
18447
  "get_collections",
18028
18448
  "Read all canonical collections (Shopify/WordPress/web-only) for a brand with their 6 SEO fields, plus best practices and the strict schema for generating SEO suggestions.",
18029
18449
  {
18030
- brandId: import_zod64.z.string().optional().describe("Brand identifier within the tenant")
18450
+ brandId: import_zod67.z.string().optional().describe("Brand identifier within the tenant")
18031
18451
  },
18032
18452
  async ({ brandId: inputBrandId }) => {
18033
18453
  const tenantId = session.requireTenant();
@@ -18058,7 +18478,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
18058
18478
  "save_collection_suggestions",
18059
18479
  "Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
18060
18480
  {
18061
- brandId: import_zod64.z.string().optional().describe("ID de la brand"),
18481
+ brandId: import_zod67.z.string().optional().describe("ID de la brand"),
18062
18482
  suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
18063
18483
  },
18064
18484
  async ({ brandId: inputBrandId, suggestions }) => {
@@ -18084,7 +18504,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
18084
18504
  }
18085
18505
 
18086
18506
  // src/tools/martin.ts
18087
- var import_zod65 = require("zod");
18507
+ var import_zod68 = require("zod");
18088
18508
  function renderResult(result) {
18089
18509
  const payload = {
18090
18510
  state: result.state,
@@ -18100,16 +18520,16 @@ function registerMartinTools(server, session) {
18100
18520
  "recordar_memoria",
18101
18521
  "Save a user preference, rule, pattern or aversion that the system will respect in future interactions. Personal to the calling user (scope: self). Applied automatically in Martin's prompt for subsequent conversations.",
18102
18522
  {
18103
- tipo: import_zod65.z.enum(["preferencia", "regla", "patron", "aversion"]).describe(
18523
+ tipo: import_zod68.z.enum(["preferencia", "regla", "patron", "aversion"]).describe(
18104
18524
  "Memory type: 'preferencia' (personal preference), 'regla' (operational rule), 'patron' (observed behavioral pattern), 'aversion' (explicit do-not-do rule)."
18105
18525
  ),
18106
- categoria: import_zod65.z.enum(["compras", "produccion", "dispatch", "ventas", "marketing", "operacion", "personal", "delegacion"]).describe("Business area this memory applies to."),
18107
- contenido: import_zod65.z.string().min(3).max(500).describe("Memory content in the user's own words (3-500 chars)."),
18108
- origen: import_zod65.z.object({
18109
- tipo: import_zod65.z.enum(["conversacion", "feedback_card", "inferido_automatico"]),
18110
- conversacionId: import_zod65.z.string().nullable(),
18111
- cardId: import_zod65.z.string().nullable(),
18112
- rationale: import_zod65.z.array(import_zod65.z.string()).optional()
18526
+ categoria: import_zod68.z.enum(["compras", "produccion", "dispatch", "ventas", "marketing", "operacion", "personal", "delegacion"]).describe("Business area this memory applies to."),
18527
+ contenido: import_zod68.z.string().min(3).max(500).describe("Memory content in the user's own words (3-500 chars)."),
18528
+ origen: import_zod68.z.object({
18529
+ tipo: import_zod68.z.enum(["conversacion", "feedback_card", "inferido_automatico"]),
18530
+ conversacionId: import_zod68.z.string().nullable(),
18531
+ cardId: import_zod68.z.string().nullable(),
18532
+ rationale: import_zod68.z.array(import_zod68.z.string()).optional()
18113
18533
  }).describe("Where this memory was inferred from (conversation, card feedback, or auto-inferred).")
18114
18534
  },
18115
18535
  async ({ tipo, categoria, contenido, origen }) => {
@@ -18128,9 +18548,9 @@ function registerMartinTools(server, session) {
18128
18548
  "olvidar_memoria",
18129
18549
  "Archive a memory that no longer applies. The memory remains visible in settings but is no longer enforced in future interactions. Requires confirmation: the first call returns a confirmation prompt; pass confirm=true on the second call to execute.",
18130
18550
  {
18131
- memoriaId: import_zod65.z.string().min(1).describe("Memory document ID to archive."),
18132
- motivo: import_zod65.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
18133
- confirm: import_zod65.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
18551
+ memoriaId: import_zod68.z.string().min(1).describe("Memory document ID to archive."),
18552
+ motivo: import_zod68.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
18553
+ confirm: import_zod68.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
18134
18554
  },
18135
18555
  async ({ memoriaId, motivo, confirm }) => {
18136
18556
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
@@ -18148,25 +18568,25 @@ function registerMartinTools(server, session) {
18148
18568
  "programar_rutina",
18149
18569
  "Schedule a recurring or one-time task. Useful for monthly reports, reminders, scheduled posts, suggested purchases. Personal to the calling user (scope: self). Requires confirmation.",
18150
18570
  {
18151
- uidDestinatario: import_zod65.z.string().optional().describe("User ID who should receive the routine output. OMIT to default to the calling user (most common case)."),
18152
- tipo: import_zod65.z.enum(["reporte", "recordatorio", "accion_delegada", "publicacion", "compra_sugerida"]).describe("Routine type from canonical TipoRutinaEnum."),
18153
- frecuencia: import_zod65.z.enum(["diaria", "semanal", "quincenal", "mensual", "trimestral", "puntual"]).describe("Execution cadence."),
18154
- config: import_zod65.z.object({
18155
- diaSemana: import_zod65.z.number().int().min(0).max(6).nullable(),
18156
- diaMes: import_zod65.z.number().int().min(1).max(31).nullable(),
18157
- hora: import_zod65.z.string().regex(/^\d{2}:\d{2}$/),
18158
- fechaPuntual: import_zod65.z.string().datetime({ offset: true }).nullable()
18571
+ uidDestinatario: import_zod68.z.string().optional().describe("User ID who should receive the routine output. OMIT to default to the calling user (most common case)."),
18572
+ tipo: import_zod68.z.enum(["reporte", "recordatorio", "accion_delegada", "publicacion", "compra_sugerida"]).describe("Routine type from canonical TipoRutinaEnum."),
18573
+ frecuencia: import_zod68.z.enum(["diaria", "semanal", "quincenal", "mensual", "trimestral", "puntual"]).describe("Execution cadence."),
18574
+ config: import_zod68.z.object({
18575
+ diaSemana: import_zod68.z.number().int().min(0).max(6).nullable(),
18576
+ diaMes: import_zod68.z.number().int().min(1).max(31).nullable(),
18577
+ hora: import_zod68.z.string().regex(/^\d{2}:\d{2}$/),
18578
+ fechaPuntual: import_zod68.z.string().datetime({ offset: true }).nullable()
18159
18579
  }).describe("Schedule configuration. Match fields to the frecuencia (semanal needs diaSemana; mensual needs diaMes; puntual needs fechaPuntual)."),
18160
- accion: import_zod65.z.object({
18161
- tool: import_zod65.z.string().min(1),
18162
- params: import_zod65.z.record(import_zod65.z.string(), import_zod65.z.unknown())
18580
+ accion: import_zod68.z.object({
18581
+ tool: import_zod68.z.string().min(1),
18582
+ params: import_zod68.z.record(import_zod68.z.string(), import_zod68.z.unknown())
18163
18583
  }).describe("Action to execute on each fire (MCP tool name + params)."),
18164
- origen: import_zod65.z.object({
18165
- tipo: import_zod65.z.enum(["conversacion", "feedback_card", "configurada_explicito"]),
18166
- conversacionId: import_zod65.z.string().nullable(),
18167
- cardId: import_zod65.z.string().nullable()
18584
+ origen: import_zod68.z.object({
18585
+ tipo: import_zod68.z.enum(["conversacion", "feedback_card", "configurada_explicito"]),
18586
+ conversacionId: import_zod68.z.string().nullable(),
18587
+ cardId: import_zod68.z.string().nullable()
18168
18588
  }).describe("Where this routine was scheduled from."),
18169
- confirm: import_zod65.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
18589
+ confirm: import_zod68.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
18170
18590
  },
18171
18591
  async ({ uidDestinatario, tipo, frecuencia, config, accion, origen, confirm }) => {
18172
18592
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
@@ -18195,8 +18615,8 @@ function registerMartinTools(server, session) {
18195
18615
  "pausar_rutina",
18196
18616
  "Pause an active routine temporarily. Can be resumed later by re-enabling it.",
18197
18617
  {
18198
- rutinaId: import_zod65.z.string().min(1).describe("Routine document ID to pause."),
18199
- motivo: import_zod65.z.string().optional().describe("Optional reason for pausing (logged for audit).")
18618
+ rutinaId: import_zod68.z.string().min(1).describe("Routine document ID to pause."),
18619
+ motivo: import_zod68.z.string().optional().describe("Optional reason for pausing (logged for audit).")
18200
18620
  },
18201
18621
  async ({ rutinaId, motivo }) => {
18202
18622
  const ctx = await buildContext(session, null);
@@ -18222,9 +18642,9 @@ function registerMartinTools(server, session) {
18222
18642
  "archivar_rutina",
18223
18643
  "Archive a routine that no longer applies. The routine is preserved for audit purposes but will NOT be executed. Requires confirmation.",
18224
18644
  {
18225
- rutinaId: import_zod65.z.string().min(1).describe("Routine document ID to archive (will not execute again)."),
18226
- motivo: import_zod65.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
18227
- confirm: import_zod65.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
18645
+ rutinaId: import_zod68.z.string().min(1).describe("Routine document ID to archive (will not execute again)."),
18646
+ motivo: import_zod68.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
18647
+ confirm: import_zod68.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
18228
18648
  },
18229
18649
  async ({ rutinaId, motivo, confirm }) => {
18230
18650
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });