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 +950 -530
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3564
|
+
z39 = offsetZ;
|
|
3521
3565
|
this.dt = dt;
|
|
3522
3566
|
} else {
|
|
3523
|
-
|
|
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
|
-
|
|
3575
|
+
z39 = dt.zone.name;
|
|
3532
3576
|
} else {
|
|
3533
|
-
|
|
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 ||
|
|
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
|
-
|
|
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 !==
|
|
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,
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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 !==
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
13225
|
-
tenantId:
|
|
13226
|
-
brandId:
|
|
13227
|
-
contenidoRef:
|
|
13228
|
-
fotoId:
|
|
13229
|
-
calendarioItemRef:
|
|
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
|
|
13234
|
-
|
|
13235
|
-
ok:
|
|
13236
|
-
fotoId:
|
|
13237
|
-
contenidoRef:
|
|
13238
|
-
plataforma:
|
|
13239
|
-
varianteUrl:
|
|
13240
|
-
formato:
|
|
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
|
-
|
|
13243
|
-
ok:
|
|
13244
|
-
error:
|
|
13245
|
-
code:
|
|
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
|
|
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:
|
|
13252
|
-
outputSchema:
|
|
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
|
-
|
|
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
|
|
13484
|
-
tenantId:
|
|
13485
|
-
brandId:
|
|
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
|
|
13488
|
-
|
|
13489
|
-
ok:
|
|
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:
|
|
13799
|
+
payload: import_zod55.z.record(import_zod55.z.string(), import_zod55.z.unknown())
|
|
13492
13800
|
}),
|
|
13493
|
-
|
|
13494
|
-
ok:
|
|
13495
|
-
error:
|
|
13496
|
-
code:
|
|
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
|
|
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:
|
|
13503
|
-
outputSchema:
|
|
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
|
-
|
|
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", "==",
|
|
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
|
|
13717
|
-
tenantId:
|
|
13718
|
-
brandId:
|
|
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
|
|
13721
|
-
|
|
13722
|
-
ok:
|
|
13723
|
-
payload:
|
|
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
|
-
|
|
13728
|
-
ok:
|
|
13729
|
-
error:
|
|
13730
|
-
code:
|
|
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
|
|
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:
|
|
13737
|
-
outputSchema:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
13882
|
-
|
|
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:
|
|
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:
|
|
14311
|
+
estado: ESTADO_CONTENIDO2.PENDIENTE_APROBACION,
|
|
13998
14312
|
plataforma,
|
|
13999
14313
|
descartados,
|
|
14000
14314
|
pipelineLinked
|
|
14001
14315
|
};
|
|
14002
14316
|
}
|
|
14003
|
-
var
|
|
14004
|
-
tenantId:
|
|
14005
|
-
brandId:
|
|
14006
|
-
plataforma:
|
|
14007
|
-
tipo:
|
|
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:
|
|
14011
|
-
languageCode:
|
|
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:
|
|
14015
|
-
datos:
|
|
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:
|
|
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 =
|
|
14023
|
-
linked:
|
|
14024
|
-
paso:
|
|
14025
|
-
motivo:
|
|
14026
|
-
code:
|
|
14027
|
-
_instrucciones:
|
|
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
|
|
14030
|
-
|
|
14031
|
-
ok:
|
|
14032
|
-
contenidoId:
|
|
14033
|
-
estado:
|
|
14034
|
-
plataforma:
|
|
14035
|
-
descartados:
|
|
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
|
-
|
|
14039
|
-
ok:
|
|
14040
|
-
error:
|
|
14041
|
-
code:
|
|
14042
|
-
activosEstaSemana:
|
|
14043
|
-
limite:
|
|
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
|
|
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:
|
|
14050
|
-
outputSchema:
|
|
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
|
-
|
|
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:
|
|
14152
|
-
|
|
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", "==",
|
|
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", "==",
|
|
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
|
|
14371
|
-
tenantId:
|
|
14372
|
-
brandId:
|
|
14373
|
-
semana:
|
|
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:
|
|
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
|
|
14381
|
-
|
|
14382
|
-
ok:
|
|
14383
|
-
payload:
|
|
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
|
-
|
|
14388
|
-
ok:
|
|
14389
|
-
error:
|
|
14390
|
-
code:
|
|
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
|
|
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:
|
|
14397
|
-
outputSchema:
|
|
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
|
-
|
|
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 =
|
|
14632
|
-
products:
|
|
14633
|
-
collections:
|
|
14634
|
-
articles:
|
|
14635
|
-
pages:
|
|
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 =
|
|
14640
|
-
products:
|
|
14641
|
-
collections:
|
|
14642
|
-
articles:
|
|
14643
|
-
pages:
|
|
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
|
|
14648
|
-
tenantId:
|
|
14649
|
-
brandId:
|
|
14650
|
-
contexto:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
14666
|
-
id:
|
|
14667
|
-
similarity:
|
|
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 =
|
|
14670
|
-
coleccion:
|
|
14671
|
-
titulo:
|
|
14672
|
-
razon:
|
|
14673
|
-
fechaInicio:
|
|
14674
|
-
fechaFin:
|
|
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 =
|
|
14677
|
-
var DetectedConflictSchema22 =
|
|
14678
|
-
var
|
|
14679
|
-
productos:
|
|
14680
|
-
colecciones:
|
|
14681
|
-
articles:
|
|
14682
|
-
pages:
|
|
14683
|
-
_instrucciones:
|
|
14684
|
-
_negativePrompt:
|
|
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:
|
|
15017
|
+
_suggestedActions: import_zod59.z.array(SuggestedActionSchema22)
|
|
14688
15018
|
});
|
|
14689
|
-
var
|
|
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:
|
|
14693
|
-
outputSchema:
|
|
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
|
-
|
|
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
|
|
14864
|
-
tenantId:
|
|
14865
|
-
brandId:
|
|
14866
|
-
keyword:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
14878
|
-
coleccion:
|
|
14879
|
-
titulo:
|
|
14880
|
-
razon:
|
|
14881
|
-
fechaInicio:
|
|
14882
|
-
fechaFin:
|
|
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
|
|
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
|
-
|
|
14889
|
-
ok:
|
|
14890
|
-
fotos:
|
|
14891
|
-
_instrucciones:
|
|
14892
|
-
_negativePrompt:
|
|
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:
|
|
14895
|
-
_fuente:
|
|
15224
|
+
_bloqueoProducto: import_zod60.z.boolean(),
|
|
15225
|
+
_fuente: import_zod60.z.enum(["tenant", "shopify_product"])
|
|
14896
15226
|
}),
|
|
14897
|
-
|
|
14898
|
-
ok:
|
|
14899
|
-
error:
|
|
14900
|
-
code:
|
|
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
|
|
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:
|
|
14907
|
-
outputSchema:
|
|
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
|
-
|
|
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
|
|
15012
|
-
tenantId:
|
|
15013
|
-
brandId:
|
|
15014
|
-
plataforma:
|
|
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:
|
|
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:
|
|
15350
|
+
keyword: import_zod61.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
|
|
15021
15351
|
});
|
|
15022
|
-
var
|
|
15023
|
-
|
|
15024
|
-
plantillaId:
|
|
15025
|
-
titulo:
|
|
15026
|
-
thumbnailUrl:
|
|
15027
|
-
similarity:
|
|
15028
|
-
_instrucciones:
|
|
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
|
-
|
|
15031
|
-
plantillaId:
|
|
15032
|
-
motivo:
|
|
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:
|
|
15040
|
-
_instrucciones:
|
|
15041
|
-
_todo:
|
|
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
|
|
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:
|
|
15048
|
-
outputSchema:
|
|
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
|
-
|
|
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 =
|
|
15300
|
-
tenantId:
|
|
15301
|
-
fotoId:
|
|
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 =
|
|
15304
|
-
fotoId:
|
|
15305
|
-
archivoOriginal:
|
|
15306
|
-
estrategia:
|
|
15307
|
-
brandBrief:
|
|
15308
|
-
segmento:
|
|
15309
|
-
personalidad:
|
|
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:
|
|
15312
|
-
catalogoVisual:
|
|
15313
|
-
estiloVisual:
|
|
15314
|
-
notaTenant:
|
|
15315
|
-
productoLinkeadoManual:
|
|
15316
|
-
_instrucciones:
|
|
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 =
|
|
15319
|
-
|
|
15320
|
-
ok:
|
|
15321
|
-
imageBase64:
|
|
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
|
-
|
|
15325
|
-
ok:
|
|
15326
|
-
code:
|
|
15327
|
-
error:
|
|
15328
|
-
_instrucciones:
|
|
15329
|
-
fix:
|
|
15330
|
-
fotoId:
|
|
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 =
|
|
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:
|
|
15365
|
-
fotoId:
|
|
15366
|
-
prompt:
|
|
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:
|
|
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:
|
|
15373
|
-
tipo:
|
|
15374
|
-
tagsPrimarios:
|
|
15375
|
-
tagsSecundarios:
|
|
15376
|
-
tagsContexto:
|
|
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 =
|
|
15379
|
-
|
|
15380
|
-
ok:
|
|
15381
|
-
fotoId:
|
|
15382
|
-
archivoEditado:
|
|
15383
|
-
thumbnailUrl:
|
|
15384
|
-
iteracion:
|
|
15385
|
-
editCosto:
|
|
15386
|
-
creditsConsumed:
|
|
15387
|
-
balanceAfter:
|
|
15388
|
-
imageBase64:
|
|
15389
|
-
_instrucciones:
|
|
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
|
-
|
|
15392
|
-
ok:
|
|
15393
|
-
error:
|
|
15394
|
-
code:
|
|
15395
|
-
details:
|
|
15396
|
-
_instrucciones:
|
|
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", "==",
|
|
15782
|
-
db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==",
|
|
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:
|
|
16203
|
-
brandId:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
16829
|
+
var import_zod67 = require("zod");
|
|
16507
16830
|
|
|
16508
16831
|
// src/tools/marketing/photos.ts
|
|
16509
|
-
var
|
|
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:
|
|
16806
|
-
keyword:
|
|
16807
|
-
plataforma:
|
|
16808
|
-
fecha:
|
|
16809
|
-
limit:
|
|
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:
|
|
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:
|
|
16906
|
-
prompt:
|
|
16907
|
-
acciones:
|
|
16908
|
-
descripcion:
|
|
16909
|
-
tipo:
|
|
16910
|
-
tagsPrimarios:
|
|
16911
|
-
tagsSecundarios:
|
|
16912
|
-
tagsContexto:
|
|
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:
|
|
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:
|
|
17026
|
-
contexto:
|
|
17027
|
-
fecha:
|
|
17028
|
-
limit:
|
|
17029
|
-
diversidad:
|
|
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:
|
|
17107
|
-
plataforma:
|
|
17108
|
-
tipoContenido:
|
|
17109
|
-
keyword:
|
|
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:
|
|
17155
|
-
semana:
|
|
17156
|
-
necesidades:
|
|
17157
|
-
|
|
17158
|
-
tema:
|
|
17159
|
-
keyword:
|
|
17160
|
-
cantidadSugerida:
|
|
17161
|
-
razon:
|
|
17162
|
-
slotsAfectados:
|
|
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
|
|
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 =
|
|
17218
|
-
products:
|
|
17219
|
-
collections:
|
|
17220
|
-
articles:
|
|
17221
|
-
pages:
|
|
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 =
|
|
17229
|
-
products:
|
|
17230
|
-
collections:
|
|
17231
|
-
articles:
|
|
17232
|
-
pages:
|
|
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:
|
|
17261
|
-
contexto:
|
|
17262
|
-
fecha:
|
|
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:
|
|
17266
|
-
mode:
|
|
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:
|
|
17478
|
-
mes:
|
|
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:
|
|
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:
|
|
17553
|
-
estado:
|
|
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:
|
|
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:
|
|
17604
|
-
plan:
|
|
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:
|
|
17633
|
-
field:
|
|
17634
|
-
value:
|
|
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:
|
|
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:
|
|
17681
|
-
brandBrief:
|
|
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:
|
|
17708
|
-
semana:
|
|
17709
|
-
modo:
|
|
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:
|
|
17738
|
-
plataforma:
|
|
17739
|
-
tipo:
|
|
17740
|
-
keyword:
|
|
17741
|
-
languageCode:
|
|
17742
|
-
fotoId:
|
|
17743
|
-
datos:
|
|
17744
|
-
calendarioItemRef:
|
|
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:
|
|
17781
|
-
datos:
|
|
17782
|
-
fotoId:
|
|
17783
|
-
keyword:
|
|
17784
|
-
languageCode:
|
|
17785
|
-
estado:
|
|
17786
|
-
calendarioItemRef:
|
|
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:
|
|
17821
|
-
mes:
|
|
17822
|
-
semana:
|
|
17823
|
-
slot:
|
|
17824
|
-
dia:
|
|
17825
|
-
plataforma:
|
|
17826
|
-
tipo:
|
|
17827
|
-
keyword:
|
|
17828
|
-
tema:
|
|
17829
|
-
productoId:
|
|
17830
|
-
estado:
|
|
17831
|
-
locationId:
|
|
17832
|
-
locationNombre:
|
|
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:
|
|
17859
|
-
mes:
|
|
17860
|
-
semana:
|
|
17861
|
-
slotIndex:
|
|
17862
|
-
cambios:
|
|
17863
|
-
dia:
|
|
17864
|
-
plataforma:
|
|
17865
|
-
tipo:
|
|
17866
|
-
keyword:
|
|
17867
|
-
tema:
|
|
17868
|
-
productoId:
|
|
17869
|
-
estado:
|
|
17870
|
-
contenidoRef:
|
|
17871
|
-
fotoIdAsignada:
|
|
17872
|
-
notas:
|
|
17873
|
-
locationId:
|
|
17874
|
-
locationNombre:
|
|
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:
|
|
17877
|
-
|
|
17878
|
-
|
|
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:
|
|
17924
|
-
fotoId:
|
|
17925
|
-
calendarioItemRef:
|
|
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:
|
|
17952
|
-
brandId:
|
|
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:
|
|
17991
|
-
motivo:
|
|
17992
|
-
brandId:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
18107
|
-
contenido:
|
|
18108
|
-
origen:
|
|
18109
|
-
tipo:
|
|
18110
|
-
conversacionId:
|
|
18111
|
-
cardId:
|
|
18112
|
-
rationale:
|
|
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:
|
|
18132
|
-
motivo:
|
|
18133
|
-
confirm:
|
|
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:
|
|
18152
|
-
tipo:
|
|
18153
|
-
frecuencia:
|
|
18154
|
-
config:
|
|
18155
|
-
diaSemana:
|
|
18156
|
-
diaMes:
|
|
18157
|
-
hora:
|
|
18158
|
-
fechaPuntual:
|
|
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:
|
|
18161
|
-
tool:
|
|
18162
|
-
params:
|
|
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:
|
|
18165
|
-
tipo:
|
|
18166
|
-
conversacionId:
|
|
18167
|
-
cardId:
|
|
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:
|
|
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:
|
|
18199
|
-
motivo:
|
|
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:
|
|
18226
|
-
motivo:
|
|
18227
|
-
confirm:
|
|
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 });
|