ponch-mcp-server 1.0.82 → 1.0.84
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 +831 -510
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2478,7 +2478,7 @@ function registerCoreTools(server, session) {
|
|
|
2478
2478
|
}
|
|
2479
2479
|
|
|
2480
2480
|
// src/tools/marketing.ts
|
|
2481
|
-
var
|
|
2481
|
+
var import_zod51 = require("zod");
|
|
2482
2482
|
|
|
2483
2483
|
// ../packages/martin-schemas/dist/index.js
|
|
2484
2484
|
var import_zod21 = require("zod");
|
|
@@ -9753,6 +9753,96 @@ async function getCurrentUsage(_tenantId, _quotaName) {
|
|
|
9753
9753
|
function getWrapperMessage(key, locale) {
|
|
9754
9754
|
return getMessage(`marketing.wrapper.${key}`, locale);
|
|
9755
9755
|
}
|
|
9756
|
+
function _extractReceived(issue) {
|
|
9757
|
+
if (issue.received !== void 0 && issue.received !== null) {
|
|
9758
|
+
return String(issue.received);
|
|
9759
|
+
}
|
|
9760
|
+
const m = issue.message?.match(/received\s+(\w+)/i);
|
|
9761
|
+
return m ? m[1] : "unknown";
|
|
9762
|
+
}
|
|
9763
|
+
function _placeholderForType(expected, locale) {
|
|
9764
|
+
switch (expected) {
|
|
9765
|
+
case "string":
|
|
9766
|
+
return locale === "en" ? '"a-string"' : '"un-texto"';
|
|
9767
|
+
case "number":
|
|
9768
|
+
return "42";
|
|
9769
|
+
case "integer":
|
|
9770
|
+
return "42";
|
|
9771
|
+
case "boolean":
|
|
9772
|
+
return "true";
|
|
9773
|
+
case "array":
|
|
9774
|
+
return "[]";
|
|
9775
|
+
case "object":
|
|
9776
|
+
return "{}";
|
|
9777
|
+
case "null":
|
|
9778
|
+
return "null";
|
|
9779
|
+
default:
|
|
9780
|
+
return locale === "en" ? '"a-value"' : '"un-valor"';
|
|
9781
|
+
}
|
|
9782
|
+
}
|
|
9783
|
+
function _unitForOrigin(origin, locale) {
|
|
9784
|
+
if (origin === "string") return locale === "en" ? "characters" : "caracteres";
|
|
9785
|
+
if (origin === "array") return locale === "en" ? "items" : "elementos";
|
|
9786
|
+
if (origin === "set") return locale === "en" ? "items" : "elementos";
|
|
9787
|
+
if (origin === "number" || origin === "bigint" || origin === "int") return "";
|
|
9788
|
+
if (origin === "date") return locale === "en" ? "date" : "fecha";
|
|
9789
|
+
return locale === "en" ? "units" : "unidades";
|
|
9790
|
+
}
|
|
9791
|
+
function getIssueHumanText(issue, locale) {
|
|
9792
|
+
const field = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
9793
|
+
switch (issue.code) {
|
|
9794
|
+
case "invalid_type": {
|
|
9795
|
+
const expected = String(issue.expected ?? "unknown");
|
|
9796
|
+
const received = _extractReceived(issue);
|
|
9797
|
+
const example = _placeholderForType(issue.expected, locale);
|
|
9798
|
+
return getMessage("marketing.wrapper.issue_invalid_type", locale, {
|
|
9799
|
+
field,
|
|
9800
|
+
expected,
|
|
9801
|
+
received,
|
|
9802
|
+
example
|
|
9803
|
+
});
|
|
9804
|
+
}
|
|
9805
|
+
case "too_small": {
|
|
9806
|
+
const minimum = String(issue.minimum ?? "");
|
|
9807
|
+
const unit = _unitForOrigin(issue.origin, locale);
|
|
9808
|
+
return getMessage("marketing.wrapper.issue_too_small", locale, {
|
|
9809
|
+
field,
|
|
9810
|
+
minimum,
|
|
9811
|
+
unit
|
|
9812
|
+
});
|
|
9813
|
+
}
|
|
9814
|
+
case "too_big": {
|
|
9815
|
+
const maximum = String(issue.maximum ?? "");
|
|
9816
|
+
const unit = _unitForOrigin(issue.origin, locale);
|
|
9817
|
+
return getMessage("marketing.wrapper.issue_too_big", locale, {
|
|
9818
|
+
field,
|
|
9819
|
+
maximum,
|
|
9820
|
+
unit
|
|
9821
|
+
});
|
|
9822
|
+
}
|
|
9823
|
+
case "invalid_enum_value":
|
|
9824
|
+
case "invalid_value": {
|
|
9825
|
+
const opts = (issue.options ?? []).map((o) => JSON.stringify(o)).join(", ");
|
|
9826
|
+
return getMessage("marketing.wrapper.issue_invalid_enum", locale, {
|
|
9827
|
+
field,
|
|
9828
|
+
options: opts
|
|
9829
|
+
});
|
|
9830
|
+
}
|
|
9831
|
+
case "invalid_string":
|
|
9832
|
+
case "invalid_format": {
|
|
9833
|
+
const hint = issue.validation ? `(${issue.validation})` : "";
|
|
9834
|
+
return getMessage("marketing.wrapper.issue_invalid_string", locale, {
|
|
9835
|
+
field,
|
|
9836
|
+
hint
|
|
9837
|
+
});
|
|
9838
|
+
}
|
|
9839
|
+
default:
|
|
9840
|
+
return getMessage("marketing.wrapper.issue_generic", locale, {
|
|
9841
|
+
field,
|
|
9842
|
+
message: issue.message
|
|
9843
|
+
});
|
|
9844
|
+
}
|
|
9845
|
+
}
|
|
9756
9846
|
var NIVELES_ORDER = ["ninguno", "ver", "editar", "completo"];
|
|
9757
9847
|
function nivelAlcanza(nivel, accion) {
|
|
9758
9848
|
return NIVELES_ORDER.indexOf(nivel) >= NIVELES_ORDER.indexOf(accion);
|
|
@@ -9855,9 +9945,16 @@ function wrapWithContract(contract, helper, options = {}) {
|
|
|
9855
9945
|
expected: issue.expected
|
|
9856
9946
|
};
|
|
9857
9947
|
});
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9948
|
+
let text;
|
|
9949
|
+
if (parseResult.error.issues.length === 0) {
|
|
9950
|
+
text = getWrapperMessage("input_invalido", locale);
|
|
9951
|
+
} else {
|
|
9952
|
+
const intro = getWrapperMessage("input_invalido_intro", locale);
|
|
9953
|
+
const lines = parseResult.error.issues.map(
|
|
9954
|
+
(i) => getIssueHumanText(i, locale)
|
|
9955
|
+
);
|
|
9956
|
+
text = `${intro} ${lines.join(" ")}`;
|
|
9957
|
+
}
|
|
9861
9958
|
await writeAuditLog({
|
|
9862
9959
|
tenantId: ctx.tenantId,
|
|
9863
9960
|
brandId: ctx.brandId ?? null,
|
|
@@ -10372,6 +10469,31 @@ var OrigenRutinaInputSchema = import_zod30.z.object({
|
|
|
10372
10469
|
conversacionId: import_zod30.z.string().nullable().describe("Conversation ID when tipo=conversacion. Pass null otherwise."),
|
|
10373
10470
|
cardId: import_zod30.z.string().nullable().describe("Card ID when tipo=feedback_card. Pass null otherwise.")
|
|
10374
10471
|
}).strict();
|
|
10472
|
+
var DIA_SEMANA_ES = ["domingo", "lunes", "martes", "mi\xE9rcoles", "jueves", "viernes", "s\xE1bado"];
|
|
10473
|
+
var DIA_SEMANA_EN = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
10474
|
+
function describeFrecuenciaForCopy(frecuencia, config, locale) {
|
|
10475
|
+
const en = locale === "en";
|
|
10476
|
+
switch (frecuencia) {
|
|
10477
|
+
case "puntual":
|
|
10478
|
+
return en ? `scheduled for ${config.fechaPuntual}` : `programada para ${config.fechaPuntual}`;
|
|
10479
|
+
case "diaria":
|
|
10480
|
+
return en ? `every day at ${config.hora}` : `cada d\xEDa a las ${config.hora}`;
|
|
10481
|
+
case "semanal": {
|
|
10482
|
+
const idx = config.diaSemana ?? 0;
|
|
10483
|
+
const dia = (en ? DIA_SEMANA_EN : DIA_SEMANA_ES)[idx];
|
|
10484
|
+
return en ? `every ${dia} at ${config.hora}` : `cada ${dia} a las ${config.hora}`;
|
|
10485
|
+
}
|
|
10486
|
+
case "quincenal": {
|
|
10487
|
+
const idx = config.diaSemana ?? 0;
|
|
10488
|
+
const dia = (en ? DIA_SEMANA_EN : DIA_SEMANA_ES)[idx];
|
|
10489
|
+
return en ? `every other ${dia} at ${config.hora}` : `cada quincena los ${dia} a las ${config.hora}`;
|
|
10490
|
+
}
|
|
10491
|
+
case "mensual":
|
|
10492
|
+
return en ? `on day ${config.diaMes} of every month at ${config.hora}` : `el d\xEDa ${config.diaMes} de cada mes a las ${config.hora}`;
|
|
10493
|
+
case "trimestral":
|
|
10494
|
+
return en ? `every quarter on day ${config.diaMes} at ${config.hora}` : `cada trimestre el d\xEDa ${config.diaMes} a las ${config.hora}`;
|
|
10495
|
+
}
|
|
10496
|
+
}
|
|
10375
10497
|
var ProgramarRutinaParamsSchema = import_zod30.z.object({
|
|
10376
10498
|
tenantId: import_zod30.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
10377
10499
|
uid: import_zod30.z.string().min(1).describe("User ID who creates the routine. Routines are scoped to this user (scope: self)."),
|
|
@@ -10408,10 +10530,12 @@ var programarRutinaRaw = {
|
|
|
10408
10530
|
affectsPublication: false,
|
|
10409
10531
|
affectsExternal: false,
|
|
10410
10532
|
martinConfirmationTemplate: (input, locale) => {
|
|
10533
|
+
const tool = input.accion.tool;
|
|
10534
|
+
const frase = describeFrecuenciaForCopy(input.frecuencia, input.config, locale);
|
|
10411
10535
|
if (locale === "en") {
|
|
10412
|
-
return `Schedule
|
|
10536
|
+
return `Schedule: ${frase}, I'll run ${tool}. Confirm?`;
|
|
10413
10537
|
}
|
|
10414
|
-
return
|
|
10538
|
+
return `Programa: ${frase}, ejecuto ${tool}. \xBFConfirmas?`;
|
|
10415
10539
|
},
|
|
10416
10540
|
martinSummaryTemplate: (input, output, locale) => {
|
|
10417
10541
|
if (!output.ok) {
|
|
@@ -10420,10 +10544,12 @@ var programarRutinaRaw = {
|
|
|
10420
10544
|
}
|
|
10421
10545
|
return getMessage("marketing.safeError.generic", locale);
|
|
10422
10546
|
}
|
|
10547
|
+
const tool = input.accion.tool;
|
|
10548
|
+
const frase = describeFrecuenciaForCopy(input.frecuencia, input.config, locale);
|
|
10423
10549
|
if (locale === "en") {
|
|
10424
|
-
return `Done.
|
|
10550
|
+
return `Done. ${frase}, I'll run ${tool}. First run: ${output.proximaEjecucionAt}.`;
|
|
10425
10551
|
}
|
|
10426
|
-
return `Listo.
|
|
10552
|
+
return `Listo. ${frase}, ejecuto ${tool}. Pr\xF3xima ejecuci\xF3n: ${output.proximaEjecucionAt}.`;
|
|
10427
10553
|
},
|
|
10428
10554
|
auditAction: "martin.rutina.crear",
|
|
10429
10555
|
extractTargetPath: (input, output) => output.ok ? `tenants/${input.tenantId}/martin_rutinas/${output.rutinaId}` : `tenants/${input.tenantId}/martin_rutinas/__unscheduled__`,
|
|
@@ -10707,18 +10833,20 @@ var import_zod35 = require("zod");
|
|
|
10707
10833
|
var import_firebase_admin7 = require("firebase-admin");
|
|
10708
10834
|
var import_zod36 = require("zod");
|
|
10709
10835
|
var import_zod37 = require("zod");
|
|
10710
|
-
var import_firebase_admin8 = require("firebase-admin");
|
|
10711
10836
|
var import_zod38 = require("zod");
|
|
10712
10837
|
var import_zod39 = require("zod");
|
|
10838
|
+
var import_firebase_admin8 = require("firebase-admin");
|
|
10713
10839
|
var import_zod40 = require("zod");
|
|
10714
|
-
var import_firebase_admin9 = require("firebase-admin");
|
|
10715
10840
|
var import_zod41 = require("zod");
|
|
10716
|
-
var import_firebase_admin10 = require("firebase-admin");
|
|
10717
10841
|
var import_zod42 = require("zod");
|
|
10842
|
+
var import_firebase_admin9 = require("firebase-admin");
|
|
10718
10843
|
var import_zod43 = require("zod");
|
|
10844
|
+
var import_firebase_admin10 = require("firebase-admin");
|
|
10719
10845
|
var import_zod44 = require("zod");
|
|
10720
10846
|
var import_zod45 = require("zod");
|
|
10721
10847
|
var import_zod46 = require("zod");
|
|
10848
|
+
var import_zod47 = require("zod");
|
|
10849
|
+
var import_zod48 = require("zod");
|
|
10722
10850
|
var import_firestore6 = require("firebase-admin/firestore");
|
|
10723
10851
|
var RULE_NEGATIVES = {
|
|
10724
10852
|
allowFaces: "no people, no faces, no hands",
|
|
@@ -12033,6 +12161,229 @@ var rawContract7 = {
|
|
|
12033
12161
|
var getCalendarContract = MartinContractSchema.parse(
|
|
12034
12162
|
rawContract7
|
|
12035
12163
|
);
|
|
12164
|
+
async function seoSnapshotReader(input) {
|
|
12165
|
+
const { db, tenantId, brandId } = input;
|
|
12166
|
+
const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
|
|
12167
|
+
const brandSnap = await brandRef.get();
|
|
12168
|
+
if (!brandSnap.exists) {
|
|
12169
|
+
return { ok: false, code: "BRAND_NOT_FOUND" };
|
|
12170
|
+
}
|
|
12171
|
+
const snap = await db.collection("tenants").doc(tenantId).collection("marketing_snapshots_semrush").where("brandId", "==", brandId).orderBy("mes", "desc").limit(1).get();
|
|
12172
|
+
if (snap.empty) {
|
|
12173
|
+
return { ok: false, code: "SEO_SNAPSHOT_MISSING" };
|
|
12174
|
+
}
|
|
12175
|
+
const doc = snap.docs[0];
|
|
12176
|
+
const data = doc.data();
|
|
12177
|
+
const mes = typeof data.mes === "string" ? data.mes : "";
|
|
12178
|
+
return {
|
|
12179
|
+
ok: true,
|
|
12180
|
+
brandId,
|
|
12181
|
+
mes,
|
|
12182
|
+
snapshot: { id: doc.id, ...data }
|
|
12183
|
+
};
|
|
12184
|
+
}
|
|
12185
|
+
var ParamsSchema8 = import_zod38.z.object({
|
|
12186
|
+
tenantId: import_zod38.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
12187
|
+
brandId: import_zod38.z.string().min(1).describe("Brand identifier within the tenant.")
|
|
12188
|
+
});
|
|
12189
|
+
var SuccessSchema = import_zod38.z.object({
|
|
12190
|
+
ok: import_zod38.z.literal(true),
|
|
12191
|
+
brandId: import_zod38.z.string(),
|
|
12192
|
+
mes: import_zod38.z.string(),
|
|
12193
|
+
snapshot: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown())
|
|
12194
|
+
});
|
|
12195
|
+
var FailureSchema = import_zod38.z.object({
|
|
12196
|
+
ok: import_zod38.z.literal(false),
|
|
12197
|
+
code: import_zod38.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"])
|
|
12198
|
+
});
|
|
12199
|
+
var OutputSchema8 = import_zod38.z.discriminatedUnion("ok", [SuccessSchema, FailureSchema]);
|
|
12200
|
+
var rawContract8 = {
|
|
12201
|
+
name: "get_seo_snapshot",
|
|
12202
|
+
description: "Read the latest Semrush SEO snapshot for a brand. Returns rank, top keywords, opportunities, and competitors.",
|
|
12203
|
+
paramsSchema: ParamsSchema8,
|
|
12204
|
+
outputSchema: OutputSchema8,
|
|
12205
|
+
requiresConfirmation: false,
|
|
12206
|
+
destructive: false,
|
|
12207
|
+
affectsPublication: false,
|
|
12208
|
+
affectsExternal: false,
|
|
12209
|
+
martinSummaryTemplate: (input, output, locale) => {
|
|
12210
|
+
if (!output.ok) {
|
|
12211
|
+
if (output.code === "BRAND_NOT_FOUND") {
|
|
12212
|
+
return locale === "en" ? `Brand ${input.brandId} not found.` : `No encontr\xE9 la brand ${input.brandId}.`;
|
|
12213
|
+
}
|
|
12214
|
+
return locale === "en" ? `No SEO snapshot for brand ${input.brandId} yet.` : `A\xFAn no hay snapshot SEO de ${input.brandId}.`;
|
|
12215
|
+
}
|
|
12216
|
+
return locale === "en" ? `Latest SEO snapshot for ${input.brandId} (${output.mes}).` : `Snapshot SEO m\xE1s reciente de ${input.brandId} (${output.mes}).`;
|
|
12217
|
+
},
|
|
12218
|
+
// AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
|
|
12219
|
+
auditAction: "marketing.seo_snapshot.leer",
|
|
12220
|
+
quotasConsumed: [],
|
|
12221
|
+
permissionScope: "module",
|
|
12222
|
+
permissionKey: "marketing",
|
|
12223
|
+
permissionAction: "ver",
|
|
12224
|
+
sideEffects: ["reads_firestore"]
|
|
12225
|
+
};
|
|
12226
|
+
var seoSnapshotReaderContract = MartinContractSchema.parse(
|
|
12227
|
+
rawContract8
|
|
12228
|
+
);
|
|
12229
|
+
var BEST_PRACTICES_EN = {
|
|
12230
|
+
title: "Primary keyword, clear and short. Page H1.",
|
|
12231
|
+
description: "Above grid: 2-3 sentences (50-70 words). Below grid: 200-400 words with long-tail keywords. Pages with descriptions rank 2.7x higher.",
|
|
12232
|
+
metaTitle: "50-60 chars. Keyword first. Format: {Keyword} | {Brand}. Max 600px.",
|
|
12233
|
+
metaDescription: "120-158 chars. Keyword + CTA + value prop. 120 mobile, 158 desktop.",
|
|
12234
|
+
handle: "Keyword in URL. NEVER change an indexed URL without a 301 redirect.",
|
|
12235
|
+
imageAlt: "Descriptive with keyword. Accessibility + Google Images + AEO.",
|
|
12236
|
+
aeo: "AI engines validate images against schema. Structured data matters."
|
|
12237
|
+
};
|
|
12238
|
+
var SCHEMA_FOR_SAVE_EN = {
|
|
12239
|
+
_instructions: "STRICT SCHEMA for save_collection_suggestions. Use EXACTLY these field names. The system WILL REJECT mismatched fields.",
|
|
12240
|
+
_example: {
|
|
12241
|
+
collectionId: 123456789,
|
|
12242
|
+
suggestedTitle: "Purple Flowers | Same-Day CDMX Delivery",
|
|
12243
|
+
suggestedDescription: "<p>Discover our purple flowers collection...</p>",
|
|
12244
|
+
suggestedMetaTitle: "Purple Flowers CDMX | Same-Day Delivery",
|
|
12245
|
+
suggestedMetaDescription: "Send purple flowers to your door in CDMX. Fresh arrangements with purple roses, tulips, and lilies. Same-day delivery.",
|
|
12246
|
+
suggestedHandle: null,
|
|
12247
|
+
suggestedImageAlt: "Bouquet of purple flowers with roses and tulips \u2014 local florist CDMX",
|
|
12248
|
+
keyword: "purple flowers cdmx",
|
|
12249
|
+
notas: "Meta title optimized with local keyword. Description expanded with long-tail keywords. Handle unchanged (already indexed)."
|
|
12250
|
+
},
|
|
12251
|
+
_rules: [
|
|
12252
|
+
"collectionId: REQUIRED. Numeric Shopify ID (not GID).",
|
|
12253
|
+
"suggestedTitle: string. Page H1. Clear with keyword.",
|
|
12254
|
+
"suggestedDescription: HTML string. 200-400 words. Include links to related collections.",
|
|
12255
|
+
"suggestedMetaTitle: string. 50-60 chars. Keyword first. NEVER over 60.",
|
|
12256
|
+
"suggestedMetaDescription: string. 120-158 chars. Keyword + CTA + value prop. NEVER over 158.",
|
|
12257
|
+
"suggestedHandle: string | null. Only change if the current handle is bad. null = no change.",
|
|
12258
|
+
"suggestedImageAlt: string. Descriptive with keyword. For accessibility + Google Images.",
|
|
12259
|
+
"keyword: string. Target keyword for this collection (from the marketing plan).",
|
|
12260
|
+
"notas: string. Brief explanation of what changed and why."
|
|
12261
|
+
],
|
|
12262
|
+
_never: [
|
|
12263
|
+
"NEVER use Spanish field names (titulo, descripcion, etc.) \u2014 use English names.",
|
|
12264
|
+
"NEVER fabricate collectionId \u2014 use the id from the collections array.",
|
|
12265
|
+
"NEVER exceed 60 chars in metaTitle or 158 chars in metaDescription.",
|
|
12266
|
+
"NEVER change the handle of an indexed collection without justification."
|
|
12267
|
+
]
|
|
12268
|
+
};
|
|
12269
|
+
async function collectionsReader(input) {
|
|
12270
|
+
const { db, tenantId, brandId } = input;
|
|
12271
|
+
const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
|
|
12272
|
+
const brandSnap = await brandRef.get();
|
|
12273
|
+
if (!brandSnap.exists) {
|
|
12274
|
+
return { ok: false, code: "BRAND_NOT_FOUND" };
|
|
12275
|
+
}
|
|
12276
|
+
const collSnap = await db.collection("tenants").doc(tenantId).collection("marketing_snapshots").doc(brandId).collection("collections").get();
|
|
12277
|
+
if (collSnap.empty) {
|
|
12278
|
+
return { ok: false, code: "NO_COLLECTIONS_NESTED" };
|
|
12279
|
+
}
|
|
12280
|
+
const items = collSnap.docs.map((d) => d.data());
|
|
12281
|
+
const brand = brandSnap.data() ?? {};
|
|
12282
|
+
const plan = brand.plan ?? {};
|
|
12283
|
+
const keywordsPrioritarios = Array.isArray(plan.keywordsPrioritarios) ? plan.keywordsPrioritarios : [];
|
|
12284
|
+
const existingSuggestions = brand.collectionSuggestions ?? {};
|
|
12285
|
+
const collections = items.map((c) => {
|
|
12286
|
+
const seo = c.seo || {};
|
|
12287
|
+
const featured = c.featuredImage || null;
|
|
12288
|
+
return {
|
|
12289
|
+
id: c.platformId,
|
|
12290
|
+
title: c.title,
|
|
12291
|
+
handle: c.handle,
|
|
12292
|
+
body_html: typeof c.descriptionHtml === "string" ? c.descriptionHtml.slice(0, 500) : null,
|
|
12293
|
+
metaTitle: seo.metaTitle ?? null,
|
|
12294
|
+
metaDescription: seo.metaDescription ?? null,
|
|
12295
|
+
products_count: null,
|
|
12296
|
+
collectionType: "canonical",
|
|
12297
|
+
image: featured ? { src: featured.url, alt: featured.altText } : null,
|
|
12298
|
+
existingSuggestion: existingSuggestions[String(c.platformId)] ?? null
|
|
12299
|
+
};
|
|
12300
|
+
});
|
|
12301
|
+
return {
|
|
12302
|
+
ok: true,
|
|
12303
|
+
brandId,
|
|
12304
|
+
totalCollections: collections.length,
|
|
12305
|
+
keywordsPrioritarios,
|
|
12306
|
+
collections,
|
|
12307
|
+
bestPractices: BEST_PRACTICES_EN,
|
|
12308
|
+
schemaParaSave: SCHEMA_FOR_SAVE_EN
|
|
12309
|
+
};
|
|
12310
|
+
}
|
|
12311
|
+
var ParamsSchema9 = import_zod39.z.object({
|
|
12312
|
+
tenantId: import_zod39.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
12313
|
+
brandId: import_zod39.z.string().min(1).describe("Brand identifier within the tenant.")
|
|
12314
|
+
});
|
|
12315
|
+
var CollectionItemSchema = import_zod39.z.object({
|
|
12316
|
+
id: import_zod39.z.unknown(),
|
|
12317
|
+
title: import_zod39.z.unknown(),
|
|
12318
|
+
handle: import_zod39.z.unknown(),
|
|
12319
|
+
body_html: import_zod39.z.string().nullable(),
|
|
12320
|
+
metaTitle: import_zod39.z.unknown(),
|
|
12321
|
+
metaDescription: import_zod39.z.unknown(),
|
|
12322
|
+
products_count: import_zod39.z.null(),
|
|
12323
|
+
collectionType: import_zod39.z.literal("canonical"),
|
|
12324
|
+
image: import_zod39.z.object({ src: import_zod39.z.unknown(), alt: import_zod39.z.unknown() }).nullable(),
|
|
12325
|
+
existingSuggestion: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown()).nullable()
|
|
12326
|
+
});
|
|
12327
|
+
var BestPracticesSchema = import_zod39.z.object({
|
|
12328
|
+
title: import_zod39.z.string(),
|
|
12329
|
+
description: import_zod39.z.string(),
|
|
12330
|
+
metaTitle: import_zod39.z.string(),
|
|
12331
|
+
metaDescription: import_zod39.z.string(),
|
|
12332
|
+
handle: import_zod39.z.string(),
|
|
12333
|
+
imageAlt: import_zod39.z.string(),
|
|
12334
|
+
aeo: import_zod39.z.string()
|
|
12335
|
+
});
|
|
12336
|
+
var SchemaForSaveSchema = import_zod39.z.object({
|
|
12337
|
+
_instructions: import_zod39.z.string(),
|
|
12338
|
+
_example: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown()),
|
|
12339
|
+
_rules: import_zod39.z.array(import_zod39.z.string()),
|
|
12340
|
+
_never: import_zod39.z.array(import_zod39.z.string())
|
|
12341
|
+
});
|
|
12342
|
+
var SuccessSchema2 = import_zod39.z.object({
|
|
12343
|
+
ok: import_zod39.z.literal(true),
|
|
12344
|
+
brandId: import_zod39.z.string(),
|
|
12345
|
+
totalCollections: import_zod39.z.number(),
|
|
12346
|
+
// Shape real: Array<{ keyword, posicion, volumen, dificultad, prioridad, accion }>.
|
|
12347
|
+
// Passthrough loose para no acoplar el reader al schema interno del plan.
|
|
12348
|
+
keywordsPrioritarios: import_zod39.z.array(import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown())),
|
|
12349
|
+
collections: import_zod39.z.array(CollectionItemSchema),
|
|
12350
|
+
bestPractices: BestPracticesSchema,
|
|
12351
|
+
schemaParaSave: SchemaForSaveSchema
|
|
12352
|
+
});
|
|
12353
|
+
var FailureSchema2 = import_zod39.z.object({
|
|
12354
|
+
ok: import_zod39.z.literal(false),
|
|
12355
|
+
code: import_zod39.z.enum(["BRAND_NOT_FOUND", "NO_COLLECTIONS_NESTED"])
|
|
12356
|
+
});
|
|
12357
|
+
var OutputSchema9 = import_zod39.z.discriminatedUnion("ok", [SuccessSchema2, FailureSchema2]);
|
|
12358
|
+
var rawContract9 = {
|
|
12359
|
+
name: "get_collections",
|
|
12360
|
+
description: "Read all canonical collections (Shopify/WordPress/web-only) for a brand with their 6 SEO fields, plus best practices and the strict schema for generating SEO suggestions.",
|
|
12361
|
+
paramsSchema: ParamsSchema9,
|
|
12362
|
+
outputSchema: OutputSchema9,
|
|
12363
|
+
requiresConfirmation: false,
|
|
12364
|
+
destructive: false,
|
|
12365
|
+
affectsPublication: false,
|
|
12366
|
+
affectsExternal: false,
|
|
12367
|
+
martinSummaryTemplate: (input, output, locale) => {
|
|
12368
|
+
if (!output.ok) {
|
|
12369
|
+
if (output.code === "BRAND_NOT_FOUND") {
|
|
12370
|
+
return locale === "en" ? `Brand ${input.brandId} not found.` : `No encontr\xE9 la brand ${input.brandId}.`;
|
|
12371
|
+
}
|
|
12372
|
+
return locale === "en" ? `No collections synced for ${input.brandId} yet.` : `A\xFAn no hay colecciones sincronizadas para ${input.brandId}.`;
|
|
12373
|
+
}
|
|
12374
|
+
return locale === "en" ? `${output.totalCollections} collections for ${input.brandId}.` : `${output.totalCollections} colecciones para ${input.brandId}.`;
|
|
12375
|
+
},
|
|
12376
|
+
// AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
|
|
12377
|
+
auditAction: "marketing.colecciones.listar",
|
|
12378
|
+
quotasConsumed: [],
|
|
12379
|
+
permissionScope: "module",
|
|
12380
|
+
permissionKey: "marketing",
|
|
12381
|
+
permissionAction: "ver",
|
|
12382
|
+
sideEffects: ["reads_firestore"]
|
|
12383
|
+
};
|
|
12384
|
+
var collectionsReaderContract = MartinContractSchema.parse(
|
|
12385
|
+
rawContract9
|
|
12386
|
+
);
|
|
12036
12387
|
var PLATAFORMA_A_FORMATO = {
|
|
12037
12388
|
gbp: "gbp_4_3",
|
|
12038
12389
|
shopify_blog: "blog_3_2",
|
|
@@ -12144,35 +12495,35 @@ async function photoAssigner(input) {
|
|
|
12144
12495
|
formato
|
|
12145
12496
|
};
|
|
12146
12497
|
}
|
|
12147
|
-
var
|
|
12148
|
-
tenantId:
|
|
12149
|
-
brandId:
|
|
12150
|
-
contenidoRef:
|
|
12151
|
-
fotoId:
|
|
12152
|
-
calendarioItemRef:
|
|
12498
|
+
var ParamsSchema10 = import_zod40.z.object({
|
|
12499
|
+
tenantId: import_zod40.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
12500
|
+
brandId: import_zod40.z.string().min(1).describe("Brand identifier within the tenant."),
|
|
12501
|
+
contenidoRef: import_zod40.z.string().min(1).describe("Document ID in marketing_contenido (the post receiving the photo)."),
|
|
12502
|
+
fotoId: import_zod40.z.string().min(1).describe("Photo ID in marketing_fotos. Photo must be in state='editada'."),
|
|
12503
|
+
calendarioItemRef: import_zod40.z.string().regex(/^semana:\d+:slot:\d+$/).optional().describe(
|
|
12153
12504
|
'Calendar slot reference in format "semana:N:slot:M". Optional \u2014 if provided, syncs the slot.fotoIdAsignada UI snapshot atomically with the content update. OMIT if there is no calendar slot to sync.'
|
|
12154
12505
|
)
|
|
12155
12506
|
});
|
|
12156
|
-
var
|
|
12157
|
-
|
|
12158
|
-
ok:
|
|
12159
|
-
fotoId:
|
|
12160
|
-
contenidoRef:
|
|
12161
|
-
plataforma:
|
|
12162
|
-
varianteUrl:
|
|
12163
|
-
formato:
|
|
12507
|
+
var OutputSchema10 = import_zod40.z.discriminatedUnion("ok", [
|
|
12508
|
+
import_zod40.z.object({
|
|
12509
|
+
ok: import_zod40.z.literal(true),
|
|
12510
|
+
fotoId: import_zod40.z.string(),
|
|
12511
|
+
contenidoRef: import_zod40.z.string(),
|
|
12512
|
+
plataforma: import_zod40.z.string(),
|
|
12513
|
+
varianteUrl: import_zod40.z.string().nullable(),
|
|
12514
|
+
formato: import_zod40.z.string()
|
|
12164
12515
|
}),
|
|
12165
|
-
|
|
12166
|
-
ok:
|
|
12167
|
-
error:
|
|
12168
|
-
code:
|
|
12516
|
+
import_zod40.z.object({
|
|
12517
|
+
ok: import_zod40.z.literal(false),
|
|
12518
|
+
error: import_zod40.z.string(),
|
|
12519
|
+
code: import_zod40.z.enum(["CONTENT_NOT_FOUND", "CONTENT_TENANT_MISMATCH", "PHOTO_NOT_FOUND", "PHOTO_NOT_READY"]).optional()
|
|
12169
12520
|
})
|
|
12170
12521
|
]);
|
|
12171
|
-
var
|
|
12522
|
+
var rawContract10 = {
|
|
12172
12523
|
name: "assign_photo_to_content",
|
|
12173
12524
|
description: 'Assign an edited photo to an existing content. Writes atomically to BOTH marketing_contenido.fotoId+mediaVariante (canonical for publish) AND marketing_calendario slot.fotoIdAsignada (UI snapshot, only if calendarioItemRef provided). NEVER use update_calendar_slot for this \u2014 that one writes to the agenda, not the post. The photo must be in state="editada".',
|
|
12174
|
-
paramsSchema:
|
|
12175
|
-
outputSchema:
|
|
12525
|
+
paramsSchema: ParamsSchema10,
|
|
12526
|
+
outputSchema: OutputSchema10,
|
|
12176
12527
|
// Cambia la foto vinculada al contenido — reversible (volver a llamar con
|
|
12177
12528
|
// otra fotoId), no publica nada externo. La foto editada queda intacta;
|
|
12178
12529
|
// solo se actualiza la referencia.
|
|
@@ -12214,7 +12565,7 @@ var rawContract8 = {
|
|
|
12214
12565
|
sideEffects: ["writes_firestore", "updates_calendar_slot"]
|
|
12215
12566
|
};
|
|
12216
12567
|
var photoAssignerContract = MartinContractSchema.parse(
|
|
12217
|
-
|
|
12568
|
+
rawContract10
|
|
12218
12569
|
);
|
|
12219
12570
|
function findPageByHeuristic(pages, pattern) {
|
|
12220
12571
|
return pages.find((p) => pattern.test(p.title || "")) || pages.find((p) => pattern.test(p.handle || "")) || null;
|
|
@@ -12403,27 +12754,27 @@ var BRAND_BRIEF_SCHEMA_HINT = {
|
|
|
12403
12754
|
escenas: [{ id: string, nombre: string, promptHint: string }]
|
|
12404
12755
|
}`
|
|
12405
12756
|
};
|
|
12406
|
-
var
|
|
12407
|
-
tenantId:
|
|
12408
|
-
brandId:
|
|
12757
|
+
var ParamsSchema11 = import_zod41.z.object({
|
|
12758
|
+
tenantId: import_zod41.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
12759
|
+
brandId: import_zod41.z.string().min(1).describe("Brand identifier within the tenant.")
|
|
12409
12760
|
});
|
|
12410
|
-
var
|
|
12411
|
-
|
|
12412
|
-
ok:
|
|
12761
|
+
var OutputSchema11 = import_zod41.z.discriminatedUnion("ok", [
|
|
12762
|
+
import_zod41.z.object({
|
|
12763
|
+
ok: import_zod41.z.literal(true),
|
|
12413
12764
|
/** Payload con instrucción + datos del negocio para que Claude genere el brief. */
|
|
12414
|
-
payload:
|
|
12765
|
+
payload: import_zod41.z.record(import_zod41.z.string(), import_zod41.z.unknown())
|
|
12415
12766
|
}),
|
|
12416
|
-
|
|
12417
|
-
ok:
|
|
12418
|
-
error:
|
|
12419
|
-
code:
|
|
12767
|
+
import_zod41.z.object({
|
|
12768
|
+
ok: import_zod41.z.literal(false),
|
|
12769
|
+
error: import_zod41.z.string(),
|
|
12770
|
+
code: import_zod41.z.enum(["BRAND_NOT_FOUND"]).optional()
|
|
12420
12771
|
})
|
|
12421
12772
|
]);
|
|
12422
|
-
var
|
|
12773
|
+
var rawContract11 = {
|
|
12423
12774
|
name: "generate_brand_brief",
|
|
12424
12775
|
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.",
|
|
12425
|
-
paramsSchema:
|
|
12426
|
-
outputSchema:
|
|
12776
|
+
paramsSchema: ParamsSchema11,
|
|
12777
|
+
outputSchema: OutputSchema11,
|
|
12427
12778
|
// Lectura pura, sin side effects de escritura ni publicación.
|
|
12428
12779
|
requiresConfirmation: false,
|
|
12429
12780
|
destructive: false,
|
|
@@ -12448,7 +12799,7 @@ var rawContract9 = {
|
|
|
12448
12799
|
sideEffects: ["reads_firestore"]
|
|
12449
12800
|
};
|
|
12450
12801
|
var brandBriefBuilderContract = MartinContractSchema.parse(
|
|
12451
|
-
|
|
12802
|
+
rawContract11
|
|
12452
12803
|
);
|
|
12453
12804
|
async function resolveLastImportId(db, tenantId, brandId) {
|
|
12454
12805
|
const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
|
|
@@ -12636,28 +12987,28 @@ var BLOG_STRATEGY_REGLAS = [
|
|
|
12636
12987
|
"blogId, handle, title, ultimoPostFecha, ultimoPostKeyword, totalArticulos \u2014 copiar del shopifyBlogs (datos reales del import)",
|
|
12637
12988
|
"defaultBlogId y defaultBlogHandle \u2014 usar el primer blog de la lista"
|
|
12638
12989
|
];
|
|
12639
|
-
var
|
|
12640
|
-
tenantId:
|
|
12641
|
-
brandId:
|
|
12990
|
+
var ParamsSchema12 = import_zod42.z.object({
|
|
12991
|
+
tenantId: import_zod42.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
12992
|
+
brandId: import_zod42.z.string().min(1).describe("Brand identifier within the tenant.")
|
|
12642
12993
|
});
|
|
12643
|
-
var
|
|
12644
|
-
|
|
12645
|
-
ok:
|
|
12646
|
-
payload:
|
|
12994
|
+
var OutputSchema12 = import_zod42.z.discriminatedUnion("ok", [
|
|
12995
|
+
import_zod42.z.object({
|
|
12996
|
+
ok: import_zod42.z.literal(true),
|
|
12997
|
+
payload: import_zod42.z.record(import_zod42.z.string(), import_zod42.z.unknown()).describe(
|
|
12647
12998
|
"Aggregated business data + instructions + schema hints for the LLM to generate a marketing plan. Includes seoSnapshot, productos, historialReciente, shopifyColecciones, shopifyBlogs, brandBrief, planActual, and schema/rules hints for coleccionesPriorizadas and blogStrategy."
|
|
12648
12999
|
)
|
|
12649
13000
|
}),
|
|
12650
|
-
|
|
12651
|
-
ok:
|
|
12652
|
-
error:
|
|
12653
|
-
code:
|
|
13001
|
+
import_zod42.z.object({
|
|
13002
|
+
ok: import_zod42.z.literal(false),
|
|
13003
|
+
error: import_zod42.z.string(),
|
|
13004
|
+
code: import_zod42.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"]).optional()
|
|
12654
13005
|
})
|
|
12655
13006
|
]);
|
|
12656
|
-
var
|
|
13007
|
+
var rawContract12 = {
|
|
12657
13008
|
name: "generate_marketing_plan",
|
|
12658
13009
|
description: "Aggregate business data (seoSnapshot, productos, brand config, brandBrief, historial, Shopify collections + blogs) into a payload for the LLM to generate a marketing plan. Read-only \u2014 does NOT save the plan. The caller saves via save_marketing_plan or update_marketing_plan_field after generating. Requires an existing seoSnapshot on the brand (returns SEO_SNAPSHOT_MISSING otherwise).",
|
|
12659
|
-
paramsSchema:
|
|
12660
|
-
outputSchema:
|
|
13010
|
+
paramsSchema: ParamsSchema12,
|
|
13011
|
+
outputSchema: OutputSchema12,
|
|
12661
13012
|
// Lectura pura, no muta nada.
|
|
12662
13013
|
requiresConfirmation: false,
|
|
12663
13014
|
destructive: false,
|
|
@@ -12682,7 +13033,7 @@ var rawContract10 = {
|
|
|
12682
13033
|
sideEffects: ["reads_firestore"]
|
|
12683
13034
|
};
|
|
12684
13035
|
var marketingPlanBuilderContract = MartinContractSchema.parse(
|
|
12685
|
-
|
|
13036
|
+
rawContract12
|
|
12686
13037
|
);
|
|
12687
13038
|
function buildGenId() {
|
|
12688
13039
|
return `mkt_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -12923,54 +13274,54 @@ async function contenidoWriter(input) {
|
|
|
12923
13274
|
pipelineLinked
|
|
12924
13275
|
};
|
|
12925
13276
|
}
|
|
12926
|
-
var
|
|
12927
|
-
tenantId:
|
|
12928
|
-
brandId:
|
|
12929
|
-
plataforma:
|
|
12930
|
-
tipo:
|
|
13277
|
+
var ParamsSchema13 = import_zod43.z.object({
|
|
13278
|
+
tenantId: import_zod43.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
13279
|
+
brandId: import_zod43.z.string().min(1).describe("Brand identifier within the tenant."),
|
|
13280
|
+
plataforma: import_zod43.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target platform for the content."),
|
|
13281
|
+
tipo: import_zod43.z.string().optional().describe(
|
|
12931
13282
|
"Content type ('post', 'blog', 'carousel', 'reel', 'story', 'review_response'). Match to the platform."
|
|
12932
13283
|
),
|
|
12933
|
-
keyword:
|
|
12934
|
-
languageCode:
|
|
13284
|
+
keyword: import_zod43.z.string().optional().describe("Primary keyword for the content. OMIT if not applicable."),
|
|
13285
|
+
languageCode: import_zod43.z.string().optional().describe(
|
|
12935
13286
|
"Content language code (e.g. 'es', 'en'). For shopify_blog auto-injects to datos.languageCode if not present."
|
|
12936
13287
|
),
|
|
12937
|
-
fotoId:
|
|
12938
|
-
datos:
|
|
13288
|
+
fotoId: import_zod43.z.string().optional().describe("Linked photo ID. OMIT if no photo associated yet (use assign_photo_to_content later)."),
|
|
13289
|
+
datos: import_zod43.z.record(import_zod43.z.string(), import_zod43.z.unknown()).describe(
|
|
12939
13290
|
"Platform-specific content payload. Validated against Blog/GBP/IG/Review schemas. Use buildDatosBlog/GBP/IG/Review helpers to construct safely."
|
|
12940
13291
|
),
|
|
12941
|
-
calendarioItemRef:
|
|
13292
|
+
calendarioItemRef: import_zod43.z.string().optional().describe(
|
|
12942
13293
|
'Calendar slot reference (format "semana:N:slot:M"). If provided, links content to that slot AND discards prior non-discarded content of the same slot.'
|
|
12943
13294
|
)
|
|
12944
13295
|
});
|
|
12945
|
-
var PipelineLinkResultSchema =
|
|
12946
|
-
linked:
|
|
12947
|
-
paso:
|
|
12948
|
-
motivo:
|
|
12949
|
-
code:
|
|
12950
|
-
_instrucciones:
|
|
13296
|
+
var PipelineLinkResultSchema = import_zod43.z.object({
|
|
13297
|
+
linked: import_zod43.z.boolean(),
|
|
13298
|
+
paso: import_zod43.z.string().nullable(),
|
|
13299
|
+
motivo: import_zod43.z.string().optional(),
|
|
13300
|
+
code: import_zod43.z.string().optional(),
|
|
13301
|
+
_instrucciones: import_zod43.z.string().optional()
|
|
12951
13302
|
});
|
|
12952
|
-
var
|
|
12953
|
-
|
|
12954
|
-
ok:
|
|
12955
|
-
contenidoId:
|
|
12956
|
-
estado:
|
|
12957
|
-
plataforma:
|
|
12958
|
-
descartados:
|
|
13303
|
+
var OutputSchema13 = import_zod43.z.discriminatedUnion("ok", [
|
|
13304
|
+
import_zod43.z.object({
|
|
13305
|
+
ok: import_zod43.z.literal(true),
|
|
13306
|
+
contenidoId: import_zod43.z.string(),
|
|
13307
|
+
estado: import_zod43.z.string(),
|
|
13308
|
+
plataforma: import_zod43.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
|
|
13309
|
+
descartados: import_zod43.z.number().int().nonnegative(),
|
|
12959
13310
|
pipelineLinked: PipelineLinkResultSchema
|
|
12960
13311
|
}),
|
|
12961
|
-
|
|
12962
|
-
ok:
|
|
12963
|
-
error:
|
|
12964
|
-
code:
|
|
12965
|
-
activosEstaSemana:
|
|
12966
|
-
limite:
|
|
13312
|
+
import_zod43.z.object({
|
|
13313
|
+
ok: import_zod43.z.literal(false),
|
|
13314
|
+
error: import_zod43.z.string(),
|
|
13315
|
+
code: import_zod43.z.enum(["CONTENT_FREQUENCY_LIMIT", "CONTENT_DATA_VALIDATION_FAILED"]).optional(),
|
|
13316
|
+
activosEstaSemana: import_zod43.z.number().int().optional(),
|
|
13317
|
+
limite: import_zod43.z.number().int().optional()
|
|
12967
13318
|
})
|
|
12968
13319
|
]);
|
|
12969
|
-
var
|
|
13320
|
+
var rawContract13 = {
|
|
12970
13321
|
name: "save_generated_content",
|
|
12971
13322
|
description: "Save newly generated marketing content to marketing_contenido. The content is saved as pendiente_aprobacion \u2014 it is NOT published until the tenant approves it. Enforces weekly frequency limits per platform. If calendarioItemRef is provided, links to the slot and discards any prior content of the same slot. Validates datos against the platform schema.",
|
|
12972
|
-
paramsSchema:
|
|
12973
|
-
outputSchema:
|
|
13323
|
+
paramsSchema: ParamsSchema13,
|
|
13324
|
+
outputSchema: OutputSchema13,
|
|
12974
13325
|
// Crea borrador + descarta borrador previo del mismo slot. Reversible
|
|
12975
13326
|
// (volver a llamar con datos corregidos crea un nuevo borrador y descarta
|
|
12976
13327
|
// el actual). NO publica nada externo — la CF de publish se encarga
|
|
@@ -13015,7 +13366,7 @@ var rawContract11 = {
|
|
|
13015
13366
|
sideEffects: ["writes_firestore", "updates_calendar_slot"]
|
|
13016
13367
|
};
|
|
13017
13368
|
var contenidoWriterContract = MartinContractSchema.parse(
|
|
13018
|
-
|
|
13369
|
+
rawContract13
|
|
13019
13370
|
);
|
|
13020
13371
|
function fmtDate(d) {
|
|
13021
13372
|
const y = d.getFullYear();
|
|
@@ -13290,34 +13641,34 @@ OBLIGATORIO: calendarioItemRef con formato EXACTO "semana:N:slot:M" (ej: "semana
|
|
|
13290
13641
|
OBLIGATORIO: fotoId \u2014 SIEMPRE pasa el ID de la foto que elegiste con get_photos_for_slot.
|
|
13291
13642
|
Si el slot tiene notas[], LEERLAS y usarlas como contexto. Si estado es revisar, regenerar adaptando a las notas.
|
|
13292
13643
|
BLOG SLOTS: Usa _blogJIT para el contexto de cada slot shopify_blog \u2014 blogTarget, tono, author, articulosExistentes para interlinking.`;
|
|
13293
|
-
var
|
|
13294
|
-
tenantId:
|
|
13295
|
-
brandId:
|
|
13296
|
-
semana:
|
|
13644
|
+
var ParamsSchema14 = import_zod44.z.object({
|
|
13645
|
+
tenantId: import_zod44.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
13646
|
+
brandId: import_zod44.z.string().min(1).describe("Brand identifier within the tenant."),
|
|
13647
|
+
semana: import_zod44.z.number().int().min(1).max(5).optional().describe(
|
|
13297
13648
|
"Week number within the current month (1-5). OMIT to default to the current week inferred from today."
|
|
13298
13649
|
),
|
|
13299
|
-
modo:
|
|
13650
|
+
modo: import_zod44.z.enum(["planificar", "generar"]).optional().describe(
|
|
13300
13651
|
"Operation mode. 'planificar' (default): propose distribution platform+keyword+tipo per day. 'generar': requires slots in pre_aprobado/revisar \u2014 generate actual content. OMIT to default to 'planificar'."
|
|
13301
13652
|
)
|
|
13302
13653
|
});
|
|
13303
|
-
var
|
|
13304
|
-
|
|
13305
|
-
ok:
|
|
13306
|
-
payload:
|
|
13654
|
+
var OutputSchema14 = import_zod44.z.discriminatedUnion("ok", [
|
|
13655
|
+
import_zod44.z.object({
|
|
13656
|
+
ok: import_zod44.z.literal(true),
|
|
13657
|
+
payload: import_zod44.z.record(import_zod44.z.string(), import_zod44.z.unknown()).describe(
|
|
13307
13658
|
"Aggregated context for the LLM. Shape varies by modo: 'planificar' returns brand skeleton + slotsYaExistentes + historial; 'generar' returns slotsParaGenerar + fotosDisponibles + brand.blogStrategy + blogJIT (if blog slots present)."
|
|
13308
13659
|
)
|
|
13309
13660
|
}),
|
|
13310
|
-
|
|
13311
|
-
ok:
|
|
13312
|
-
error:
|
|
13313
|
-
code:
|
|
13661
|
+
import_zod44.z.object({
|
|
13662
|
+
ok: import_zod44.z.literal(false),
|
|
13663
|
+
error: import_zod44.z.string(),
|
|
13664
|
+
code: import_zod44.z.enum(["BRAND_NOT_FOUND", "WEEK_HAS_NO_SLOTS"]).optional()
|
|
13314
13665
|
})
|
|
13315
13666
|
]);
|
|
13316
|
-
var
|
|
13667
|
+
var rawContract14 = {
|
|
13317
13668
|
name: "generate_weekly_content",
|
|
13318
13669
|
description: "Build the week's content. Two modes: 'planificar' aggregates context for the LLM to propose distribution (LLM then uses add_calendar_slot/update_calendar_slot per day); 'generar' returns pre-approved slots + photos + blog JIT context for the LLM to generate actual content (LLM then calls save_generated_content per slot). AUTO-CREATES the monthly marketing_calendario if it does not exist. NEVER generates content directly \u2014 always returns a payload for the LLM consumer.",
|
|
13319
|
-
paramsSchema:
|
|
13320
|
-
outputSchema:
|
|
13670
|
+
paramsSchema: ParamsSchema14,
|
|
13671
|
+
outputSchema: OutputSchema14,
|
|
13321
13672
|
// Auto-create de calendario es bootstrap reversible (volver a llamar usa el
|
|
13322
13673
|
// calendario existente). NO publica nada externo. modo='generar' tampoco
|
|
13323
13674
|
// publica — solo prepara contexto.
|
|
@@ -13362,7 +13713,7 @@ var rawContract12 = {
|
|
|
13362
13713
|
sideEffects: ["reads_firestore", "writes_firestore"]
|
|
13363
13714
|
};
|
|
13364
13715
|
var weeklyContentBuilderContract = MartinContractSchema.parse(
|
|
13365
|
-
|
|
13716
|
+
rawContract14
|
|
13366
13717
|
);
|
|
13367
13718
|
var DEFAULT_TEXT_THRESHOLD = 0.35;
|
|
13368
13719
|
var DEFAULT_IMAGE_THRESHOLD = 0.08;
|
|
@@ -13551,69 +13902,69 @@ async function contentFinder(input) {
|
|
|
13551
13902
|
}
|
|
13552
13903
|
return result;
|
|
13553
13904
|
}
|
|
13554
|
-
var IncludeSchema =
|
|
13555
|
-
products:
|
|
13556
|
-
collections:
|
|
13557
|
-
articles:
|
|
13558
|
-
pages:
|
|
13905
|
+
var IncludeSchema = import_zod45.z.object({
|
|
13906
|
+
products: import_zod45.z.boolean(),
|
|
13907
|
+
collections: import_zod45.z.boolean(),
|
|
13908
|
+
articles: import_zod45.z.boolean(),
|
|
13909
|
+
pages: import_zod45.z.boolean()
|
|
13559
13910
|
}).describe(
|
|
13560
13911
|
"Toggles for which Shopify content types to search. Default truthy for products/collections/articles, false for pages. Set false to skip a category for performance."
|
|
13561
13912
|
);
|
|
13562
|
-
var LimitSchema =
|
|
13563
|
-
products:
|
|
13564
|
-
collections:
|
|
13565
|
-
articles:
|
|
13566
|
-
pages:
|
|
13913
|
+
var LimitSchema = import_zod45.z.object({
|
|
13914
|
+
products: import_zod45.z.number().int().min(0).max(20),
|
|
13915
|
+
collections: import_zod45.z.number().int().min(0).max(10),
|
|
13916
|
+
articles: import_zod45.z.number().int().min(0).max(20),
|
|
13917
|
+
pages: import_zod45.z.number().int().min(0).max(10)
|
|
13567
13918
|
}).describe(
|
|
13568
13919
|
"Per-category result count caps. Defaults: products 5, collections 3, articles 5, pages 2."
|
|
13569
13920
|
);
|
|
13570
|
-
var
|
|
13571
|
-
tenantId:
|
|
13572
|
-
brandId:
|
|
13573
|
-
contexto:
|
|
13921
|
+
var ParamsSchema15 = import_zod45.z.object({
|
|
13922
|
+
tenantId: import_zod45.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
13923
|
+
brandId: import_zod45.z.string().min(1).describe("Brand identifier within the tenant."),
|
|
13924
|
+
contexto: import_zod45.z.string().min(1).describe(
|
|
13574
13925
|
"Search context: a paragraph, keyword, or intent string. Embedded for vector search."
|
|
13575
13926
|
),
|
|
13576
|
-
fecha:
|
|
13927
|
+
fecha: import_zod45.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
|
|
13577
13928
|
"Content date in YYYY-MM-DD. Used to detect active season and prioritize matching collections."
|
|
13578
13929
|
),
|
|
13579
13930
|
include: IncludeSchema,
|
|
13580
13931
|
limit: LimitSchema,
|
|
13581
|
-
diversidad:
|
|
13932
|
+
diversidad: import_zod45.z.boolean().describe(
|
|
13582
13933
|
"Whether to apply Jaccard title diversification + handle dedupe to results. Default true."
|
|
13583
13934
|
),
|
|
13584
|
-
mode:
|
|
13935
|
+
mode: import_zod45.z.enum(["text", "hybrid"]).optional().describe(
|
|
13585
13936
|
"Search mode. 'text' (default): single query per collection against embeddingText, fast. 'hybrid': parallel text + image queries with Reciprocal Rank Fusion, 2x queries \u2014 useful when text mode returns few results or to surface visually-similar items with weak SEO."
|
|
13586
13937
|
)
|
|
13587
13938
|
});
|
|
13588
|
-
var VectorResultSchema =
|
|
13589
|
-
id:
|
|
13590
|
-
similarity:
|
|
13939
|
+
var VectorResultSchema = import_zod45.z.object({
|
|
13940
|
+
id: import_zod45.z.string(),
|
|
13941
|
+
similarity: import_zod45.z.number().optional()
|
|
13591
13942
|
}).passthrough();
|
|
13592
|
-
var TemporadaSchema2 =
|
|
13593
|
-
coleccion:
|
|
13594
|
-
titulo:
|
|
13595
|
-
razon:
|
|
13596
|
-
fechaInicio:
|
|
13597
|
-
fechaFin:
|
|
13943
|
+
var TemporadaSchema2 = import_zod45.z.object({
|
|
13944
|
+
coleccion: import_zod45.z.string().nullable(),
|
|
13945
|
+
titulo: import_zod45.z.string().nullable(),
|
|
13946
|
+
razon: import_zod45.z.string().nullable(),
|
|
13947
|
+
fechaInicio: import_zod45.z.string().nullable(),
|
|
13948
|
+
fechaFin: import_zod45.z.string().nullable()
|
|
13598
13949
|
});
|
|
13599
|
-
var SuggestedActionSchema2 =
|
|
13600
|
-
var DetectedConflictSchema2 =
|
|
13601
|
-
var
|
|
13602
|
-
productos:
|
|
13603
|
-
colecciones:
|
|
13604
|
-
articles:
|
|
13605
|
-
pages:
|
|
13606
|
-
_instrucciones:
|
|
13607
|
-
_negativePrompt:
|
|
13950
|
+
var SuggestedActionSchema2 = import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown());
|
|
13951
|
+
var DetectedConflictSchema2 = import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown());
|
|
13952
|
+
var OutputSchema15 = import_zod45.z.object({
|
|
13953
|
+
productos: import_zod45.z.array(VectorResultSchema),
|
|
13954
|
+
colecciones: import_zod45.z.array(VectorResultSchema),
|
|
13955
|
+
articles: import_zod45.z.array(VectorResultSchema),
|
|
13956
|
+
pages: import_zod45.z.array(VectorResultSchema),
|
|
13957
|
+
_instrucciones: import_zod45.z.string(),
|
|
13958
|
+
_negativePrompt: import_zod45.z.string(),
|
|
13608
13959
|
_temporadaActiva: TemporadaSchema2.nullable(),
|
|
13609
13960
|
_detectedConflict: DetectedConflictSchema2.optional(),
|
|
13610
|
-
_suggestedActions:
|
|
13961
|
+
_suggestedActions: import_zod45.z.array(SuggestedActionSchema2)
|
|
13611
13962
|
});
|
|
13612
|
-
var
|
|
13963
|
+
var rawContract15 = {
|
|
13613
13964
|
name: "find_content_for_topic",
|
|
13614
13965
|
description: "Find Shopify content (products, collections, articles, pages) semantically related to a context/keyword + date. Uses multimodal vector search (1408d Vertex). Mode 'text' (default) queries embeddingText (fast); 'hybrid' merges text + image results via Reciprocal Rank Fusion (rescues items with weak text SEO). Auto-injects JIT context: linking instructions, brand visual negatives, active season collection, and conflict detection (>85% similar article).",
|
|
13615
|
-
paramsSchema:
|
|
13616
|
-
outputSchema:
|
|
13966
|
+
paramsSchema: ParamsSchema15,
|
|
13967
|
+
outputSchema: OutputSchema15,
|
|
13617
13968
|
// Lectura pura (vector search). Failures internas en search degradan a
|
|
13618
13969
|
// arrays vacíos — el helper NO falla.
|
|
13619
13970
|
requiresConfirmation: false,
|
|
@@ -13636,7 +13987,7 @@ var rawContract13 = {
|
|
|
13636
13987
|
sideEffects: ["reads_firestore"]
|
|
13637
13988
|
};
|
|
13638
13989
|
var contentFinderContract = MartinContractSchema.parse(
|
|
13639
|
-
|
|
13990
|
+
rawContract15
|
|
13640
13991
|
);
|
|
13641
13992
|
var DEFAULT_PHOTO_THRESHOLD = 0.7;
|
|
13642
13993
|
var DEFAULT_SHOPIFY_THRESHOLD = 0.65;
|
|
@@ -13783,51 +14134,51 @@ async function slotAssetFinder(input) {
|
|
|
13783
14134
|
_fuente: fuente
|
|
13784
14135
|
};
|
|
13785
14136
|
}
|
|
13786
|
-
var
|
|
13787
|
-
tenantId:
|
|
13788
|
-
brandId:
|
|
13789
|
-
keyword:
|
|
14137
|
+
var ParamsSchema16 = import_zod46.z.object({
|
|
14138
|
+
tenantId: import_zod46.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
14139
|
+
brandId: import_zod46.z.string().min(1).describe("Brand identifier within the tenant."),
|
|
14140
|
+
keyword: import_zod46.z.string().min(1).describe(
|
|
13790
14141
|
"Slot keyword to search photos for. Used as embedding query for multimodal vector search."
|
|
13791
14142
|
),
|
|
13792
|
-
plataforma:
|
|
14143
|
+
plataforma: import_zod46.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe(
|
|
13793
14144
|
"Target platform \u2014 determines the variant format to resolve (gbp_4_3, blog_3_2, ig_4_5, ig_1_1)."
|
|
13794
14145
|
),
|
|
13795
|
-
fecha:
|
|
14146
|
+
fecha: import_zod46.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
|
|
13796
14147
|
"Slot date in YYYY-MM-DD. Used to detect active season and apply seasonal catalog overrides."
|
|
13797
14148
|
),
|
|
13798
|
-
limit:
|
|
14149
|
+
limit: import_zod46.z.number().int().min(1).max(10).optional().describe("Max number of photos to return. Default 5. Max 10.")
|
|
13799
14150
|
});
|
|
13800
|
-
var TemporadaSchema22 =
|
|
13801
|
-
coleccion:
|
|
13802
|
-
titulo:
|
|
13803
|
-
razon:
|
|
13804
|
-
fechaInicio:
|
|
13805
|
-
fechaFin:
|
|
14151
|
+
var TemporadaSchema22 = import_zod46.z.object({
|
|
14152
|
+
coleccion: import_zod46.z.string().nullable(),
|
|
14153
|
+
titulo: import_zod46.z.string().nullable(),
|
|
14154
|
+
razon: import_zod46.z.string().nullable(),
|
|
14155
|
+
fechaInicio: import_zod46.z.string().nullable(),
|
|
14156
|
+
fechaFin: import_zod46.z.string().nullable()
|
|
13806
14157
|
});
|
|
13807
|
-
var
|
|
14158
|
+
var OutputSchema16 = import_zod46.z.discriminatedUnion("ok", [
|
|
13808
14159
|
// Note: success case does not include `ok: true` literal in helper return —
|
|
13809
14160
|
// helper returns the success shape directly. Adapter to discriminated union
|
|
13810
14161
|
// happens at wrapper level if needed; here we accept both shapes.
|
|
13811
|
-
|
|
13812
|
-
ok:
|
|
13813
|
-
fotos:
|
|
13814
|
-
_instrucciones:
|
|
13815
|
-
_negativePrompt:
|
|
14162
|
+
import_zod46.z.object({
|
|
14163
|
+
ok: import_zod46.z.literal(true),
|
|
14164
|
+
fotos: import_zod46.z.array(import_zod46.z.record(import_zod46.z.string(), import_zod46.z.unknown())),
|
|
14165
|
+
_instrucciones: import_zod46.z.string(),
|
|
14166
|
+
_negativePrompt: import_zod46.z.string(),
|
|
13816
14167
|
_temporadaActiva: TemporadaSchema22.nullable(),
|
|
13817
|
-
_bloqueoProducto:
|
|
13818
|
-
_fuente:
|
|
14168
|
+
_bloqueoProducto: import_zod46.z.boolean(),
|
|
14169
|
+
_fuente: import_zod46.z.enum(["tenant", "shopify_product"])
|
|
13819
14170
|
}),
|
|
13820
|
-
|
|
13821
|
-
ok:
|
|
13822
|
-
error:
|
|
13823
|
-
code:
|
|
14171
|
+
import_zod46.z.object({
|
|
14172
|
+
ok: import_zod46.z.literal(false),
|
|
14173
|
+
error: import_zod46.z.string(),
|
|
14174
|
+
code: import_zod46.z.enum(["BRAND_NOT_FOUND"]).optional()
|
|
13824
14175
|
})
|
|
13825
14176
|
]);
|
|
13826
|
-
var
|
|
14177
|
+
var rawContract16 = {
|
|
13827
14178
|
name: "get_photos_for_slot",
|
|
13828
14179
|
description: "Find tenant photos for a calendar slot (keyword + platform + date) via multimodal vector search (1408d Vertex). Resolves the platform-specific variant (gbp_4_3/blog_3_2/ig_4_5/ig_1_1). Auto-injects JIT context: how to choose, brand visual negatives, active season overrides, and source layer (tenant photos vs Shopify product fallback). If <3 photos returned, consider calling request_photo_shoot to alert the tenant.",
|
|
13829
|
-
paramsSchema:
|
|
13830
|
-
outputSchema:
|
|
14180
|
+
paramsSchema: ParamsSchema16,
|
|
14181
|
+
outputSchema: OutputSchema16,
|
|
13831
14182
|
requiresConfirmation: false,
|
|
13832
14183
|
destructive: false,
|
|
13833
14184
|
affectsPublication: false,
|
|
@@ -13854,7 +14205,7 @@ var rawContract14 = {
|
|
|
13854
14205
|
sideEffects: ["reads_firestore"]
|
|
13855
14206
|
};
|
|
13856
14207
|
var slotAssetFinderContract = MartinContractSchema.parse(
|
|
13857
|
-
|
|
14208
|
+
rawContract16
|
|
13858
14209
|
);
|
|
13859
14210
|
var DEFAULT_SIMILARITY_THRESHOLD = 0.6;
|
|
13860
14211
|
function cosineSimilarity(a, b) {
|
|
@@ -13931,44 +14282,44 @@ async function canvaTemplateSelector(input) {
|
|
|
13931
14282
|
_instrucciones: "Plantilla Canva seleccionada. Llama a marketingDesignWithCanva({contenidoId, plantillaId, fotoVariantePath, textos}) para renderizar la pieza final."
|
|
13932
14283
|
};
|
|
13933
14284
|
}
|
|
13934
|
-
var
|
|
13935
|
-
tenantId:
|
|
13936
|
-
brandId:
|
|
13937
|
-
plataforma:
|
|
14285
|
+
var ParamsSchema17 = import_zod47.z.object({
|
|
14286
|
+
tenantId: import_zod47.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
14287
|
+
brandId: import_zod47.z.string().min(1).describe("Brand identifier within the tenant."),
|
|
14288
|
+
plataforma: import_zod47.z.string().min(1).describe(
|
|
13938
14289
|
"Target platform (e.g. 'gbp', 'shopify_blog', 'instagram'). Filters templates that declare this plataforma."
|
|
13939
14290
|
),
|
|
13940
|
-
tipoContenido:
|
|
14291
|
+
tipoContenido: import_zod47.z.string().min(1).describe(
|
|
13941
14292
|
"Content type (e.g. 'post', 'carousel', 'story', 'blog'). Filters templates that declare this tipoContenido."
|
|
13942
14293
|
),
|
|
13943
|
-
keyword:
|
|
14294
|
+
keyword: import_zod47.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
|
|
13944
14295
|
});
|
|
13945
|
-
var
|
|
13946
|
-
|
|
13947
|
-
plantillaId:
|
|
13948
|
-
titulo:
|
|
13949
|
-
thumbnailUrl:
|
|
13950
|
-
similarity:
|
|
13951
|
-
_instrucciones:
|
|
14296
|
+
var OutputSchema17 = import_zod47.z.union([
|
|
14297
|
+
import_zod47.z.object({
|
|
14298
|
+
plantillaId: import_zod47.z.string(),
|
|
14299
|
+
titulo: import_zod47.z.string().nullable(),
|
|
14300
|
+
thumbnailUrl: import_zod47.z.string().nullable(),
|
|
14301
|
+
similarity: import_zod47.z.number(),
|
|
14302
|
+
_instrucciones: import_zod47.z.string()
|
|
13952
14303
|
}),
|
|
13953
|
-
|
|
13954
|
-
plantillaId:
|
|
13955
|
-
motivo:
|
|
14304
|
+
import_zod47.z.object({
|
|
14305
|
+
plantillaId: import_zod47.z.null(),
|
|
14306
|
+
motivo: import_zod47.z.enum([
|
|
13956
14307
|
"brand_no_encontrada",
|
|
13957
14308
|
"no_canva",
|
|
13958
14309
|
"no_match_plataforma",
|
|
13959
14310
|
"modo_cliente_no_soportado",
|
|
13960
14311
|
"similarity_baja"
|
|
13961
14312
|
]),
|
|
13962
|
-
similarity:
|
|
13963
|
-
_instrucciones:
|
|
13964
|
-
_todo:
|
|
14313
|
+
similarity: import_zod47.z.number().optional(),
|
|
14314
|
+
_instrucciones: import_zod47.z.string().optional(),
|
|
14315
|
+
_todo: import_zod47.z.string().optional()
|
|
13965
14316
|
})
|
|
13966
14317
|
]);
|
|
13967
|
-
var
|
|
14318
|
+
var rawContract17 = {
|
|
13968
14319
|
name: "select_canva_template",
|
|
13969
14320
|
description: "Select the best Canva template from the tenant for a slot. Filters templates by plataforma+tipoContenido, then cosine-similarity matches embeddings vs the keyword query. Returns plantillaId on match (similarity >= 0.60). On miss returns plantillaId=null with motivo enum (no_canva, no_match_plataforma, modo_cliente_no_soportado, similarity_baja, brand_no_encontrada). The LLM should degrade gracefully (generate without Canva template) on any miss.",
|
|
13970
|
-
paramsSchema:
|
|
13971
|
-
outputSchema:
|
|
14321
|
+
paramsSchema: ParamsSchema17,
|
|
14322
|
+
outputSchema: OutputSchema17,
|
|
13972
14323
|
// Lectura pura. NO falla — todo "no encontré" es resultado válido del helper.
|
|
13973
14324
|
requiresConfirmation: false,
|
|
13974
14325
|
destructive: false,
|
|
@@ -13995,7 +14346,7 @@ var rawContract15 = {
|
|
|
13995
14346
|
sideEffects: ["reads_firestore"]
|
|
13996
14347
|
};
|
|
13997
14348
|
var canvaTemplateSelectorContract = MartinContractSchema.parse(
|
|
13998
|
-
|
|
14349
|
+
rawContract17
|
|
13999
14350
|
);
|
|
14000
14351
|
function buildDirectorPlanInstrucciones(catalogoVisual) {
|
|
14001
14352
|
const etiquetas = catalogoVisual.etiquetas || {};
|
|
@@ -14219,38 +14570,38 @@ async function photoDirectorExecute(input) {
|
|
|
14219
14570
|
_instrucciones: buildDirectorExecuteSuccessInstrucciones(result.balanceAfter)
|
|
14220
14571
|
};
|
|
14221
14572
|
}
|
|
14222
|
-
var PlanParamsSchema =
|
|
14223
|
-
tenantId:
|
|
14224
|
-
fotoId:
|
|
14573
|
+
var PlanParamsSchema = import_zod48.z.object({
|
|
14574
|
+
tenantId: import_zod48.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
14575
|
+
fotoId: import_zod48.z.string().min(1).describe("Photo ID in marketing_fotos. The photo must have an archivoOriginal uploaded.")
|
|
14225
14576
|
});
|
|
14226
|
-
var PlanContextSchema =
|
|
14227
|
-
fotoId:
|
|
14228
|
-
archivoOriginal:
|
|
14229
|
-
estrategia:
|
|
14230
|
-
brandBrief:
|
|
14231
|
-
segmento:
|
|
14232
|
-
personalidad:
|
|
14577
|
+
var PlanContextSchema = import_zod48.z.object({
|
|
14578
|
+
fotoId: import_zod48.z.string(),
|
|
14579
|
+
archivoOriginal: import_zod48.z.string(),
|
|
14580
|
+
estrategia: import_zod48.z.string(),
|
|
14581
|
+
brandBrief: import_zod48.z.object({
|
|
14582
|
+
segmento: import_zod48.z.string().nullable(),
|
|
14583
|
+
personalidad: import_zod48.z.unknown().nullable()
|
|
14233
14584
|
}),
|
|
14234
|
-
visualRules:
|
|
14235
|
-
catalogoVisual:
|
|
14236
|
-
estiloVisual:
|
|
14237
|
-
notaTenant:
|
|
14238
|
-
productoLinkeadoManual:
|
|
14239
|
-
_instrucciones:
|
|
14585
|
+
visualRules: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()),
|
|
14586
|
+
catalogoVisual: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()),
|
|
14587
|
+
estiloVisual: import_zod48.z.unknown().nullable(),
|
|
14588
|
+
notaTenant: import_zod48.z.unknown().nullable(),
|
|
14589
|
+
productoLinkeadoManual: import_zod48.z.unknown().nullable(),
|
|
14590
|
+
_instrucciones: import_zod48.z.string()
|
|
14240
14591
|
});
|
|
14241
|
-
var PlanOutputSchema =
|
|
14242
|
-
|
|
14243
|
-
ok:
|
|
14244
|
-
imageBase64:
|
|
14592
|
+
var PlanOutputSchema = import_zod48.z.discriminatedUnion("ok", [
|
|
14593
|
+
import_zod48.z.object({
|
|
14594
|
+
ok: import_zod48.z.literal(true),
|
|
14595
|
+
imageBase64: import_zod48.z.string().describe("Compressed photo as base64 for transport to the LLM."),
|
|
14245
14596
|
context: PlanContextSchema
|
|
14246
14597
|
}),
|
|
14247
|
-
|
|
14248
|
-
ok:
|
|
14249
|
-
code:
|
|
14250
|
-
error:
|
|
14251
|
-
_instrucciones:
|
|
14252
|
-
fix:
|
|
14253
|
-
fotoId:
|
|
14598
|
+
import_zod48.z.object({
|
|
14599
|
+
ok: import_zod48.z.literal(false),
|
|
14600
|
+
code: import_zod48.z.string(),
|
|
14601
|
+
error: import_zod48.z.string(),
|
|
14602
|
+
_instrucciones: import_zod48.z.string().optional(),
|
|
14603
|
+
fix: import_zod48.z.string().optional(),
|
|
14604
|
+
fotoId: import_zod48.z.string().optional()
|
|
14254
14605
|
})
|
|
14255
14606
|
]);
|
|
14256
14607
|
var planRawContract = {
|
|
@@ -14280,43 +14631,43 @@ var planRawContract = {
|
|
|
14280
14631
|
var photoDirectorPlanContract = MartinContractSchema.parse(
|
|
14281
14632
|
planRawContract
|
|
14282
14633
|
);
|
|
14283
|
-
var ExecuteParamsSchema =
|
|
14634
|
+
var ExecuteParamsSchema = import_zod48.z.object({
|
|
14284
14635
|
// tenantId is injected by the wrapper from ctx (A7 module-scope pattern,
|
|
14285
14636
|
// even though the helper function itself derives tenantId via the foto's
|
|
14286
14637
|
// brandId). Needed here for extractTargetPath canonical path.
|
|
14287
|
-
tenantId:
|
|
14288
|
-
fotoId:
|
|
14289
|
-
prompt:
|
|
14638
|
+
tenantId: import_zod48.z.string().min(1).describe("Tenant identifier (the business account)."),
|
|
14639
|
+
fotoId: import_zod48.z.string().min(1).describe("Photo ID to edit (must have been planned via plan_photo_edit)."),
|
|
14640
|
+
prompt: import_zod48.z.string().nullable().describe(
|
|
14290
14641
|
"English prompt for Gemini Image Edit when acciones includes edit_background. Pass null when strategy is 'tal_cual' (no editing \u2014 just regenerate thumbnail + embedding with new tags)."
|
|
14291
14642
|
),
|
|
14292
|
-
acciones:
|
|
14643
|
+
acciones: import_zod48.z.array(import_zod48.z.enum(["edit_background", "none"])).describe(
|
|
14293
14644
|
"Edit operations to perform. Use ['edit_background'] for AI background edit, ['none'] when strategy is 'tal_cual'."
|
|
14294
14645
|
),
|
|
14295
|
-
descripcion:
|
|
14296
|
-
tipo:
|
|
14297
|
-
tagsPrimarios:
|
|
14298
|
-
tagsSecundarios:
|
|
14299
|
-
tagsContexto:
|
|
14646
|
+
descripcion: import_zod48.z.string().describe("Semantic description in Spanish (saved as descripcionVision on the photo)."),
|
|
14647
|
+
tipo: import_zod48.z.string().nullable().describe("Photo type from the brand catalogoVisual. null only if catalog has no matching type."),
|
|
14648
|
+
tagsPrimarios: import_zod48.z.array(import_zod48.z.string()).describe("Primary tags from the catalogoVisual."),
|
|
14649
|
+
tagsSecundarios: import_zod48.z.array(import_zod48.z.string()).describe("Secondary tags from the catalogoVisual."),
|
|
14650
|
+
tagsContexto: import_zod48.z.array(import_zod48.z.string()).describe("Contextual tags (occasion, mood, style).")
|
|
14300
14651
|
});
|
|
14301
|
-
var ExecuteOutputSchema =
|
|
14302
|
-
|
|
14303
|
-
ok:
|
|
14304
|
-
fotoId:
|
|
14305
|
-
archivoEditado:
|
|
14306
|
-
thumbnailUrl:
|
|
14307
|
-
iteracion:
|
|
14308
|
-
editCosto:
|
|
14309
|
-
creditsConsumed:
|
|
14310
|
-
balanceAfter:
|
|
14311
|
-
imageBase64:
|
|
14312
|
-
_instrucciones:
|
|
14652
|
+
var ExecuteOutputSchema = import_zod48.z.discriminatedUnion("ok", [
|
|
14653
|
+
import_zod48.z.object({
|
|
14654
|
+
ok: import_zod48.z.literal(true),
|
|
14655
|
+
fotoId: import_zod48.z.string(),
|
|
14656
|
+
archivoEditado: import_zod48.z.string().optional(),
|
|
14657
|
+
thumbnailUrl: import_zod48.z.string().optional(),
|
|
14658
|
+
iteracion: import_zod48.z.number().optional(),
|
|
14659
|
+
editCosto: import_zod48.z.number().optional(),
|
|
14660
|
+
creditsConsumed: import_zod48.z.number().optional(),
|
|
14661
|
+
balanceAfter: import_zod48.z.number().optional(),
|
|
14662
|
+
imageBase64: import_zod48.z.string().nullable(),
|
|
14663
|
+
_instrucciones: import_zod48.z.string()
|
|
14313
14664
|
}),
|
|
14314
|
-
|
|
14315
|
-
ok:
|
|
14316
|
-
error:
|
|
14317
|
-
code:
|
|
14318
|
-
details:
|
|
14319
|
-
_instrucciones:
|
|
14665
|
+
import_zod48.z.object({
|
|
14666
|
+
ok: import_zod48.z.literal(false),
|
|
14667
|
+
error: import_zod48.z.string(),
|
|
14668
|
+
code: import_zod48.z.string(),
|
|
14669
|
+
details: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()).optional(),
|
|
14670
|
+
_instrucciones: import_zod48.z.string()
|
|
14320
14671
|
})
|
|
14321
14672
|
]);
|
|
14322
14673
|
var executeRawContract = {
|
|
@@ -15024,6 +15375,12 @@ function callGetCalendar(input) {
|
|
|
15024
15375
|
function callAddCalendarSlot(input) {
|
|
15025
15376
|
return callCF("marketingAddCalendarSlotCallable", input);
|
|
15026
15377
|
}
|
|
15378
|
+
function callSeoSnapshotReader(input) {
|
|
15379
|
+
return callCF("marketingSeoSnapshotReaderCallable", input);
|
|
15380
|
+
}
|
|
15381
|
+
function callCollectionsReader(input) {
|
|
15382
|
+
return callCF("marketingCollectionsReaderCallable", input);
|
|
15383
|
+
}
|
|
15027
15384
|
function callBrandBriefWriter(input) {
|
|
15028
15385
|
return callCF("marketingBrandBriefWriterCallable", input);
|
|
15029
15386
|
}
|
|
@@ -15092,7 +15449,7 @@ function callListarRutinas(input) {
|
|
|
15092
15449
|
}
|
|
15093
15450
|
|
|
15094
15451
|
// src/tools/marketing/photos.ts
|
|
15095
|
-
var
|
|
15452
|
+
var import_zod49 = require("zod");
|
|
15096
15453
|
|
|
15097
15454
|
// src/services/marketingEmbeddings.ts
|
|
15098
15455
|
var import_google_auth_library = require("google-auth-library");
|
|
@@ -15388,11 +15745,11 @@ REGLAS:
|
|
|
15388
15745
|
|
|
15389
15746
|
USAR: antes de generar contenido de cualquier slot del calendario.`,
|
|
15390
15747
|
{
|
|
15391
|
-
brandId:
|
|
15392
|
-
keyword:
|
|
15393
|
-
plataforma:
|
|
15394
|
-
fecha:
|
|
15395
|
-
limit:
|
|
15748
|
+
brandId: import_zod49.z.string().optional().describe("ID de la brand"),
|
|
15749
|
+
keyword: import_zod49.z.string().describe("Keyword del slot"),
|
|
15750
|
+
plataforma: import_zod49.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
|
|
15751
|
+
fecha: import_zod49.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
|
|
15752
|
+
limit: import_zod49.z.number().int().min(1).max(10).default(5)
|
|
15396
15753
|
},
|
|
15397
15754
|
async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
|
|
15398
15755
|
const tenantId = session.requireTenant();
|
|
@@ -15440,7 +15797,7 @@ DESPUES de ver la foto, decide:
|
|
|
15440
15797
|
|
|
15441
15798
|
Luego llama execute_photo_edit con tu analisis y prompt.`,
|
|
15442
15799
|
{
|
|
15443
|
-
fotoId:
|
|
15800
|
+
fotoId: import_zod49.z.string().describe("ID de la foto")
|
|
15444
15801
|
},
|
|
15445
15802
|
async ({ fotoId }) => {
|
|
15446
15803
|
const tenantId = session.requireTenant();
|
|
@@ -15488,14 +15845,14 @@ Retorna la foto editada para que la revises. Si no te gusta:
|
|
|
15488
15845
|
|
|
15489
15846
|
Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
|
|
15490
15847
|
{
|
|
15491
|
-
fotoId:
|
|
15492
|
-
prompt:
|
|
15493
|
-
acciones:
|
|
15494
|
-
descripcion:
|
|
15495
|
-
tipo:
|
|
15496
|
-
tagsPrimarios:
|
|
15497
|
-
tagsSecundarios:
|
|
15498
|
-
tagsContexto:
|
|
15848
|
+
fotoId: import_zod49.z.string(),
|
|
15849
|
+
prompt: import_zod49.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
|
|
15850
|
+
acciones: import_zod49.z.array(import_zod49.z.enum(["edit_background", "none"])),
|
|
15851
|
+
descripcion: import_zod49.z.string().describe("Descripcion semantica en espanol"),
|
|
15852
|
+
tipo: import_zod49.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
|
|
15853
|
+
tagsPrimarios: import_zod49.z.array(import_zod49.z.string()),
|
|
15854
|
+
tagsSecundarios: import_zod49.z.array(import_zod49.z.string()),
|
|
15855
|
+
tagsContexto: import_zod49.z.array(import_zod49.z.string())
|
|
15499
15856
|
},
|
|
15500
15857
|
async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
|
|
15501
15858
|
const tenantId = session.requireTenant();
|
|
@@ -15571,7 +15928,7 @@ Retorna:
|
|
|
15571
15928
|
- creditos: { balance, planId, periodEnd, costoPhotoEdit }
|
|
15572
15929
|
- _instrucciones: que hacer segun el estado`,
|
|
15573
15930
|
{
|
|
15574
|
-
fotoId:
|
|
15931
|
+
fotoId: import_zod49.z.string().describe("ID de la foto")
|
|
15575
15932
|
},
|
|
15576
15933
|
async ({ fotoId }) => {
|
|
15577
15934
|
const tenantId = session.requireTenant();
|
|
@@ -15671,11 +16028,11 @@ Retorna:
|
|
|
15671
16028
|
"find_products_for_content",
|
|
15672
16029
|
`[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.`,
|
|
15673
16030
|
{
|
|
15674
|
-
brandId:
|
|
15675
|
-
contexto:
|
|
15676
|
-
fecha:
|
|
15677
|
-
limit:
|
|
15678
|
-
diversidad:
|
|
16031
|
+
brandId: import_zod49.z.string().optional().describe("ID de la brand"),
|
|
16032
|
+
contexto: import_zod49.z.string().describe("Parrafo, keyword o intencion del contenido"),
|
|
16033
|
+
fecha: import_zod49.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
16034
|
+
limit: import_zod49.z.number().int().min(1).max(10).default(5),
|
|
16035
|
+
diversidad: import_zod49.z.boolean().default(true)
|
|
15679
16036
|
},
|
|
15680
16037
|
async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
|
|
15681
16038
|
console.warn(
|
|
@@ -15752,10 +16109,10 @@ RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no
|
|
|
15752
16109
|
|
|
15753
16110
|
USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
|
|
15754
16111
|
{
|
|
15755
|
-
brandId:
|
|
15756
|
-
plataforma:
|
|
15757
|
-
tipoContenido:
|
|
15758
|
-
keyword:
|
|
16112
|
+
brandId: import_zod49.z.string().optional().describe("ID de la brand"),
|
|
16113
|
+
plataforma: import_zod49.z.string().describe("gbp | instagram | shopify_blog"),
|
|
16114
|
+
tipoContenido: import_zod49.z.string().describe("post | carousel | story | blog"),
|
|
16115
|
+
keyword: import_zod49.z.string().describe("Keyword del slot")
|
|
15759
16116
|
},
|
|
15760
16117
|
async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
|
|
15761
16118
|
const tenantId = session.requireTenant();
|
|
@@ -15800,15 +16157,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
|
|
|
15800
16157
|
|
|
15801
16158
|
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.`,
|
|
15802
16159
|
{
|
|
15803
|
-
brandId:
|
|
15804
|
-
semana:
|
|
15805
|
-
necesidades:
|
|
15806
|
-
|
|
15807
|
-
tema:
|
|
15808
|
-
keyword:
|
|
15809
|
-
cantidadSugerida:
|
|
15810
|
-
razon:
|
|
15811
|
-
slotsAfectados:
|
|
16160
|
+
brandId: import_zod49.z.string().optional().describe("ID de la brand"),
|
|
16161
|
+
semana: import_zod49.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD").describe("Semana en ISO del lunes"),
|
|
16162
|
+
necesidades: import_zod49.z.array(
|
|
16163
|
+
import_zod49.z.object({
|
|
16164
|
+
tema: import_zod49.z.string(),
|
|
16165
|
+
keyword: import_zod49.z.string(),
|
|
16166
|
+
cantidadSugerida: import_zod49.z.number().int().positive(),
|
|
16167
|
+
razon: import_zod49.z.string(),
|
|
16168
|
+
slotsAfectados: import_zod49.z.array(import_zod49.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
|
|
15812
16169
|
})
|
|
15813
16170
|
).min(1)
|
|
15814
16171
|
},
|
|
@@ -15871,7 +16228,7 @@ IMPORTANTE \u2014 campo 'razon': Este texto lo ve el tenant en la app. Escribe e
|
|
|
15871
16228
|
}
|
|
15872
16229
|
|
|
15873
16230
|
// src/tools/marketing/content.ts
|
|
15874
|
-
var
|
|
16231
|
+
var import_zod50 = require("zod");
|
|
15875
16232
|
var _logOverride = null;
|
|
15876
16233
|
async function logToMcpLogs(entry) {
|
|
15877
16234
|
if (_logOverride) return _logOverride(entry);
|
|
@@ -15884,22 +16241,22 @@ async function logToMcpLogs(entry) {
|
|
|
15884
16241
|
} catch {
|
|
15885
16242
|
}
|
|
15886
16243
|
}
|
|
15887
|
-
var IncludeSchema2 =
|
|
15888
|
-
products:
|
|
15889
|
-
collections:
|
|
15890
|
-
articles:
|
|
15891
|
-
pages:
|
|
16244
|
+
var IncludeSchema2 = import_zod50.z.object({
|
|
16245
|
+
products: import_zod50.z.boolean().default(true),
|
|
16246
|
+
collections: import_zod50.z.boolean().default(true),
|
|
16247
|
+
articles: import_zod50.z.boolean().default(true),
|
|
16248
|
+
pages: import_zod50.z.boolean().default(false)
|
|
15892
16249
|
}).default({
|
|
15893
16250
|
products: true,
|
|
15894
16251
|
collections: true,
|
|
15895
16252
|
articles: true,
|
|
15896
16253
|
pages: false
|
|
15897
16254
|
});
|
|
15898
|
-
var LimitSchema2 =
|
|
15899
|
-
products:
|
|
15900
|
-
collections:
|
|
15901
|
-
articles:
|
|
15902
|
-
pages:
|
|
16255
|
+
var LimitSchema2 = import_zod50.z.object({
|
|
16256
|
+
products: import_zod50.z.number().int().min(0).max(20).default(5),
|
|
16257
|
+
collections: import_zod50.z.number().int().min(0).max(10).default(3),
|
|
16258
|
+
articles: import_zod50.z.number().int().min(0).max(20).default(5),
|
|
16259
|
+
pages: import_zod50.z.number().int().min(0).max(10).default(2)
|
|
15903
16260
|
}).default({ products: 5, collections: 3, articles: 5, pages: 2 });
|
|
15904
16261
|
function registerContentTools(server, session) {
|
|
15905
16262
|
server.tool(
|
|
@@ -15927,13 +16284,13 @@ MODOS (parametro mode):
|
|
|
15927
16284
|
|
|
15928
16285
|
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.`,
|
|
15929
16286
|
{
|
|
15930
|
-
brandId:
|
|
15931
|
-
contexto:
|
|
15932
|
-
fecha:
|
|
16287
|
+
brandId: import_zod50.z.string().optional().describe("ID de la brand"),
|
|
16288
|
+
contexto: import_zod50.z.string().min(1).describe("Parrafo, keyword o intencion"),
|
|
16289
|
+
fecha: import_zod50.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
|
|
15933
16290
|
include: IncludeSchema2.optional(),
|
|
15934
16291
|
limit: LimitSchema2.optional(),
|
|
15935
|
-
diversidad:
|
|
15936
|
-
mode:
|
|
16292
|
+
diversidad: import_zod50.z.boolean().default(true),
|
|
16293
|
+
mode: import_zod50.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)")
|
|
15937
16294
|
},
|
|
15938
16295
|
async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode }) => {
|
|
15939
16296
|
const tenantId = session.requireTenant();
|
|
@@ -15991,17 +16348,20 @@ Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = e
|
|
|
15991
16348
|
return { content: [{ type: "text", text: JSON.stringify(errPayload) }] };
|
|
15992
16349
|
}
|
|
15993
16350
|
const result = dispatchResult.structuredOutput;
|
|
15994
|
-
const slimResult = (item) =>
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16351
|
+
const slimResult = (item) => {
|
|
16352
|
+
const featuredImage = item.featuredImage;
|
|
16353
|
+
return {
|
|
16354
|
+
id: item.id,
|
|
16355
|
+
similarity: item.similarity,
|
|
16356
|
+
title: item.title ?? item.nombre ?? null,
|
|
16357
|
+
handle: item.handle ?? null,
|
|
16358
|
+
description: typeof item.description === "string" ? item.description.slice(0, 200) : null,
|
|
16359
|
+
image: featuredImage ? { src: featuredImage.url ?? null, alt: featuredImage.altText ?? null } : null,
|
|
16360
|
+
collectionHandles: item.collections ?? null,
|
|
16361
|
+
modesFound: item.modesFound ?? null,
|
|
16362
|
+
rrfScore: item.rrfScore ?? null
|
|
16363
|
+
};
|
|
16364
|
+
};
|
|
16005
16365
|
const slimmed = {
|
|
16006
16366
|
productos: result.productos.map(slimResult),
|
|
16007
16367
|
colecciones: result.colecciones.map(slimResult),
|
|
@@ -16141,8 +16501,8 @@ function registerMarketingTools(server, session) {
|
|
|
16141
16501
|
"get_calendar",
|
|
16142
16502
|
"Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
|
|
16143
16503
|
{
|
|
16144
|
-
brandId:
|
|
16145
|
-
mes:
|
|
16504
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand"),
|
|
16505
|
+
mes: import_zod51.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
|
|
16146
16506
|
},
|
|
16147
16507
|
async ({ brandId: inputBrandId, mes }) => {
|
|
16148
16508
|
const tenantId = session.requireTenant();
|
|
@@ -16178,29 +16538,46 @@ function registerMarketingTools(server, session) {
|
|
|
16178
16538
|
);
|
|
16179
16539
|
server.tool(
|
|
16180
16540
|
"get_seo_snapshot",
|
|
16181
|
-
"
|
|
16541
|
+
"Read the latest Semrush SEO snapshot for a brand: rank, top keywords, opportunities, competitors.",
|
|
16182
16542
|
{
|
|
16183
|
-
brandId:
|
|
16543
|
+
brandId: import_zod51.z.string().optional().describe("Brand identifier within the tenant")
|
|
16184
16544
|
},
|
|
16185
16545
|
async ({ brandId: inputBrandId }) => {
|
|
16186
16546
|
const tenantId = session.requireTenant();
|
|
16187
16547
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
16188
|
-
const
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
|
|
16192
|
-
|
|
16193
|
-
|
|
16548
|
+
const ctx = await buildContext(session, brandId);
|
|
16549
|
+
const result = await dispatchWithContract({
|
|
16550
|
+
contract: seoSnapshotReaderContract,
|
|
16551
|
+
helper: seoSnapshotReader,
|
|
16552
|
+
callable: callSeoSnapshotReader,
|
|
16553
|
+
input: { tenantId, brandId },
|
|
16554
|
+
ctx
|
|
16555
|
+
});
|
|
16556
|
+
let payload;
|
|
16557
|
+
if (result.state === "success" && result.structuredOutput) {
|
|
16558
|
+
const out = result.structuredOutput;
|
|
16559
|
+
if (out.ok) {
|
|
16560
|
+
payload = out.snapshot;
|
|
16561
|
+
} else {
|
|
16562
|
+
payload = { ok: false, code: out.code, mensaje: result.text };
|
|
16563
|
+
}
|
|
16564
|
+
} else {
|
|
16565
|
+
payload = {
|
|
16566
|
+
ok: false,
|
|
16567
|
+
state: result.state,
|
|
16568
|
+
mensaje: result.text,
|
|
16569
|
+
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
16570
|
+
};
|
|
16194
16571
|
}
|
|
16195
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
16572
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
16196
16573
|
}
|
|
16197
16574
|
);
|
|
16198
16575
|
server.tool(
|
|
16199
16576
|
"get_photo_gallery",
|
|
16200
16577
|
"Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
|
|
16201
16578
|
{
|
|
16202
|
-
brandId:
|
|
16203
|
-
estado:
|
|
16579
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand"),
|
|
16580
|
+
estado: import_zod51.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
|
|
16204
16581
|
},
|
|
16205
16582
|
async ({ brandId: inputBrandId, estado }) => {
|
|
16206
16583
|
session.requireTenant();
|
|
@@ -16240,7 +16617,7 @@ function registerMarketingTools(server, session) {
|
|
|
16240
16617
|
"generate_marketing_plan",
|
|
16241
16618
|
"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.",
|
|
16242
16619
|
{
|
|
16243
|
-
brandId:
|
|
16620
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand")
|
|
16244
16621
|
},
|
|
16245
16622
|
async ({ brandId: inputBrandId }) => {
|
|
16246
16623
|
const tenantId = session.requireTenant();
|
|
@@ -16254,8 +16631,8 @@ function registerMarketingTools(server, session) {
|
|
|
16254
16631
|
"save_marketing_plan",
|
|
16255
16632
|
"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).",
|
|
16256
16633
|
{
|
|
16257
|
-
brandId:
|
|
16258
|
-
plan:
|
|
16634
|
+
brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
|
|
16635
|
+
plan: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Full marketing plan object. Common fields: keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, coleccionesPriorizadas, blogStrategy.")
|
|
16259
16636
|
},
|
|
16260
16637
|
async ({ brandId: inputBrandId, plan }) => {
|
|
16261
16638
|
const tenantId = session.requireTenant();
|
|
@@ -16283,9 +16660,9 @@ function registerMarketingTools(server, session) {
|
|
|
16283
16660
|
"update_marketing_plan_field",
|
|
16284
16661
|
"Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
|
|
16285
16662
|
{
|
|
16286
|
-
brandId:
|
|
16287
|
-
field:
|
|
16288
|
-
value:
|
|
16663
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand"),
|
|
16664
|
+
field: import_zod51.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
|
|
16665
|
+
value: import_zod51.z.unknown().describe("Valor del campo")
|
|
16289
16666
|
},
|
|
16290
16667
|
async ({ brandId: inputBrandId, field, value }) => {
|
|
16291
16668
|
const tenantId = session.requireTenant();
|
|
@@ -16298,7 +16675,7 @@ function registerMarketingTools(server, session) {
|
|
|
16298
16675
|
"generate_brand_brief",
|
|
16299
16676
|
"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.",
|
|
16300
16677
|
{
|
|
16301
|
-
brandId:
|
|
16678
|
+
brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
|
|
16302
16679
|
},
|
|
16303
16680
|
async ({ brandId: inputBrandId }) => {
|
|
16304
16681
|
const tenantId = session.requireTenant();
|
|
@@ -16331,8 +16708,8 @@ function registerMarketingTools(server, session) {
|
|
|
16331
16708
|
"save_brand_brief",
|
|
16332
16709
|
"Save the Brand Brief at tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Partial merge \u2014 only the brandBrief field is overwritten.",
|
|
16333
16710
|
{
|
|
16334
|
-
brandId:
|
|
16335
|
-
brandBrief:
|
|
16711
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand"),
|
|
16712
|
+
brandBrief: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Full Brand Brief object.")
|
|
16336
16713
|
},
|
|
16337
16714
|
async ({ brandId: inputBrandId, brandBrief }) => {
|
|
16338
16715
|
const tenantId = session.requireTenant();
|
|
@@ -16358,9 +16735,9 @@ function registerMarketingTools(server, session) {
|
|
|
16358
16735
|
"generate_weekly_content",
|
|
16359
16736
|
"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.",
|
|
16360
16737
|
{
|
|
16361
|
-
brandId:
|
|
16362
|
-
semana:
|
|
16363
|
-
modo:
|
|
16738
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand"),
|
|
16739
|
+
semana: import_zod51.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
|
|
16740
|
+
modo: import_zod51.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
|
|
16364
16741
|
},
|
|
16365
16742
|
async ({ brandId: inputBrandId, semana, modo }) => {
|
|
16366
16743
|
const tenantId = session.requireTenant();
|
|
@@ -16388,14 +16765,14 @@ function registerMarketingTools(server, session) {
|
|
|
16388
16765
|
|
|
16389
16766
|
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.`,
|
|
16390
16767
|
{
|
|
16391
|
-
brandId:
|
|
16392
|
-
plataforma:
|
|
16393
|
-
tipo:
|
|
16394
|
-
keyword:
|
|
16395
|
-
languageCode:
|
|
16396
|
-
fotoId:
|
|
16397
|
-
datos:
|
|
16398
|
-
calendarioItemRef:
|
|
16768
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand"),
|
|
16769
|
+
plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
|
|
16770
|
+
tipo: import_zod51.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
|
|
16771
|
+
keyword: import_zod51.z.string().optional().describe("Keyword target"),
|
|
16772
|
+
languageCode: import_zod51.z.string().optional().describe("Idioma (es/en)"),
|
|
16773
|
+
fotoId: import_zod51.z.string().optional().describe("ID de la foto a asociar"),
|
|
16774
|
+
datos: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
|
|
16775
|
+
calendarioItemRef: import_zod51.z.string().optional().describe("Referencia al item del calendario")
|
|
16399
16776
|
},
|
|
16400
16777
|
async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
|
|
16401
16778
|
const tenantId = session.requireTenant();
|
|
@@ -16431,13 +16808,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
|
|
|
16431
16808
|
NO puede cambiar: tenantId, brandId, id (inmutables).
|
|
16432
16809
|
Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
|
|
16433
16810
|
{
|
|
16434
|
-
contenidoId:
|
|
16435
|
-
datos:
|
|
16436
|
-
fotoId:
|
|
16437
|
-
keyword:
|
|
16438
|
-
languageCode:
|
|
16439
|
-
estado:
|
|
16440
|
-
calendarioItemRef:
|
|
16811
|
+
contenidoId: import_zod51.z.string().describe("ID del doc en marketing_contenido"),
|
|
16812
|
+
datos: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
|
|
16813
|
+
fotoId: import_zod51.z.string().nullable().optional().describe("Actualizar foto asociada"),
|
|
16814
|
+
keyword: import_zod51.z.string().nullable().optional().describe("Actualizar keyword"),
|
|
16815
|
+
languageCode: import_zod51.z.string().optional().describe("Actualizar idioma"),
|
|
16816
|
+
estado: import_zod51.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
|
|
16817
|
+
calendarioItemRef: import_zod51.z.string().nullable().optional().describe("Vincular a un slot del calendario")
|
|
16441
16818
|
},
|
|
16442
16819
|
async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
|
|
16443
16820
|
const tenantId = session.requireTenant();
|
|
@@ -16471,19 +16848,19 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
16471
16848
|
"add_calendar_slot",
|
|
16472
16849
|
"Add a NEW slot to the editorial calendar. To modify an existing slot use update_calendar_slot instead.",
|
|
16473
16850
|
{
|
|
16474
|
-
brandId:
|
|
16475
|
-
mes:
|
|
16476
|
-
semana:
|
|
16477
|
-
slot:
|
|
16478
|
-
dia:
|
|
16479
|
-
plataforma:
|
|
16480
|
-
tipo:
|
|
16481
|
-
keyword:
|
|
16482
|
-
tema:
|
|
16483
|
-
productoId:
|
|
16484
|
-
estado:
|
|
16485
|
-
locationId:
|
|
16486
|
-
locationNombre:
|
|
16851
|
+
brandId: import_zod51.z.string().describe('Brand ID (e.g. "ponch", "teleglobos").'),
|
|
16852
|
+
mes: import_zod51.z.string().describe('Calendar month in YYYY-MM format (e.g. "2026-05").'),
|
|
16853
|
+
semana: import_zod51.z.number().describe("Week number within the month (1-5)."),
|
|
16854
|
+
slot: import_zod51.z.object({
|
|
16855
|
+
dia: import_zod51.z.string().describe("Slot date in YYYY-MM-DD format. Must fall within the week's fechaInicio/fechaFin range."),
|
|
16856
|
+
plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target publishing platform."),
|
|
16857
|
+
tipo: import_zod51.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Content type."),
|
|
16858
|
+
keyword: import_zod51.z.string().describe("Primary keyword for the content."),
|
|
16859
|
+
tema: import_zod51.z.string().optional().describe("Content topic/theme. OMIT this field if it does not apply. Do NOT send empty string or null."),
|
|
16860
|
+
productoId: import_zod51.z.string().optional().describe("Linked product ID. OMIT this field if no product is linked."),
|
|
16861
|
+
estado: import_zod51.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe('Initial status. OMIT to use default "planificado".'),
|
|
16862
|
+
locationId: import_zod51.z.string().optional().describe("GBP location ID \u2014 only when plataforma=gbp AND tenant is multi-location. OMIT otherwise."),
|
|
16863
|
+
locationNombre: import_zod51.z.string().optional().describe("Human-readable GBP location name. OMIT if locationId is not provided.")
|
|
16487
16864
|
}).describe("New slot data.")
|
|
16488
16865
|
},
|
|
16489
16866
|
async ({ brandId, mes, semana, slot }) => {
|
|
@@ -16509,27 +16886,27 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
16509
16886
|
"update_calendar_slot",
|
|
16510
16887
|
"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.",
|
|
16511
16888
|
{
|
|
16512
|
-
brandId:
|
|
16513
|
-
mes:
|
|
16514
|
-
semana:
|
|
16515
|
-
slotIndex:
|
|
16516
|
-
cambios:
|
|
16517
|
-
dia:
|
|
16518
|
-
plataforma:
|
|
16519
|
-
tipo:
|
|
16520
|
-
keyword:
|
|
16521
|
-
tema:
|
|
16522
|
-
productoId:
|
|
16523
|
-
estado:
|
|
16524
|
-
contenidoRef:
|
|
16525
|
-
fotoIdAsignada:
|
|
16526
|
-
notas:
|
|
16527
|
-
locationId:
|
|
16528
|
-
locationNombre:
|
|
16889
|
+
brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
|
|
16890
|
+
mes: import_zod51.z.string().describe("Calendar month in YYYY-MM format."),
|
|
16891
|
+
semana: import_zod51.z.number().describe("Week number within the month (1-5)."),
|
|
16892
|
+
slotIndex: import_zod51.z.number().describe("Slot index (0-based). Must point to an existing slot \u2014 to add new slots use add_calendar_slot."),
|
|
16893
|
+
cambios: import_zod51.z.object({
|
|
16894
|
+
dia: import_zod51.z.string().nullable().optional().describe("Slot date in YYYY-MM-DD. OMIT if not changing date. Use null to explicitly clear."),
|
|
16895
|
+
plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional().describe("OMIT if not changing platform."),
|
|
16896
|
+
tipo: import_zod51.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional().describe("OMIT if not changing content type."),
|
|
16897
|
+
keyword: import_zod51.z.string().nullable().optional().describe("OMIT if not changing keyword."),
|
|
16898
|
+
tema: import_zod51.z.string().nullable().optional().describe("OMIT if not changing topic."),
|
|
16899
|
+
productoId: import_zod51.z.string().nullable().optional().describe("OMIT if not changing linked product. Use null to unlink."),
|
|
16900
|
+
estado: import_zod51.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("OMIT if not changing status manually."),
|
|
16901
|
+
contenidoRef: import_zod51.z.string().nullable().optional().describe("OMIT \u2014 managed by the system, not by callers."),
|
|
16902
|
+
fotoIdAsignada: import_zod51.z.string().nullable().optional().describe("Use assign_photo_to_content for photos. OMIT here."),
|
|
16903
|
+
notas: import_zod51.z.array(NotaCalendarioSchema).optional().describe("Append-only notes for the slot."),
|
|
16904
|
+
locationId: import_zod51.z.string().nullable().optional().describe("GBP location ID \u2014 only when plataforma=gbp."),
|
|
16905
|
+
locationNombre: import_zod51.z.string().nullable().optional().describe("Human-readable GBP location name (for UI).")
|
|
16529
16906
|
}).strict().describe("Fields to update on the slot. Only declared fields accepted; unknown fields are rejected. OMIT any field you are not changing."),
|
|
16530
|
-
accionContenidoExistente:
|
|
16531
|
-
|
|
16532
|
-
|
|
16907
|
+
accionContenidoExistente: import_zod51.z.union([
|
|
16908
|
+
import_zod51.z.enum(["descartar", "nuevo_slot", "mantener"]),
|
|
16909
|
+
import_zod51.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
|
|
16533
16910
|
]).optional().describe(
|
|
16534
16911
|
'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).'
|
|
16535
16912
|
)
|
|
@@ -16574,9 +16951,9 @@ ESCRIBE EN DOS LUGARES:
|
|
|
16574
16951
|
|
|
16575
16952
|
NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
|
|
16576
16953
|
{
|
|
16577
|
-
contenidoRef:
|
|
16578
|
-
fotoId:
|
|
16579
|
-
calendarioItemRef:
|
|
16954
|
+
contenidoRef: import_zod51.z.string().describe("ID del doc en marketing_contenido"),
|
|
16955
|
+
fotoId: import_zod51.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
|
|
16956
|
+
calendarioItemRef: import_zod51.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
|
|
16580
16957
|
},
|
|
16581
16958
|
async ({ contenidoRef, fotoId, calendarioItemRef }) => {
|
|
16582
16959
|
const tenantId = session.requireTenant();
|
|
@@ -16602,7 +16979,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
16602
16979
|
"approve_content",
|
|
16603
16980
|
"Aprueba contenido para publicacion. Valida transicion de estado.",
|
|
16604
16981
|
{
|
|
16605
|
-
contenidoId:
|
|
16982
|
+
contenidoId: import_zod51.z.string().describe("ID del contenido a aprobar")
|
|
16606
16983
|
},
|
|
16607
16984
|
async ({ contenidoId }) => {
|
|
16608
16985
|
session.requireTenant();
|
|
@@ -16626,8 +17003,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
16626
17003
|
"reject_content",
|
|
16627
17004
|
"Rechaza contenido con motivo. Valida transicion de estado.",
|
|
16628
17005
|
{
|
|
16629
|
-
contenidoId:
|
|
16630
|
-
motivo:
|
|
17006
|
+
contenidoId: import_zod51.z.string().describe("ID del contenido a rechazar"),
|
|
17007
|
+
motivo: import_zod51.z.string().describe("Motivo del rechazo")
|
|
16631
17008
|
},
|
|
16632
17009
|
async ({ contenidoId, motivo }) => {
|
|
16633
17010
|
session.requireTenant();
|
|
@@ -16648,96 +17025,40 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
16648
17025
|
);
|
|
16649
17026
|
server.tool(
|
|
16650
17027
|
"get_collections",
|
|
16651
|
-
"
|
|
17028
|
+
"Read all canonical collections (Shopify/WordPress/web-only) for a brand with their 6 SEO fields, plus best practices and the strict schema for generating SEO suggestions.",
|
|
16652
17029
|
{
|
|
16653
|
-
brandId:
|
|
17030
|
+
brandId: import_zod51.z.string().optional().describe("Brand identifier within the tenant")
|
|
16654
17031
|
},
|
|
16655
17032
|
async ({ brandId: inputBrandId }) => {
|
|
16656
17033
|
const tenantId = session.requireTenant();
|
|
16657
17034
|
const brandId = inputBrandId ?? session.requireBrand();
|
|
16658
|
-
const
|
|
16659
|
-
const
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
17035
|
+
const ctx = await buildContext(session, brandId);
|
|
17036
|
+
const result = await dispatchWithContract({
|
|
17037
|
+
contract: collectionsReaderContract,
|
|
17038
|
+
helper: collectionsReader,
|
|
17039
|
+
callable: callCollectionsReader,
|
|
17040
|
+
input: { tenantId, brandId },
|
|
17041
|
+
ctx
|
|
17042
|
+
});
|
|
17043
|
+
let payload;
|
|
17044
|
+
if (result.state === "success" && result.structuredOutput) {
|
|
17045
|
+
payload = result.structuredOutput;
|
|
17046
|
+
} else {
|
|
17047
|
+
payload = {
|
|
16663
17048
|
ok: false,
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
|
|
16668
|
-
const plan = brand?.plan ?? {};
|
|
16669
|
-
const keywords = plan.keywordsPrioritarios ?? [];
|
|
16670
|
-
const existingSuggestions = brand?.collectionSuggestions ?? {};
|
|
16671
|
-
const collections = items.map((c) => {
|
|
16672
|
-
const seo = c.seo || {};
|
|
16673
|
-
const featured = c.featuredImage || null;
|
|
16674
|
-
return {
|
|
16675
|
-
id: c.platformId,
|
|
16676
|
-
title: c.title,
|
|
16677
|
-
handle: c.handle,
|
|
16678
|
-
body_html: typeof c.descriptionHtml === "string" ? c.descriptionHtml.slice(0, 500) : null,
|
|
16679
|
-
metaTitle: seo.metaTitle ?? null,
|
|
16680
|
-
metaDescription: seo.metaDescription ?? null,
|
|
16681
|
-
products_count: null,
|
|
16682
|
-
// adapter v2 aun no calcula products_count por collection
|
|
16683
|
-
collectionType: "canonical",
|
|
16684
|
-
image: featured ? { src: featured.url, alt: featured.altText } : null,
|
|
16685
|
-
existingSuggestion: existingSuggestions[String(c.platformId)] ?? null
|
|
17049
|
+
state: result.state,
|
|
17050
|
+
mensaje: result.text,
|
|
17051
|
+
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
16686
17052
|
};
|
|
16687
|
-
}
|
|
16688
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
16689
|
-
ok: true,
|
|
16690
|
-
totalCollections: collections.length,
|
|
16691
|
-
keywordsPrioritarios: keywords,
|
|
16692
|
-
collections,
|
|
16693
|
-
bestPractices: {
|
|
16694
|
-
title: "Keyword principal, claro y corto. H1 de la p\xE1gina.",
|
|
16695
|
-
description: "Arriba del grid: 2-3 frases (50-70 palabras). Abajo: 200-400 palabras con long-tail keywords. P\xE1ginas con descripci\xF3n rankean 2.7x m\xE1s.",
|
|
16696
|
-
metaTitle: "50-60 chars. Keyword al inicio. Formato: {Keyword} | {Brand}. Max 600px.",
|
|
16697
|
-
metaDescription: "120-158 chars. Keyword + CTA + value prop. 120 mobile, 158 desktop.",
|
|
16698
|
-
handle: "Keyword en URL. NUNCA cambiar URL indexado sin redirect 301.",
|
|
16699
|
-
imageAlt: "Descriptivo con keyword. Accesibilidad + Google Images + AEO.",
|
|
16700
|
-
aeo: "AI engines validan im\xE1genes contra schema. Structured data importa."
|
|
16701
|
-
},
|
|
16702
|
-
schemaParaSave: {
|
|
16703
|
-
_instrucciones: "SCHEMA ESTRICTO para save_collection_suggestions. Usa EXACTAMENTE estos nombres de campo. El sistema RECHAZAR\xC1 campos que no coincidan.",
|
|
16704
|
-
_ejemplo: {
|
|
16705
|
-
collectionId: 123456789,
|
|
16706
|
-
suggestedTitle: "Flores Moradas | Env\xEDo CDMX",
|
|
16707
|
-
suggestedDescription: "<p>Descubre nuestra colecci\xF3n de flores moradas...</p>",
|
|
16708
|
-
suggestedMetaTitle: "Flores Moradas CDMX | Env\xEDo Mismo D\xEDa",
|
|
16709
|
-
suggestedMetaDescription: "Env\xEDa flores moradas a domicilio en CDMX. Arreglos frescos con rosas, tulipanes y lirios morados. Entrega el mismo d\xEDa.",
|
|
16710
|
-
suggestedHandle: null,
|
|
16711
|
-
suggestedImageAlt: "Ramo de flores moradas con rosas y tulipanes - Ponch y Capric\xF3 florer\xEDa CDMX",
|
|
16712
|
-
keyword: "flores moradas cdmx",
|
|
16713
|
-
notas: "Meta title optimizado con keyword local. Description ampliada con long-tail keywords. Handle no cambia (ya indexado)."
|
|
16714
|
-
},
|
|
16715
|
-
_reglas: [
|
|
16716
|
-
"collectionId: OBLIGATORIO. ID num\xE9rico de Shopify (no GID)",
|
|
16717
|
-
"suggestedTitle: string. H1 de la p\xE1gina. Claro con keyword",
|
|
16718
|
-
"suggestedDescription: string HTML. 200-400 palabras. Incluir links a colecciones relacionadas",
|
|
16719
|
-
"suggestedMetaTitle: string. 50-60 chars. Keyword al inicio. NUNCA m\xE1s de 60",
|
|
16720
|
-
"suggestedMetaDescription: string. 120-158 chars. Keyword + CTA + value prop. NUNCA m\xE1s de 158",
|
|
16721
|
-
"suggestedHandle: string | null. Solo cambiar si el handle actual es malo. null = no cambiar",
|
|
16722
|
-
"suggestedImageAlt: string. Descriptivo con keyword. Para accesibilidad + Google Images",
|
|
16723
|
-
"keyword: string. Keyword target de esta colecci\xF3n (del plan de marketing)",
|
|
16724
|
-
"notas: string. Explicaci\xF3n breve de qu\xE9 cambiaste y por qu\xE9"
|
|
16725
|
-
],
|
|
16726
|
-
_nunca: [
|
|
16727
|
-
"NUNCA usar titulo, descripcion, metaTitulo, metaDescripcion \u2014 usar los nombres en ingl\xE9s",
|
|
16728
|
-
"NUNCA inventar collectionId \u2014 usar el id del array collections",
|
|
16729
|
-
"NUNCA meta title > 60 chars o meta description > 158 chars",
|
|
16730
|
-
"NUNCA cambiar handle de colecci\xF3n indexada sin justificaci\xF3n"
|
|
16731
|
-
]
|
|
16732
|
-
}
|
|
16733
|
-
}) }] };
|
|
17053
|
+
}
|
|
17054
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
16734
17055
|
}
|
|
16735
17056
|
);
|
|
16736
17057
|
server.tool(
|
|
16737
17058
|
"save_collection_suggestions",
|
|
16738
17059
|
"Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
|
|
16739
17060
|
{
|
|
16740
|
-
brandId:
|
|
17061
|
+
brandId: import_zod51.z.string().optional().describe("ID de la brand"),
|
|
16741
17062
|
suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
|
|
16742
17063
|
},
|
|
16743
17064
|
async ({ brandId: inputBrandId, suggestions }) => {
|
|
@@ -16763,7 +17084,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
|
|
|
16763
17084
|
}
|
|
16764
17085
|
|
|
16765
17086
|
// src/tools/martin.ts
|
|
16766
|
-
var
|
|
17087
|
+
var import_zod52 = require("zod");
|
|
16767
17088
|
function renderResult(result) {
|
|
16768
17089
|
const payload = {
|
|
16769
17090
|
state: result.state,
|
|
@@ -16779,16 +17100,16 @@ function registerMartinTools(server, session) {
|
|
|
16779
17100
|
"recordar_memoria",
|
|
16780
17101
|
"Save a user preference, rule, pattern or aversion that the system will respect in future interactions. Personal to the calling user (scope: self). Applied automatically in Martin's prompt for subsequent conversations.",
|
|
16781
17102
|
{
|
|
16782
|
-
tipo:
|
|
17103
|
+
tipo: import_zod52.z.enum(["preferencia", "regla", "patron", "aversion"]).describe(
|
|
16783
17104
|
"Memory type: 'preferencia' (personal preference), 'regla' (operational rule), 'patron' (observed behavioral pattern), 'aversion' (explicit do-not-do rule)."
|
|
16784
17105
|
),
|
|
16785
|
-
categoria:
|
|
16786
|
-
contenido:
|
|
16787
|
-
origen:
|
|
16788
|
-
tipo:
|
|
16789
|
-
conversacionId:
|
|
16790
|
-
cardId:
|
|
16791
|
-
rationale:
|
|
17106
|
+
categoria: import_zod52.z.enum(["compras", "produccion", "dispatch", "ventas", "marketing", "operacion", "personal", "delegacion"]).describe("Business area this memory applies to."),
|
|
17107
|
+
contenido: import_zod52.z.string().min(3).max(500).describe("Memory content in the user's own words (3-500 chars)."),
|
|
17108
|
+
origen: import_zod52.z.object({
|
|
17109
|
+
tipo: import_zod52.z.enum(["conversacion", "feedback_card", "inferido_automatico"]),
|
|
17110
|
+
conversacionId: import_zod52.z.string().nullable(),
|
|
17111
|
+
cardId: import_zod52.z.string().nullable(),
|
|
17112
|
+
rationale: import_zod52.z.array(import_zod52.z.string()).optional()
|
|
16792
17113
|
}).describe("Where this memory was inferred from (conversation, card feedback, or auto-inferred).")
|
|
16793
17114
|
},
|
|
16794
17115
|
async ({ tipo, categoria, contenido, origen }) => {
|
|
@@ -16807,9 +17128,9 @@ function registerMartinTools(server, session) {
|
|
|
16807
17128
|
"olvidar_memoria",
|
|
16808
17129
|
"Archive a memory that no longer applies. The memory remains visible in settings but is no longer enforced in future interactions. Requires confirmation: the first call returns a confirmation prompt; pass confirm=true on the second call to execute.",
|
|
16809
17130
|
{
|
|
16810
|
-
memoriaId:
|
|
16811
|
-
motivo:
|
|
16812
|
-
confirm:
|
|
17131
|
+
memoriaId: import_zod52.z.string().min(1).describe("Memory document ID to archive."),
|
|
17132
|
+
motivo: import_zod52.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
|
|
17133
|
+
confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
|
|
16813
17134
|
},
|
|
16814
17135
|
async ({ memoriaId, motivo, confirm }) => {
|
|
16815
17136
|
const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
|
|
@@ -16827,25 +17148,25 @@ function registerMartinTools(server, session) {
|
|
|
16827
17148
|
"programar_rutina",
|
|
16828
17149
|
"Schedule a recurring or one-time task. Useful for monthly reports, reminders, scheduled posts, suggested purchases. Personal to the calling user (scope: self). Requires confirmation.",
|
|
16829
17150
|
{
|
|
16830
|
-
uidDestinatario:
|
|
16831
|
-
tipo:
|
|
16832
|
-
frecuencia:
|
|
16833
|
-
config:
|
|
16834
|
-
diaSemana:
|
|
16835
|
-
diaMes:
|
|
16836
|
-
hora:
|
|
16837
|
-
fechaPuntual:
|
|
17151
|
+
uidDestinatario: import_zod52.z.string().optional().describe("User ID who should receive the routine output. OMIT to default to the calling user (most common case)."),
|
|
17152
|
+
tipo: import_zod52.z.enum(["reporte", "recordatorio", "accion_delegada", "publicacion", "compra_sugerida"]).describe("Routine type from canonical TipoRutinaEnum."),
|
|
17153
|
+
frecuencia: import_zod52.z.enum(["diaria", "semanal", "quincenal", "mensual", "trimestral", "puntual"]).describe("Execution cadence."),
|
|
17154
|
+
config: import_zod52.z.object({
|
|
17155
|
+
diaSemana: import_zod52.z.number().int().min(0).max(6).nullable(),
|
|
17156
|
+
diaMes: import_zod52.z.number().int().min(1).max(31).nullable(),
|
|
17157
|
+
hora: import_zod52.z.string().regex(/^\d{2}:\d{2}$/),
|
|
17158
|
+
fechaPuntual: import_zod52.z.string().datetime({ offset: true }).nullable()
|
|
16838
17159
|
}).describe("Schedule configuration. Match fields to the frecuencia (semanal needs diaSemana; mensual needs diaMes; puntual needs fechaPuntual)."),
|
|
16839
|
-
accion:
|
|
16840
|
-
tool:
|
|
16841
|
-
params:
|
|
17160
|
+
accion: import_zod52.z.object({
|
|
17161
|
+
tool: import_zod52.z.string().min(1),
|
|
17162
|
+
params: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.unknown())
|
|
16842
17163
|
}).describe("Action to execute on each fire (MCP tool name + params)."),
|
|
16843
|
-
origen:
|
|
16844
|
-
tipo:
|
|
16845
|
-
conversacionId:
|
|
16846
|
-
cardId:
|
|
17164
|
+
origen: import_zod52.z.object({
|
|
17165
|
+
tipo: import_zod52.z.enum(["conversacion", "feedback_card", "configurada_explicito"]),
|
|
17166
|
+
conversacionId: import_zod52.z.string().nullable(),
|
|
17167
|
+
cardId: import_zod52.z.string().nullable()
|
|
16847
17168
|
}).describe("Where this routine was scheduled from."),
|
|
16848
|
-
confirm:
|
|
17169
|
+
confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
|
|
16849
17170
|
},
|
|
16850
17171
|
async ({ uidDestinatario, tipo, frecuencia, config, accion, origen, confirm }) => {
|
|
16851
17172
|
const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
|
|
@@ -16874,8 +17195,8 @@ function registerMartinTools(server, session) {
|
|
|
16874
17195
|
"pausar_rutina",
|
|
16875
17196
|
"Pause an active routine temporarily. Can be resumed later by re-enabling it.",
|
|
16876
17197
|
{
|
|
16877
|
-
rutinaId:
|
|
16878
|
-
motivo:
|
|
17198
|
+
rutinaId: import_zod52.z.string().min(1).describe("Routine document ID to pause."),
|
|
17199
|
+
motivo: import_zod52.z.string().optional().describe("Optional reason for pausing (logged for audit).")
|
|
16879
17200
|
},
|
|
16880
17201
|
async ({ rutinaId, motivo }) => {
|
|
16881
17202
|
const ctx = await buildContext(session, null);
|
|
@@ -16901,9 +17222,9 @@ function registerMartinTools(server, session) {
|
|
|
16901
17222
|
"archivar_rutina",
|
|
16902
17223
|
"Archive a routine that no longer applies. The routine is preserved for audit purposes but will NOT be executed. Requires confirmation.",
|
|
16903
17224
|
{
|
|
16904
|
-
rutinaId:
|
|
16905
|
-
motivo:
|
|
16906
|
-
confirm:
|
|
17225
|
+
rutinaId: import_zod52.z.string().min(1).describe("Routine document ID to archive (will not execute again)."),
|
|
17226
|
+
motivo: import_zod52.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
|
|
17227
|
+
confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
|
|
16907
17228
|
},
|
|
16908
17229
|
async ({ rutinaId, motivo, confirm }) => {
|
|
16909
17230
|
const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
|