ponch-mcp-server 1.0.75 → 1.0.77
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 +775 -480
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,14 +47,14 @@ function resolveAuth() {
|
|
|
47
47
|
const resolvedPath = saPath.startsWith("/") ? saPath : (0, import_path.join)(process.cwd(), saPath);
|
|
48
48
|
if ((0, import_fs.existsSync)(resolvedPath)) {
|
|
49
49
|
return {
|
|
50
|
-
|
|
50
|
+
canSwitchTenant: true,
|
|
51
|
+
// service account = admin del SaaS
|
|
51
52
|
tenantId: null,
|
|
52
53
|
brandId: null,
|
|
53
54
|
brands: [],
|
|
54
55
|
userId: null,
|
|
55
56
|
userName: null,
|
|
56
57
|
rol: "super_admin",
|
|
57
|
-
// service account = admin total
|
|
58
58
|
serviceAccountPath: resolvedPath,
|
|
59
59
|
credentialsPath: null
|
|
60
60
|
};
|
|
@@ -65,7 +65,7 @@ function resolveAuth() {
|
|
|
65
65
|
try {
|
|
66
66
|
const creds = JSON.parse((0, import_fs.readFileSync)(CREDENTIALS_PATH, "utf-8"));
|
|
67
67
|
return {
|
|
68
|
-
|
|
68
|
+
canSwitchTenant: creds.rol === "super_admin",
|
|
69
69
|
tenantId: creds.tenantId ?? null,
|
|
70
70
|
brandId: creds.brands?.[0]?.id ?? null,
|
|
71
71
|
brands: creds.brands ?? [],
|
|
@@ -80,7 +80,7 @@ function resolveAuth() {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
return {
|
|
83
|
-
|
|
83
|
+
canSwitchTenant: false,
|
|
84
84
|
tenantId: null,
|
|
85
85
|
brandId: null,
|
|
86
86
|
brands: [],
|
|
@@ -108,6 +108,10 @@ function readCredentials() {
|
|
|
108
108
|
var Session = class {
|
|
109
109
|
context;
|
|
110
110
|
_authContext;
|
|
111
|
+
_clientIdentity = {
|
|
112
|
+
mcpClient: null,
|
|
113
|
+
mcpClientVersion: null
|
|
114
|
+
};
|
|
111
115
|
constructor(auth) {
|
|
112
116
|
this._authContext = auth;
|
|
113
117
|
this.context = {
|
|
@@ -115,8 +119,26 @@ var Session = class {
|
|
|
115
119
|
brandId: auth.brandId
|
|
116
120
|
};
|
|
117
121
|
}
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Captura identidad del cliente MCP. Llamado al recibir handshake
|
|
124
|
+
* `initialize` del SDK. Idempotente — si se llama 2 veces, segunda no
|
|
125
|
+
* sobrescribe (el cliente no cambia mid-session).
|
|
126
|
+
*/
|
|
127
|
+
setMcpClientIdentity(name, version) {
|
|
128
|
+
if (this._clientIdentity.mcpClient === null) {
|
|
129
|
+
this._clientIdentity.mcpClient = name;
|
|
130
|
+
this._clientIdentity.mcpClientVersion = version;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
get mcpClientIdentity() {
|
|
134
|
+
return this._clientIdentity;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* true → super_admin del SaaS (puede cambiar tenant). false → tenant
|
|
138
|
+
* member normal. Independiente del SDK que esté corriendo el MCP.
|
|
139
|
+
*/
|
|
140
|
+
get canSwitchTenant() {
|
|
141
|
+
return this._authContext.canSwitchTenant;
|
|
120
142
|
}
|
|
121
143
|
get tenantId() {
|
|
122
144
|
return this.context.tenantId;
|
|
@@ -137,19 +159,27 @@ var Session = class {
|
|
|
137
159
|
get userId() {
|
|
138
160
|
return this._authContext.userId;
|
|
139
161
|
}
|
|
140
|
-
/** Rol del usuario (
|
|
162
|
+
/** Rol del usuario (super_admin | ROL00X del tenant). */
|
|
141
163
|
get rol() {
|
|
142
164
|
return this._authContext.rol;
|
|
143
165
|
}
|
|
144
166
|
setContext(tenantId, brandId) {
|
|
145
|
-
if (this._authContext.
|
|
146
|
-
throw new Error("set_context solo disponible
|
|
167
|
+
if (!this._authContext.canSwitchTenant) {
|
|
168
|
+
throw new Error("set_context solo disponible para usuarios con permiso multi-tenant (super_admin).");
|
|
147
169
|
}
|
|
148
170
|
this.context.tenantId = tenantId;
|
|
149
171
|
this.context.brandId = brandId;
|
|
150
172
|
}
|
|
151
173
|
/**
|
|
152
|
-
*
|
|
174
|
+
* Revoca canSwitchTenant en runtime. Usado cuando el bootstrap detecta
|
|
175
|
+
* token expirado — el usuario debe re-autenticar via connect_account
|
|
176
|
+
* antes de poder cambiar de tenant.
|
|
177
|
+
*/
|
|
178
|
+
revokeCrossTenant() {
|
|
179
|
+
this._authContext.canSwitchTenant = false;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Actualiza el contexto después de connect_account.
|
|
153
183
|
*/
|
|
154
184
|
setTenantContext(tenantId, brandId, brands, userId, userName) {
|
|
155
185
|
this.context.tenantId = tenantId;
|
|
@@ -160,16 +190,10 @@ var Session = class {
|
|
|
160
190
|
this._authContext.userId = userId;
|
|
161
191
|
this._authContext.userName = userName;
|
|
162
192
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Fuerza el modo (usado cuando credentials existen pero token expiro)
|
|
165
|
-
*/
|
|
166
|
-
forceMode(newMode) {
|
|
167
|
-
this._authContext.mode = newMode;
|
|
168
|
-
}
|
|
169
193
|
requireTenant() {
|
|
170
194
|
if (!this.context.tenantId) {
|
|
171
195
|
throw new Error(
|
|
172
|
-
this._authContext.
|
|
196
|
+
this._authContext.canSwitchTenant ? "Usa set_context para seleccionar un tenant primero." : "No conectado. Usa connect_account para conectar tu cuenta de Ponch."
|
|
173
197
|
);
|
|
174
198
|
}
|
|
175
199
|
return this.context.tenantId;
|
|
@@ -188,11 +212,11 @@ var import_app = require("firebase/app");
|
|
|
188
212
|
var import_lite = require("firebase/firestore/lite");
|
|
189
213
|
var import_auth = require("firebase/auth");
|
|
190
214
|
var import_fs2 = require("fs");
|
|
191
|
-
var
|
|
215
|
+
var sdkMode = "none";
|
|
192
216
|
var adminDb;
|
|
193
217
|
var clientDb;
|
|
194
218
|
function initFirebaseAdmin(serviceAccountPath) {
|
|
195
|
-
if (
|
|
219
|
+
if (sdkMode !== "none") return;
|
|
196
220
|
const serviceAccount = JSON.parse(
|
|
197
221
|
(0, import_fs2.readFileSync)(serviceAccountPath, "utf-8")
|
|
198
222
|
);
|
|
@@ -200,10 +224,10 @@ function initFirebaseAdmin(serviceAccountPath) {
|
|
|
200
224
|
credential: import_firebase_admin.default.credential.cert(serviceAccount)
|
|
201
225
|
});
|
|
202
226
|
adminDb = import_firebase_admin.default.firestore();
|
|
203
|
-
|
|
227
|
+
sdkMode = "admin";
|
|
204
228
|
}
|
|
205
229
|
async function initFirebaseClient() {
|
|
206
|
-
if (
|
|
230
|
+
if (sdkMode !== "none") return true;
|
|
207
231
|
const creds = readCredentials();
|
|
208
232
|
if (!creds) {
|
|
209
233
|
console.error("[firestore] No hay credentials.json");
|
|
@@ -221,7 +245,7 @@ async function initFirebaseClient() {
|
|
|
221
245
|
if (newCustomToken) {
|
|
222
246
|
await (0, import_auth.signInWithCustomToken)(auth, newCustomToken);
|
|
223
247
|
clientDb = (0, import_lite.getFirestore)(app);
|
|
224
|
-
|
|
248
|
+
sdkMode = "client";
|
|
225
249
|
saveCredentials({ ...creds, customToken: newCustomToken });
|
|
226
250
|
console.error("[firestore] Firebase Client re-autenticado con refreshToken");
|
|
227
251
|
return true;
|
|
@@ -241,7 +265,7 @@ async function initFirebaseClient() {
|
|
|
241
265
|
console.error("[firestore] refreshToken guardado para futuras sesiones");
|
|
242
266
|
}
|
|
243
267
|
clientDb = (0, import_lite.getFirestore)(app);
|
|
244
|
-
|
|
268
|
+
sdkMode = "client";
|
|
245
269
|
console.error("[firestore] Firebase Client autenticado con customToken");
|
|
246
270
|
return true;
|
|
247
271
|
} catch (err) {
|
|
@@ -306,27 +330,27 @@ async function reauthClient(customToken) {
|
|
|
306
330
|
console.error("[firestore] refreshToken guardado");
|
|
307
331
|
}
|
|
308
332
|
clientDb = (0, import_lite.getFirestore)(app);
|
|
309
|
-
|
|
333
|
+
sdkMode = "client";
|
|
310
334
|
return true;
|
|
311
335
|
} catch (err) {
|
|
312
336
|
console.error("[firestore] Error re-autenticando:", err);
|
|
313
337
|
return false;
|
|
314
338
|
}
|
|
315
339
|
}
|
|
316
|
-
function
|
|
317
|
-
return
|
|
340
|
+
function getSdkMode() {
|
|
341
|
+
return sdkMode;
|
|
318
342
|
}
|
|
319
343
|
function getAdminDb() {
|
|
320
|
-
if (
|
|
321
|
-
throw new Error("getAdminDb:
|
|
344
|
+
if (sdkMode !== "admin") {
|
|
345
|
+
throw new Error("getAdminDb: SDK admin requerido (actual: " + sdkMode + ")");
|
|
322
346
|
}
|
|
323
347
|
return adminDb;
|
|
324
348
|
}
|
|
325
349
|
async function queryByTenant(session, collectionName, filters = [], options = {}) {
|
|
326
350
|
const tenantId = session.requireTenant();
|
|
327
|
-
if (
|
|
351
|
+
if (sdkMode === "admin") {
|
|
328
352
|
return queryByTenantAdmin(tenantId, collectionName, filters, options);
|
|
329
|
-
} else if (
|
|
353
|
+
} else if (sdkMode === "client") {
|
|
330
354
|
return queryByTenantClient(tenantId, collectionName, filters, options);
|
|
331
355
|
}
|
|
332
356
|
throw new Error("Firebase no inicializado");
|
|
@@ -362,11 +386,11 @@ async function queryByTenantClient(tenantId, collectionName, filters, options) {
|
|
|
362
386
|
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
|
|
363
387
|
}
|
|
364
388
|
async function readDoc(collectionName, docId) {
|
|
365
|
-
if (
|
|
389
|
+
if (sdkMode === "admin") {
|
|
366
390
|
const doc = await adminDb.collection(collectionName).doc(docId).get();
|
|
367
391
|
if (!doc.exists) return null;
|
|
368
392
|
return { id: doc.id, ...doc.data() };
|
|
369
|
-
} else if (
|
|
393
|
+
} else if (sdkMode === "client") {
|
|
370
394
|
const ref = (0, import_lite.doc)(clientDb, collectionName, docId);
|
|
371
395
|
const doc = await (0, import_lite.getDoc)(ref);
|
|
372
396
|
if (!doc.exists()) return null;
|
|
@@ -374,10 +398,21 @@ async function readDoc(collectionName, docId) {
|
|
|
374
398
|
}
|
|
375
399
|
throw new Error("Firebase no inicializado");
|
|
376
400
|
}
|
|
401
|
+
async function listCollection(path) {
|
|
402
|
+
if (sdkMode === "admin") {
|
|
403
|
+
const snap = await adminDb.collection(path).get();
|
|
404
|
+
return snap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
|
|
405
|
+
} else if (sdkMode === "client") {
|
|
406
|
+
const ref = (0, import_lite.collection)(clientDb, path);
|
|
407
|
+
const snap = await (0, import_lite.getDocs)(ref);
|
|
408
|
+
return snap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
|
|
409
|
+
}
|
|
410
|
+
throw new Error("Firebase no inicializado");
|
|
411
|
+
}
|
|
377
412
|
async function writeDoc(collectionName, docId, data) {
|
|
378
|
-
if (
|
|
413
|
+
if (sdkMode === "admin") {
|
|
379
414
|
await adminDb.collection(collectionName).doc(docId).set(data, { merge: true });
|
|
380
|
-
} else if (
|
|
415
|
+
} else if (sdkMode === "client") {
|
|
381
416
|
const ref = (0, import_lite.doc)(clientDb, collectionName, docId);
|
|
382
417
|
await (0, import_lite.setDoc)(ref, data, { merge: true });
|
|
383
418
|
} else {
|
|
@@ -385,9 +420,9 @@ async function writeDoc(collectionName, docId, data) {
|
|
|
385
420
|
}
|
|
386
421
|
}
|
|
387
422
|
async function updateDoc(collectionName, docId, data) {
|
|
388
|
-
if (
|
|
423
|
+
if (sdkMode === "admin") {
|
|
389
424
|
await adminDb.collection(collectionName).doc(docId).update(data);
|
|
390
|
-
} else if (
|
|
425
|
+
} else if (sdkMode === "client") {
|
|
391
426
|
const ref = (0, import_lite.doc)(clientDb, collectionName, docId);
|
|
392
427
|
await (0, import_lite.updateDoc)(ref, data);
|
|
393
428
|
} else {
|
|
@@ -395,7 +430,7 @@ async function updateDoc(collectionName, docId, data) {
|
|
|
395
430
|
}
|
|
396
431
|
}
|
|
397
432
|
function serverTimestamp() {
|
|
398
|
-
if (
|
|
433
|
+
if (sdkMode === "admin") {
|
|
399
434
|
return import_firebase_admin.default.firestore.FieldValue.serverTimestamp();
|
|
400
435
|
} else {
|
|
401
436
|
return (0, import_lite.serverTimestamp)();
|
|
@@ -407,10 +442,10 @@ var import_zod = require("zod");
|
|
|
407
442
|
var import_functions = require("firebase/functions");
|
|
408
443
|
var import_app2 = require("firebase/app");
|
|
409
444
|
function registerContextTools(server, session) {
|
|
410
|
-
if (session.
|
|
445
|
+
if (session.canSwitchTenant) {
|
|
411
446
|
server.tool(
|
|
412
447
|
"set_context",
|
|
413
|
-
"Establece el tenant y brand para esta sesion. Solo disponible
|
|
448
|
+
"Establece el tenant y brand para esta sesion. Solo disponible para super_admin.",
|
|
414
449
|
{
|
|
415
450
|
tenantId: import_zod.z.string().describe('ID del tenant (ej: "atteyo")'),
|
|
416
451
|
brandId: import_zod.z.string().describe('ID de la brand (ej: "ponch")')
|
|
@@ -418,8 +453,8 @@ function registerContextTools(server, session) {
|
|
|
418
453
|
async ({ tenantId, brandId }) => {
|
|
419
454
|
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
420
455
|
if (!brand) {
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
456
|
+
const brands = await listCollection(`tenants/${tenantId}/marketing_config`);
|
|
457
|
+
if (brands.length === 0) {
|
|
423
458
|
return {
|
|
424
459
|
content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Tenant "${tenantId}" no tiene marketing_config` }) }]
|
|
425
460
|
};
|
|
@@ -430,7 +465,7 @@ function registerContextTools(server, session) {
|
|
|
430
465
|
text: JSON.stringify({
|
|
431
466
|
ok: false,
|
|
432
467
|
error: `Brand "${brandId}" no existe en tenant "${tenantId}"`,
|
|
433
|
-
brandsDisponibles:
|
|
468
|
+
brandsDisponibles: brands.map((b) => b.id)
|
|
434
469
|
})
|
|
435
470
|
}]
|
|
436
471
|
};
|
|
@@ -2120,7 +2155,7 @@ var TenantStatusFieldsSchema = import_zod14.z.object({
|
|
|
2120
2155
|
cancellation: CancellationInfoSchema
|
|
2121
2156
|
}).passthrough();
|
|
2122
2157
|
var AuditActorSchema = import_zod15.z.object({
|
|
2123
|
-
type: import_zod15.z.enum(["user", "martin", "system", "cf_scheduled", "webhook"]),
|
|
2158
|
+
type: import_zod15.z.enum(["user", "martin", "mcp_client", "system", "cf_scheduled", "webhook"]),
|
|
2124
2159
|
uid: import_zod15.z.string().nullable(),
|
|
2125
2160
|
nombre: import_zod15.z.string(),
|
|
2126
2161
|
metadata: import_zod15.z.record(import_zod15.z.string(), import_zod15.z.any()).optional()
|
|
@@ -2726,7 +2761,7 @@ function registerCoreTools(server, session) {
|
|
|
2726
2761
|
}
|
|
2727
2762
|
|
|
2728
2763
|
// src/tools/marketing.ts
|
|
2729
|
-
var
|
|
2764
|
+
var import_zod38 = require("zod");
|
|
2730
2765
|
|
|
2731
2766
|
// ../packages/marketing-business-logic/dist/index.js
|
|
2732
2767
|
var import_firebase_admin2 = require("firebase-admin");
|
|
@@ -2734,23 +2769,29 @@ var import_firebase_admin3 = require("firebase-admin");
|
|
|
2734
2769
|
var import_zod27 = require("zod");
|
|
2735
2770
|
var import_zod28 = require("zod");
|
|
2736
2771
|
var import_zod29 = require("zod");
|
|
2772
|
+
var import_fs3 = require("fs");
|
|
2773
|
+
var import_path2 = require("path");
|
|
2774
|
+
var import_url = require("url");
|
|
2737
2775
|
var import_firebase_admin4 = require("firebase-admin");
|
|
2738
2776
|
var import_firebase_admin5 = require("firebase-admin");
|
|
2739
2777
|
var import_firebase_admin6 = require("firebase-admin");
|
|
2740
2778
|
var import_zod30 = require("zod");
|
|
2741
|
-
var import_zod31 = require("zod");
|
|
2742
2779
|
var import_firebase_admin7 = require("firebase-admin");
|
|
2780
|
+
var import_zod31 = require("zod");
|
|
2743
2781
|
var import_zod32 = require("zod");
|
|
2744
2782
|
var import_firebase_admin8 = require("firebase-admin");
|
|
2783
|
+
var import_zod33 = require("zod");
|
|
2745
2784
|
var import_firebase_admin9 = require("firebase-admin");
|
|
2785
|
+
var import_firebase_admin10 = require("firebase-admin");
|
|
2746
2786
|
var import_firestore3 = require("firebase-admin/firestore");
|
|
2747
2787
|
var import_firestore4 = require("firebase-admin/firestore");
|
|
2748
2788
|
var import_firestore5 = require("firebase-admin/firestore");
|
|
2749
2789
|
var import_firestore6 = require("firebase-admin/firestore");
|
|
2750
|
-
var import_zod33 = require("zod");
|
|
2751
|
-
var import_firestore7 = require("firebase-admin/firestore");
|
|
2752
2790
|
var import_zod34 = require("zod");
|
|
2791
|
+
var import_firestore7 = require("firebase-admin/firestore");
|
|
2792
|
+
var import_zod35 = require("zod");
|
|
2753
2793
|
var import_firestore8 = require("firebase-admin/firestore");
|
|
2794
|
+
var import_meta = {};
|
|
2754
2795
|
var RULE_NEGATIVES = {
|
|
2755
2796
|
allowFaces: "no people, no faces, no hands",
|
|
2756
2797
|
allowProductTransform: "no distorted products, no warped objects",
|
|
@@ -3011,6 +3052,74 @@ var planWriter = {
|
|
|
3011
3052
|
save,
|
|
3012
3053
|
updateField
|
|
3013
3054
|
};
|
|
3055
|
+
var _localeCache = {};
|
|
3056
|
+
function _resolveLocaleFile(locale) {
|
|
3057
|
+
let baseDir;
|
|
3058
|
+
try {
|
|
3059
|
+
baseDir = typeof __dirname !== "undefined" ? __dirname : (0, import_path2.dirname)((0, import_url.fileURLToPath)(import_meta.url));
|
|
3060
|
+
} catch {
|
|
3061
|
+
baseDir = process.cwd();
|
|
3062
|
+
}
|
|
3063
|
+
const candidates = [
|
|
3064
|
+
(0, import_path2.join)(baseDir, "..", "..", "..", "..", "src", "i18n", "locales", `${locale}.json`),
|
|
3065
|
+
(0, import_path2.join)(baseDir, "..", "..", "..", "src", "i18n", "locales", `${locale}.json`)
|
|
3066
|
+
];
|
|
3067
|
+
for (const candidate of candidates) {
|
|
3068
|
+
try {
|
|
3069
|
+
(0, import_fs3.readFileSync)(candidate, "utf-8");
|
|
3070
|
+
return candidate;
|
|
3071
|
+
} catch {
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
throw new Error(
|
|
3076
|
+
`getMessage: locale file '${locale}.json' not found. Tried:
|
|
3077
|
+
` + candidates.map((c) => ` - ${c}`).join("\n")
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
function _loadLocale(locale) {
|
|
3081
|
+
if (_localeCache[locale]) return _localeCache[locale];
|
|
3082
|
+
const path2 = _resolveLocaleFile(locale);
|
|
3083
|
+
const raw = (0, import_fs3.readFileSync)(path2, "utf-8");
|
|
3084
|
+
const data = JSON.parse(raw);
|
|
3085
|
+
_localeCache[locale] = data;
|
|
3086
|
+
return data;
|
|
3087
|
+
}
|
|
3088
|
+
function _resolvePath(obj, path2) {
|
|
3089
|
+
const parts = path2.split(".");
|
|
3090
|
+
let current = obj;
|
|
3091
|
+
for (const part of parts) {
|
|
3092
|
+
if (current === null || typeof current !== "object") return null;
|
|
3093
|
+
current = current[part];
|
|
3094
|
+
}
|
|
3095
|
+
return typeof current === "string" ? current : null;
|
|
3096
|
+
}
|
|
3097
|
+
function _interpolate(template, vars, context) {
|
|
3098
|
+
return template.replace(/\{(\w+)\}/g, (_match, key) => {
|
|
3099
|
+
if (!vars || !(key in vars)) {
|
|
3100
|
+
throw new Error(
|
|
3101
|
+
`getMessage: variable '${key}' in template '${context.path}' (${context.locale}) not provided in vars. Pass { ${key}: '...' } when calling getMessage.`
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
return vars[key];
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
var SUPPORTED_LOCALES = ["es", "en"];
|
|
3108
|
+
function getMessage(path2, locale, vars) {
|
|
3109
|
+
if (!SUPPORTED_LOCALES.includes(locale)) {
|
|
3110
|
+
throw new Error(
|
|
3111
|
+
`getMessage: locale '${locale}' not supported. Active locales: ${SUPPORTED_LOCALES.join(", ")}.`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
const data = _loadLocale(locale);
|
|
3115
|
+
const template = _resolvePath(data, path2);
|
|
3116
|
+
if (template === null) {
|
|
3117
|
+
throw new Error(
|
|
3118
|
+
`getMessage: path '${path2}' not found in locale '${locale}'. Add it to src/i18n/locales/${locale}.json.`
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
return _interpolate(template, vars, { path: path2, locale });
|
|
3122
|
+
}
|
|
3014
3123
|
var DisabledReasonCodeSchema = import_zod29.z.enum([
|
|
3015
3124
|
"paywall",
|
|
3016
3125
|
// Acción requiere upgrade de plan
|
|
@@ -3036,44 +3145,8 @@ var DisabledOutputSchema = import_zod29.z.object({
|
|
|
3036
3145
|
*/
|
|
3037
3146
|
detail: import_zod29.z.record(import_zod29.z.string(), import_zod29.z.string()).optional()
|
|
3038
3147
|
});
|
|
3039
|
-
var TEMPLATES = {
|
|
3040
|
-
es: {
|
|
3041
|
-
paywall: "Esta acci\xF3n requiere actualizar tu plan.",
|
|
3042
|
-
oauth_missing: "Necesitas conectar {service} primero.",
|
|
3043
|
-
data_missing: "Falta informaci\xF3n: {detail}",
|
|
3044
|
-
plan_not_includes: "Tu plan no incluye {feature}",
|
|
3045
|
-
feature_disabled: "Esta funci\xF3n est\xE1 deshabilitada para tu cuenta.",
|
|
3046
|
-
other: "{detail}"
|
|
3047
|
-
},
|
|
3048
|
-
en: {
|
|
3049
|
-
paywall: "This action requires upgrading your plan.",
|
|
3050
|
-
oauth_missing: "You need to connect {service} first.",
|
|
3051
|
-
data_missing: "Missing information: {detail}",
|
|
3052
|
-
plan_not_includes: "Your plan doesn't include {feature}",
|
|
3053
|
-
feature_disabled: "This feature is disabled for your account.",
|
|
3054
|
-
other: "{detail}"
|
|
3055
|
-
}
|
|
3056
|
-
};
|
|
3057
3148
|
function getDisabledMessage(code, locale, vars) {
|
|
3058
|
-
|
|
3059
|
-
throw new Error(
|
|
3060
|
-
`Locale '${locale}' no soportado en disabledMessages. Locales activos: ${Object.keys(TEMPLATES).join(", ")}.`
|
|
3061
|
-
);
|
|
3062
|
-
}
|
|
3063
|
-
const template = TEMPLATES[locale][code];
|
|
3064
|
-
if (!template) {
|
|
3065
|
-
throw new Error(
|
|
3066
|
-
`Disabled code '${code}' no tiene template definido para locale '${locale}'.`
|
|
3067
|
-
);
|
|
3068
|
-
}
|
|
3069
|
-
return template.replace(/\{(\w+)\}/g, (_match, key) => {
|
|
3070
|
-
if (!vars || !(key in vars)) {
|
|
3071
|
-
throw new Error(
|
|
3072
|
-
`getDisabledMessage: variable '${key}' del template '${code}' (${locale}) no provista en vars. Helper debe pasar { ${key}: '...' } en detail.`
|
|
3073
|
-
);
|
|
3074
|
-
}
|
|
3075
|
-
return vars[key];
|
|
3076
|
-
});
|
|
3149
|
+
return getMessage(`marketing.disabled.${code}`, locale, vars);
|
|
3077
3150
|
}
|
|
3078
3151
|
var SideEffectEnum = import_zod28.z.enum([
|
|
3079
3152
|
"reads_firestore",
|
|
@@ -3105,6 +3178,10 @@ var ExtractTargetPathSchema = import_zod28.z.custom((val) => typeof val === "fun
|
|
|
3105
3178
|
var ExtractChangesSchema = import_zod28.z.custom((val) => typeof val === "function", {
|
|
3106
3179
|
message: "extractChanges debe ser funci\xF3n (input, output) => { before, after }"
|
|
3107
3180
|
});
|
|
3181
|
+
var InputPredicateSchema = import_zod28.z.custom(
|
|
3182
|
+
(val) => typeof val === "function",
|
|
3183
|
+
{ message: "predicado debe ser funci\xF3n (input) => boolean" }
|
|
3184
|
+
);
|
|
3108
3185
|
var MartinContractSchema = import_zod28.z.object({
|
|
3109
3186
|
// Schema versioning (Dim 4 backbone)
|
|
3110
3187
|
schemaVersion: import_zod28.z.literal(1).default(1),
|
|
@@ -3120,6 +3197,16 @@ var MartinContractSchema = import_zod28.z.object({
|
|
|
3120
3197
|
destructive: import_zod28.z.boolean(),
|
|
3121
3198
|
affectsPublication: import_zod28.z.boolean(),
|
|
3122
3199
|
affectsExternal: import_zod28.z.boolean(),
|
|
3200
|
+
/**
|
|
3201
|
+
* Override runtime del flag `destructive`. Si el predicado retorna
|
|
3202
|
+
* true para un input específico, el wrapper trata la invocación
|
|
3203
|
+
* como destructiva aun si `destructive: false`. Útil cuando una
|
|
3204
|
+
* misma tool tiene 2 modos según args (ej. update con
|
|
3205
|
+
* accionContenidoExistente='descartar').
|
|
3206
|
+
*/
|
|
3207
|
+
isDestructiveForInput: InputPredicateSchema.optional(),
|
|
3208
|
+
/** Mismo patrón para affectsPublication (override runtime). */
|
|
3209
|
+
isPublicationForInput: InputPredicateSchema.optional(),
|
|
3123
3210
|
// Presentación al usuario (i18n)
|
|
3124
3211
|
martinSummaryTemplate: SummaryTemplateSchema,
|
|
3125
3212
|
martinConfirmationTemplate: ConfirmationTemplateSchema.optional(),
|
|
@@ -3260,7 +3347,7 @@ var OutputSchema = import_zod27.z.discriminatedUnion("ok", [
|
|
|
3260
3347
|
]);
|
|
3261
3348
|
var rawContract = {
|
|
3262
3349
|
name: "save_marketing_plan",
|
|
3263
|
-
description: "
|
|
3350
|
+
description: "Save a marketing plan for a brand. If the plan object includes a blogStrategy field, it is extracted and stored at brand level (not nested inside plan).",
|
|
3264
3351
|
paramsSchema: ParamsSchema,
|
|
3265
3352
|
outputSchema: OutputSchema,
|
|
3266
3353
|
// No es destructivo (sobrescribe plan completo, pero plan se regenera fácil),
|
|
@@ -3506,6 +3593,95 @@ async function contenidoUpdater(input) {
|
|
|
3506
3593
|
camposActualizados: Object.keys(update).filter((k) => k !== "updatedAt")
|
|
3507
3594
|
};
|
|
3508
3595
|
}
|
|
3596
|
+
async function addCalendarSlot(input) {
|
|
3597
|
+
const { db, tenantId, brandId, mes, semana, slot } = input;
|
|
3598
|
+
const calQuery = await db.collection("tenants").doc(tenantId).collection("marketing_calendario").where("brandId", "==", brandId).where("mes", "==", mes).limit(1).get();
|
|
3599
|
+
if (calQuery.empty) {
|
|
3600
|
+
return { ok: false, error: `Calendario ${mes} no encontrado para brand ${brandId}` };
|
|
3601
|
+
}
|
|
3602
|
+
const calDocRef = calQuery.docs[0].ref;
|
|
3603
|
+
let resultSlotIndex = -1;
|
|
3604
|
+
await db.runTransaction(async (tx) => {
|
|
3605
|
+
const freshSnap = await tx.get(calDocRef);
|
|
3606
|
+
if (!freshSnap.exists) {
|
|
3607
|
+
throw new Error(`addCalendarSlot: calendario ${calDocRef.id} desapareci\xF3`);
|
|
3608
|
+
}
|
|
3609
|
+
const freshCal = freshSnap.data();
|
|
3610
|
+
const freshSemanas = freshCal.semanas ?? [];
|
|
3611
|
+
const freshSemana = freshSemanas[semana - 1];
|
|
3612
|
+
if (!freshSemana) {
|
|
3613
|
+
throw new Error(`Semana ${semana} no existe en el calendario ${mes}`);
|
|
3614
|
+
}
|
|
3615
|
+
const freshItems = freshSemana.items ?? [];
|
|
3616
|
+
freshItems.push({
|
|
3617
|
+
...slot,
|
|
3618
|
+
estado: slot.estado ?? ESTADO_CALENDARIO_ITEM.PLANIFICADO
|
|
3619
|
+
});
|
|
3620
|
+
resultSlotIndex = freshItems.length - 1;
|
|
3621
|
+
freshSemana.items = freshItems;
|
|
3622
|
+
freshSemanas[semana - 1] = freshSemana;
|
|
3623
|
+
tx.update(calDocRef, {
|
|
3624
|
+
semanas: freshSemanas,
|
|
3625
|
+
updatedAt: import_firebase_admin6.firestore.FieldValue.serverTimestamp()
|
|
3626
|
+
});
|
|
3627
|
+
});
|
|
3628
|
+
return { ok: true, slotIndex: resultSlotIndex };
|
|
3629
|
+
}
|
|
3630
|
+
var SlotSchema = import_zod30.z.object({
|
|
3631
|
+
dia: import_zod30.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Formato YYYY-MM-DD"),
|
|
3632
|
+
plataforma: import_zod30.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
|
|
3633
|
+
tipo: import_zod30.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]),
|
|
3634
|
+
keyword: import_zod30.z.string().min(1),
|
|
3635
|
+
tema: import_zod30.z.string().optional(),
|
|
3636
|
+
productoId: import_zod30.z.string().optional(),
|
|
3637
|
+
estado: import_zod30.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
|
|
3638
|
+
locationId: import_zod30.z.string().optional(),
|
|
3639
|
+
locationNombre: import_zod30.z.string().optional()
|
|
3640
|
+
});
|
|
3641
|
+
var ParamsSchema2 = import_zod30.z.object({
|
|
3642
|
+
tenantId: import_zod30.z.string().min(1),
|
|
3643
|
+
brandId: import_zod30.z.string().min(1),
|
|
3644
|
+
mes: import_zod30.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM"),
|
|
3645
|
+
semana: import_zod30.z.number().int().min(1).max(5),
|
|
3646
|
+
slot: SlotSchema
|
|
3647
|
+
});
|
|
3648
|
+
var OutputSchema2 = import_zod30.z.discriminatedUnion("ok", [
|
|
3649
|
+
import_zod30.z.object({ ok: import_zod30.z.literal(true), slotIndex: import_zod30.z.number().int() }),
|
|
3650
|
+
import_zod30.z.object({ ok: import_zod30.z.literal(false), error: import_zod30.z.string() })
|
|
3651
|
+
]);
|
|
3652
|
+
var rawContract2 = {
|
|
3653
|
+
name: "add_calendar_slot",
|
|
3654
|
+
description: "Add a new slot to the editorial calendar. Does NOT modify existing slots \u2014 use update_calendar_slot for that.",
|
|
3655
|
+
paramsSchema: ParamsSchema2,
|
|
3656
|
+
outputSchema: OutputSchema2,
|
|
3657
|
+
requiresConfirmation: false,
|
|
3658
|
+
destructive: false,
|
|
3659
|
+
affectsPublication: false,
|
|
3660
|
+
affectsExternal: false,
|
|
3661
|
+
martinSummaryTemplate: (input, output, locale) => {
|
|
3662
|
+
if (!output.ok) {
|
|
3663
|
+
return locale === "en" ? `I couldn't add the slot: ${output.error}` : `No pude agregar el slot: ${output.error}`;
|
|
3664
|
+
}
|
|
3665
|
+
return locale === "en" ? `Added a slot in week ${input.semana} of ${input.mes}.` : `Agregu\xE9 un slot en la semana ${input.semana} de ${input.mes}.`;
|
|
3666
|
+
},
|
|
3667
|
+
auditAction: "marketing.calendario.slot.agregar",
|
|
3668
|
+
extractTargetPath: (input, output) => {
|
|
3669
|
+
if (!output.ok) return "";
|
|
3670
|
+
return `tenants/${input.tenantId}/marketing_calendario/(brand=${input.brandId},mes=${input.mes}).semana=${input.semana}.slot=${output.slotIndex}`;
|
|
3671
|
+
},
|
|
3672
|
+
extractChanges: (input, output) => ({
|
|
3673
|
+
before: null,
|
|
3674
|
+
after: output.ok ? input.slot : null
|
|
3675
|
+
}),
|
|
3676
|
+
quotasConsumed: [],
|
|
3677
|
+
permissionScope: "module",
|
|
3678
|
+
permissionKey: "marketing",
|
|
3679
|
+
permissionAction: "editar",
|
|
3680
|
+
sideEffects: ["writes_firestore", "updates_calendar_slot"]
|
|
3681
|
+
};
|
|
3682
|
+
var addCalendarSlotContract = MartinContractSchema.parse(
|
|
3683
|
+
rawContract2
|
|
3684
|
+
);
|
|
3509
3685
|
var CAMPOS_SEMANTICOS = ["keyword", "tema", "plataforma", "tipo"];
|
|
3510
3686
|
function mapContenidoEstadoToSlotEstado(contenidoEstado) {
|
|
3511
3687
|
switch (contenidoEstado) {
|
|
@@ -3542,26 +3718,24 @@ async function calendarSlotUpdater(input) {
|
|
|
3542
3718
|
return { ok: false, error: `Semana ${semana} no existe` };
|
|
3543
3719
|
}
|
|
3544
3720
|
const items = semanaData.items ?? [];
|
|
3545
|
-
|
|
3721
|
+
if (slotIndex >= items.length) {
|
|
3722
|
+
return {
|
|
3723
|
+
ok: false,
|
|
3724
|
+
error: `slotIndex=${slotIndex} >= items.length=${items.length} for week=${semana}`,
|
|
3725
|
+
code: "SLOT_NOT_FOUND"
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3546
3728
|
const tocaSemantica = CAMPOS_SEMANTICOS.some(
|
|
3547
3729
|
(campo) => campo in cambios && cambios[campo] !== void 0
|
|
3548
3730
|
);
|
|
3549
|
-
if (isNewSlot) {
|
|
3550
|
-
return handleAddSlot({ db, calDocRef, semana, cambios });
|
|
3551
|
-
}
|
|
3552
3731
|
const slotAnterior = items[slotIndex];
|
|
3553
3732
|
const oldContenidoRef = slotAnterior.contenidoRef ?? null;
|
|
3554
3733
|
if (oldContenidoRef && tocaSemantica && !accionContenidoExistente) {
|
|
3555
3734
|
return {
|
|
3556
3735
|
ok: false,
|
|
3557
|
-
error: `
|
|
3736
|
+
error: `slot has contenidoRef=${oldContenidoRef} and cambios touch semantic fields=[${CAMPOS_SEMANTICOS.filter((f) => f in cambios).join(",")}]; require accionContenidoExistente`,
|
|
3558
3737
|
code: "ACCION_CONTENIDO_EXISTENTE_REQUIRED",
|
|
3559
|
-
opciones: [
|
|
3560
|
-
`'descartar' \u2014 marcar "${oldContenidoRef}" como descartado y aplicar cambios en este slot`,
|
|
3561
|
-
`'mover:semana:N:slot:M' \u2014 mover "${oldContenidoRef}" al slot destino N/M (target debe estar vacio) y aplicar cambios aqui`,
|
|
3562
|
-
`'nuevo_slot' \u2014 NO tocar este slot ni "${oldContenidoRef}". Crear un slot nuevo el mismo dia con los cambios (util para multiples publicaciones/dia \u2014 ej: almuerzo + cena GBP)`,
|
|
3563
|
-
`'mantener' \u2014 mantener "${oldContenidoRef}" aqui y aplicar cambios (caso typo fix, el contenido viejo sigue valido para el nuevo keyword/tema)`
|
|
3564
|
-
]
|
|
3738
|
+
opciones: ["descartar", "mover", "nuevo_slot", "mantener"]
|
|
3565
3739
|
};
|
|
3566
3740
|
}
|
|
3567
3741
|
let contenidosADescartar = [];
|
|
@@ -3581,7 +3755,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3581
3755
|
if (!m) {
|
|
3582
3756
|
return {
|
|
3583
3757
|
ok: false,
|
|
3584
|
-
error: `accionContenidoExistente
|
|
3758
|
+
error: `accionContenidoExistente="${accionContenidoExistente}" invalid format (expected: mover:semana:N:slot:M)`,
|
|
3585
3759
|
code: "MOVE_TARGET_INVALID"
|
|
3586
3760
|
};
|
|
3587
3761
|
}
|
|
@@ -3590,7 +3764,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3590
3764
|
if (targetSemanaNum === semana && targetSlotIdx === slotIndex) {
|
|
3591
3765
|
return {
|
|
3592
3766
|
ok: false,
|
|
3593
|
-
error: "
|
|
3767
|
+
error: "move target same as origin (semantic noop)",
|
|
3594
3768
|
code: "MOVE_TARGET_INVALID"
|
|
3595
3769
|
};
|
|
3596
3770
|
}
|
|
@@ -3598,7 +3772,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3598
3772
|
if (!targetSemana) {
|
|
3599
3773
|
return {
|
|
3600
3774
|
ok: false,
|
|
3601
|
-
error: `
|
|
3775
|
+
error: `target semana=${targetSemanaNum} does not exist`,
|
|
3602
3776
|
code: "MOVE_TARGET_INVALID"
|
|
3603
3777
|
};
|
|
3604
3778
|
}
|
|
@@ -3606,14 +3780,14 @@ async function calendarSlotUpdater(input) {
|
|
|
3606
3780
|
if (!targetItems[targetSlotIdx]) {
|
|
3607
3781
|
return {
|
|
3608
3782
|
ok: false,
|
|
3609
|
-
error: `
|
|
3783
|
+
error: `target slot=${targetSlotIdx} does not exist in semana=${targetSemanaNum}`,
|
|
3610
3784
|
code: "MOVE_TARGET_INVALID"
|
|
3611
3785
|
};
|
|
3612
3786
|
}
|
|
3613
3787
|
if (targetItems[targetSlotIdx].contenidoRef) {
|
|
3614
3788
|
return {
|
|
3615
3789
|
ok: false,
|
|
3616
|
-
error: `
|
|
3790
|
+
error: `target slot already has contenidoRef=${targetItems[targetSlotIdx].contenidoRef}`,
|
|
3617
3791
|
code: "MOVE_TARGET_OCCUPIED"
|
|
3618
3792
|
};
|
|
3619
3793
|
}
|
|
@@ -3694,7 +3868,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3694
3868
|
freshSemanas[semana - 1] = freshSemana;
|
|
3695
3869
|
tx.update(calDocRef, {
|
|
3696
3870
|
semanas: freshSemanas,
|
|
3697
|
-
updatedAt:
|
|
3871
|
+
updatedAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
|
|
3698
3872
|
});
|
|
3699
3873
|
});
|
|
3700
3874
|
if (moveTarget && oldContenidoRef) {
|
|
@@ -3715,7 +3889,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3715
3889
|
await c.ref.update({
|
|
3716
3890
|
estado: "descartado",
|
|
3717
3891
|
rechazadoMotivo: "Slot del calendario fue modificado",
|
|
3718
|
-
rechazadoAt:
|
|
3892
|
+
rechazadoAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
|
|
3719
3893
|
});
|
|
3720
3894
|
descartados++;
|
|
3721
3895
|
} catch (err) {
|
|
@@ -3733,35 +3907,6 @@ async function calendarSlotUpdater(input) {
|
|
|
3733
3907
|
...movedTo ? { movedTo } : {}
|
|
3734
3908
|
};
|
|
3735
3909
|
}
|
|
3736
|
-
async function handleAddSlot(args) {
|
|
3737
|
-
const { db, calDocRef, semana, cambios } = args;
|
|
3738
|
-
let resultSlotIndex = -1;
|
|
3739
|
-
await db.runTransaction(async (tx) => {
|
|
3740
|
-
const freshSnap = await tx.get(calDocRef);
|
|
3741
|
-
if (!freshSnap.exists) {
|
|
3742
|
-
throw new Error(`handleAddSlot: calendario ${calDocRef.id} desaparecio`);
|
|
3743
|
-
}
|
|
3744
|
-
const freshCal = freshSnap.data();
|
|
3745
|
-
const freshSemanas = freshCal.semanas ?? [];
|
|
3746
|
-
const freshSemana = freshSemanas[semana - 1];
|
|
3747
|
-
if (!freshSemana) {
|
|
3748
|
-
throw new Error(`handleAddSlot: semana ${semana} desaparecio`);
|
|
3749
|
-
}
|
|
3750
|
-
const freshItems = freshSemana.items ?? [];
|
|
3751
|
-
freshItems.push({
|
|
3752
|
-
...cambios,
|
|
3753
|
-
estado: cambios.estado ?? ESTADO_CALENDARIO_ITEM.PLANIFICADO
|
|
3754
|
-
});
|
|
3755
|
-
resultSlotIndex = freshItems.length - 1;
|
|
3756
|
-
freshSemana.items = freshItems;
|
|
3757
|
-
freshSemanas[semana - 1] = freshSemana;
|
|
3758
|
-
tx.update(calDocRef, {
|
|
3759
|
-
semanas: freshSemanas,
|
|
3760
|
-
updatedAt: import_firebase_admin6.firestore.FieldValue.serverTimestamp()
|
|
3761
|
-
});
|
|
3762
|
-
});
|
|
3763
|
-
return { ok: true, action: "added", slotIndex: resultSlotIndex, descartados: 0 };
|
|
3764
|
-
}
|
|
3765
3910
|
async function handleNuevoSlot(args) {
|
|
3766
3911
|
const { db, calDocRef, semana, cambios, currentSlot } = args;
|
|
3767
3912
|
const dia = cambios.dia ?? currentSlot.dia ?? null;
|
|
@@ -3788,7 +3933,7 @@ async function handleNuevoSlot(args) {
|
|
|
3788
3933
|
freshSemanas[semana - 1] = freshSemana;
|
|
3789
3934
|
tx.update(calDocRef, {
|
|
3790
3935
|
semanas: freshSemanas,
|
|
3791
|
-
updatedAt:
|
|
3936
|
+
updatedAt: import_firebase_admin7.firestore.FieldValue.serverTimestamp()
|
|
3792
3937
|
});
|
|
3793
3938
|
});
|
|
3794
3939
|
return {
|
|
@@ -3798,47 +3943,53 @@ async function handleNuevoSlot(args) {
|
|
|
3798
3943
|
descartados: 0
|
|
3799
3944
|
};
|
|
3800
3945
|
}
|
|
3801
|
-
var
|
|
3802
|
-
tenantId:
|
|
3803
|
-
brandId:
|
|
3804
|
-
mes:
|
|
3805
|
-
semana:
|
|
3806
|
-
slotIndex:
|
|
3807
|
-
cambios:
|
|
3808
|
-
accionContenidoExistente:
|
|
3946
|
+
var ParamsSchema3 = import_zod31.z.object({
|
|
3947
|
+
tenantId: import_zod31.z.string().min(1),
|
|
3948
|
+
brandId: import_zod31.z.string().min(1),
|
|
3949
|
+
mes: import_zod31.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM"),
|
|
3950
|
+
semana: import_zod31.z.number().int().min(1).max(5),
|
|
3951
|
+
slotIndex: import_zod31.z.number().int().min(0),
|
|
3952
|
+
cambios: import_zod31.z.record(import_zod31.z.string(), import_zod31.z.unknown()),
|
|
3953
|
+
accionContenidoExistente: import_zod31.z.string().optional()
|
|
3809
3954
|
});
|
|
3810
|
-
var
|
|
3811
|
-
|
|
3812
|
-
ok:
|
|
3813
|
-
action:
|
|
3814
|
-
slotIndex:
|
|
3815
|
-
descartados:
|
|
3816
|
-
movedTo:
|
|
3955
|
+
var OutputSchema3 = import_zod31.z.discriminatedUnion("ok", [
|
|
3956
|
+
import_zod31.z.object({
|
|
3957
|
+
ok: import_zod31.z.literal(true),
|
|
3958
|
+
action: import_zod31.z.enum(["updated", "nuevo_slot", "moved"]),
|
|
3959
|
+
slotIndex: import_zod31.z.number().int(),
|
|
3960
|
+
descartados: import_zod31.z.number().int(),
|
|
3961
|
+
movedTo: import_zod31.z.string().optional()
|
|
3817
3962
|
}),
|
|
3818
|
-
|
|
3819
|
-
ok:
|
|
3820
|
-
error:
|
|
3821
|
-
code:
|
|
3822
|
-
opciones:
|
|
3963
|
+
import_zod31.z.object({
|
|
3964
|
+
ok: import_zod31.z.literal(false),
|
|
3965
|
+
error: import_zod31.z.string(),
|
|
3966
|
+
code: import_zod31.z.enum(["SLOT_NOT_FOUND", "ACCION_CONTENIDO_EXISTENTE_REQUIRED", "MOVE_TARGET_OCCUPIED", "MOVE_TARGET_INVALID"]).optional(),
|
|
3967
|
+
opciones: import_zod31.z.array(import_zod31.z.string()).optional()
|
|
3823
3968
|
})
|
|
3824
3969
|
]);
|
|
3825
|
-
var
|
|
3970
|
+
var rawContract3 = {
|
|
3826
3971
|
name: "update_calendar_slot",
|
|
3827
|
-
description: "
|
|
3828
|
-
paramsSchema:
|
|
3829
|
-
outputSchema:
|
|
3972
|
+
description: "MODIFY an existing slot in the editorial calendar. Does NOT add new slots \u2014 use add_calendar_slot for that. If the slot already had a contenidoRef and the changes touch semantic fields (keyword/tema/plataforma/tipo), requires accionContenidoExistente with 4 options: descartar | mover:semana:N:slot:M | nuevo_slot | mantener.",
|
|
3973
|
+
paramsSchema: ParamsSchema3,
|
|
3974
|
+
outputSchema: OutputSchema3,
|
|
3975
|
+
// Por default, modificar un slot es reversible (cambias campos cosméticos).
|
|
3976
|
+
// PERO si accionContenidoExistente='descartar', el helper marca el
|
|
3977
|
+
// contenido vinculado como descartado — eso SÍ es destructivo. El predicado
|
|
3978
|
+
// runtime `isDestructiveForInput` lo detecta y obliga al wrapper a pedir
|
|
3979
|
+
// confirmación en ese caso específico.
|
|
3830
3980
|
requiresConfirmation: false,
|
|
3831
3981
|
destructive: false,
|
|
3832
|
-
|
|
3982
|
+
isDestructiveForInput: (input) => input.accionContenidoExistente === "descartar",
|
|
3833
3983
|
affectsPublication: false,
|
|
3834
3984
|
affectsExternal: false,
|
|
3835
3985
|
martinSummaryTemplate: (input, output, locale) => {
|
|
3836
3986
|
if (!output.ok) {
|
|
3837
|
-
if (
|
|
3838
|
-
|
|
3987
|
+
if (output.code) {
|
|
3988
|
+
return getMessage(`marketing.errors.${output.code}`, locale);
|
|
3989
|
+
}
|
|
3990
|
+
return getMessage("marketing.safeError.generic", locale);
|
|
3839
3991
|
}
|
|
3840
3992
|
const verb = {
|
|
3841
|
-
added: locale === "en" ? "added" : "agregu\xE9",
|
|
3842
3993
|
updated: locale === "en" ? "updated" : "actualic\xE9",
|
|
3843
3994
|
nuevo_slot: locale === "en" ? "created a new slot" : "cre\xE9 un slot nuevo",
|
|
3844
3995
|
moved: locale === "en" ? "moved" : "mov\xED"
|
|
@@ -3848,6 +3999,15 @@ var rawContract2 = {
|
|
|
3848
3999
|
}
|
|
3849
4000
|
return `${verb[0].toUpperCase() + verb.slice(1)} el slot en la semana ${input.semana} (${input.mes}).`;
|
|
3850
4001
|
},
|
|
4002
|
+
// Confirmation message when isDestructiveForInput returns true
|
|
4003
|
+
// (accionContenidoExistente === 'descartar'). Only invoked if the wrapper
|
|
4004
|
+
// detects the destructive runtime case and asks the user for OK.
|
|
4005
|
+
martinConfirmationTemplate: (input, locale) => {
|
|
4006
|
+
if (locale === "en") {
|
|
4007
|
+
return `Are you sure? This will discard the existing content linked to slot ${input.slotIndex} in week ${input.semana} (${input.mes}). The content will be marked as discarded and cannot be easily recovered.`;
|
|
4008
|
+
}
|
|
4009
|
+
return `\xBFConfirmas? Esto descarta el contenido existente del slot ${input.slotIndex} en la semana ${input.semana} (${input.mes}). El contenido queda marcado como descartado y no se recupera f\xE1cilmente.`;
|
|
4010
|
+
},
|
|
3851
4011
|
auditAction: "marketing.calendario.slot.actualizar",
|
|
3852
4012
|
extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_calendario/(brand=${input.brandId},mes=${input.mes}).semana=${input.semana}.slot=${input.slotIndex}`,
|
|
3853
4013
|
extractChanges: (input, output) => ({
|
|
@@ -3868,7 +4028,7 @@ var rawContract2 = {
|
|
|
3868
4028
|
sideEffects: ["writes_firestore", "updates_calendar_slot"]
|
|
3869
4029
|
};
|
|
3870
4030
|
var calendarSlotUpdaterContract = MartinContractSchema.parse(
|
|
3871
|
-
|
|
4031
|
+
rawContract3
|
|
3872
4032
|
);
|
|
3873
4033
|
async function getCalendar(input) {
|
|
3874
4034
|
const { db, tenantId, brandId, mes } = input;
|
|
@@ -3890,23 +4050,23 @@ async function getCalendar(input) {
|
|
|
3890
4050
|
calendario: { id: doc.id, ...doc.data() }
|
|
3891
4051
|
};
|
|
3892
4052
|
}
|
|
3893
|
-
var
|
|
3894
|
-
tenantId:
|
|
3895
|
-
brandId:
|
|
3896
|
-
mes:
|
|
4053
|
+
var ParamsSchema4 = import_zod32.z.object({
|
|
4054
|
+
tenantId: import_zod32.z.string().min(1),
|
|
4055
|
+
brandId: import_zod32.z.string().min(1),
|
|
4056
|
+
mes: import_zod32.z.string().regex(/^\d{4}-\d{2}$/, "Formato YYYY-MM")
|
|
3897
4057
|
});
|
|
3898
|
-
var
|
|
3899
|
-
ok:
|
|
3900
|
-
mes:
|
|
3901
|
-
brandId:
|
|
3902
|
-
calendario:
|
|
3903
|
-
mensaje:
|
|
4058
|
+
var OutputSchema4 = import_zod32.z.object({
|
|
4059
|
+
ok: import_zod32.z.literal(true),
|
|
4060
|
+
mes: import_zod32.z.string(),
|
|
4061
|
+
brandId: import_zod32.z.string(),
|
|
4062
|
+
calendario: import_zod32.z.record(import_zod32.z.string(), import_zod32.z.unknown()).nullable(),
|
|
4063
|
+
mensaje: import_zod32.z.string().optional()
|
|
3904
4064
|
});
|
|
3905
|
-
var
|
|
4065
|
+
var rawContract4 = {
|
|
3906
4066
|
name: "get_calendar",
|
|
3907
|
-
description: "
|
|
3908
|
-
paramsSchema:
|
|
3909
|
-
outputSchema:
|
|
4067
|
+
description: "Read the editorial calendar for a given brand and month. Returns weeks with planned items per platform.",
|
|
4068
|
+
paramsSchema: ParamsSchema4,
|
|
4069
|
+
outputSchema: OutputSchema4,
|
|
3910
4070
|
requiresConfirmation: false,
|
|
3911
4071
|
destructive: false,
|
|
3912
4072
|
affectsPublication: false,
|
|
@@ -3928,7 +4088,7 @@ var rawContract3 = {
|
|
|
3928
4088
|
sideEffects: ["reads_firestore"]
|
|
3929
4089
|
};
|
|
3930
4090
|
var getCalendarContract = MartinContractSchema.parse(
|
|
3931
|
-
|
|
4091
|
+
rawContract4
|
|
3932
4092
|
);
|
|
3933
4093
|
var PLATAFORMA_A_FORMATO = {
|
|
3934
4094
|
gbp: "gbp_4_3",
|
|
@@ -3966,12 +4126,12 @@ async function photoAssigner(input) {
|
|
|
3966
4126
|
mediaVariante: {
|
|
3967
4127
|
url: varianteUrl,
|
|
3968
4128
|
formato,
|
|
3969
|
-
linkedAt:
|
|
4129
|
+
linkedAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp(),
|
|
3970
4130
|
permalink: null,
|
|
3971
4131
|
// lo llenara publishToInstagram/etc en el futuro
|
|
3972
4132
|
mediaExternalId: null
|
|
3973
4133
|
},
|
|
3974
|
-
editadoAt:
|
|
4134
|
+
editadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
|
|
3975
4135
|
};
|
|
3976
4136
|
let slotResolved = null;
|
|
3977
4137
|
if (calendarioItemRef) {
|
|
@@ -4021,7 +4181,7 @@ async function photoAssigner(input) {
|
|
|
4021
4181
|
tx.update(contenidoRefDoc, contenidoUpdatePayload);
|
|
4022
4182
|
tx.update(calDocRef, {
|
|
4023
4183
|
semanas,
|
|
4024
|
-
updatedAt:
|
|
4184
|
+
updatedAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp()
|
|
4025
4185
|
});
|
|
4026
4186
|
});
|
|
4027
4187
|
} else {
|
|
@@ -4223,26 +4383,26 @@ var BRAND_BRIEF_SCHEMA_HINT = {
|
|
|
4223
4383
|
escenas: [{ id: string, nombre: string, promptHint: string }]
|
|
4224
4384
|
}`
|
|
4225
4385
|
};
|
|
4226
|
-
var
|
|
4227
|
-
tenantId:
|
|
4228
|
-
brandId:
|
|
4386
|
+
var ParamsSchema5 = import_zod33.z.object({
|
|
4387
|
+
tenantId: import_zod33.z.string().min(1),
|
|
4388
|
+
brandId: import_zod33.z.string().min(1)
|
|
4229
4389
|
});
|
|
4230
|
-
var
|
|
4231
|
-
|
|
4232
|
-
ok:
|
|
4390
|
+
var OutputSchema5 = import_zod33.z.discriminatedUnion("ok", [
|
|
4391
|
+
import_zod33.z.object({
|
|
4392
|
+
ok: import_zod33.z.literal(true),
|
|
4233
4393
|
/** Payload con instrucción + datos del negocio para que Claude genere el brief. */
|
|
4234
|
-
payload:
|
|
4394
|
+
payload: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown())
|
|
4235
4395
|
}),
|
|
4236
|
-
|
|
4237
|
-
ok:
|
|
4238
|
-
error:
|
|
4396
|
+
import_zod33.z.object({
|
|
4397
|
+
ok: import_zod33.z.literal(false),
|
|
4398
|
+
error: import_zod33.z.string()
|
|
4239
4399
|
})
|
|
4240
4400
|
]);
|
|
4241
|
-
var
|
|
4401
|
+
var rawContract5 = {
|
|
4242
4402
|
name: "generate_brand_brief",
|
|
4243
|
-
description: "
|
|
4244
|
-
paramsSchema:
|
|
4245
|
-
outputSchema:
|
|
4403
|
+
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.",
|
|
4404
|
+
paramsSchema: ParamsSchema5,
|
|
4405
|
+
outputSchema: OutputSchema5,
|
|
4246
4406
|
// Lectura pura, sin side effects de escritura ni publicación.
|
|
4247
4407
|
requiresConfirmation: false,
|
|
4248
4408
|
destructive: false,
|
|
@@ -4265,7 +4425,7 @@ var rawContract4 = {
|
|
|
4265
4425
|
sideEffects: ["reads_firestore"]
|
|
4266
4426
|
};
|
|
4267
4427
|
var brandBriefBuilderContract = MartinContractSchema.parse(
|
|
4268
|
-
|
|
4428
|
+
rawContract5
|
|
4269
4429
|
);
|
|
4270
4430
|
async function resolveLastImportId(db, tenantId, brandId) {
|
|
4271
4431
|
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
@@ -4509,7 +4669,7 @@ async function contenidoWriter(input) {
|
|
|
4509
4669
|
await db.doc(`tenants/${tenantId}/marketing_contenido/${existente.id}`).update({
|
|
4510
4670
|
estado: "descartado",
|
|
4511
4671
|
rechazadoMotivo: "Reemplazado por contenido nuevo para el mismo slot",
|
|
4512
|
-
rechazadoAt:
|
|
4672
|
+
rechazadoAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp()
|
|
4513
4673
|
});
|
|
4514
4674
|
descartados++;
|
|
4515
4675
|
}
|
|
@@ -4568,7 +4728,7 @@ async function contenidoWriter(input) {
|
|
|
4568
4728
|
mediaUrl: null,
|
|
4569
4729
|
calendarioItemRef: calendarioItemRef ?? null,
|
|
4570
4730
|
datos,
|
|
4571
|
-
creadoAt:
|
|
4731
|
+
creadoAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp(),
|
|
4572
4732
|
creadoPorId: "mcp-cowork",
|
|
4573
4733
|
origen: "ai_assisted"
|
|
4574
4734
|
});
|
|
@@ -4626,7 +4786,7 @@ async function contenidoWriter(input) {
|
|
|
4626
4786
|
tx.create(contenidoRef, contenido);
|
|
4627
4787
|
tx.update(calDocRef, {
|
|
4628
4788
|
semanas,
|
|
4629
|
-
updatedAt:
|
|
4789
|
+
updatedAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp()
|
|
4630
4790
|
});
|
|
4631
4791
|
});
|
|
4632
4792
|
} else {
|
|
@@ -4744,7 +4904,7 @@ async function weeklyContentBuilder(input) {
|
|
|
4744
4904
|
brandId,
|
|
4745
4905
|
mes: targetMes,
|
|
4746
4906
|
semanas,
|
|
4747
|
-
creadoAt:
|
|
4907
|
+
creadoAt: import_firebase_admin10.firestore.FieldValue.serverTimestamp(),
|
|
4748
4908
|
creadoPorId: "mcp-cowork",
|
|
4749
4909
|
updatedAt: null
|
|
4750
4910
|
};
|
|
@@ -4996,7 +5156,7 @@ function diversify(items, jaccardThreshold = 0.6) {
|
|
|
4996
5156
|
}
|
|
4997
5157
|
async function contentFinder(input) {
|
|
4998
5158
|
const { db, tenantId, brandId, contexto, fecha, include, limit, diversidad, deps } = input;
|
|
4999
|
-
const
|
|
5159
|
+
const mode = input.mode ?? "text";
|
|
5000
5160
|
const textThreshold = deps.thresholds?.text ?? DEFAULT_TEXT_THRESHOLD;
|
|
5001
5161
|
const imageThreshold = deps.thresholds?.image ?? DEFAULT_IMAGE_THRESHOLD;
|
|
5002
5162
|
const conflictThreshold = deps.thresholds?.conflictSimilarity ?? DEFAULT_CONFLICT_THRESHOLD;
|
|
@@ -5027,7 +5187,7 @@ async function contentFinder(input) {
|
|
|
5027
5187
|
queryEmbedding: queryEmbedding ?? void 0,
|
|
5028
5188
|
queryText
|
|
5029
5189
|
};
|
|
5030
|
-
if (
|
|
5190
|
+
if (mode === "text") {
|
|
5031
5191
|
return safeSearch(true, {
|
|
5032
5192
|
...baseParams,
|
|
5033
5193
|
limit: lim,
|
|
@@ -6256,6 +6416,90 @@ async function checkQuota(tenantId, quotaName) {
|
|
|
6256
6416
|
async function getCurrentUsage(_tenantId, _quotaName) {
|
|
6257
6417
|
return 0;
|
|
6258
6418
|
}
|
|
6419
|
+
var NIVELES_CANONICOS = [
|
|
6420
|
+
"ninguno",
|
|
6421
|
+
"ver",
|
|
6422
|
+
"editar",
|
|
6423
|
+
"completo"
|
|
6424
|
+
];
|
|
6425
|
+
var CACHE_TTL_MS = 60 * 1e3;
|
|
6426
|
+
var _rolesCache = /* @__PURE__ */ new Map();
|
|
6427
|
+
var _userCache = /* @__PURE__ */ new Map();
|
|
6428
|
+
function _getCached(map, key) {
|
|
6429
|
+
const entry = map.get(key);
|
|
6430
|
+
if (!entry) return void 0;
|
|
6431
|
+
if (Date.now() - entry.t > CACHE_TTL_MS) {
|
|
6432
|
+
map.delete(key);
|
|
6433
|
+
return void 0;
|
|
6434
|
+
}
|
|
6435
|
+
return entry.data;
|
|
6436
|
+
}
|
|
6437
|
+
function _setCached(map, key, data) {
|
|
6438
|
+
map.set(key, { t: Date.now(), data });
|
|
6439
|
+
}
|
|
6440
|
+
async function _readRolesDoc(db, tenantId) {
|
|
6441
|
+
const key = `roles:${tenantId}`;
|
|
6442
|
+
const cached = _getCached(_rolesCache, key);
|
|
6443
|
+
if (cached !== void 0) return cached;
|
|
6444
|
+
const ref = db.collection("configuracion").doc(`${tenantId}_roles`);
|
|
6445
|
+
const snap = await ref.get();
|
|
6446
|
+
const data = snap.exists ? snap.data() : null;
|
|
6447
|
+
_setCached(_rolesCache, key, data);
|
|
6448
|
+
return data;
|
|
6449
|
+
}
|
|
6450
|
+
async function _readUserDoc(db, uid) {
|
|
6451
|
+
const key = `user:${uid}`;
|
|
6452
|
+
const cached = _getCached(_userCache, key);
|
|
6453
|
+
if (cached !== void 0) return cached;
|
|
6454
|
+
const ref = db.collection("usuarios").doc(uid);
|
|
6455
|
+
const snap = await ref.get();
|
|
6456
|
+
const data = snap.exists ? snap.data() : null;
|
|
6457
|
+
_setCached(_userCache, key, data);
|
|
6458
|
+
return data;
|
|
6459
|
+
}
|
|
6460
|
+
async function getUserContext({
|
|
6461
|
+
db,
|
|
6462
|
+
tenantId,
|
|
6463
|
+
uid
|
|
6464
|
+
}) {
|
|
6465
|
+
const userDoc = await _readUserDoc(db, uid);
|
|
6466
|
+
if (!userDoc) {
|
|
6467
|
+
throw new Error(`userContext: usuario ${uid} no encontrado.`);
|
|
6468
|
+
}
|
|
6469
|
+
const rolId = userDoc.rol ?? "empleado";
|
|
6470
|
+
const rolNombre = rolId === "super_admin" ? "Super Admin" : await _resolveRolNombre(db, tenantId, rolId);
|
|
6471
|
+
return {
|
|
6472
|
+
uid,
|
|
6473
|
+
nombre: userDoc.nombre ?? userDoc.email ?? "Usuario",
|
|
6474
|
+
email: userDoc.email ?? null,
|
|
6475
|
+
rolId,
|
|
6476
|
+
rolNombre,
|
|
6477
|
+
idiomaPreferido: userDoc.idiomaPreferido ?? "es"
|
|
6478
|
+
};
|
|
6479
|
+
}
|
|
6480
|
+
async function _resolveRolNombre(db, tenantId, rolId) {
|
|
6481
|
+
const rolesData = await _readRolesDoc(db, tenantId);
|
|
6482
|
+
if (!rolesData) return null;
|
|
6483
|
+
const rolData = rolesData[rolId];
|
|
6484
|
+
return rolData?.nombre ?? null;
|
|
6485
|
+
}
|
|
6486
|
+
async function getUserPermissionLevel({
|
|
6487
|
+
db,
|
|
6488
|
+
tenantId,
|
|
6489
|
+
userRol,
|
|
6490
|
+
modulo
|
|
6491
|
+
}) {
|
|
6492
|
+
if (userRol === "super_admin") return "completo";
|
|
6493
|
+
if (!userRol) return "ninguno";
|
|
6494
|
+
const rolesData = await _readRolesDoc(db, tenantId);
|
|
6495
|
+
if (!rolesData) return "ninguno";
|
|
6496
|
+
const rolData = rolesData[userRol];
|
|
6497
|
+
if (!rolData || rolData.activo === false) return "ninguno";
|
|
6498
|
+
const permisos = rolData.permisosPorModulo || {};
|
|
6499
|
+
const nivel = permisos[modulo];
|
|
6500
|
+
if (!nivel || !NIVELES_CANONICOS.includes(nivel)) return "ninguno";
|
|
6501
|
+
return nivel;
|
|
6502
|
+
}
|
|
6259
6503
|
async function resolveTenantIdioma(tenantId) {
|
|
6260
6504
|
const db = (0, import_firestore5.getFirestore)();
|
|
6261
6505
|
const tenantSnap = await db.doc(`tenants/${tenantId}`).get();
|
|
@@ -6271,72 +6515,44 @@ async function resolveTenantIdioma(tenantId) {
|
|
|
6271
6515
|
}
|
|
6272
6516
|
return idioma;
|
|
6273
6517
|
}
|
|
6274
|
-
var MESSAGES = {
|
|
6275
|
-
es: {
|
|
6276
|
-
generic: "Tuve un problema. Intent\xE9moslo de nuevo en un momento.",
|
|
6277
|
-
quota_exceeded: "Alcanzaste el l\xEDmite mensual.",
|
|
6278
|
-
not_found: "No lo encontr\xE9. \xBFPodr\xEDas verificar el dato?",
|
|
6279
|
-
permission: "No tengo permiso para hacer eso desde tu cuenta.",
|
|
6280
|
-
timeout: "Esto est\xE1 tardando m\xE1s de lo normal. \xBFLo intento de nuevo?"
|
|
6281
|
-
},
|
|
6282
|
-
en: {
|
|
6283
|
-
generic: "I had a problem. Let's try again in a moment.",
|
|
6284
|
-
quota_exceeded: "You reached the monthly limit.",
|
|
6285
|
-
not_found: "I couldn't find it. Could you verify the data?",
|
|
6286
|
-
permission: "I don't have permission to do that from your account.",
|
|
6287
|
-
timeout: "This is taking longer than usual. Should I try again?"
|
|
6288
|
-
}
|
|
6289
|
-
};
|
|
6290
6518
|
function martinSafeError(err, locale) {
|
|
6291
|
-
if (!(locale in MESSAGES)) {
|
|
6292
|
-
throw new Error(
|
|
6293
|
-
`Locale '${locale}' no soportado en martinSafeError. Locales activos: ${Object.keys(MESSAGES).join(", ")}. Agregar traducciones antes de usar.`
|
|
6294
|
-
);
|
|
6295
|
-
}
|
|
6296
|
-
const msgs = MESSAGES[locale];
|
|
6297
6519
|
const e = err;
|
|
6298
6520
|
const message = e?.message ?? "";
|
|
6299
6521
|
const code = e?.code ?? "";
|
|
6300
|
-
|
|
6301
|
-
if (/
|
|
6302
|
-
if (/
|
|
6303
|
-
if (
|
|
6304
|
-
|
|
6305
|
-
}
|
|
6306
|
-
|
|
6307
|
-
es: {
|
|
6308
|
-
denied_role: "No tengo permiso para hacer eso desde tu cuenta. P\xEDdelo al encargado o admin que te autorice.",
|
|
6309
|
-
input_invalido: "Falta algo o est\xE1 mal. \xBFLo intentamos de nuevo?",
|
|
6310
|
-
confirm_default: "\xBFConfirmas?"
|
|
6311
|
-
},
|
|
6312
|
-
en: {
|
|
6313
|
-
denied_role: "I don't have permission to do that from your account. Ask the manager or admin to authorize it.",
|
|
6314
|
-
input_invalido: "Something is missing or off. Could you try again?",
|
|
6315
|
-
confirm_default: "Are you sure?"
|
|
6316
|
-
}
|
|
6317
|
-
};
|
|
6522
|
+
let key = "generic";
|
|
6523
|
+
if (/quota|límite|limit/i.test(message)) key = "quota_exceeded";
|
|
6524
|
+
else if (/not.found|no.encontrado|no existe/i.test(message)) key = "not_found";
|
|
6525
|
+
else if (/permission|permiso/i.test(message)) key = "permission";
|
|
6526
|
+
else if (code === "deadline-exceeded" || /timeout/i.test(message)) key = "timeout";
|
|
6527
|
+
return getMessage(`marketing.safeError.${key}`, locale);
|
|
6528
|
+
}
|
|
6318
6529
|
function getWrapperMessage(key, locale) {
|
|
6319
|
-
|
|
6320
|
-
throw new Error(
|
|
6321
|
-
`Locale '${locale}' no soportado en getWrapperMessage. Locales activos: ${Object.keys(MESSAGES2).join(", ")}. Agregar traducciones antes de usar.`
|
|
6322
|
-
);
|
|
6323
|
-
}
|
|
6324
|
-
const msg = MESSAGES2[locale][key];
|
|
6325
|
-
if (!msg) {
|
|
6326
|
-
throw new Error(
|
|
6327
|
-
`Mensaje '${key}' no definido en wrapperMessages para locale '${locale}'.`
|
|
6328
|
-
);
|
|
6329
|
-
}
|
|
6330
|
-
return msg;
|
|
6530
|
+
return getMessage(`marketing.wrapper.${key}`, locale);
|
|
6331
6531
|
}
|
|
6332
6532
|
var NIVELES_ORDER = ["ninguno", "ver", "editar", "completo"];
|
|
6333
|
-
function
|
|
6533
|
+
function nivelAlcanza2(nivel, accion) {
|
|
6334
6534
|
return NIVELES_ORDER.indexOf(nivel) >= NIVELES_ORDER.indexOf(accion);
|
|
6335
6535
|
}
|
|
6336
6536
|
function wrapWithContract(contract, helper, options = {}) {
|
|
6337
6537
|
return async function wrappedTool(input, ctx) {
|
|
6338
6538
|
const startMs = Date.now();
|
|
6339
6539
|
const locale = ctx.user.idiomaPreferido;
|
|
6540
|
+
const actorType = ctx.user.clientType === "martin" ? "martin" : "mcp_client";
|
|
6541
|
+
const buildActor = () => {
|
|
6542
|
+
const base = {
|
|
6543
|
+
type: actorType,
|
|
6544
|
+
uid: ctx.user.uid,
|
|
6545
|
+
nombre: ctx.user.nombre
|
|
6546
|
+
};
|
|
6547
|
+
const cm = ctx.user.clientMetadata;
|
|
6548
|
+
if (cm && (cm.mcpClient || cm.mcpClientVersion)) {
|
|
6549
|
+
base.metadata = {
|
|
6550
|
+
...cm.mcpClient ? { mcpClient: cm.mcpClient } : {},
|
|
6551
|
+
...cm.mcpClientVersion ? { mcpClientVersion: cm.mcpClientVersion } : {}
|
|
6552
|
+
};
|
|
6553
|
+
}
|
|
6554
|
+
return base;
|
|
6555
|
+
};
|
|
6340
6556
|
let accesoOk = false;
|
|
6341
6557
|
let reqDesc = "";
|
|
6342
6558
|
switch (contract.permissionScope) {
|
|
@@ -6361,7 +6577,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6361
6577
|
userRol: ctx.user.rol,
|
|
6362
6578
|
modulo: contract.permissionKey
|
|
6363
6579
|
});
|
|
6364
|
-
accesoOk =
|
|
6580
|
+
accesoOk = nivelAlcanza2(nivel, contract.permissionAction);
|
|
6365
6581
|
break;
|
|
6366
6582
|
}
|
|
6367
6583
|
case "self": {
|
|
@@ -6385,7 +6601,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6385
6601
|
await writeAuditLog({
|
|
6386
6602
|
tenantId: ctx.tenantId,
|
|
6387
6603
|
brandId: ctx.brandId ?? null,
|
|
6388
|
-
actor:
|
|
6604
|
+
actor: buildActor(),
|
|
6389
6605
|
action: contract.auditAction,
|
|
6390
6606
|
motivo: `Acceso denegado \u2014 rol '${ctx.user.rol}' sin permiso para ${reqDesc}`,
|
|
6391
6607
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6417,7 +6633,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6417
6633
|
await writeAuditLog({
|
|
6418
6634
|
tenantId: ctx.tenantId,
|
|
6419
6635
|
brandId: ctx.brandId ?? null,
|
|
6420
|
-
actor:
|
|
6636
|
+
actor: buildActor(),
|
|
6421
6637
|
action: contract.auditAction,
|
|
6422
6638
|
motivo: "Input inv\xE1lido \u2014 Zod parse failed",
|
|
6423
6639
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6433,7 +6649,10 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6433
6649
|
};
|
|
6434
6650
|
}
|
|
6435
6651
|
const parsedInput = parseResult.data;
|
|
6436
|
-
|
|
6652
|
+
const effectiveDestructive = contract.isDestructiveForInput?.(parsedInput) ?? contract.destructive;
|
|
6653
|
+
const effectivePublication = contract.isPublicationForInput?.(parsedInput) ?? contract.affectsPublication;
|
|
6654
|
+
const effectiveRequiresConfirmation = contract.requiresConfirmation || effectiveDestructive || effectivePublication;
|
|
6655
|
+
if (effectiveRequiresConfirmation && !ctx.confirmationGranted) {
|
|
6437
6656
|
const confirmMsg = contract.martinConfirmationTemplate ? contract.martinConfirmationTemplate(parsedInput, locale) : getWrapperMessage("confirm_default", locale);
|
|
6438
6657
|
return {
|
|
6439
6658
|
text: confirmMsg,
|
|
@@ -6458,7 +6677,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6458
6677
|
await writeAuditLog({
|
|
6459
6678
|
tenantId: ctx.tenantId,
|
|
6460
6679
|
brandId: ctx.brandId ?? null,
|
|
6461
|
-
actor:
|
|
6680
|
+
actor: buildActor(),
|
|
6462
6681
|
action: contract.auditAction,
|
|
6463
6682
|
motivo: `Quota excedida \u2014 ${quotaName}: ${check.current}/${check.limit}`,
|
|
6464
6683
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6480,9 +6699,9 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6480
6699
|
await writeAuditLog({
|
|
6481
6700
|
tenantId: ctx.tenantId,
|
|
6482
6701
|
brandId: ctx.brandId ?? null,
|
|
6483
|
-
actor:
|
|
6702
|
+
actor: buildActor(),
|
|
6484
6703
|
action: contract.auditAction,
|
|
6485
|
-
motivo:
|
|
6704
|
+
motivo: `Acci\xF3n solicitada v\xEDa ${actorType}`,
|
|
6486
6705
|
conversacionId: ctx.conversacionId ?? null,
|
|
6487
6706
|
status: "error",
|
|
6488
6707
|
errorMessage: err?.message ?? "unknown",
|
|
@@ -6509,7 +6728,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6509
6728
|
await writeAuditLog({
|
|
6510
6729
|
tenantId: ctx.tenantId,
|
|
6511
6730
|
brandId: ctx.brandId ?? null,
|
|
6512
|
-
actor:
|
|
6731
|
+
actor: buildActor(),
|
|
6513
6732
|
action: contract.auditAction,
|
|
6514
6733
|
motivo: `Output del helper inv\xE1lido \u2014 bug de "${contract.name}"`,
|
|
6515
6734
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6542,7 +6761,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6542
6761
|
await writeAuditLog({
|
|
6543
6762
|
tenantId: ctx.tenantId,
|
|
6544
6763
|
brandId: ctx.brandId ?? null,
|
|
6545
|
-
actor:
|
|
6764
|
+
actor: buildActor(),
|
|
6546
6765
|
action: contract.auditAction,
|
|
6547
6766
|
motivo: `disabled \u2014 ${code}${detail ? `: ${JSON.stringify(detail)}` : ""}`,
|
|
6548
6767
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6561,11 +6780,11 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6561
6780
|
await writeAuditLog({
|
|
6562
6781
|
tenantId: ctx.tenantId,
|
|
6563
6782
|
brandId: ctx.brandId ?? null,
|
|
6564
|
-
actor:
|
|
6783
|
+
actor: buildActor(),
|
|
6565
6784
|
action: contract.auditAction,
|
|
6566
6785
|
targetPath,
|
|
6567
6786
|
changes,
|
|
6568
|
-
motivo:
|
|
6787
|
+
motivo: `Acci\xF3n solicitada v\xEDa ${actorType}`,
|
|
6569
6788
|
conversacionId: ctx.conversacionId ?? null,
|
|
6570
6789
|
status: "success",
|
|
6571
6790
|
durationMs: Date.now() - startMs
|
|
@@ -6578,100 +6797,113 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6578
6797
|
};
|
|
6579
6798
|
};
|
|
6580
6799
|
}
|
|
6581
|
-
var RecordarMemoriaParamsSchema =
|
|
6800
|
+
var RecordarMemoriaParamsSchema = import_zod34.z.object({
|
|
6582
6801
|
tipo: TipoMemoriaEnum,
|
|
6583
6802
|
categoria: CategoriaMemoriaEnum,
|
|
6584
|
-
contenido:
|
|
6803
|
+
contenido: import_zod34.z.string().min(3).max(500)
|
|
6585
6804
|
});
|
|
6586
|
-
var RecordarMemoriaOutputSchema =
|
|
6587
|
-
memoriaId:
|
|
6588
|
-
status:
|
|
6805
|
+
var RecordarMemoriaOutputSchema = import_zod34.z.object({
|
|
6806
|
+
memoriaId: import_zod34.z.string(),
|
|
6807
|
+
status: import_zod34.z.literal("creada")
|
|
6589
6808
|
});
|
|
6590
|
-
var OlvidarMemoriaParamsSchema =
|
|
6591
|
-
memoriaId:
|
|
6592
|
-
motivo:
|
|
6809
|
+
var OlvidarMemoriaParamsSchema = import_zod34.z.object({
|
|
6810
|
+
memoriaId: import_zod34.z.string(),
|
|
6811
|
+
motivo: import_zod34.z.string().optional()
|
|
6593
6812
|
});
|
|
6594
|
-
var OlvidarMemoriaOutputSchema =
|
|
6595
|
-
status:
|
|
6813
|
+
var OlvidarMemoriaOutputSchema = import_zod34.z.object({
|
|
6814
|
+
status: import_zod34.z.literal("archivada")
|
|
6596
6815
|
});
|
|
6597
|
-
var ConfigInputSchema =
|
|
6598
|
-
diaSemana:
|
|
6599
|
-
diaMes:
|
|
6600
|
-
hora:
|
|
6816
|
+
var ConfigInputSchema = import_zod35.z.object({
|
|
6817
|
+
diaSemana: import_zod35.z.number().int().min(0).max(6).nullable(),
|
|
6818
|
+
diaMes: import_zod35.z.number().int().min(1).max(31).nullable(),
|
|
6819
|
+
hora: import_zod35.z.string().regex(/^\d{2}:\d{2}$/),
|
|
6601
6820
|
// Timezone NO viene en input — hereda de tenants/{tid}.zonaHoraria (audit fix 2).
|
|
6602
|
-
fechaPuntual:
|
|
6821
|
+
fechaPuntual: import_zod35.z.string().datetime({ offset: true }).nullable()
|
|
6603
6822
|
}).strict();
|
|
6604
|
-
var AccionInputSchema =
|
|
6605
|
-
tool:
|
|
6606
|
-
params:
|
|
6823
|
+
var AccionInputSchema = import_zod35.z.object({
|
|
6824
|
+
tool: import_zod35.z.string().min(1),
|
|
6825
|
+
params: import_zod35.z.record(import_zod35.z.string(), import_zod35.z.unknown())
|
|
6607
6826
|
}).strict();
|
|
6608
|
-
var ProgramarRutinaParamsSchema =
|
|
6827
|
+
var ProgramarRutinaParamsSchema = import_zod35.z.object({
|
|
6609
6828
|
tipo: TipoRutinaEnum,
|
|
6610
6829
|
frecuencia: FrecuenciaRutinaEnum,
|
|
6611
6830
|
config: ConfigInputSchema,
|
|
6612
6831
|
accion: AccionInputSchema,
|
|
6613
|
-
uidDestinatario:
|
|
6832
|
+
uidDestinatario: import_zod35.z.string()
|
|
6614
6833
|
});
|
|
6615
|
-
var ProgramarRutinaOutputSchema =
|
|
6616
|
-
rutinaId:
|
|
6617
|
-
proximaEjecucionAt:
|
|
6834
|
+
var ProgramarRutinaOutputSchema = import_zod35.z.object({
|
|
6835
|
+
rutinaId: import_zod35.z.string(),
|
|
6836
|
+
proximaEjecucionAt: import_zod35.z.string().datetime()
|
|
6618
6837
|
});
|
|
6619
|
-
var PausarRutinaParamsSchema =
|
|
6620
|
-
rutinaId:
|
|
6621
|
-
motivo:
|
|
6838
|
+
var PausarRutinaParamsSchema = import_zod35.z.object({
|
|
6839
|
+
rutinaId: import_zod35.z.string(),
|
|
6840
|
+
motivo: import_zod35.z.string().optional()
|
|
6622
6841
|
});
|
|
6623
|
-
var PausarRutinaOutputSchema =
|
|
6624
|
-
status:
|
|
6842
|
+
var PausarRutinaOutputSchema = import_zod35.z.object({
|
|
6843
|
+
status: import_zod35.z.literal("pausada")
|
|
6625
6844
|
});
|
|
6626
|
-
var ArchivarRutinaParamsSchema =
|
|
6627
|
-
rutinaId:
|
|
6628
|
-
motivo:
|
|
6845
|
+
var ArchivarRutinaParamsSchema = import_zod35.z.object({
|
|
6846
|
+
rutinaId: import_zod35.z.string(),
|
|
6847
|
+
motivo: import_zod35.z.string().optional()
|
|
6629
6848
|
});
|
|
6630
|
-
var ArchivarRutinaOutputSchema =
|
|
6631
|
-
status:
|
|
6849
|
+
var ArchivarRutinaOutputSchema = import_zod35.z.object({
|
|
6850
|
+
status: import_zod35.z.literal("archivada")
|
|
6632
6851
|
});
|
|
6633
|
-
var ListarRutinasParamsSchema =
|
|
6634
|
-
uid:
|
|
6852
|
+
var ListarRutinasParamsSchema = import_zod35.z.object({
|
|
6853
|
+
uid: import_zod35.z.string().optional()
|
|
6635
6854
|
});
|
|
6636
|
-
var ListarRutinasOutputSchema =
|
|
6637
|
-
rutinas:
|
|
6855
|
+
var ListarRutinasOutputSchema = import_zod35.z.object({
|
|
6856
|
+
rutinas: import_zod35.z.array(MartinRutinaSchema)
|
|
6638
6857
|
});
|
|
6639
6858
|
|
|
6640
|
-
// src/tools/
|
|
6641
|
-
function
|
|
6859
|
+
// src/tools/buildContext.ts
|
|
6860
|
+
async function buildContext(session, brandId, opts = {}) {
|
|
6861
|
+
const tenantId = session.requireTenant();
|
|
6862
|
+
const ci = session.mcpClientIdentity;
|
|
6863
|
+
let userCtx = null;
|
|
6864
|
+
if (session.userId && getSdkMode() === "admin") {
|
|
6865
|
+
try {
|
|
6866
|
+
userCtx = await getUserContext({
|
|
6867
|
+
db: getAdminDb(),
|
|
6868
|
+
tenantId,
|
|
6869
|
+
uid: session.userId
|
|
6870
|
+
});
|
|
6871
|
+
} catch {
|
|
6872
|
+
userCtx = null;
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6642
6875
|
return {
|
|
6643
|
-
tenantId
|
|
6876
|
+
tenantId,
|
|
6644
6877
|
brandId,
|
|
6645
6878
|
user: {
|
|
6646
6879
|
uid: session.userId ?? "cowork-admin",
|
|
6647
|
-
nombre: session.userName ?? "Cowork Admin",
|
|
6648
|
-
rol: session.rol ?? "super_admin",
|
|
6649
|
-
|
|
6650
|
-
|
|
6880
|
+
nombre: userCtx?.nombre ?? session.userName ?? "Cowork Admin",
|
|
6881
|
+
rol: userCtx?.rolId ?? session.rol ?? "super_admin",
|
|
6882
|
+
rolNombre: userCtx?.rolNombre ?? null,
|
|
6883
|
+
idiomaPreferido: userCtx?.idiomaPreferido ?? "es",
|
|
6884
|
+
// Any caller entering through this factory is an MCP client (Claude
|
|
6885
|
+
// Desktop, Cursor, custom LLM scripts), NOT the Martin product app.
|
|
6886
|
+
// When Martin product app ships (211.3+), its server-side caller will
|
|
6887
|
+
// override clientType to 'martin' via the same `_martinContext` channel.
|
|
6888
|
+
clientType: "mcp_client",
|
|
6889
|
+
clientMetadata: {
|
|
6890
|
+
mcpClient: ci.mcpClient,
|
|
6891
|
+
mcpClientVersion: ci.mcpClientVersion
|
|
6892
|
+
}
|
|
6651
6893
|
},
|
|
6652
6894
|
conversacionId: opts.conversacionId ?? null,
|
|
6653
|
-
|
|
6895
|
+
// HITO 6 A7.8: default false — secure default para acciones destructivas.
|
|
6896
|
+
// Tools sin requiresConfirmation lo ignoran; tools con
|
|
6897
|
+
// requiresConfirmation: true necesitan el flag explícito para ejecutar.
|
|
6898
|
+
confirmationGranted: opts.confirmationGranted ?? false,
|
|
6654
6899
|
doubleConfirmationGranted: opts.doubleConfirmationGranted ?? false
|
|
6655
6900
|
};
|
|
6656
6901
|
}
|
|
6657
6902
|
async function dispatchWithContract(args) {
|
|
6658
6903
|
const { contract, helper, callable, input, ctx } = args;
|
|
6659
|
-
if (
|
|
6904
|
+
if (getSdkMode() === "admin") {
|
|
6660
6905
|
const db = getAdminDb();
|
|
6661
|
-
const permissionResolver =
|
|
6662
|
-
if (args2.userRol === "super_admin") return "completo";
|
|
6663
|
-
const ref = db.collection("configuracion").doc(`${args2.tenantId}_roles`);
|
|
6664
|
-
const snap = await ref.get();
|
|
6665
|
-
if (!snap.exists) return "ninguno";
|
|
6666
|
-
const rolesData = snap.data();
|
|
6667
|
-
const rolData = rolesData[args2.userRol];
|
|
6668
|
-
if (!rolData || rolData.activo === false) return "ninguno";
|
|
6669
|
-
const nivel = rolData.permisosPorModulo?.[args2.modulo];
|
|
6670
|
-
if (nivel === "completo" || nivel === "editar" || nivel === "ver" || nivel === "ninguno") {
|
|
6671
|
-
return nivel;
|
|
6672
|
-
}
|
|
6673
|
-
return "ninguno";
|
|
6674
|
-
};
|
|
6906
|
+
const permissionResolver = (args2) => getUserPermissionLevel({ db, ...args2 });
|
|
6675
6907
|
const wrapped = wrapWithContract(
|
|
6676
6908
|
contract,
|
|
6677
6909
|
async (i) => helper({ ...i, db }),
|
|
@@ -6679,15 +6911,20 @@ async function dispatchWithContract(args) {
|
|
|
6679
6911
|
);
|
|
6680
6912
|
return wrapped(input, ctx);
|
|
6681
6913
|
}
|
|
6682
|
-
|
|
6914
|
+
const result = await callable({
|
|
6683
6915
|
...input,
|
|
6684
6916
|
_martinContext: {
|
|
6685
6917
|
conversacionId: ctx.conversacionId ?? null,
|
|
6686
6918
|
confirmationGranted: ctx.confirmationGranted === true,
|
|
6687
6919
|
doubleConfirmationGranted: ctx.doubleConfirmationGranted === true,
|
|
6688
|
-
userIdiomaPreferido: ctx.user.idiomaPreferido
|
|
6920
|
+
userIdiomaPreferido: ctx.user.idiomaPreferido,
|
|
6921
|
+
// HITO 6 A7.5: propagar clientType + identidad concreta para que
|
|
6922
|
+
// el factory CF arme actor.type + actor.metadata en el audit log.
|
|
6923
|
+
clientType: ctx.user.clientType ?? "mcp_client",
|
|
6924
|
+
clientMetadata: ctx.user.clientMetadata ?? null
|
|
6689
6925
|
}
|
|
6690
6926
|
});
|
|
6927
|
+
return result;
|
|
6691
6928
|
}
|
|
6692
6929
|
|
|
6693
6930
|
// src/services/marketingHelperCallables.ts
|
|
@@ -6711,6 +6948,9 @@ async function callCF(name, input) {
|
|
|
6711
6948
|
function callGetCalendar(input) {
|
|
6712
6949
|
return callCF("marketingGetCalendarCallable", input);
|
|
6713
6950
|
}
|
|
6951
|
+
function callAddCalendarSlot(input) {
|
|
6952
|
+
return callCF("marketingAddCalendarSlotCallable", input);
|
|
6953
|
+
}
|
|
6714
6954
|
function callBrandBriefWriter(input) {
|
|
6715
6955
|
return callCF("marketingBrandBriefWriterCallable", input);
|
|
6716
6956
|
}
|
|
@@ -6761,7 +7001,7 @@ function callPhotoDirectorExecute(input) {
|
|
|
6761
7001
|
}
|
|
6762
7002
|
|
|
6763
7003
|
// src/tools/marketing/photos.ts
|
|
6764
|
-
var
|
|
7004
|
+
var import_zod37 = require("zod");
|
|
6765
7005
|
|
|
6766
7006
|
// src/services/marketingEmbeddings.ts
|
|
6767
7007
|
var import_google_auth_library = require("google-auth-library");
|
|
@@ -6816,7 +7056,7 @@ async function generateTextEmbedding(text, session) {
|
|
|
6816
7056
|
if (!trimmed) {
|
|
6817
7057
|
throw new Error("generateTextEmbedding: text no puede estar vacio");
|
|
6818
7058
|
}
|
|
6819
|
-
if (
|
|
7059
|
+
if (getSdkMode() === "client") {
|
|
6820
7060
|
return null;
|
|
6821
7061
|
}
|
|
6822
7062
|
return callEmbeddingCF({ text: trimmed }, session);
|
|
@@ -6910,7 +7150,7 @@ async function findNearestInCollection(params) {
|
|
|
6910
7150
|
`findNearestInCollection: vectorField obligatorio (embeddingText | embeddingImage), recibido ${vectorField}`
|
|
6911
7151
|
);
|
|
6912
7152
|
}
|
|
6913
|
-
if (
|
|
7153
|
+
if (getSdkMode() === "client") {
|
|
6914
7154
|
if (!queryText || typeof queryText !== "string") {
|
|
6915
7155
|
throw new Error(
|
|
6916
7156
|
"findNearestInCollection en Modo B (client) requiere queryText. El MCP cliente no puede generar embeddings localmente; la CF puente marketingVectorSearchCallable los genera server-side."
|
|
@@ -7029,7 +7269,7 @@ async function findNearestInCollectionWithOverride(params) {
|
|
|
7029
7269
|
}
|
|
7030
7270
|
|
|
7031
7271
|
// src/tools/marketing/content.ts
|
|
7032
|
-
var
|
|
7272
|
+
var import_zod36 = require("zod");
|
|
7033
7273
|
var _logOverride = null;
|
|
7034
7274
|
async function logToMcpLogs(entry) {
|
|
7035
7275
|
if (_logOverride) return _logOverride(entry);
|
|
@@ -7042,28 +7282,28 @@ async function logToMcpLogs(entry) {
|
|
|
7042
7282
|
} catch {
|
|
7043
7283
|
}
|
|
7044
7284
|
}
|
|
7045
|
-
var IncludeSchema =
|
|
7046
|
-
products:
|
|
7047
|
-
collections:
|
|
7048
|
-
articles:
|
|
7049
|
-
pages:
|
|
7285
|
+
var IncludeSchema = import_zod36.z.object({
|
|
7286
|
+
products: import_zod36.z.boolean().default(true),
|
|
7287
|
+
collections: import_zod36.z.boolean().default(true),
|
|
7288
|
+
articles: import_zod36.z.boolean().default(true),
|
|
7289
|
+
pages: import_zod36.z.boolean().default(false)
|
|
7050
7290
|
}).default({
|
|
7051
7291
|
products: true,
|
|
7052
7292
|
collections: true,
|
|
7053
7293
|
articles: true,
|
|
7054
7294
|
pages: false
|
|
7055
7295
|
});
|
|
7056
|
-
var LimitSchema =
|
|
7057
|
-
products:
|
|
7058
|
-
collections:
|
|
7059
|
-
articles:
|
|
7060
|
-
pages:
|
|
7296
|
+
var LimitSchema = import_zod36.z.object({
|
|
7297
|
+
products: import_zod36.z.number().int().min(0).max(20).default(5),
|
|
7298
|
+
collections: import_zod36.z.number().int().min(0).max(10).default(3),
|
|
7299
|
+
articles: import_zod36.z.number().int().min(0).max(20).default(5),
|
|
7300
|
+
pages: import_zod36.z.number().int().min(0).max(10).default(2)
|
|
7061
7301
|
}).default({ products: 5, collections: 3, articles: 5, pages: 2 });
|
|
7062
7302
|
async function findContentForTopicHandler(input, session) {
|
|
7063
7303
|
const tenantId = session.requireTenant();
|
|
7064
7304
|
const brandId = input.brandId;
|
|
7065
|
-
const
|
|
7066
|
-
const r =
|
|
7305
|
+
const mode = getSdkMode();
|
|
7306
|
+
const r = mode === "admin" ? await contentFinder({
|
|
7067
7307
|
db: getAdminDb(),
|
|
7068
7308
|
tenantId,
|
|
7069
7309
|
brandId,
|
|
@@ -7143,15 +7383,15 @@ MODOS (parametro mode):
|
|
|
7143
7383
|
|
|
7144
7384
|
Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = el doc aparecio en ambos modos (alta confianza). 1 = aparecio solo en text (problema visual) o solo en image (problema SEO). Esos "solo image" son candidatos perfectos para recomendar optimizacion al tenant.`,
|
|
7145
7385
|
{
|
|
7146
|
-
brandId:
|
|
7147
|
-
contexto:
|
|
7148
|
-
fecha:
|
|
7386
|
+
brandId: import_zod36.z.string().optional().describe("ID de la brand"),
|
|
7387
|
+
contexto: import_zod36.z.string().min(1).describe("Parrafo, keyword o intencion"),
|
|
7388
|
+
fecha: import_zod36.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
7149
7389
|
include: IncludeSchema.optional(),
|
|
7150
7390
|
limit: LimitSchema.optional(),
|
|
7151
|
-
diversidad:
|
|
7152
|
-
mode:
|
|
7391
|
+
diversidad: import_zod36.z.boolean().default(true),
|
|
7392
|
+
mode: import_zod36.z.enum(["text", "hybrid"]).default("text").describe("Modo de busqueda: 'text' (rapido, 1 query) o 'hybrid' (text+image via RRF, 2x queries, rescata SEO debil)")
|
|
7153
7393
|
},
|
|
7154
|
-
async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode
|
|
7394
|
+
async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode }) => {
|
|
7155
7395
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7156
7396
|
const resolvedInclude = include || {
|
|
7157
7397
|
products: true,
|
|
@@ -7174,7 +7414,7 @@ Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = e
|
|
|
7174
7414
|
include: resolvedInclude,
|
|
7175
7415
|
limit: resolvedLimit,
|
|
7176
7416
|
diversidad,
|
|
7177
|
-
mode
|
|
7417
|
+
mode
|
|
7178
7418
|
},
|
|
7179
7419
|
session
|
|
7180
7420
|
);
|
|
@@ -7253,16 +7493,16 @@ REGLAS:
|
|
|
7253
7493
|
|
|
7254
7494
|
USAR: antes de generar contenido de cualquier slot del calendario.`,
|
|
7255
7495
|
{
|
|
7256
|
-
brandId:
|
|
7257
|
-
keyword:
|
|
7258
|
-
plataforma:
|
|
7259
|
-
fecha:
|
|
7260
|
-
limit:
|
|
7496
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7497
|
+
keyword: import_zod37.z.string().describe("Keyword del slot"),
|
|
7498
|
+
plataforma: import_zod37.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
|
|
7499
|
+
fecha: import_zod37.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
|
|
7500
|
+
limit: import_zod37.z.number().int().min(1).max(10).default(5)
|
|
7261
7501
|
},
|
|
7262
7502
|
async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
|
|
7263
7503
|
const tenantId = session.requireTenant();
|
|
7264
7504
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7265
|
-
const result =
|
|
7505
|
+
const result = getSdkMode() === "admin" ? await slotAssetFinder({
|
|
7266
7506
|
db: getAdminDb(),
|
|
7267
7507
|
tenantId,
|
|
7268
7508
|
brandId,
|
|
@@ -7298,12 +7538,12 @@ DESPUES de ver la foto, decide:
|
|
|
7298
7538
|
|
|
7299
7539
|
Luego llama execute_photo_edit con tu analisis y prompt.`,
|
|
7300
7540
|
{
|
|
7301
|
-
fotoId:
|
|
7541
|
+
fotoId: import_zod37.z.string().describe("ID de la foto")
|
|
7302
7542
|
},
|
|
7303
7543
|
async ({ fotoId }) => {
|
|
7304
7544
|
const tenantId = session.requireTenant();
|
|
7305
7545
|
const lang = await resolveTenantIdioma(tenantId);
|
|
7306
|
-
const result =
|
|
7546
|
+
const result = getSdkMode() === "admin" ? await photoDirectorPlan({
|
|
7307
7547
|
db: getAdminDb(),
|
|
7308
7548
|
tenantId,
|
|
7309
7549
|
fotoId,
|
|
@@ -7331,19 +7571,19 @@ Retorna la foto editada para que la revises. Si no te gusta:
|
|
|
7331
7571
|
|
|
7332
7572
|
Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
|
|
7333
7573
|
{
|
|
7334
|
-
fotoId:
|
|
7335
|
-
prompt:
|
|
7336
|
-
acciones:
|
|
7337
|
-
descripcion:
|
|
7338
|
-
tipo:
|
|
7339
|
-
tagsPrimarios:
|
|
7340
|
-
tagsSecundarios:
|
|
7341
|
-
tagsContexto:
|
|
7574
|
+
fotoId: import_zod37.z.string(),
|
|
7575
|
+
prompt: import_zod37.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
|
|
7576
|
+
acciones: import_zod37.z.array(import_zod37.z.enum(["edit_background", "none"])),
|
|
7577
|
+
descripcion: import_zod37.z.string().describe("Descripcion semantica en espanol"),
|
|
7578
|
+
tipo: import_zod37.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
|
|
7579
|
+
tagsPrimarios: import_zod37.z.array(import_zod37.z.string()),
|
|
7580
|
+
tagsSecundarios: import_zod37.z.array(import_zod37.z.string()),
|
|
7581
|
+
tagsContexto: import_zod37.z.array(import_zod37.z.string())
|
|
7342
7582
|
},
|
|
7343
7583
|
async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
|
|
7344
7584
|
const tenantId = session.requireTenant();
|
|
7345
7585
|
const lang = await resolveTenantIdioma(tenantId);
|
|
7346
|
-
if (
|
|
7586
|
+
if (getSdkMode() === "admin") {
|
|
7347
7587
|
const executePhotoEditAdapter = async (payload) => {
|
|
7348
7588
|
const cfUrl = process.env.MARKETING_PHOTO_EDIT_CF_URL || "https://us-central1-atteyo-ops.cloudfunctions.net/marketingExecutePhotoEdit";
|
|
7349
7589
|
const { GoogleAuth: GoogleAuth2 } = await import("google-auth-library");
|
|
@@ -7415,7 +7655,7 @@ Retorna:
|
|
|
7415
7655
|
- creditos: { balance, planId, periodEnd, costoPhotoEdit }
|
|
7416
7656
|
- _instrucciones: que hacer segun el estado`,
|
|
7417
7657
|
{
|
|
7418
|
-
fotoId:
|
|
7658
|
+
fotoId: import_zod37.z.string().describe("ID de la foto")
|
|
7419
7659
|
},
|
|
7420
7660
|
async ({ fotoId }) => {
|
|
7421
7661
|
const tenantId = session.requireTenant();
|
|
@@ -7515,11 +7755,11 @@ Retorna:
|
|
|
7515
7755
|
"find_products_for_content",
|
|
7516
7756
|
`[DEPRECATED 179.5] Wrapper interno de find_content_for_topic. Para nuevas integraciones, usa find_content_for_topic con include.products=true directamente. Este wrapper existe solo por compat hacia atras.`,
|
|
7517
7757
|
{
|
|
7518
|
-
brandId:
|
|
7519
|
-
contexto:
|
|
7520
|
-
fecha:
|
|
7521
|
-
limit:
|
|
7522
|
-
diversidad:
|
|
7758
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7759
|
+
contexto: import_zod37.z.string().describe("Parrafo, keyword o intencion del contenido"),
|
|
7760
|
+
fecha: import_zod37.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
7761
|
+
limit: import_zod37.z.number().int().min(1).max(10).default(5),
|
|
7762
|
+
diversidad: import_zod37.z.boolean().default(true)
|
|
7523
7763
|
},
|
|
7524
7764
|
async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
|
|
7525
7765
|
console.warn(
|
|
@@ -7572,15 +7812,15 @@ RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no
|
|
|
7572
7812
|
|
|
7573
7813
|
USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
|
|
7574
7814
|
{
|
|
7575
|
-
brandId:
|
|
7576
|
-
plataforma:
|
|
7577
|
-
tipoContenido:
|
|
7578
|
-
keyword:
|
|
7815
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7816
|
+
plataforma: import_zod37.z.string().describe("gbp | instagram | shopify_blog"),
|
|
7817
|
+
tipoContenido: import_zod37.z.string().describe("post | carousel | story | blog"),
|
|
7818
|
+
keyword: import_zod37.z.string().describe("Keyword del slot")
|
|
7579
7819
|
},
|
|
7580
7820
|
async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
|
|
7581
7821
|
const tenantId = session.requireTenant();
|
|
7582
7822
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7583
|
-
const result =
|
|
7823
|
+
const result = getSdkMode() === "admin" ? await canvaTemplateSelector({
|
|
7584
7824
|
db: getAdminDb(),
|
|
7585
7825
|
tenantId,
|
|
7586
7826
|
brandId,
|
|
@@ -7597,7 +7837,7 @@ USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/
|
|
|
7597
7837
|
);
|
|
7598
7838
|
server.tool(
|
|
7599
7839
|
"request_photo_shoot",
|
|
7600
|
-
`Agrega necesidades al FotoBriefing semanal del tenant.
|
|
7840
|
+
`Agrega necesidades al FotoBriefing semanal del tenant. The LLM uses this when it detects photo gaps mientras planifica contenido (get_photos_for_slot retorna < 3 fotos).
|
|
7601
7841
|
|
|
7602
7842
|
EFECTO: merge de necesidades en marketing_fotobriefings/{tenantId}_{brandId}_{semana}. Si el doc no existe, lo crea. Idempotente por tema.
|
|
7603
7843
|
|
|
@@ -7605,15 +7845,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
|
|
|
7605
7845
|
|
|
7606
7846
|
IMPORTANTE \u2014 campo 'razon': Este texto lo ve el tenant en la app. Escribe en lenguaje amigable y claro, NO tecnico. Ejemplo MALO: "get_photos_for_slot retorno 0 fotos para shopify_blog". Ejemplo BUENO: "No hay fotos de alcatraz para el blog del 8 de abril". Siempre en espanol si el tenant habla espanol.`,
|
|
7607
7847
|
{
|
|
7608
|
-
brandId:
|
|
7609
|
-
semana:
|
|
7610
|
-
necesidades:
|
|
7611
|
-
|
|
7612
|
-
tema:
|
|
7613
|
-
keyword:
|
|
7614
|
-
cantidadSugerida:
|
|
7615
|
-
razon:
|
|
7616
|
-
slotsAfectados:
|
|
7848
|
+
brandId: import_zod37.z.string().optional().describe("ID de la brand"),
|
|
7849
|
+
semana: import_zod37.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD").describe("Semana en ISO del lunes"),
|
|
7850
|
+
necesidades: import_zod37.z.array(
|
|
7851
|
+
import_zod37.z.object({
|
|
7852
|
+
tema: import_zod37.z.string(),
|
|
7853
|
+
keyword: import_zod37.z.string(),
|
|
7854
|
+
cantidadSugerida: import_zod37.z.number().int().positive(),
|
|
7855
|
+
razon: import_zod37.z.string(),
|
|
7856
|
+
slotsAfectados: import_zod37.z.array(import_zod37.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
|
|
7617
7857
|
})
|
|
7618
7858
|
).min(1)
|
|
7619
7859
|
},
|
|
@@ -7759,14 +7999,14 @@ function registerMarketingTools(server, session) {
|
|
|
7759
7999
|
"get_calendar",
|
|
7760
8000
|
"Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
|
|
7761
8001
|
{
|
|
7762
|
-
brandId:
|
|
7763
|
-
mes:
|
|
8002
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8003
|
+
mes: import_zod38.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
|
|
7764
8004
|
},
|
|
7765
8005
|
async ({ brandId: inputBrandId, mes }) => {
|
|
7766
8006
|
const tenantId = session.requireTenant();
|
|
7767
8007
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7768
8008
|
const targetMes = mes ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
7769
|
-
const ctx =
|
|
8009
|
+
const ctx = await buildContext(session, brandId);
|
|
7770
8010
|
const result = await dispatchWithContract({
|
|
7771
8011
|
contract: getCalendarContract,
|
|
7772
8012
|
helper: getCalendar,
|
|
@@ -7793,7 +8033,7 @@ function registerMarketingTools(server, session) {
|
|
|
7793
8033
|
"get_seo_snapshot",
|
|
7794
8034
|
"Lee el snapshot SEO de la brand: rank, keywords, oportunidades, competidores.",
|
|
7795
8035
|
{
|
|
7796
|
-
brandId:
|
|
8036
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand")
|
|
7797
8037
|
},
|
|
7798
8038
|
async ({ brandId: inputBrandId }) => {
|
|
7799
8039
|
const tenantId = session.requireTenant();
|
|
@@ -7812,8 +8052,8 @@ function registerMarketingTools(server, session) {
|
|
|
7812
8052
|
"get_photo_gallery",
|
|
7813
8053
|
"Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
|
|
7814
8054
|
{
|
|
7815
|
-
brandId:
|
|
7816
|
-
estado:
|
|
8055
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8056
|
+
estado: import_zod38.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
|
|
7817
8057
|
},
|
|
7818
8058
|
async ({ brandId: inputBrandId, estado }) => {
|
|
7819
8059
|
session.requireTenant();
|
|
@@ -7851,32 +8091,32 @@ function registerMarketingTools(server, session) {
|
|
|
7851
8091
|
);
|
|
7852
8092
|
server.tool(
|
|
7853
8093
|
"generate_marketing_plan",
|
|
7854
|
-
"
|
|
8094
|
+
"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.",
|
|
7855
8095
|
{
|
|
7856
|
-
brandId:
|
|
8096
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand")
|
|
7857
8097
|
},
|
|
7858
8098
|
async ({ brandId: inputBrandId }) => {
|
|
7859
8099
|
const tenantId = session.requireTenant();
|
|
7860
8100
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7861
|
-
const result =
|
|
8101
|
+
const result = getSdkMode() === "admin" ? await marketingPlanBuilder({ db: getAdminDb(), tenantId, brandId }) : await callMarketingPlanBuilder({ tenantId, brandId });
|
|
7862
8102
|
const payload = result.ok ? result.payload : result;
|
|
7863
8103
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
7864
8104
|
}
|
|
7865
8105
|
);
|
|
7866
8106
|
server.tool(
|
|
7867
8107
|
"save_marketing_plan",
|
|
7868
|
-
"
|
|
8108
|
+
"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).",
|
|
7869
8109
|
{
|
|
7870
|
-
brandId:
|
|
7871
|
-
plan:
|
|
8110
|
+
brandId: import_zod38.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
|
|
8111
|
+
plan: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Full marketing plan object. Common fields: keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, coleccionesPriorizadas, blogStrategy.")
|
|
7872
8112
|
},
|
|
7873
8113
|
async ({ brandId: inputBrandId, plan }) => {
|
|
7874
8114
|
const tenantId = session.requireTenant();
|
|
7875
8115
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7876
|
-
const ctx =
|
|
8116
|
+
const ctx = await buildContext(session, brandId);
|
|
7877
8117
|
const result = await dispatchWithContract({
|
|
7878
8118
|
contract: planWriterSaveContract,
|
|
7879
|
-
helper: planWriter.save,
|
|
8119
|
+
helper: ({ db, ...rest }) => planWriter.save({ db, ...rest }),
|
|
7880
8120
|
callable: callPlanWriterSave,
|
|
7881
8121
|
input: { tenantId, brandId, plan },
|
|
7882
8122
|
ctx
|
|
@@ -7885,8 +8125,8 @@ function registerMarketingTools(server, session) {
|
|
|
7885
8125
|
ok: false,
|
|
7886
8126
|
state: result.state,
|
|
7887
8127
|
mensaje: result.text,
|
|
7888
|
-
// HITO 6 A6.7:
|
|
7889
|
-
//
|
|
8128
|
+
// HITO 6 A6.7: include structured Zod details so
|
|
8129
|
+
// the LLM can self-recover on the next attempt.
|
|
7890
8130
|
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
7891
8131
|
};
|
|
7892
8132
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
@@ -7896,27 +8136,27 @@ function registerMarketingTools(server, session) {
|
|
|
7896
8136
|
"update_marketing_plan_field",
|
|
7897
8137
|
"Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
|
|
7898
8138
|
{
|
|
7899
|
-
brandId:
|
|
7900
|
-
field:
|
|
7901
|
-
value:
|
|
8139
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8140
|
+
field: import_zod38.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
|
|
8141
|
+
value: import_zod38.z.unknown().describe("Valor del campo")
|
|
7902
8142
|
},
|
|
7903
8143
|
async ({ brandId: inputBrandId, field, value }) => {
|
|
7904
8144
|
const tenantId = session.requireTenant();
|
|
7905
8145
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7906
|
-
const result =
|
|
8146
|
+
const result = getSdkMode() === "admin" ? await planWriter.updateField({ db: getAdminDb(), tenantId, brandId, field, value }) : await callPlanWriterUpdateField({ tenantId, brandId, field, value });
|
|
7907
8147
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
7908
8148
|
}
|
|
7909
8149
|
);
|
|
7910
8150
|
server.tool(
|
|
7911
8151
|
"generate_brand_brief",
|
|
7912
|
-
"
|
|
8152
|
+
"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.",
|
|
7913
8153
|
{
|
|
7914
|
-
brandId:
|
|
8154
|
+
brandId: import_zod38.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
|
|
7915
8155
|
},
|
|
7916
8156
|
async ({ brandId: inputBrandId }) => {
|
|
7917
8157
|
const tenantId = session.requireTenant();
|
|
7918
8158
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7919
|
-
const ctx =
|
|
8159
|
+
const ctx = await buildContext(session, brandId);
|
|
7920
8160
|
const result = await dispatchWithContract({
|
|
7921
8161
|
contract: brandBriefBuilderContract,
|
|
7922
8162
|
helper: brandBriefBuilder,
|
|
@@ -7937,54 +8177,54 @@ function registerMarketingTools(server, session) {
|
|
|
7937
8177
|
);
|
|
7938
8178
|
server.tool(
|
|
7939
8179
|
"save_brand_brief",
|
|
7940
|
-
"
|
|
8180
|
+
"Save the Brand Brief at tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Partial merge \u2014 only the brandBrief field is overwritten.",
|
|
7941
8181
|
{
|
|
7942
|
-
brandId:
|
|
7943
|
-
brandBrief:
|
|
8182
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8183
|
+
brandBrief: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Full Brand Brief object.")
|
|
7944
8184
|
},
|
|
7945
8185
|
async ({ brandId: inputBrandId, brandBrief }) => {
|
|
7946
8186
|
const tenantId = session.requireTenant();
|
|
7947
8187
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7948
|
-
const result =
|
|
8188
|
+
const result = getSdkMode() === "admin" ? await brandBriefWriter({ db: getAdminDb(), tenantId, brandId, brandBrief }) : await callBrandBriefWriter({ tenantId, brandId, brandBrief });
|
|
7949
8189
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
7950
8190
|
}
|
|
7951
8191
|
);
|
|
7952
8192
|
server.tool(
|
|
7953
8193
|
"generate_weekly_content",
|
|
7954
|
-
"
|
|
8194
|
+
"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.",
|
|
7955
8195
|
{
|
|
7956
|
-
brandId:
|
|
7957
|
-
semana:
|
|
7958
|
-
modo:
|
|
8196
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8197
|
+
semana: import_zod38.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
|
|
8198
|
+
modo: import_zod38.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
|
|
7959
8199
|
},
|
|
7960
8200
|
async ({ brandId: inputBrandId, semana, modo }) => {
|
|
7961
8201
|
const tenantId = session.requireTenant();
|
|
7962
8202
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7963
|
-
const result =
|
|
8203
|
+
const result = getSdkMode() === "admin" ? await weeklyContentBuilder({ db: getAdminDb(), tenantId, brandId, semana, modo }) : await callWeeklyContentBuilder({ tenantId, brandId, semana, modo });
|
|
7964
8204
|
const payload = result.ok ? result.payload : result;
|
|
7965
8205
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
7966
8206
|
}
|
|
7967
8207
|
);
|
|
7968
8208
|
server.tool(
|
|
7969
8209
|
"save_generated_content",
|
|
7970
|
-
`
|
|
8210
|
+
`Save generated content to Firestore. Uses buildContenido to validate the structure.
|
|
7971
8211
|
|
|
7972
|
-
|
|
8212
|
+
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.`,
|
|
7973
8213
|
{
|
|
7974
|
-
brandId:
|
|
7975
|
-
plataforma:
|
|
7976
|
-
tipo:
|
|
7977
|
-
keyword:
|
|
7978
|
-
languageCode:
|
|
7979
|
-
fotoId:
|
|
7980
|
-
datos:
|
|
7981
|
-
calendarioItemRef:
|
|
8214
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8215
|
+
plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
|
|
8216
|
+
tipo: import_zod38.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
|
|
8217
|
+
keyword: import_zod38.z.string().optional().describe("Keyword target"),
|
|
8218
|
+
languageCode: import_zod38.z.string().optional().describe("Idioma (es/en)"),
|
|
8219
|
+
fotoId: import_zod38.z.string().optional().describe("ID de la foto a asociar"),
|
|
8220
|
+
datos: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
|
|
8221
|
+
calendarioItemRef: import_zod38.z.string().optional().describe("Referencia al item del calendario")
|
|
7982
8222
|
},
|
|
7983
8223
|
async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
|
|
7984
8224
|
const tenantId = session.requireTenant();
|
|
7985
8225
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7986
8226
|
try {
|
|
7987
|
-
const result =
|
|
8227
|
+
const result = getSdkMode() === "admin" ? await contenidoWriter({
|
|
7988
8228
|
db: getAdminDb(),
|
|
7989
8229
|
tenantId,
|
|
7990
8230
|
brandId,
|
|
@@ -8023,17 +8263,17 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
|
|
|
8023
8263
|
NO puede cambiar: tenantId, brandId, id (inmutables).
|
|
8024
8264
|
Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
|
|
8025
8265
|
{
|
|
8026
|
-
contenidoId:
|
|
8027
|
-
datos:
|
|
8028
|
-
fotoId:
|
|
8029
|
-
keyword:
|
|
8030
|
-
languageCode:
|
|
8031
|
-
estado:
|
|
8032
|
-
calendarioItemRef:
|
|
8266
|
+
contenidoId: import_zod38.z.string().describe("ID del doc en marketing_contenido"),
|
|
8267
|
+
datos: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
|
|
8268
|
+
fotoId: import_zod38.z.string().nullable().optional().describe("Actualizar foto asociada"),
|
|
8269
|
+
keyword: import_zod38.z.string().nullable().optional().describe("Actualizar keyword"),
|
|
8270
|
+
languageCode: import_zod38.z.string().optional().describe("Actualizar idioma"),
|
|
8271
|
+
estado: import_zod38.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
|
|
8272
|
+
calendarioItemRef: import_zod38.z.string().nullable().optional().describe("Vincular a un slot del calendario")
|
|
8033
8273
|
},
|
|
8034
8274
|
async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
|
|
8035
8275
|
const tenantId = session.requireTenant();
|
|
8036
|
-
const result =
|
|
8276
|
+
const result = getSdkMode() === "admin" ? await contenidoUpdater({
|
|
8037
8277
|
db: getAdminDb(),
|
|
8038
8278
|
tenantId,
|
|
8039
8279
|
contenidoId,
|
|
@@ -8056,42 +8296,88 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
8056
8296
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8057
8297
|
}
|
|
8058
8298
|
);
|
|
8299
|
+
server.tool(
|
|
8300
|
+
"add_calendar_slot",
|
|
8301
|
+
"Add a NEW slot to the editorial calendar. To modify an existing slot use update_calendar_slot instead.",
|
|
8302
|
+
{
|
|
8303
|
+
brandId: import_zod38.z.string().describe('Brand ID (e.g. "ponch", "teleglobos").'),
|
|
8304
|
+
mes: import_zod38.z.string().describe('Calendar month in YYYY-MM format (e.g. "2026-05").'),
|
|
8305
|
+
semana: import_zod38.z.number().describe("Week number within the month (1-5)."),
|
|
8306
|
+
slot: import_zod38.z.object({
|
|
8307
|
+
dia: import_zod38.z.string().describe("Slot date in YYYY-MM-DD format. Must fall within the week's fechaInicio/fechaFin range."),
|
|
8308
|
+
plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target publishing platform."),
|
|
8309
|
+
tipo: import_zod38.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Content type."),
|
|
8310
|
+
keyword: import_zod38.z.string().describe("Primary keyword for the content."),
|
|
8311
|
+
tema: import_zod38.z.string().optional().describe("Content topic/theme. OMIT this field if it does not apply. Do NOT send empty string or null."),
|
|
8312
|
+
productoId: import_zod38.z.string().optional().describe("Linked product ID. OMIT this field if no product is linked."),
|
|
8313
|
+
estado: import_zod38.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe('Initial status. OMIT to use default "planificado".'),
|
|
8314
|
+
locationId: import_zod38.z.string().optional().describe("GBP location ID \u2014 only when plataforma=gbp AND tenant is multi-location. OMIT otherwise."),
|
|
8315
|
+
locationNombre: import_zod38.z.string().optional().describe("Human-readable GBP location name. OMIT if locationId is not provided.")
|
|
8316
|
+
}).describe("New slot data.")
|
|
8317
|
+
},
|
|
8318
|
+
async ({ brandId, mes, semana, slot }) => {
|
|
8319
|
+
const tenantId = session.requireTenant();
|
|
8320
|
+
const ctx = await buildContext(session, brandId);
|
|
8321
|
+
const result = await dispatchWithContract({
|
|
8322
|
+
contract: addCalendarSlotContract,
|
|
8323
|
+
helper: addCalendarSlot,
|
|
8324
|
+
callable: callAddCalendarSlot,
|
|
8325
|
+
input: { tenantId, brandId, mes, semana, slot },
|
|
8326
|
+
ctx
|
|
8327
|
+
});
|
|
8328
|
+
const payload = result.state === "success" ? result.structuredOutput : {
|
|
8329
|
+
ok: false,
|
|
8330
|
+
state: result.state,
|
|
8331
|
+
mensaje: result.text,
|
|
8332
|
+
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
8333
|
+
};
|
|
8334
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
8335
|
+
}
|
|
8336
|
+
);
|
|
8059
8337
|
server.tool(
|
|
8060
8338
|
"update_calendar_slot",
|
|
8061
|
-
"
|
|
8339
|
+
"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.",
|
|
8062
8340
|
{
|
|
8063
|
-
brandId:
|
|
8064
|
-
mes:
|
|
8065
|
-
semana:
|
|
8066
|
-
slotIndex:
|
|
8067
|
-
cambios:
|
|
8068
|
-
dia:
|
|
8069
|
-
plataforma:
|
|
8070
|
-
tipo:
|
|
8071
|
-
keyword:
|
|
8072
|
-
tema:
|
|
8073
|
-
productoId:
|
|
8074
|
-
estado:
|
|
8075
|
-
contenidoRef:
|
|
8076
|
-
fotoIdAsignada:
|
|
8077
|
-
notas:
|
|
8078
|
-
locationId:
|
|
8079
|
-
locationNombre:
|
|
8080
|
-
}).strict().describe("
|
|
8081
|
-
accionContenidoExistente:
|
|
8082
|
-
|
|
8083
|
-
|
|
8341
|
+
brandId: import_zod38.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
|
|
8342
|
+
mes: import_zod38.z.string().describe("Calendar month in YYYY-MM format."),
|
|
8343
|
+
semana: import_zod38.z.number().describe("Week number within the month (1-5)."),
|
|
8344
|
+
slotIndex: import_zod38.z.number().describe("Slot index (0-based). Must point to an existing slot \u2014 to add new slots use add_calendar_slot."),
|
|
8345
|
+
cambios: import_zod38.z.object({
|
|
8346
|
+
dia: import_zod38.z.string().nullable().optional().describe("Slot date in YYYY-MM-DD. OMIT if not changing date. Use null to explicitly clear."),
|
|
8347
|
+
plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional().describe("OMIT if not changing platform."),
|
|
8348
|
+
tipo: import_zod38.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional().describe("OMIT if not changing content type."),
|
|
8349
|
+
keyword: import_zod38.z.string().nullable().optional().describe("OMIT if not changing keyword."),
|
|
8350
|
+
tema: import_zod38.z.string().nullable().optional().describe("OMIT if not changing topic."),
|
|
8351
|
+
productoId: import_zod38.z.string().nullable().optional().describe("OMIT if not changing linked product. Use null to unlink."),
|
|
8352
|
+
estado: import_zod38.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("OMIT if not changing status manually."),
|
|
8353
|
+
contenidoRef: import_zod38.z.string().nullable().optional().describe("OMIT \u2014 managed by the system, not by callers."),
|
|
8354
|
+
fotoIdAsignada: import_zod38.z.string().nullable().optional().describe("Use assign_photo_to_content for photos. OMIT here."),
|
|
8355
|
+
notas: import_zod38.z.array(NotaCalendarioSchema).optional().describe("Append-only notes for the slot."),
|
|
8356
|
+
locationId: import_zod38.z.string().nullable().optional().describe("GBP location ID \u2014 only when plataforma=gbp."),
|
|
8357
|
+
locationNombre: import_zod38.z.string().nullable().optional().describe("Human-readable GBP location name (for UI).")
|
|
8358
|
+
}).strict().describe("Fields to update on the slot. Only declared fields accepted; unknown fields are rejected. OMIT any field you are not changing."),
|
|
8359
|
+
accionContenidoExistente: import_zod38.z.union([
|
|
8360
|
+
import_zod38.z.enum(["descartar", "nuevo_slot", "mantener"]),
|
|
8361
|
+
import_zod38.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
|
|
8084
8362
|
]).optional().describe(
|
|
8085
|
-
|
|
8363
|
+
'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).'
|
|
8086
8364
|
)
|
|
8087
8365
|
},
|
|
8088
8366
|
async ({ brandId: inputBrandId, mes, semana, slotIndex, cambios, accionContenidoExistente }) => {
|
|
8089
8367
|
const tenantId = session.requireTenant();
|
|
8090
8368
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
8091
|
-
const ctx =
|
|
8369
|
+
const ctx = await buildContext(session, brandId);
|
|
8092
8370
|
const result = await dispatchWithContract({
|
|
8093
8371
|
contract: calendarSlotUpdaterContract,
|
|
8094
|
-
|
|
8372
|
+
// Wrap en arrow para que TS infiera el tipo del input desde el
|
|
8373
|
+
// schema del contract (acepta `cambios: Record<string, unknown>`),
|
|
8374
|
+
// no desde la interface estricta CalendarSlotUpdaterInput.
|
|
8375
|
+
// El helper internamente acepta el shape — Zod ya valido en el wrapper.
|
|
8376
|
+
helper: (input) => calendarSlotUpdater({
|
|
8377
|
+
...input,
|
|
8378
|
+
cambios: input.cambios,
|
|
8379
|
+
accionContenidoExistente: input.accionContenidoExistente
|
|
8380
|
+
}),
|
|
8095
8381
|
callable: callCalendarSlotUpdater,
|
|
8096
8382
|
input: { tenantId, brandId, mes, semana, slotIndex, cambios, accionContenidoExistente },
|
|
8097
8383
|
ctx
|
|
@@ -8100,8 +8386,8 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
8100
8386
|
ok: false,
|
|
8101
8387
|
state: result.state,
|
|
8102
8388
|
mensaje: result.text,
|
|
8103
|
-
// HITO 6 A6.7:
|
|
8104
|
-
//
|
|
8389
|
+
// HITO 6 A6.7: include structured Zod details so
|
|
8390
|
+
// the LLM can self-recover on the next attempt.
|
|
8105
8391
|
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
8106
8392
|
};
|
|
8107
8393
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
@@ -8117,14 +8403,14 @@ ESCRIBE EN DOS LUGARES:
|
|
|
8117
8403
|
|
|
8118
8404
|
NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
|
|
8119
8405
|
{
|
|
8120
|
-
contenidoRef:
|
|
8121
|
-
fotoId:
|
|
8122
|
-
calendarioItemRef:
|
|
8406
|
+
contenidoRef: import_zod38.z.string().describe("ID del doc en marketing_contenido"),
|
|
8407
|
+
fotoId: import_zod38.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
|
|
8408
|
+
calendarioItemRef: import_zod38.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
|
|
8123
8409
|
},
|
|
8124
8410
|
async ({ contenidoRef, fotoId, calendarioItemRef }) => {
|
|
8125
8411
|
const tenantId = session.requireTenant();
|
|
8126
8412
|
const brandId = session.requireBrand();
|
|
8127
|
-
const result =
|
|
8413
|
+
const result = getSdkMode() === "admin" ? await photoAssigner({ db: getAdminDb(), tenantId, brandId, contenidoRef, fotoId, calendarioItemRef }) : await callPhotoAssigner({ tenantId, brandId, contenidoRef, fotoId, calendarioItemRef });
|
|
8128
8414
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8129
8415
|
}
|
|
8130
8416
|
);
|
|
@@ -8132,7 +8418,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8132
8418
|
"approve_content",
|
|
8133
8419
|
"Aprueba contenido para publicacion. Valida transicion de estado.",
|
|
8134
8420
|
{
|
|
8135
|
-
contenidoId:
|
|
8421
|
+
contenidoId: import_zod38.z.string().describe("ID del contenido a aprobar")
|
|
8136
8422
|
},
|
|
8137
8423
|
async ({ contenidoId }) => {
|
|
8138
8424
|
session.requireTenant();
|
|
@@ -8156,8 +8442,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8156
8442
|
"reject_content",
|
|
8157
8443
|
"Rechaza contenido con motivo. Valida transicion de estado.",
|
|
8158
8444
|
{
|
|
8159
|
-
contenidoId:
|
|
8160
|
-
motivo:
|
|
8445
|
+
contenidoId: import_zod38.z.string().describe("ID del contenido a rechazar"),
|
|
8446
|
+
motivo: import_zod38.z.string().describe("Motivo del rechazo")
|
|
8161
8447
|
},
|
|
8162
8448
|
async ({ contenidoId, motivo }) => {
|
|
8163
8449
|
session.requireTenant();
|
|
@@ -8180,7 +8466,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8180
8466
|
"get_collections",
|
|
8181
8467
|
"Lee todas las colecciones canonicas (Shopify/WordPress/webonly) con sus 6 campos SEO actuales. Incluye best practices 2026.",
|
|
8182
8468
|
{
|
|
8183
|
-
brandId:
|
|
8469
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand")
|
|
8184
8470
|
},
|
|
8185
8471
|
async ({ brandId: inputBrandId }) => {
|
|
8186
8472
|
const tenantId = session.requireTenant();
|
|
@@ -8267,13 +8553,13 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8267
8553
|
"save_collection_suggestions",
|
|
8268
8554
|
"Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
|
|
8269
8555
|
{
|
|
8270
|
-
brandId:
|
|
8556
|
+
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8271
8557
|
suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
|
|
8272
8558
|
},
|
|
8273
8559
|
async ({ brandId: inputBrandId, suggestions }) => {
|
|
8274
8560
|
const tenantId = session.requireTenant();
|
|
8275
8561
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
8276
|
-
const result =
|
|
8562
|
+
const result = getSdkMode() === "admin" ? await collectionSuggestionsWriter({ db: getAdminDb(), tenantId, brandId, suggestions }) : await callCollectionSuggestionsWriter({ tenantId, brandId, suggestions });
|
|
8277
8563
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8278
8564
|
}
|
|
8279
8565
|
);
|
|
@@ -8281,8 +8567,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8281
8567
|
|
|
8282
8568
|
// src/index.ts
|
|
8283
8569
|
async function buildSystemPrompt2(session) {
|
|
8284
|
-
const
|
|
8285
|
-
if (
|
|
8570
|
+
const mode = getSdkMode();
|
|
8571
|
+
if (mode === "admin") {
|
|
8286
8572
|
const db = getAdminDb();
|
|
8287
8573
|
return buildSystemPrompt({
|
|
8288
8574
|
db,
|
|
@@ -8299,7 +8585,9 @@ async function buildSystemPrompt2(session) {
|
|
|
8299
8585
|
async function main() {
|
|
8300
8586
|
const authContext = resolveAuth();
|
|
8301
8587
|
const session = new Session(authContext);
|
|
8302
|
-
console.error(
|
|
8588
|
+
console.error(
|
|
8589
|
+
`[ponch-mcp] Auth resuelto. canSwitchTenant=${authContext.canSwitchTenant} tenantId=${authContext.tenantId ?? "(none)"} rol=${authContext.rol ?? "(none)"}`
|
|
8590
|
+
);
|
|
8303
8591
|
let firebaseReady = false;
|
|
8304
8592
|
if (authContext.serviceAccountPath) {
|
|
8305
8593
|
initFirebaseAdmin(authContext.serviceAccountPath);
|
|
@@ -8311,7 +8599,7 @@ async function main() {
|
|
|
8311
8599
|
console.error(`[ponch-mcp] Firebase Client autenticado. Tenant: ${authContext.tenantId}`);
|
|
8312
8600
|
} else {
|
|
8313
8601
|
console.error("[ponch-mcp] Token expirado. Usa connect_account para reconectar.");
|
|
8314
|
-
session.
|
|
8602
|
+
session.revokeCrossTenant();
|
|
8315
8603
|
}
|
|
8316
8604
|
} else {
|
|
8317
8605
|
console.error("[ponch-mcp] Sin conexion. Usa connect_account para conectar.");
|
|
@@ -8320,6 +8608,13 @@ async function main() {
|
|
|
8320
8608
|
name: "ponch",
|
|
8321
8609
|
version: "1.0.1"
|
|
8322
8610
|
});
|
|
8611
|
+
server.server.oninitialized = () => {
|
|
8612
|
+
const ci = server.server.getClientVersion();
|
|
8613
|
+
if (ci) {
|
|
8614
|
+
session.setMcpClientIdentity(ci.name ?? null, ci.version ?? null);
|
|
8615
|
+
console.error(`[ponch-mcp] Cliente MCP: ${ci.name ?? "(sin name)"} v${ci.version ?? "(sin version)"}`);
|
|
8616
|
+
}
|
|
8617
|
+
};
|
|
8323
8618
|
server.resource(
|
|
8324
8619
|
"system-prompt",
|
|
8325
8620
|
"ponch://system-prompt",
|