ponch-mcp-server 1.0.76 → 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 +461 -271
- 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()
|
|
@@ -2734,6 +2769,9 @@ 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");
|
|
@@ -2753,6 +2791,7 @@ var import_zod34 = require("zod");
|
|
|
2753
2791
|
var import_firestore7 = require("firebase-admin/firestore");
|
|
2754
2792
|
var import_zod35 = require("zod");
|
|
2755
2793
|
var import_firestore8 = require("firebase-admin/firestore");
|
|
2794
|
+
var import_meta = {};
|
|
2756
2795
|
var RULE_NEGATIVES = {
|
|
2757
2796
|
allowFaces: "no people, no faces, no hands",
|
|
2758
2797
|
allowProductTransform: "no distorted products, no warped objects",
|
|
@@ -3013,6 +3052,74 @@ var planWriter = {
|
|
|
3013
3052
|
save,
|
|
3014
3053
|
updateField
|
|
3015
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
|
+
}
|
|
3016
3123
|
var DisabledReasonCodeSchema = import_zod29.z.enum([
|
|
3017
3124
|
"paywall",
|
|
3018
3125
|
// Acción requiere upgrade de plan
|
|
@@ -3038,44 +3145,8 @@ var DisabledOutputSchema = import_zod29.z.object({
|
|
|
3038
3145
|
*/
|
|
3039
3146
|
detail: import_zod29.z.record(import_zod29.z.string(), import_zod29.z.string()).optional()
|
|
3040
3147
|
});
|
|
3041
|
-
var TEMPLATES = {
|
|
3042
|
-
es: {
|
|
3043
|
-
paywall: "Esta acci\xF3n requiere actualizar tu plan.",
|
|
3044
|
-
oauth_missing: "Necesitas conectar {service} primero.",
|
|
3045
|
-
data_missing: "Falta informaci\xF3n: {detail}",
|
|
3046
|
-
plan_not_includes: "Tu plan no incluye {feature}",
|
|
3047
|
-
feature_disabled: "Esta funci\xF3n est\xE1 deshabilitada para tu cuenta.",
|
|
3048
|
-
other: "{detail}"
|
|
3049
|
-
},
|
|
3050
|
-
en: {
|
|
3051
|
-
paywall: "This action requires upgrading your plan.",
|
|
3052
|
-
oauth_missing: "You need to connect {service} first.",
|
|
3053
|
-
data_missing: "Missing information: {detail}",
|
|
3054
|
-
plan_not_includes: "Your plan doesn't include {feature}",
|
|
3055
|
-
feature_disabled: "This feature is disabled for your account.",
|
|
3056
|
-
other: "{detail}"
|
|
3057
|
-
}
|
|
3058
|
-
};
|
|
3059
3148
|
function getDisabledMessage(code, locale, vars) {
|
|
3060
|
-
|
|
3061
|
-
throw new Error(
|
|
3062
|
-
`Locale '${locale}' no soportado en disabledMessages. Locales activos: ${Object.keys(TEMPLATES).join(", ")}.`
|
|
3063
|
-
);
|
|
3064
|
-
}
|
|
3065
|
-
const template = TEMPLATES[locale][code];
|
|
3066
|
-
if (!template) {
|
|
3067
|
-
throw new Error(
|
|
3068
|
-
`Disabled code '${code}' no tiene template definido para locale '${locale}'.`
|
|
3069
|
-
);
|
|
3070
|
-
}
|
|
3071
|
-
return template.replace(/\{(\w+)\}/g, (_match, key) => {
|
|
3072
|
-
if (!vars || !(key in vars)) {
|
|
3073
|
-
throw new Error(
|
|
3074
|
-
`getDisabledMessage: variable '${key}' del template '${code}' (${locale}) no provista en vars. Helper debe pasar { ${key}: '...' } en detail.`
|
|
3075
|
-
);
|
|
3076
|
-
}
|
|
3077
|
-
return vars[key];
|
|
3078
|
-
});
|
|
3149
|
+
return getMessage(`marketing.disabled.${code}`, locale, vars);
|
|
3079
3150
|
}
|
|
3080
3151
|
var SideEffectEnum = import_zod28.z.enum([
|
|
3081
3152
|
"reads_firestore",
|
|
@@ -3107,6 +3178,10 @@ var ExtractTargetPathSchema = import_zod28.z.custom((val) => typeof val === "fun
|
|
|
3107
3178
|
var ExtractChangesSchema = import_zod28.z.custom((val) => typeof val === "function", {
|
|
3108
3179
|
message: "extractChanges debe ser funci\xF3n (input, output) => { before, after }"
|
|
3109
3180
|
});
|
|
3181
|
+
var InputPredicateSchema = import_zod28.z.custom(
|
|
3182
|
+
(val) => typeof val === "function",
|
|
3183
|
+
{ message: "predicado debe ser funci\xF3n (input) => boolean" }
|
|
3184
|
+
);
|
|
3110
3185
|
var MartinContractSchema = import_zod28.z.object({
|
|
3111
3186
|
// Schema versioning (Dim 4 backbone)
|
|
3112
3187
|
schemaVersion: import_zod28.z.literal(1).default(1),
|
|
@@ -3122,6 +3197,16 @@ var MartinContractSchema = import_zod28.z.object({
|
|
|
3122
3197
|
destructive: import_zod28.z.boolean(),
|
|
3123
3198
|
affectsPublication: import_zod28.z.boolean(),
|
|
3124
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(),
|
|
3125
3210
|
// Presentación al usuario (i18n)
|
|
3126
3211
|
martinSummaryTemplate: SummaryTemplateSchema,
|
|
3127
3212
|
martinConfirmationTemplate: ConfirmationTemplateSchema.optional(),
|
|
@@ -3262,7 +3347,7 @@ var OutputSchema = import_zod27.z.discriminatedUnion("ok", [
|
|
|
3262
3347
|
]);
|
|
3263
3348
|
var rawContract = {
|
|
3264
3349
|
name: "save_marketing_plan",
|
|
3265
|
-
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).",
|
|
3266
3351
|
paramsSchema: ParamsSchema,
|
|
3267
3352
|
outputSchema: OutputSchema,
|
|
3268
3353
|
// No es destructivo (sobrescribe plan completo, pero plan se regenera fácil),
|
|
@@ -3566,7 +3651,7 @@ var OutputSchema2 = import_zod30.z.discriminatedUnion("ok", [
|
|
|
3566
3651
|
]);
|
|
3567
3652
|
var rawContract2 = {
|
|
3568
3653
|
name: "add_calendar_slot",
|
|
3569
|
-
description: "
|
|
3654
|
+
description: "Add a new slot to the editorial calendar. Does NOT modify existing slots \u2014 use update_calendar_slot for that.",
|
|
3570
3655
|
paramsSchema: ParamsSchema2,
|
|
3571
3656
|
outputSchema: OutputSchema2,
|
|
3572
3657
|
requiresConfirmation: false,
|
|
@@ -3636,7 +3721,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3636
3721
|
if (slotIndex >= items.length) {
|
|
3637
3722
|
return {
|
|
3638
3723
|
ok: false,
|
|
3639
|
-
error: `slotIndex
|
|
3724
|
+
error: `slotIndex=${slotIndex} >= items.length=${items.length} for week=${semana}`,
|
|
3640
3725
|
code: "SLOT_NOT_FOUND"
|
|
3641
3726
|
};
|
|
3642
3727
|
}
|
|
@@ -3648,14 +3733,9 @@ async function calendarSlotUpdater(input) {
|
|
|
3648
3733
|
if (oldContenidoRef && tocaSemantica && !accionContenidoExistente) {
|
|
3649
3734
|
return {
|
|
3650
3735
|
ok: false,
|
|
3651
|
-
error: `
|
|
3736
|
+
error: `slot has contenidoRef=${oldContenidoRef} and cambios touch semantic fields=[${CAMPOS_SEMANTICOS.filter((f) => f in cambios).join(",")}]; require accionContenidoExistente`,
|
|
3652
3737
|
code: "ACCION_CONTENIDO_EXISTENTE_REQUIRED",
|
|
3653
|
-
opciones: [
|
|
3654
|
-
`'descartar' \u2014 marcar "${oldContenidoRef}" como descartado y aplicar cambios en este slot`,
|
|
3655
|
-
`'mover:semana:N:slot:M' \u2014 mover "${oldContenidoRef}" al slot destino N/M (target debe estar vacio) y aplicar cambios aqui`,
|
|
3656
|
-
`'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)`,
|
|
3657
|
-
`'mantener' \u2014 mantener "${oldContenidoRef}" aqui y aplicar cambios (caso typo fix, el contenido viejo sigue valido para el nuevo keyword/tema)`
|
|
3658
|
-
]
|
|
3738
|
+
opciones: ["descartar", "mover", "nuevo_slot", "mantener"]
|
|
3659
3739
|
};
|
|
3660
3740
|
}
|
|
3661
3741
|
let contenidosADescartar = [];
|
|
@@ -3675,7 +3755,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3675
3755
|
if (!m) {
|
|
3676
3756
|
return {
|
|
3677
3757
|
ok: false,
|
|
3678
|
-
error: `accionContenidoExistente
|
|
3758
|
+
error: `accionContenidoExistente="${accionContenidoExistente}" invalid format (expected: mover:semana:N:slot:M)`,
|
|
3679
3759
|
code: "MOVE_TARGET_INVALID"
|
|
3680
3760
|
};
|
|
3681
3761
|
}
|
|
@@ -3684,7 +3764,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3684
3764
|
if (targetSemanaNum === semana && targetSlotIdx === slotIndex) {
|
|
3685
3765
|
return {
|
|
3686
3766
|
ok: false,
|
|
3687
|
-
error: "
|
|
3767
|
+
error: "move target same as origin (semantic noop)",
|
|
3688
3768
|
code: "MOVE_TARGET_INVALID"
|
|
3689
3769
|
};
|
|
3690
3770
|
}
|
|
@@ -3692,7 +3772,7 @@ async function calendarSlotUpdater(input) {
|
|
|
3692
3772
|
if (!targetSemana) {
|
|
3693
3773
|
return {
|
|
3694
3774
|
ok: false,
|
|
3695
|
-
error: `
|
|
3775
|
+
error: `target semana=${targetSemanaNum} does not exist`,
|
|
3696
3776
|
code: "MOVE_TARGET_INVALID"
|
|
3697
3777
|
};
|
|
3698
3778
|
}
|
|
@@ -3700,14 +3780,14 @@ async function calendarSlotUpdater(input) {
|
|
|
3700
3780
|
if (!targetItems[targetSlotIdx]) {
|
|
3701
3781
|
return {
|
|
3702
3782
|
ok: false,
|
|
3703
|
-
error: `
|
|
3783
|
+
error: `target slot=${targetSlotIdx} does not exist in semana=${targetSemanaNum}`,
|
|
3704
3784
|
code: "MOVE_TARGET_INVALID"
|
|
3705
3785
|
};
|
|
3706
3786
|
}
|
|
3707
3787
|
if (targetItems[targetSlotIdx].contenidoRef) {
|
|
3708
3788
|
return {
|
|
3709
3789
|
ok: false,
|
|
3710
|
-
error: `
|
|
3790
|
+
error: `target slot already has contenidoRef=${targetItems[targetSlotIdx].contenidoRef}`,
|
|
3711
3791
|
code: "MOVE_TARGET_OCCUPIED"
|
|
3712
3792
|
};
|
|
3713
3793
|
}
|
|
@@ -3889,18 +3969,25 @@ var OutputSchema3 = import_zod31.z.discriminatedUnion("ok", [
|
|
|
3889
3969
|
]);
|
|
3890
3970
|
var rawContract3 = {
|
|
3891
3971
|
name: "update_calendar_slot",
|
|
3892
|
-
description: "
|
|
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.",
|
|
3893
3973
|
paramsSchema: ParamsSchema3,
|
|
3894
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.
|
|
3895
3980
|
requiresConfirmation: false,
|
|
3896
3981
|
destructive: false,
|
|
3897
|
-
|
|
3982
|
+
isDestructiveForInput: (input) => input.accionContenidoExistente === "descartar",
|
|
3898
3983
|
affectsPublication: false,
|
|
3899
3984
|
affectsExternal: false,
|
|
3900
3985
|
martinSummaryTemplate: (input, output, locale) => {
|
|
3901
3986
|
if (!output.ok) {
|
|
3902
|
-
if (
|
|
3903
|
-
|
|
3987
|
+
if (output.code) {
|
|
3988
|
+
return getMessage(`marketing.errors.${output.code}`, locale);
|
|
3989
|
+
}
|
|
3990
|
+
return getMessage("marketing.safeError.generic", locale);
|
|
3904
3991
|
}
|
|
3905
3992
|
const verb = {
|
|
3906
3993
|
updated: locale === "en" ? "updated" : "actualic\xE9",
|
|
@@ -3912,6 +3999,15 @@ var rawContract3 = {
|
|
|
3912
3999
|
}
|
|
3913
4000
|
return `${verb[0].toUpperCase() + verb.slice(1)} el slot en la semana ${input.semana} (${input.mes}).`;
|
|
3914
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
|
+
},
|
|
3915
4011
|
auditAction: "marketing.calendario.slot.actualizar",
|
|
3916
4012
|
extractTargetPath: (input) => `tenants/${input.tenantId}/marketing_calendario/(brand=${input.brandId},mes=${input.mes}).semana=${input.semana}.slot=${input.slotIndex}`,
|
|
3917
4013
|
extractChanges: (input, output) => ({
|
|
@@ -3968,7 +4064,7 @@ var OutputSchema4 = import_zod32.z.object({
|
|
|
3968
4064
|
});
|
|
3969
4065
|
var rawContract4 = {
|
|
3970
4066
|
name: "get_calendar",
|
|
3971
|
-
description: "
|
|
4067
|
+
description: "Read the editorial calendar for a given brand and month. Returns weeks with planned items per platform.",
|
|
3972
4068
|
paramsSchema: ParamsSchema4,
|
|
3973
4069
|
outputSchema: OutputSchema4,
|
|
3974
4070
|
requiresConfirmation: false,
|
|
@@ -4304,7 +4400,7 @@ var OutputSchema5 = import_zod33.z.discriminatedUnion("ok", [
|
|
|
4304
4400
|
]);
|
|
4305
4401
|
var rawContract5 = {
|
|
4306
4402
|
name: "generate_brand_brief",
|
|
4307
|
-
description: "
|
|
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.",
|
|
4308
4404
|
paramsSchema: ParamsSchema5,
|
|
4309
4405
|
outputSchema: OutputSchema5,
|
|
4310
4406
|
// Lectura pura, sin side effects de escritura ni publicación.
|
|
@@ -5060,7 +5156,7 @@ function diversify(items, jaccardThreshold = 0.6) {
|
|
|
5060
5156
|
}
|
|
5061
5157
|
async function contentFinder(input) {
|
|
5062
5158
|
const { db, tenantId, brandId, contexto, fecha, include, limit, diversidad, deps } = input;
|
|
5063
|
-
const
|
|
5159
|
+
const mode = input.mode ?? "text";
|
|
5064
5160
|
const textThreshold = deps.thresholds?.text ?? DEFAULT_TEXT_THRESHOLD;
|
|
5065
5161
|
const imageThreshold = deps.thresholds?.image ?? DEFAULT_IMAGE_THRESHOLD;
|
|
5066
5162
|
const conflictThreshold = deps.thresholds?.conflictSimilarity ?? DEFAULT_CONFLICT_THRESHOLD;
|
|
@@ -5091,7 +5187,7 @@ async function contentFinder(input) {
|
|
|
5091
5187
|
queryEmbedding: queryEmbedding ?? void 0,
|
|
5092
5188
|
queryText
|
|
5093
5189
|
};
|
|
5094
|
-
if (
|
|
5190
|
+
if (mode === "text") {
|
|
5095
5191
|
return safeSearch(true, {
|
|
5096
5192
|
...baseParams,
|
|
5097
5193
|
limit: lim,
|
|
@@ -6320,6 +6416,90 @@ async function checkQuota(tenantId, quotaName) {
|
|
|
6320
6416
|
async function getCurrentUsage(_tenantId, _quotaName) {
|
|
6321
6417
|
return 0;
|
|
6322
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
|
+
}
|
|
6323
6503
|
async function resolveTenantIdioma(tenantId) {
|
|
6324
6504
|
const db = (0, import_firestore5.getFirestore)();
|
|
6325
6505
|
const tenantSnap = await db.doc(`tenants/${tenantId}`).get();
|
|
@@ -6335,72 +6515,44 @@ async function resolveTenantIdioma(tenantId) {
|
|
|
6335
6515
|
}
|
|
6336
6516
|
return idioma;
|
|
6337
6517
|
}
|
|
6338
|
-
var MESSAGES = {
|
|
6339
|
-
es: {
|
|
6340
|
-
generic: "Tuve un problema. Intent\xE9moslo de nuevo en un momento.",
|
|
6341
|
-
quota_exceeded: "Alcanzaste el l\xEDmite mensual.",
|
|
6342
|
-
not_found: "No lo encontr\xE9. \xBFPodr\xEDas verificar el dato?",
|
|
6343
|
-
permission: "No tengo permiso para hacer eso desde tu cuenta.",
|
|
6344
|
-
timeout: "Esto est\xE1 tardando m\xE1s de lo normal. \xBFLo intento de nuevo?"
|
|
6345
|
-
},
|
|
6346
|
-
en: {
|
|
6347
|
-
generic: "I had a problem. Let's try again in a moment.",
|
|
6348
|
-
quota_exceeded: "You reached the monthly limit.",
|
|
6349
|
-
not_found: "I couldn't find it. Could you verify the data?",
|
|
6350
|
-
permission: "I don't have permission to do that from your account.",
|
|
6351
|
-
timeout: "This is taking longer than usual. Should I try again?"
|
|
6352
|
-
}
|
|
6353
|
-
};
|
|
6354
6518
|
function martinSafeError(err, locale) {
|
|
6355
|
-
if (!(locale in MESSAGES)) {
|
|
6356
|
-
throw new Error(
|
|
6357
|
-
`Locale '${locale}' no soportado en martinSafeError. Locales activos: ${Object.keys(MESSAGES).join(", ")}. Agregar traducciones antes de usar.`
|
|
6358
|
-
);
|
|
6359
|
-
}
|
|
6360
|
-
const msgs = MESSAGES[locale];
|
|
6361
6519
|
const e = err;
|
|
6362
6520
|
const message = e?.message ?? "";
|
|
6363
6521
|
const code = e?.code ?? "";
|
|
6364
|
-
|
|
6365
|
-
if (/
|
|
6366
|
-
if (/
|
|
6367
|
-
if (
|
|
6368
|
-
|
|
6369
|
-
}
|
|
6370
|
-
|
|
6371
|
-
es: {
|
|
6372
|
-
denied_role: "No tengo permiso para hacer eso desde tu cuenta. P\xEDdelo al encargado o admin que te autorice.",
|
|
6373
|
-
input_invalido: "Falta algo o est\xE1 mal. \xBFLo intentamos de nuevo?",
|
|
6374
|
-
confirm_default: "\xBFConfirmas?"
|
|
6375
|
-
},
|
|
6376
|
-
en: {
|
|
6377
|
-
denied_role: "I don't have permission to do that from your account. Ask the manager or admin to authorize it.",
|
|
6378
|
-
input_invalido: "Something is missing or off. Could you try again?",
|
|
6379
|
-
confirm_default: "Are you sure?"
|
|
6380
|
-
}
|
|
6381
|
-
};
|
|
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
|
+
}
|
|
6382
6529
|
function getWrapperMessage(key, locale) {
|
|
6383
|
-
|
|
6384
|
-
throw new Error(
|
|
6385
|
-
`Locale '${locale}' no soportado en getWrapperMessage. Locales activos: ${Object.keys(MESSAGES2).join(", ")}. Agregar traducciones antes de usar.`
|
|
6386
|
-
);
|
|
6387
|
-
}
|
|
6388
|
-
const msg = MESSAGES2[locale][key];
|
|
6389
|
-
if (!msg) {
|
|
6390
|
-
throw new Error(
|
|
6391
|
-
`Mensaje '${key}' no definido en wrapperMessages para locale '${locale}'.`
|
|
6392
|
-
);
|
|
6393
|
-
}
|
|
6394
|
-
return msg;
|
|
6530
|
+
return getMessage(`marketing.wrapper.${key}`, locale);
|
|
6395
6531
|
}
|
|
6396
6532
|
var NIVELES_ORDER = ["ninguno", "ver", "editar", "completo"];
|
|
6397
|
-
function
|
|
6533
|
+
function nivelAlcanza2(nivel, accion) {
|
|
6398
6534
|
return NIVELES_ORDER.indexOf(nivel) >= NIVELES_ORDER.indexOf(accion);
|
|
6399
6535
|
}
|
|
6400
6536
|
function wrapWithContract(contract, helper, options = {}) {
|
|
6401
6537
|
return async function wrappedTool(input, ctx) {
|
|
6402
6538
|
const startMs = Date.now();
|
|
6403
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
|
+
};
|
|
6404
6556
|
let accesoOk = false;
|
|
6405
6557
|
let reqDesc = "";
|
|
6406
6558
|
switch (contract.permissionScope) {
|
|
@@ -6425,7 +6577,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6425
6577
|
userRol: ctx.user.rol,
|
|
6426
6578
|
modulo: contract.permissionKey
|
|
6427
6579
|
});
|
|
6428
|
-
accesoOk =
|
|
6580
|
+
accesoOk = nivelAlcanza2(nivel, contract.permissionAction);
|
|
6429
6581
|
break;
|
|
6430
6582
|
}
|
|
6431
6583
|
case "self": {
|
|
@@ -6449,7 +6601,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6449
6601
|
await writeAuditLog({
|
|
6450
6602
|
tenantId: ctx.tenantId,
|
|
6451
6603
|
brandId: ctx.brandId ?? null,
|
|
6452
|
-
actor:
|
|
6604
|
+
actor: buildActor(),
|
|
6453
6605
|
action: contract.auditAction,
|
|
6454
6606
|
motivo: `Acceso denegado \u2014 rol '${ctx.user.rol}' sin permiso para ${reqDesc}`,
|
|
6455
6607
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6481,7 +6633,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6481
6633
|
await writeAuditLog({
|
|
6482
6634
|
tenantId: ctx.tenantId,
|
|
6483
6635
|
brandId: ctx.brandId ?? null,
|
|
6484
|
-
actor:
|
|
6636
|
+
actor: buildActor(),
|
|
6485
6637
|
action: contract.auditAction,
|
|
6486
6638
|
motivo: "Input inv\xE1lido \u2014 Zod parse failed",
|
|
6487
6639
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6497,7 +6649,10 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6497
6649
|
};
|
|
6498
6650
|
}
|
|
6499
6651
|
const parsedInput = parseResult.data;
|
|
6500
|
-
|
|
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) {
|
|
6501
6656
|
const confirmMsg = contract.martinConfirmationTemplate ? contract.martinConfirmationTemplate(parsedInput, locale) : getWrapperMessage("confirm_default", locale);
|
|
6502
6657
|
return {
|
|
6503
6658
|
text: confirmMsg,
|
|
@@ -6522,7 +6677,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6522
6677
|
await writeAuditLog({
|
|
6523
6678
|
tenantId: ctx.tenantId,
|
|
6524
6679
|
brandId: ctx.brandId ?? null,
|
|
6525
|
-
actor:
|
|
6680
|
+
actor: buildActor(),
|
|
6526
6681
|
action: contract.auditAction,
|
|
6527
6682
|
motivo: `Quota excedida \u2014 ${quotaName}: ${check.current}/${check.limit}`,
|
|
6528
6683
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6544,9 +6699,9 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6544
6699
|
await writeAuditLog({
|
|
6545
6700
|
tenantId: ctx.tenantId,
|
|
6546
6701
|
brandId: ctx.brandId ?? null,
|
|
6547
|
-
actor:
|
|
6702
|
+
actor: buildActor(),
|
|
6548
6703
|
action: contract.auditAction,
|
|
6549
|
-
motivo:
|
|
6704
|
+
motivo: `Acci\xF3n solicitada v\xEDa ${actorType}`,
|
|
6550
6705
|
conversacionId: ctx.conversacionId ?? null,
|
|
6551
6706
|
status: "error",
|
|
6552
6707
|
errorMessage: err?.message ?? "unknown",
|
|
@@ -6573,7 +6728,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6573
6728
|
await writeAuditLog({
|
|
6574
6729
|
tenantId: ctx.tenantId,
|
|
6575
6730
|
brandId: ctx.brandId ?? null,
|
|
6576
|
-
actor:
|
|
6731
|
+
actor: buildActor(),
|
|
6577
6732
|
action: contract.auditAction,
|
|
6578
6733
|
motivo: `Output del helper inv\xE1lido \u2014 bug de "${contract.name}"`,
|
|
6579
6734
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6606,7 +6761,7 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6606
6761
|
await writeAuditLog({
|
|
6607
6762
|
tenantId: ctx.tenantId,
|
|
6608
6763
|
brandId: ctx.brandId ?? null,
|
|
6609
|
-
actor:
|
|
6764
|
+
actor: buildActor(),
|
|
6610
6765
|
action: contract.auditAction,
|
|
6611
6766
|
motivo: `disabled \u2014 ${code}${detail ? `: ${JSON.stringify(detail)}` : ""}`,
|
|
6612
6767
|
conversacionId: ctx.conversacionId ?? null,
|
|
@@ -6625,11 +6780,11 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
6625
6780
|
await writeAuditLog({
|
|
6626
6781
|
tenantId: ctx.tenantId,
|
|
6627
6782
|
brandId: ctx.brandId ?? null,
|
|
6628
|
-
actor:
|
|
6783
|
+
actor: buildActor(),
|
|
6629
6784
|
action: contract.auditAction,
|
|
6630
6785
|
targetPath,
|
|
6631
6786
|
changes,
|
|
6632
|
-
motivo:
|
|
6787
|
+
motivo: `Acci\xF3n solicitada v\xEDa ${actorType}`,
|
|
6633
6788
|
conversacionId: ctx.conversacionId ?? null,
|
|
6634
6789
|
status: "success",
|
|
6635
6790
|
durationMs: Date.now() - startMs
|
|
@@ -6701,41 +6856,54 @@ var ListarRutinasOutputSchema = import_zod35.z.object({
|
|
|
6701
6856
|
rutinas: import_zod35.z.array(MartinRutinaSchema)
|
|
6702
6857
|
});
|
|
6703
6858
|
|
|
6704
|
-
// src/tools/
|
|
6705
|
-
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
|
+
}
|
|
6706
6875
|
return {
|
|
6707
|
-
tenantId
|
|
6876
|
+
tenantId,
|
|
6708
6877
|
brandId,
|
|
6709
6878
|
user: {
|
|
6710
6879
|
uid: session.userId ?? "cowork-admin",
|
|
6711
|
-
nombre: session.userName ?? "Cowork Admin",
|
|
6712
|
-
rol: session.rol ?? "super_admin",
|
|
6713
|
-
|
|
6714
|
-
|
|
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
|
+
}
|
|
6715
6893
|
},
|
|
6716
6894
|
conversacionId: opts.conversacionId ?? null,
|
|
6717
|
-
|
|
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,
|
|
6718
6899
|
doubleConfirmationGranted: opts.doubleConfirmationGranted ?? false
|
|
6719
6900
|
};
|
|
6720
6901
|
}
|
|
6721
6902
|
async function dispatchWithContract(args) {
|
|
6722
6903
|
const { contract, helper, callable, input, ctx } = args;
|
|
6723
|
-
if (
|
|
6904
|
+
if (getSdkMode() === "admin") {
|
|
6724
6905
|
const db = getAdminDb();
|
|
6725
|
-
const permissionResolver =
|
|
6726
|
-
if (args2.userRol === "super_admin") return "completo";
|
|
6727
|
-
const ref = db.collection("configuracion").doc(`${args2.tenantId}_roles`);
|
|
6728
|
-
const snap = await ref.get();
|
|
6729
|
-
if (!snap.exists) return "ninguno";
|
|
6730
|
-
const rolesData = snap.data();
|
|
6731
|
-
const rolData = rolesData[args2.userRol];
|
|
6732
|
-
if (!rolData || rolData.activo === false) return "ninguno";
|
|
6733
|
-
const nivel = rolData.permisosPorModulo?.[args2.modulo];
|
|
6734
|
-
if (nivel === "completo" || nivel === "editar" || nivel === "ver" || nivel === "ninguno") {
|
|
6735
|
-
return nivel;
|
|
6736
|
-
}
|
|
6737
|
-
return "ninguno";
|
|
6738
|
-
};
|
|
6906
|
+
const permissionResolver = (args2) => getUserPermissionLevel({ db, ...args2 });
|
|
6739
6907
|
const wrapped = wrapWithContract(
|
|
6740
6908
|
contract,
|
|
6741
6909
|
async (i) => helper({ ...i, db }),
|
|
@@ -6743,15 +6911,20 @@ async function dispatchWithContract(args) {
|
|
|
6743
6911
|
);
|
|
6744
6912
|
return wrapped(input, ctx);
|
|
6745
6913
|
}
|
|
6746
|
-
|
|
6914
|
+
const result = await callable({
|
|
6747
6915
|
...input,
|
|
6748
6916
|
_martinContext: {
|
|
6749
6917
|
conversacionId: ctx.conversacionId ?? null,
|
|
6750
6918
|
confirmationGranted: ctx.confirmationGranted === true,
|
|
6751
6919
|
doubleConfirmationGranted: ctx.doubleConfirmationGranted === true,
|
|
6752
|
-
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
|
|
6753
6925
|
}
|
|
6754
6926
|
});
|
|
6927
|
+
return result;
|
|
6755
6928
|
}
|
|
6756
6929
|
|
|
6757
6930
|
// src/services/marketingHelperCallables.ts
|
|
@@ -6883,7 +7056,7 @@ async function generateTextEmbedding(text, session) {
|
|
|
6883
7056
|
if (!trimmed) {
|
|
6884
7057
|
throw new Error("generateTextEmbedding: text no puede estar vacio");
|
|
6885
7058
|
}
|
|
6886
|
-
if (
|
|
7059
|
+
if (getSdkMode() === "client") {
|
|
6887
7060
|
return null;
|
|
6888
7061
|
}
|
|
6889
7062
|
return callEmbeddingCF({ text: trimmed }, session);
|
|
@@ -6977,7 +7150,7 @@ async function findNearestInCollection(params) {
|
|
|
6977
7150
|
`findNearestInCollection: vectorField obligatorio (embeddingText | embeddingImage), recibido ${vectorField}`
|
|
6978
7151
|
);
|
|
6979
7152
|
}
|
|
6980
|
-
if (
|
|
7153
|
+
if (getSdkMode() === "client") {
|
|
6981
7154
|
if (!queryText || typeof queryText !== "string") {
|
|
6982
7155
|
throw new Error(
|
|
6983
7156
|
"findNearestInCollection en Modo B (client) requiere queryText. El MCP cliente no puede generar embeddings localmente; la CF puente marketingVectorSearchCallable los genera server-side."
|
|
@@ -7129,8 +7302,8 @@ var LimitSchema = import_zod36.z.object({
|
|
|
7129
7302
|
async function findContentForTopicHandler(input, session) {
|
|
7130
7303
|
const tenantId = session.requireTenant();
|
|
7131
7304
|
const brandId = input.brandId;
|
|
7132
|
-
const
|
|
7133
|
-
const r =
|
|
7305
|
+
const mode = getSdkMode();
|
|
7306
|
+
const r = mode === "admin" ? await contentFinder({
|
|
7134
7307
|
db: getAdminDb(),
|
|
7135
7308
|
tenantId,
|
|
7136
7309
|
brandId,
|
|
@@ -7218,7 +7391,7 @@ Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = e
|
|
|
7218
7391
|
diversidad: import_zod36.z.boolean().default(true),
|
|
7219
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)")
|
|
7220
7393
|
},
|
|
7221
|
-
async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode
|
|
7394
|
+
async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode }) => {
|
|
7222
7395
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7223
7396
|
const resolvedInclude = include || {
|
|
7224
7397
|
products: true,
|
|
@@ -7241,7 +7414,7 @@ Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = e
|
|
|
7241
7414
|
include: resolvedInclude,
|
|
7242
7415
|
limit: resolvedLimit,
|
|
7243
7416
|
diversidad,
|
|
7244
|
-
mode
|
|
7417
|
+
mode
|
|
7245
7418
|
},
|
|
7246
7419
|
session
|
|
7247
7420
|
);
|
|
@@ -7329,7 +7502,7 @@ USAR: antes de generar contenido de cualquier slot del calendario.`,
|
|
|
7329
7502
|
async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
|
|
7330
7503
|
const tenantId = session.requireTenant();
|
|
7331
7504
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7332
|
-
const result =
|
|
7505
|
+
const result = getSdkMode() === "admin" ? await slotAssetFinder({
|
|
7333
7506
|
db: getAdminDb(),
|
|
7334
7507
|
tenantId,
|
|
7335
7508
|
brandId,
|
|
@@ -7370,7 +7543,7 @@ Luego llama execute_photo_edit con tu analisis y prompt.`,
|
|
|
7370
7543
|
async ({ fotoId }) => {
|
|
7371
7544
|
const tenantId = session.requireTenant();
|
|
7372
7545
|
const lang = await resolveTenantIdioma(tenantId);
|
|
7373
|
-
const result =
|
|
7546
|
+
const result = getSdkMode() === "admin" ? await photoDirectorPlan({
|
|
7374
7547
|
db: getAdminDb(),
|
|
7375
7548
|
tenantId,
|
|
7376
7549
|
fotoId,
|
|
@@ -7410,7 +7583,7 @@ Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si
|
|
|
7410
7583
|
async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
|
|
7411
7584
|
const tenantId = session.requireTenant();
|
|
7412
7585
|
const lang = await resolveTenantIdioma(tenantId);
|
|
7413
|
-
if (
|
|
7586
|
+
if (getSdkMode() === "admin") {
|
|
7414
7587
|
const executePhotoEditAdapter = async (payload) => {
|
|
7415
7588
|
const cfUrl = process.env.MARKETING_PHOTO_EDIT_CF_URL || "https://us-central1-atteyo-ops.cloudfunctions.net/marketingExecutePhotoEdit";
|
|
7416
7589
|
const { GoogleAuth: GoogleAuth2 } = await import("google-auth-library");
|
|
@@ -7647,7 +7820,7 @@ USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/
|
|
|
7647
7820
|
async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
|
|
7648
7821
|
const tenantId = session.requireTenant();
|
|
7649
7822
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7650
|
-
const result =
|
|
7823
|
+
const result = getSdkMode() === "admin" ? await canvaTemplateSelector({
|
|
7651
7824
|
db: getAdminDb(),
|
|
7652
7825
|
tenantId,
|
|
7653
7826
|
brandId,
|
|
@@ -7664,7 +7837,7 @@ USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/
|
|
|
7664
7837
|
);
|
|
7665
7838
|
server.tool(
|
|
7666
7839
|
"request_photo_shoot",
|
|
7667
|
-
`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).
|
|
7668
7841
|
|
|
7669
7842
|
EFECTO: merge de necesidades en marketing_fotobriefings/{tenantId}_{brandId}_{semana}. Si el doc no existe, lo crea. Idempotente por tema.
|
|
7670
7843
|
|
|
@@ -7833,7 +8006,7 @@ function registerMarketingTools(server, session) {
|
|
|
7833
8006
|
const tenantId = session.requireTenant();
|
|
7834
8007
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7835
8008
|
const targetMes = mes ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
7836
|
-
const ctx =
|
|
8009
|
+
const ctx = await buildContext(session, brandId);
|
|
7837
8010
|
const result = await dispatchWithContract({
|
|
7838
8011
|
contract: getCalendarContract,
|
|
7839
8012
|
helper: getCalendar,
|
|
@@ -7918,32 +8091,32 @@ function registerMarketingTools(server, session) {
|
|
|
7918
8091
|
);
|
|
7919
8092
|
server.tool(
|
|
7920
8093
|
"generate_marketing_plan",
|
|
7921
|
-
"
|
|
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.",
|
|
7922
8095
|
{
|
|
7923
8096
|
brandId: import_zod38.z.string().optional().describe("ID de la brand")
|
|
7924
8097
|
},
|
|
7925
8098
|
async ({ brandId: inputBrandId }) => {
|
|
7926
8099
|
const tenantId = session.requireTenant();
|
|
7927
8100
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7928
|
-
const result =
|
|
8101
|
+
const result = getSdkMode() === "admin" ? await marketingPlanBuilder({ db: getAdminDb(), tenantId, brandId }) : await callMarketingPlanBuilder({ tenantId, brandId });
|
|
7929
8102
|
const payload = result.ok ? result.payload : result;
|
|
7930
8103
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
7931
8104
|
}
|
|
7932
8105
|
);
|
|
7933
8106
|
server.tool(
|
|
7934
8107
|
"save_marketing_plan",
|
|
7935
|
-
"
|
|
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).",
|
|
7936
8109
|
{
|
|
7937
|
-
brandId: import_zod38.z.string().optional().describe("ID
|
|
7938
|
-
plan: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("
|
|
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.")
|
|
7939
8112
|
},
|
|
7940
8113
|
async ({ brandId: inputBrandId, plan }) => {
|
|
7941
8114
|
const tenantId = session.requireTenant();
|
|
7942
8115
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7943
|
-
const ctx =
|
|
8116
|
+
const ctx = await buildContext(session, brandId);
|
|
7944
8117
|
const result = await dispatchWithContract({
|
|
7945
8118
|
contract: planWriterSaveContract,
|
|
7946
|
-
helper: planWriter.save,
|
|
8119
|
+
helper: ({ db, ...rest }) => planWriter.save({ db, ...rest }),
|
|
7947
8120
|
callable: callPlanWriterSave,
|
|
7948
8121
|
input: { tenantId, brandId, plan },
|
|
7949
8122
|
ctx
|
|
@@ -7952,8 +8125,8 @@ function registerMarketingTools(server, session) {
|
|
|
7952
8125
|
ok: false,
|
|
7953
8126
|
state: result.state,
|
|
7954
8127
|
mensaje: result.text,
|
|
7955
|
-
// HITO 6 A6.7:
|
|
7956
|
-
//
|
|
8128
|
+
// HITO 6 A6.7: include structured Zod details so
|
|
8129
|
+
// the LLM can self-recover on the next attempt.
|
|
7957
8130
|
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
7958
8131
|
};
|
|
7959
8132
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
@@ -7970,20 +8143,20 @@ function registerMarketingTools(server, session) {
|
|
|
7970
8143
|
async ({ brandId: inputBrandId, field, value }) => {
|
|
7971
8144
|
const tenantId = session.requireTenant();
|
|
7972
8145
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7973
|
-
const result =
|
|
8146
|
+
const result = getSdkMode() === "admin" ? await planWriter.updateField({ db: getAdminDb(), tenantId, brandId, field, value }) : await callPlanWriterUpdateField({ tenantId, brandId, field, value });
|
|
7974
8147
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
7975
8148
|
}
|
|
7976
8149
|
);
|
|
7977
8150
|
server.tool(
|
|
7978
8151
|
"generate_brand_brief",
|
|
7979
|
-
"
|
|
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.",
|
|
7980
8153
|
{
|
|
7981
|
-
brandId: import_zod38.z.string().optional().describe("ID
|
|
8154
|
+
brandId: import_zod38.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
|
|
7982
8155
|
},
|
|
7983
8156
|
async ({ brandId: inputBrandId }) => {
|
|
7984
8157
|
const tenantId = session.requireTenant();
|
|
7985
8158
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
7986
|
-
const ctx =
|
|
8159
|
+
const ctx = await buildContext(session, brandId);
|
|
7987
8160
|
const result = await dispatchWithContract({
|
|
7988
8161
|
contract: brandBriefBuilderContract,
|
|
7989
8162
|
helper: brandBriefBuilder,
|
|
@@ -8004,21 +8177,21 @@ function registerMarketingTools(server, session) {
|
|
|
8004
8177
|
);
|
|
8005
8178
|
server.tool(
|
|
8006
8179
|
"save_brand_brief",
|
|
8007
|
-
"
|
|
8180
|
+
"Save the Brand Brief at tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Partial merge \u2014 only the brandBrief field is overwritten.",
|
|
8008
8181
|
{
|
|
8009
8182
|
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8010
|
-
brandBrief: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Brand Brief
|
|
8183
|
+
brandBrief: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown()).describe("Full Brand Brief object.")
|
|
8011
8184
|
},
|
|
8012
8185
|
async ({ brandId: inputBrandId, brandBrief }) => {
|
|
8013
8186
|
const tenantId = session.requireTenant();
|
|
8014
8187
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
8015
|
-
const result =
|
|
8188
|
+
const result = getSdkMode() === "admin" ? await brandBriefWriter({ db: getAdminDb(), tenantId, brandId, brandBrief }) : await callBrandBriefWriter({ tenantId, brandId, brandBrief });
|
|
8016
8189
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8017
8190
|
}
|
|
8018
8191
|
);
|
|
8019
8192
|
server.tool(
|
|
8020
8193
|
"generate_weekly_content",
|
|
8021
|
-
"
|
|
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.",
|
|
8022
8195
|
{
|
|
8023
8196
|
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8024
8197
|
semana: import_zod38.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
|
|
@@ -8027,16 +8200,16 @@ function registerMarketingTools(server, session) {
|
|
|
8027
8200
|
async ({ brandId: inputBrandId, semana, modo }) => {
|
|
8028
8201
|
const tenantId = session.requireTenant();
|
|
8029
8202
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
8030
|
-
const result =
|
|
8203
|
+
const result = getSdkMode() === "admin" ? await weeklyContentBuilder({ db: getAdminDb(), tenantId, brandId, semana, modo }) : await callWeeklyContentBuilder({ tenantId, brandId, semana, modo });
|
|
8031
8204
|
const payload = result.ok ? result.payload : result;
|
|
8032
8205
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
8033
8206
|
}
|
|
8034
8207
|
);
|
|
8035
8208
|
server.tool(
|
|
8036
8209
|
"save_generated_content",
|
|
8037
|
-
`
|
|
8210
|
+
`Save generated content to Firestore. Uses buildContenido to validate the structure.
|
|
8038
8211
|
|
|
8039
|
-
|
|
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.`,
|
|
8040
8213
|
{
|
|
8041
8214
|
brandId: import_zod38.z.string().optional().describe("ID de la brand"),
|
|
8042
8215
|
plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
|
|
@@ -8051,7 +8224,7 @@ IMPORTANTE: Si seleccionaste una foto con get_photos_for_slot, SIEMPRE pasa foto
|
|
|
8051
8224
|
const tenantId = session.requireTenant();
|
|
8052
8225
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
8053
8226
|
try {
|
|
8054
|
-
const result =
|
|
8227
|
+
const result = getSdkMode() === "admin" ? await contenidoWriter({
|
|
8055
8228
|
db: getAdminDb(),
|
|
8056
8229
|
tenantId,
|
|
8057
8230
|
brandId,
|
|
@@ -8100,7 +8273,7 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
8100
8273
|
},
|
|
8101
8274
|
async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
|
|
8102
8275
|
const tenantId = session.requireTenant();
|
|
8103
|
-
const result =
|
|
8276
|
+
const result = getSdkMode() === "admin" ? await contenidoUpdater({
|
|
8104
8277
|
db: getAdminDb(),
|
|
8105
8278
|
tenantId,
|
|
8106
8279
|
contenidoId,
|
|
@@ -8125,26 +8298,26 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
8125
8298
|
);
|
|
8126
8299
|
server.tool(
|
|
8127
8300
|
"add_calendar_slot",
|
|
8128
|
-
"
|
|
8301
|
+
"Add a NEW slot to the editorial calendar. To modify an existing slot use update_calendar_slot instead.",
|
|
8129
8302
|
{
|
|
8130
|
-
brandId: import_zod38.z.string().describe(
|
|
8131
|
-
mes: import_zod38.z.string().describe(
|
|
8132
|
-
semana: import_zod38.z.number().describe("
|
|
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)."),
|
|
8133
8306
|
slot: import_zod38.z.object({
|
|
8134
|
-
dia: import_zod38.z.string().describe("
|
|
8135
|
-
plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("
|
|
8136
|
-
tipo: import_zod38.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("
|
|
8137
|
-
keyword: import_zod38.z.string().describe("
|
|
8138
|
-
tema: import_zod38.z.string().optional().describe("
|
|
8139
|
-
productoId: import_zod38.z.string().optional().describe("
|
|
8140
|
-
estado: import_zod38.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe(
|
|
8141
|
-
locationId: import_zod38.z.string().optional().describe("GBP location ID \u2014
|
|
8142
|
-
locationNombre: import_zod38.z.string().optional().describe("
|
|
8143
|
-
}).describe("
|
|
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.")
|
|
8144
8317
|
},
|
|
8145
8318
|
async ({ brandId, mes, semana, slot }) => {
|
|
8146
8319
|
const tenantId = session.requireTenant();
|
|
8147
|
-
const ctx =
|
|
8320
|
+
const ctx = await buildContext(session, brandId);
|
|
8148
8321
|
const result = await dispatchWithContract({
|
|
8149
8322
|
contract: addCalendarSlotContract,
|
|
8150
8323
|
helper: addCalendarSlot,
|
|
@@ -8163,40 +8336,48 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
8163
8336
|
);
|
|
8164
8337
|
server.tool(
|
|
8165
8338
|
"update_calendar_slot",
|
|
8166
|
-
"
|
|
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.",
|
|
8167
8340
|
{
|
|
8168
|
-
brandId: import_zod38.z.string().optional().describe("ID
|
|
8169
|
-
mes: import_zod38.z.string().describe("
|
|
8170
|
-
semana: import_zod38.z.number().describe("
|
|
8171
|
-
slotIndex: import_zod38.z.number().describe("
|
|
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."),
|
|
8172
8345
|
cambios: import_zod38.z.object({
|
|
8173
|
-
dia: import_zod38.z.string().nullable().optional(),
|
|
8174
|
-
plataforma: import_zod38.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional(),
|
|
8175
|
-
tipo: import_zod38.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional(),
|
|
8176
|
-
keyword: import_zod38.z.string().nullable().optional(),
|
|
8177
|
-
tema: import_zod38.z.string().nullable().optional(),
|
|
8178
|
-
productoId: import_zod38.z.string().nullable().optional(),
|
|
8179
|
-
estado: import_zod38.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional(),
|
|
8180
|
-
contenidoRef: import_zod38.z.string().nullable().optional(),
|
|
8181
|
-
fotoIdAsignada: import_zod38.z.string().nullable().optional(),
|
|
8182
|
-
notas: import_zod38.z.array(NotaCalendarioSchema).optional(),
|
|
8183
|
-
locationId: import_zod38.z.string().nullable().optional().describe("GBP location ID \u2014
|
|
8184
|
-
locationNombre: import_zod38.z.string().nullable().optional().describe("
|
|
8185
|
-
}).strict().describe("
|
|
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."),
|
|
8186
8359
|
accionContenidoExistente: import_zod38.z.union([
|
|
8187
8360
|
import_zod38.z.enum(["descartar", "nuevo_slot", "mantener"]),
|
|
8188
|
-
import_zod38.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "
|
|
8361
|
+
import_zod38.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
|
|
8189
8362
|
]).optional().describe(
|
|
8190
|
-
|
|
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).'
|
|
8191
8364
|
)
|
|
8192
8365
|
},
|
|
8193
8366
|
async ({ brandId: inputBrandId, mes, semana, slotIndex, cambios, accionContenidoExistente }) => {
|
|
8194
8367
|
const tenantId = session.requireTenant();
|
|
8195
8368
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
8196
|
-
const ctx =
|
|
8369
|
+
const ctx = await buildContext(session, brandId);
|
|
8197
8370
|
const result = await dispatchWithContract({
|
|
8198
8371
|
contract: calendarSlotUpdaterContract,
|
|
8199
|
-
|
|
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
|
+
}),
|
|
8200
8381
|
callable: callCalendarSlotUpdater,
|
|
8201
8382
|
input: { tenantId, brandId, mes, semana, slotIndex, cambios, accionContenidoExistente },
|
|
8202
8383
|
ctx
|
|
@@ -8205,8 +8386,8 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
8205
8386
|
ok: false,
|
|
8206
8387
|
state: result.state,
|
|
8207
8388
|
mensaje: result.text,
|
|
8208
|
-
// HITO 6 A6.7:
|
|
8209
|
-
//
|
|
8389
|
+
// HITO 6 A6.7: include structured Zod details so
|
|
8390
|
+
// the LLM can self-recover on the next attempt.
|
|
8210
8391
|
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
8211
8392
|
};
|
|
8212
8393
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
@@ -8229,7 +8410,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8229
8410
|
async ({ contenidoRef, fotoId, calendarioItemRef }) => {
|
|
8230
8411
|
const tenantId = session.requireTenant();
|
|
8231
8412
|
const brandId = session.requireBrand();
|
|
8232
|
-
const result =
|
|
8413
|
+
const result = getSdkMode() === "admin" ? await photoAssigner({ db: getAdminDb(), tenantId, brandId, contenidoRef, fotoId, calendarioItemRef }) : await callPhotoAssigner({ tenantId, brandId, contenidoRef, fotoId, calendarioItemRef });
|
|
8233
8414
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8234
8415
|
}
|
|
8235
8416
|
);
|
|
@@ -8378,7 +8559,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8378
8559
|
async ({ brandId: inputBrandId, suggestions }) => {
|
|
8379
8560
|
const tenantId = session.requireTenant();
|
|
8380
8561
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
8381
|
-
const result =
|
|
8562
|
+
const result = getSdkMode() === "admin" ? await collectionSuggestionsWriter({ db: getAdminDb(), tenantId, brandId, suggestions }) : await callCollectionSuggestionsWriter({ tenantId, brandId, suggestions });
|
|
8382
8563
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8383
8564
|
}
|
|
8384
8565
|
);
|
|
@@ -8386,8 +8567,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
8386
8567
|
|
|
8387
8568
|
// src/index.ts
|
|
8388
8569
|
async function buildSystemPrompt2(session) {
|
|
8389
|
-
const
|
|
8390
|
-
if (
|
|
8570
|
+
const mode = getSdkMode();
|
|
8571
|
+
if (mode === "admin") {
|
|
8391
8572
|
const db = getAdminDb();
|
|
8392
8573
|
return buildSystemPrompt({
|
|
8393
8574
|
db,
|
|
@@ -8404,7 +8585,9 @@ async function buildSystemPrompt2(session) {
|
|
|
8404
8585
|
async function main() {
|
|
8405
8586
|
const authContext = resolveAuth();
|
|
8406
8587
|
const session = new Session(authContext);
|
|
8407
|
-
console.error(
|
|
8588
|
+
console.error(
|
|
8589
|
+
`[ponch-mcp] Auth resuelto. canSwitchTenant=${authContext.canSwitchTenant} tenantId=${authContext.tenantId ?? "(none)"} rol=${authContext.rol ?? "(none)"}`
|
|
8590
|
+
);
|
|
8408
8591
|
let firebaseReady = false;
|
|
8409
8592
|
if (authContext.serviceAccountPath) {
|
|
8410
8593
|
initFirebaseAdmin(authContext.serviceAccountPath);
|
|
@@ -8416,7 +8599,7 @@ async function main() {
|
|
|
8416
8599
|
console.error(`[ponch-mcp] Firebase Client autenticado. Tenant: ${authContext.tenantId}`);
|
|
8417
8600
|
} else {
|
|
8418
8601
|
console.error("[ponch-mcp] Token expirado. Usa connect_account para reconectar.");
|
|
8419
|
-
session.
|
|
8602
|
+
session.revokeCrossTenant();
|
|
8420
8603
|
}
|
|
8421
8604
|
} else {
|
|
8422
8605
|
console.error("[ponch-mcp] Sin conexion. Usa connect_account para conectar.");
|
|
@@ -8425,6 +8608,13 @@ async function main() {
|
|
|
8425
8608
|
name: "ponch",
|
|
8426
8609
|
version: "1.0.1"
|
|
8427
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
|
+
};
|
|
8428
8618
|
server.resource(
|
|
8429
8619
|
"system-prompt",
|
|
8430
8620
|
"ponch://system-prompt",
|