ponch-mcp-server 1.0.83 → 1.0.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2478,7 +2478,7 @@ function registerCoreTools(server, session) {
2478
2478
  }
2479
2479
 
2480
2480
  // src/tools/marketing.ts
2481
- var import_zod49 = require("zod");
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,95 @@ 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_value": {
9824
+ const opts = (issue.values ?? []).map((o) => JSON.stringify(o)).join(", ");
9825
+ return getMessage("marketing.wrapper.issue_invalid_enum", locale, {
9826
+ field,
9827
+ options: opts
9828
+ });
9829
+ }
9830
+ case "invalid_string":
9831
+ case "invalid_format": {
9832
+ const hint = issue.validation ? `(${issue.validation})` : "";
9833
+ return getMessage("marketing.wrapper.issue_invalid_string", locale, {
9834
+ field,
9835
+ hint
9836
+ });
9837
+ }
9838
+ default:
9839
+ return getMessage("marketing.wrapper.issue_generic", locale, {
9840
+ field,
9841
+ message: issue.message
9842
+ });
9843
+ }
9844
+ }
9756
9845
  var NIVELES_ORDER = ["ninguno", "ver", "editar", "completo"];
9757
9846
  function nivelAlcanza(nivel, accion) {
9758
9847
  return NIVELES_ORDER.indexOf(nivel) >= NIVELES_ORDER.indexOf(accion);
@@ -9855,9 +9944,16 @@ function wrapWithContract(contract, helper, options = {}) {
9855
9944
  expected: issue.expected
9856
9945
  };
9857
9946
  });
9858
- const camposFallidos = validationErrors.map((v) => v.path.join(".")).filter(Boolean);
9859
- const baseMsg = getWrapperMessage("input_invalido", locale);
9860
- const text = camposFallidos.length ? locale === "en" ? `${baseMsg} Issues with: ${camposFallidos.join(", ")}.` : `${baseMsg} Hubo problemas con: ${camposFallidos.join(", ")}.` : baseMsg;
9947
+ let text;
9948
+ if (parseResult.error.issues.length === 0) {
9949
+ text = getWrapperMessage("input_invalido", locale);
9950
+ } else {
9951
+ const intro = getWrapperMessage("input_invalido_intro", locale);
9952
+ const lines = parseResult.error.issues.map(
9953
+ (i) => getIssueHumanText(i, locale)
9954
+ );
9955
+ text = `${intro} ${lines.join(" ")}`;
9956
+ }
9861
9957
  await writeAuditLog({
9862
9958
  tenantId: ctx.tenantId,
9863
9959
  brandId: ctx.brandId ?? null,
@@ -10736,18 +10832,20 @@ var import_zod35 = require("zod");
10736
10832
  var import_firebase_admin7 = require("firebase-admin");
10737
10833
  var import_zod36 = require("zod");
10738
10834
  var import_zod37 = require("zod");
10739
- var import_firebase_admin8 = require("firebase-admin");
10740
10835
  var import_zod38 = require("zod");
10741
10836
  var import_zod39 = require("zod");
10837
+ var import_firebase_admin8 = require("firebase-admin");
10742
10838
  var import_zod40 = require("zod");
10743
- var import_firebase_admin9 = require("firebase-admin");
10744
10839
  var import_zod41 = require("zod");
10745
- var import_firebase_admin10 = require("firebase-admin");
10746
10840
  var import_zod42 = require("zod");
10841
+ var import_firebase_admin9 = require("firebase-admin");
10747
10842
  var import_zod43 = require("zod");
10843
+ var import_firebase_admin10 = require("firebase-admin");
10748
10844
  var import_zod44 = require("zod");
10749
10845
  var import_zod45 = require("zod");
10750
10846
  var import_zod46 = require("zod");
10847
+ var import_zod47 = require("zod");
10848
+ var import_zod48 = require("zod");
10751
10849
  var import_firestore6 = require("firebase-admin/firestore");
10752
10850
  var RULE_NEGATIVES = {
10753
10851
  allowFaces: "no people, no faces, no hands",
@@ -12062,6 +12160,229 @@ var rawContract7 = {
12062
12160
  var getCalendarContract = MartinContractSchema.parse(
12063
12161
  rawContract7
12064
12162
  );
12163
+ async function seoSnapshotReader(input) {
12164
+ const { db, tenantId, brandId } = input;
12165
+ const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
12166
+ const brandSnap = await brandRef.get();
12167
+ if (!brandSnap.exists) {
12168
+ return { ok: false, code: "BRAND_NOT_FOUND" };
12169
+ }
12170
+ const snap = await db.collection("tenants").doc(tenantId).collection("marketing_snapshots_semrush").where("brandId", "==", brandId).orderBy("mes", "desc").limit(1).get();
12171
+ if (snap.empty) {
12172
+ return { ok: false, code: "SEO_SNAPSHOT_MISSING" };
12173
+ }
12174
+ const doc = snap.docs[0];
12175
+ const data = doc.data();
12176
+ const mes = typeof data.mes === "string" ? data.mes : "";
12177
+ return {
12178
+ ok: true,
12179
+ brandId,
12180
+ mes,
12181
+ snapshot: { id: doc.id, ...data }
12182
+ };
12183
+ }
12184
+ var ParamsSchema8 = import_zod38.z.object({
12185
+ tenantId: import_zod38.z.string().min(1).describe("Tenant identifier (the business account)."),
12186
+ brandId: import_zod38.z.string().min(1).describe("Brand identifier within the tenant.")
12187
+ });
12188
+ var SuccessSchema = import_zod38.z.object({
12189
+ ok: import_zod38.z.literal(true),
12190
+ brandId: import_zod38.z.string(),
12191
+ mes: import_zod38.z.string(),
12192
+ snapshot: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown())
12193
+ });
12194
+ var FailureSchema = import_zod38.z.object({
12195
+ ok: import_zod38.z.literal(false),
12196
+ code: import_zod38.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"])
12197
+ });
12198
+ var OutputSchema8 = import_zod38.z.discriminatedUnion("ok", [SuccessSchema, FailureSchema]);
12199
+ var rawContract8 = {
12200
+ name: "get_seo_snapshot",
12201
+ description: "Read the latest Semrush SEO snapshot for a brand. Returns rank, top keywords, opportunities, and competitors.",
12202
+ paramsSchema: ParamsSchema8,
12203
+ outputSchema: OutputSchema8,
12204
+ requiresConfirmation: false,
12205
+ destructive: false,
12206
+ affectsPublication: false,
12207
+ affectsExternal: false,
12208
+ martinSummaryTemplate: (input, output, locale) => {
12209
+ if (!output.ok) {
12210
+ if (output.code === "BRAND_NOT_FOUND") {
12211
+ return locale === "en" ? `Brand ${input.brandId} not found.` : `No encontr\xE9 la brand ${input.brandId}.`;
12212
+ }
12213
+ return locale === "en" ? `No SEO snapshot for brand ${input.brandId} yet.` : `A\xFAn no hay snapshot SEO de ${input.brandId}.`;
12214
+ }
12215
+ return locale === "en" ? `Latest SEO snapshot for ${input.brandId} (${output.mes}).` : `Snapshot SEO m\xE1s reciente de ${input.brandId} (${output.mes}).`;
12216
+ },
12217
+ // AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
12218
+ auditAction: "marketing.seo_snapshot.leer",
12219
+ quotasConsumed: [],
12220
+ permissionScope: "module",
12221
+ permissionKey: "marketing",
12222
+ permissionAction: "ver",
12223
+ sideEffects: ["reads_firestore"]
12224
+ };
12225
+ var seoSnapshotReaderContract = MartinContractSchema.parse(
12226
+ rawContract8
12227
+ );
12228
+ var BEST_PRACTICES_EN = {
12229
+ title: "Primary keyword, clear and short. Page H1.",
12230
+ 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.",
12231
+ metaTitle: "50-60 chars. Keyword first. Format: {Keyword} | {Brand}. Max 600px.",
12232
+ metaDescription: "120-158 chars. Keyword + CTA + value prop. 120 mobile, 158 desktop.",
12233
+ handle: "Keyword in URL. NEVER change an indexed URL without a 301 redirect.",
12234
+ imageAlt: "Descriptive with keyword. Accessibility + Google Images + AEO.",
12235
+ aeo: "AI engines validate images against schema. Structured data matters."
12236
+ };
12237
+ var SCHEMA_FOR_SAVE_EN = {
12238
+ _instructions: "STRICT SCHEMA for save_collection_suggestions. Use EXACTLY these field names. The system WILL REJECT mismatched fields.",
12239
+ _example: {
12240
+ collectionId: 123456789,
12241
+ suggestedTitle: "Purple Flowers | Same-Day CDMX Delivery",
12242
+ suggestedDescription: "<p>Discover our purple flowers collection...</p>",
12243
+ suggestedMetaTitle: "Purple Flowers CDMX | Same-Day Delivery",
12244
+ suggestedMetaDescription: "Send purple flowers to your door in CDMX. Fresh arrangements with purple roses, tulips, and lilies. Same-day delivery.",
12245
+ suggestedHandle: null,
12246
+ suggestedImageAlt: "Bouquet of purple flowers with roses and tulips \u2014 local florist CDMX",
12247
+ keyword: "purple flowers cdmx",
12248
+ notas: "Meta title optimized with local keyword. Description expanded with long-tail keywords. Handle unchanged (already indexed)."
12249
+ },
12250
+ _rules: [
12251
+ "collectionId: REQUIRED. Numeric Shopify ID (not GID).",
12252
+ "suggestedTitle: string. Page H1. Clear with keyword.",
12253
+ "suggestedDescription: HTML string. 200-400 words. Include links to related collections.",
12254
+ "suggestedMetaTitle: string. 50-60 chars. Keyword first. NEVER over 60.",
12255
+ "suggestedMetaDescription: string. 120-158 chars. Keyword + CTA + value prop. NEVER over 158.",
12256
+ "suggestedHandle: string | null. Only change if the current handle is bad. null = no change.",
12257
+ "suggestedImageAlt: string. Descriptive with keyword. For accessibility + Google Images.",
12258
+ "keyword: string. Target keyword for this collection (from the marketing plan).",
12259
+ "notas: string. Brief explanation of what changed and why."
12260
+ ],
12261
+ _never: [
12262
+ "NEVER use Spanish field names (titulo, descripcion, etc.) \u2014 use English names.",
12263
+ "NEVER fabricate collectionId \u2014 use the id from the collections array.",
12264
+ "NEVER exceed 60 chars in metaTitle or 158 chars in metaDescription.",
12265
+ "NEVER change the handle of an indexed collection without justification."
12266
+ ]
12267
+ };
12268
+ async function collectionsReader(input) {
12269
+ const { db, tenantId, brandId } = input;
12270
+ const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
12271
+ const brandSnap = await brandRef.get();
12272
+ if (!brandSnap.exists) {
12273
+ return { ok: false, code: "BRAND_NOT_FOUND" };
12274
+ }
12275
+ const collSnap = await db.collection("tenants").doc(tenantId).collection("marketing_snapshots").doc(brandId).collection("collections").get();
12276
+ if (collSnap.empty) {
12277
+ return { ok: false, code: "NO_COLLECTIONS_NESTED" };
12278
+ }
12279
+ const items = collSnap.docs.map((d) => d.data());
12280
+ const brand = brandSnap.data() ?? {};
12281
+ const plan = brand.plan ?? {};
12282
+ const keywordsPrioritarios = Array.isArray(plan.keywordsPrioritarios) ? plan.keywordsPrioritarios : [];
12283
+ const existingSuggestions = brand.collectionSuggestions ?? {};
12284
+ const collections = items.map((c) => {
12285
+ const seo = c.seo || {};
12286
+ const featured = c.featuredImage || null;
12287
+ return {
12288
+ id: c.platformId,
12289
+ title: c.title,
12290
+ handle: c.handle,
12291
+ body_html: typeof c.descriptionHtml === "string" ? c.descriptionHtml.slice(0, 500) : null,
12292
+ metaTitle: seo.metaTitle ?? null,
12293
+ metaDescription: seo.metaDescription ?? null,
12294
+ products_count: null,
12295
+ collectionType: "canonical",
12296
+ image: featured ? { src: featured.url, alt: featured.altText } : null,
12297
+ existingSuggestion: existingSuggestions[String(c.platformId)] ?? null
12298
+ };
12299
+ });
12300
+ return {
12301
+ ok: true,
12302
+ brandId,
12303
+ totalCollections: collections.length,
12304
+ keywordsPrioritarios,
12305
+ collections,
12306
+ bestPractices: BEST_PRACTICES_EN,
12307
+ schemaParaSave: SCHEMA_FOR_SAVE_EN
12308
+ };
12309
+ }
12310
+ var ParamsSchema9 = import_zod39.z.object({
12311
+ tenantId: import_zod39.z.string().min(1).describe("Tenant identifier (the business account)."),
12312
+ brandId: import_zod39.z.string().min(1).describe("Brand identifier within the tenant.")
12313
+ });
12314
+ var CollectionItemSchema = import_zod39.z.object({
12315
+ id: import_zod39.z.unknown(),
12316
+ title: import_zod39.z.unknown(),
12317
+ handle: import_zod39.z.unknown(),
12318
+ body_html: import_zod39.z.string().nullable(),
12319
+ metaTitle: import_zod39.z.unknown(),
12320
+ metaDescription: import_zod39.z.unknown(),
12321
+ products_count: import_zod39.z.null(),
12322
+ collectionType: import_zod39.z.literal("canonical"),
12323
+ image: import_zod39.z.object({ src: import_zod39.z.unknown(), alt: import_zod39.z.unknown() }).nullable(),
12324
+ existingSuggestion: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown()).nullable()
12325
+ });
12326
+ var BestPracticesSchema = import_zod39.z.object({
12327
+ title: import_zod39.z.string(),
12328
+ description: import_zod39.z.string(),
12329
+ metaTitle: import_zod39.z.string(),
12330
+ metaDescription: import_zod39.z.string(),
12331
+ handle: import_zod39.z.string(),
12332
+ imageAlt: import_zod39.z.string(),
12333
+ aeo: import_zod39.z.string()
12334
+ });
12335
+ var SchemaForSaveSchema = import_zod39.z.object({
12336
+ _instructions: import_zod39.z.string(),
12337
+ _example: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown()),
12338
+ _rules: import_zod39.z.array(import_zod39.z.string()),
12339
+ _never: import_zod39.z.array(import_zod39.z.string())
12340
+ });
12341
+ var SuccessSchema2 = import_zod39.z.object({
12342
+ ok: import_zod39.z.literal(true),
12343
+ brandId: import_zod39.z.string(),
12344
+ totalCollections: import_zod39.z.number(),
12345
+ // Shape real: Array<{ keyword, posicion, volumen, dificultad, prioridad, accion }>.
12346
+ // Passthrough loose para no acoplar el reader al schema interno del plan.
12347
+ keywordsPrioritarios: import_zod39.z.array(import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown())),
12348
+ collections: import_zod39.z.array(CollectionItemSchema),
12349
+ bestPractices: BestPracticesSchema,
12350
+ schemaParaSave: SchemaForSaveSchema
12351
+ });
12352
+ var FailureSchema2 = import_zod39.z.object({
12353
+ ok: import_zod39.z.literal(false),
12354
+ code: import_zod39.z.enum(["BRAND_NOT_FOUND", "NO_COLLECTIONS_NESTED"])
12355
+ });
12356
+ var OutputSchema9 = import_zod39.z.discriminatedUnion("ok", [SuccessSchema2, FailureSchema2]);
12357
+ var rawContract9 = {
12358
+ name: "get_collections",
12359
+ 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.",
12360
+ paramsSchema: ParamsSchema9,
12361
+ outputSchema: OutputSchema9,
12362
+ requiresConfirmation: false,
12363
+ destructive: false,
12364
+ affectsPublication: false,
12365
+ affectsExternal: false,
12366
+ martinSummaryTemplate: (input, output, locale) => {
12367
+ if (!output.ok) {
12368
+ if (output.code === "BRAND_NOT_FOUND") {
12369
+ return locale === "en" ? `Brand ${input.brandId} not found.` : `No encontr\xE9 la brand ${input.brandId}.`;
12370
+ }
12371
+ return locale === "en" ? `No collections synced for ${input.brandId} yet.` : `A\xFAn no hay colecciones sincronizadas para ${input.brandId}.`;
12372
+ }
12373
+ return locale === "en" ? `${output.totalCollections} collections for ${input.brandId}.` : `${output.totalCollections} colecciones para ${input.brandId}.`;
12374
+ },
12375
+ // AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
12376
+ auditAction: "marketing.colecciones.listar",
12377
+ quotasConsumed: [],
12378
+ permissionScope: "module",
12379
+ permissionKey: "marketing",
12380
+ permissionAction: "ver",
12381
+ sideEffects: ["reads_firestore"]
12382
+ };
12383
+ var collectionsReaderContract = MartinContractSchema.parse(
12384
+ rawContract9
12385
+ );
12065
12386
  var PLATAFORMA_A_FORMATO = {
12066
12387
  gbp: "gbp_4_3",
12067
12388
  shopify_blog: "blog_3_2",
@@ -12173,35 +12494,35 @@ async function photoAssigner(input) {
12173
12494
  formato
12174
12495
  };
12175
12496
  }
12176
- var ParamsSchema8 = import_zod38.z.object({
12177
- tenantId: import_zod38.z.string().min(1).describe("Tenant identifier (the business account)."),
12178
- brandId: import_zod38.z.string().min(1).describe("Brand identifier within the tenant."),
12179
- contenidoRef: import_zod38.z.string().min(1).describe("Document ID in marketing_contenido (the post receiving the photo)."),
12180
- fotoId: import_zod38.z.string().min(1).describe("Photo ID in marketing_fotos. Photo must be in state='editada'."),
12181
- calendarioItemRef: import_zod38.z.string().regex(/^semana:\d+:slot:\d+$/).optional().describe(
12497
+ var ParamsSchema10 = import_zod40.z.object({
12498
+ tenantId: import_zod40.z.string().min(1).describe("Tenant identifier (the business account)."),
12499
+ brandId: import_zod40.z.string().min(1).describe("Brand identifier within the tenant."),
12500
+ contenidoRef: import_zod40.z.string().min(1).describe("Document ID in marketing_contenido (the post receiving the photo)."),
12501
+ fotoId: import_zod40.z.string().min(1).describe("Photo ID in marketing_fotos. Photo must be in state='editada'."),
12502
+ calendarioItemRef: import_zod40.z.string().regex(/^semana:\d+:slot:\d+$/).optional().describe(
12182
12503
  '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.'
12183
12504
  )
12184
12505
  });
12185
- var OutputSchema8 = import_zod38.z.discriminatedUnion("ok", [
12186
- import_zod38.z.object({
12187
- ok: import_zod38.z.literal(true),
12188
- fotoId: import_zod38.z.string(),
12189
- contenidoRef: import_zod38.z.string(),
12190
- plataforma: import_zod38.z.string(),
12191
- varianteUrl: import_zod38.z.string().nullable(),
12192
- formato: import_zod38.z.string()
12506
+ var OutputSchema10 = import_zod40.z.discriminatedUnion("ok", [
12507
+ import_zod40.z.object({
12508
+ ok: import_zod40.z.literal(true),
12509
+ fotoId: import_zod40.z.string(),
12510
+ contenidoRef: import_zod40.z.string(),
12511
+ plataforma: import_zod40.z.string(),
12512
+ varianteUrl: import_zod40.z.string().nullable(),
12513
+ formato: import_zod40.z.string()
12193
12514
  }),
12194
- import_zod38.z.object({
12195
- ok: import_zod38.z.literal(false),
12196
- error: import_zod38.z.string(),
12197
- code: import_zod38.z.enum(["CONTENT_NOT_FOUND", "CONTENT_TENANT_MISMATCH", "PHOTO_NOT_FOUND", "PHOTO_NOT_READY"]).optional()
12515
+ import_zod40.z.object({
12516
+ ok: import_zod40.z.literal(false),
12517
+ error: import_zod40.z.string(),
12518
+ code: import_zod40.z.enum(["CONTENT_NOT_FOUND", "CONTENT_TENANT_MISMATCH", "PHOTO_NOT_FOUND", "PHOTO_NOT_READY"]).optional()
12198
12519
  })
12199
12520
  ]);
12200
- var rawContract8 = {
12521
+ var rawContract10 = {
12201
12522
  name: "assign_photo_to_content",
12202
12523
  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".',
12203
- paramsSchema: ParamsSchema8,
12204
- outputSchema: OutputSchema8,
12524
+ paramsSchema: ParamsSchema10,
12525
+ outputSchema: OutputSchema10,
12205
12526
  // Cambia la foto vinculada al contenido — reversible (volver a llamar con
12206
12527
  // otra fotoId), no publica nada externo. La foto editada queda intacta;
12207
12528
  // solo se actualiza la referencia.
@@ -12243,7 +12564,7 @@ var rawContract8 = {
12243
12564
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
12244
12565
  };
12245
12566
  var photoAssignerContract = MartinContractSchema.parse(
12246
- rawContract8
12567
+ rawContract10
12247
12568
  );
12248
12569
  function findPageByHeuristic(pages, pattern) {
12249
12570
  return pages.find((p) => pattern.test(p.title || "")) || pages.find((p) => pattern.test(p.handle || "")) || null;
@@ -12432,27 +12753,27 @@ var BRAND_BRIEF_SCHEMA_HINT = {
12432
12753
  escenas: [{ id: string, nombre: string, promptHint: string }]
12433
12754
  }`
12434
12755
  };
12435
- var ParamsSchema9 = import_zod39.z.object({
12436
- tenantId: import_zod39.z.string().min(1).describe("Tenant identifier (the business account)."),
12437
- brandId: import_zod39.z.string().min(1).describe("Brand identifier within the tenant.")
12756
+ var ParamsSchema11 = import_zod41.z.object({
12757
+ tenantId: import_zod41.z.string().min(1).describe("Tenant identifier (the business account)."),
12758
+ brandId: import_zod41.z.string().min(1).describe("Brand identifier within the tenant.")
12438
12759
  });
12439
- var OutputSchema9 = import_zod39.z.discriminatedUnion("ok", [
12440
- import_zod39.z.object({
12441
- ok: import_zod39.z.literal(true),
12760
+ var OutputSchema11 = import_zod41.z.discriminatedUnion("ok", [
12761
+ import_zod41.z.object({
12762
+ ok: import_zod41.z.literal(true),
12442
12763
  /** Payload con instrucción + datos del negocio para que Claude genere el brief. */
12443
- payload: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown())
12764
+ payload: import_zod41.z.record(import_zod41.z.string(), import_zod41.z.unknown())
12444
12765
  }),
12445
- import_zod39.z.object({
12446
- ok: import_zod39.z.literal(false),
12447
- error: import_zod39.z.string(),
12448
- code: import_zod39.z.enum(["BRAND_NOT_FOUND"]).optional()
12766
+ import_zod41.z.object({
12767
+ ok: import_zod41.z.literal(false),
12768
+ error: import_zod41.z.string(),
12769
+ code: import_zod41.z.enum(["BRAND_NOT_FOUND"]).optional()
12449
12770
  })
12450
12771
  ]);
12451
- var rawContract9 = {
12772
+ var rawContract11 = {
12452
12773
  name: "generate_brand_brief",
12453
12774
  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.",
12454
- paramsSchema: ParamsSchema9,
12455
- outputSchema: OutputSchema9,
12775
+ paramsSchema: ParamsSchema11,
12776
+ outputSchema: OutputSchema11,
12456
12777
  // Lectura pura, sin side effects de escritura ni publicación.
12457
12778
  requiresConfirmation: false,
12458
12779
  destructive: false,
@@ -12477,7 +12798,7 @@ var rawContract9 = {
12477
12798
  sideEffects: ["reads_firestore"]
12478
12799
  };
12479
12800
  var brandBriefBuilderContract = MartinContractSchema.parse(
12480
- rawContract9
12801
+ rawContract11
12481
12802
  );
12482
12803
  async function resolveLastImportId(db, tenantId, brandId) {
12483
12804
  const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
@@ -12665,28 +12986,28 @@ var BLOG_STRATEGY_REGLAS = [
12665
12986
  "blogId, handle, title, ultimoPostFecha, ultimoPostKeyword, totalArticulos \u2014 copiar del shopifyBlogs (datos reales del import)",
12666
12987
  "defaultBlogId y defaultBlogHandle \u2014 usar el primer blog de la lista"
12667
12988
  ];
12668
- var ParamsSchema10 = import_zod40.z.object({
12669
- tenantId: import_zod40.z.string().min(1).describe("Tenant identifier (the business account)."),
12670
- brandId: import_zod40.z.string().min(1).describe("Brand identifier within the tenant.")
12989
+ var ParamsSchema12 = import_zod42.z.object({
12990
+ tenantId: import_zod42.z.string().min(1).describe("Tenant identifier (the business account)."),
12991
+ brandId: import_zod42.z.string().min(1).describe("Brand identifier within the tenant.")
12671
12992
  });
12672
- var OutputSchema10 = import_zod40.z.discriminatedUnion("ok", [
12673
- import_zod40.z.object({
12674
- ok: import_zod40.z.literal(true),
12675
- payload: import_zod40.z.record(import_zod40.z.string(), import_zod40.z.unknown()).describe(
12993
+ var OutputSchema12 = import_zod42.z.discriminatedUnion("ok", [
12994
+ import_zod42.z.object({
12995
+ ok: import_zod42.z.literal(true),
12996
+ payload: import_zod42.z.record(import_zod42.z.string(), import_zod42.z.unknown()).describe(
12676
12997
  "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."
12677
12998
  )
12678
12999
  }),
12679
- import_zod40.z.object({
12680
- ok: import_zod40.z.literal(false),
12681
- error: import_zod40.z.string(),
12682
- code: import_zod40.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"]).optional()
13000
+ import_zod42.z.object({
13001
+ ok: import_zod42.z.literal(false),
13002
+ error: import_zod42.z.string(),
13003
+ code: import_zod42.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"]).optional()
12683
13004
  })
12684
13005
  ]);
12685
- var rawContract10 = {
13006
+ var rawContract12 = {
12686
13007
  name: "generate_marketing_plan",
12687
13008
  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).",
12688
- paramsSchema: ParamsSchema10,
12689
- outputSchema: OutputSchema10,
13009
+ paramsSchema: ParamsSchema12,
13010
+ outputSchema: OutputSchema12,
12690
13011
  // Lectura pura, no muta nada.
12691
13012
  requiresConfirmation: false,
12692
13013
  destructive: false,
@@ -12711,7 +13032,7 @@ var rawContract10 = {
12711
13032
  sideEffects: ["reads_firestore"]
12712
13033
  };
12713
13034
  var marketingPlanBuilderContract = MartinContractSchema.parse(
12714
- rawContract10
13035
+ rawContract12
12715
13036
  );
12716
13037
  function buildGenId() {
12717
13038
  return `mkt_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
@@ -12952,54 +13273,54 @@ async function contenidoWriter(input) {
12952
13273
  pipelineLinked
12953
13274
  };
12954
13275
  }
12955
- var ParamsSchema11 = import_zod41.z.object({
12956
- tenantId: import_zod41.z.string().min(1).describe("Tenant identifier (the business account)."),
12957
- brandId: import_zod41.z.string().min(1).describe("Brand identifier within the tenant."),
12958
- plataforma: import_zod41.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target platform for the content."),
12959
- tipo: import_zod41.z.string().optional().describe(
13276
+ var ParamsSchema13 = import_zod43.z.object({
13277
+ tenantId: import_zod43.z.string().min(1).describe("Tenant identifier (the business account)."),
13278
+ brandId: import_zod43.z.string().min(1).describe("Brand identifier within the tenant."),
13279
+ plataforma: import_zod43.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target platform for the content."),
13280
+ tipo: import_zod43.z.string().optional().describe(
12960
13281
  "Content type ('post', 'blog', 'carousel', 'reel', 'story', 'review_response'). Match to the platform."
12961
13282
  ),
12962
- keyword: import_zod41.z.string().optional().describe("Primary keyword for the content. OMIT if not applicable."),
12963
- languageCode: import_zod41.z.string().optional().describe(
13283
+ keyword: import_zod43.z.string().optional().describe("Primary keyword for the content. OMIT if not applicable."),
13284
+ languageCode: import_zod43.z.string().optional().describe(
12964
13285
  "Content language code (e.g. 'es', 'en'). For shopify_blog auto-injects to datos.languageCode if not present."
12965
13286
  ),
12966
- fotoId: import_zod41.z.string().optional().describe("Linked photo ID. OMIT if no photo associated yet (use assign_photo_to_content later)."),
12967
- datos: import_zod41.z.record(import_zod41.z.string(), import_zod41.z.unknown()).describe(
13287
+ fotoId: import_zod43.z.string().optional().describe("Linked photo ID. OMIT if no photo associated yet (use assign_photo_to_content later)."),
13288
+ datos: import_zod43.z.record(import_zod43.z.string(), import_zod43.z.unknown()).describe(
12968
13289
  "Platform-specific content payload. Validated against Blog/GBP/IG/Review schemas. Use buildDatosBlog/GBP/IG/Review helpers to construct safely."
12969
13290
  ),
12970
- calendarioItemRef: import_zod41.z.string().optional().describe(
13291
+ calendarioItemRef: import_zod43.z.string().optional().describe(
12971
13292
  '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.'
12972
13293
  )
12973
13294
  });
12974
- var PipelineLinkResultSchema = import_zod41.z.object({
12975
- linked: import_zod41.z.boolean(),
12976
- paso: import_zod41.z.string().nullable(),
12977
- motivo: import_zod41.z.string().optional(),
12978
- code: import_zod41.z.string().optional(),
12979
- _instrucciones: import_zod41.z.string().optional()
13295
+ var PipelineLinkResultSchema = import_zod43.z.object({
13296
+ linked: import_zod43.z.boolean(),
13297
+ paso: import_zod43.z.string().nullable(),
13298
+ motivo: import_zod43.z.string().optional(),
13299
+ code: import_zod43.z.string().optional(),
13300
+ _instrucciones: import_zod43.z.string().optional()
12980
13301
  });
12981
- var OutputSchema11 = import_zod41.z.discriminatedUnion("ok", [
12982
- import_zod41.z.object({
12983
- ok: import_zod41.z.literal(true),
12984
- contenidoId: import_zod41.z.string(),
12985
- estado: import_zod41.z.string(),
12986
- plataforma: import_zod41.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
12987
- descartados: import_zod41.z.number().int().nonnegative(),
13302
+ var OutputSchema13 = import_zod43.z.discriminatedUnion("ok", [
13303
+ import_zod43.z.object({
13304
+ ok: import_zod43.z.literal(true),
13305
+ contenidoId: import_zod43.z.string(),
13306
+ estado: import_zod43.z.string(),
13307
+ plataforma: import_zod43.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
13308
+ descartados: import_zod43.z.number().int().nonnegative(),
12988
13309
  pipelineLinked: PipelineLinkResultSchema
12989
13310
  }),
12990
- import_zod41.z.object({
12991
- ok: import_zod41.z.literal(false),
12992
- error: import_zod41.z.string(),
12993
- code: import_zod41.z.enum(["CONTENT_FREQUENCY_LIMIT", "CONTENT_DATA_VALIDATION_FAILED"]).optional(),
12994
- activosEstaSemana: import_zod41.z.number().int().optional(),
12995
- limite: import_zod41.z.number().int().optional()
13311
+ import_zod43.z.object({
13312
+ ok: import_zod43.z.literal(false),
13313
+ error: import_zod43.z.string(),
13314
+ code: import_zod43.z.enum(["CONTENT_FREQUENCY_LIMIT", "CONTENT_DATA_VALIDATION_FAILED"]).optional(),
13315
+ activosEstaSemana: import_zod43.z.number().int().optional(),
13316
+ limite: import_zod43.z.number().int().optional()
12996
13317
  })
12997
13318
  ]);
12998
- var rawContract11 = {
13319
+ var rawContract13 = {
12999
13320
  name: "save_generated_content",
13000
13321
  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.",
13001
- paramsSchema: ParamsSchema11,
13002
- outputSchema: OutputSchema11,
13322
+ paramsSchema: ParamsSchema13,
13323
+ outputSchema: OutputSchema13,
13003
13324
  // Crea borrador + descarta borrador previo del mismo slot. Reversible
13004
13325
  // (volver a llamar con datos corregidos crea un nuevo borrador y descarta
13005
13326
  // el actual). NO publica nada externo — la CF de publish se encarga
@@ -13044,7 +13365,7 @@ var rawContract11 = {
13044
13365
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
13045
13366
  };
13046
13367
  var contenidoWriterContract = MartinContractSchema.parse(
13047
- rawContract11
13368
+ rawContract13
13048
13369
  );
13049
13370
  function fmtDate(d) {
13050
13371
  const y = d.getFullYear();
@@ -13319,34 +13640,34 @@ OBLIGATORIO: calendarioItemRef con formato EXACTO "semana:N:slot:M" (ej: "semana
13319
13640
  OBLIGATORIO: fotoId \u2014 SIEMPRE pasa el ID de la foto que elegiste con get_photos_for_slot.
13320
13641
  Si el slot tiene notas[], LEERLAS y usarlas como contexto. Si estado es revisar, regenerar adaptando a las notas.
13321
13642
  BLOG SLOTS: Usa _blogJIT para el contexto de cada slot shopify_blog \u2014 blogTarget, tono, author, articulosExistentes para interlinking.`;
13322
- var ParamsSchema12 = import_zod42.z.object({
13323
- tenantId: import_zod42.z.string().min(1).describe("Tenant identifier (the business account)."),
13324
- brandId: import_zod42.z.string().min(1).describe("Brand identifier within the tenant."),
13325
- semana: import_zod42.z.number().int().min(1).max(5).optional().describe(
13643
+ var ParamsSchema14 = import_zod44.z.object({
13644
+ tenantId: import_zod44.z.string().min(1).describe("Tenant identifier (the business account)."),
13645
+ brandId: import_zod44.z.string().min(1).describe("Brand identifier within the tenant."),
13646
+ semana: import_zod44.z.number().int().min(1).max(5).optional().describe(
13326
13647
  "Week number within the current month (1-5). OMIT to default to the current week inferred from today."
13327
13648
  ),
13328
- modo: import_zod42.z.enum(["planificar", "generar"]).optional().describe(
13649
+ modo: import_zod44.z.enum(["planificar", "generar"]).optional().describe(
13329
13650
  "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'."
13330
13651
  )
13331
13652
  });
13332
- var OutputSchema12 = import_zod42.z.discriminatedUnion("ok", [
13333
- import_zod42.z.object({
13334
- ok: import_zod42.z.literal(true),
13335
- payload: import_zod42.z.record(import_zod42.z.string(), import_zod42.z.unknown()).describe(
13653
+ var OutputSchema14 = import_zod44.z.discriminatedUnion("ok", [
13654
+ import_zod44.z.object({
13655
+ ok: import_zod44.z.literal(true),
13656
+ payload: import_zod44.z.record(import_zod44.z.string(), import_zod44.z.unknown()).describe(
13336
13657
  "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)."
13337
13658
  )
13338
13659
  }),
13339
- import_zod42.z.object({
13340
- ok: import_zod42.z.literal(false),
13341
- error: import_zod42.z.string(),
13342
- code: import_zod42.z.enum(["BRAND_NOT_FOUND", "WEEK_HAS_NO_SLOTS"]).optional()
13660
+ import_zod44.z.object({
13661
+ ok: import_zod44.z.literal(false),
13662
+ error: import_zod44.z.string(),
13663
+ code: import_zod44.z.enum(["BRAND_NOT_FOUND", "WEEK_HAS_NO_SLOTS"]).optional()
13343
13664
  })
13344
13665
  ]);
13345
- var rawContract12 = {
13666
+ var rawContract14 = {
13346
13667
  name: "generate_weekly_content",
13347
13668
  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.",
13348
- paramsSchema: ParamsSchema12,
13349
- outputSchema: OutputSchema12,
13669
+ paramsSchema: ParamsSchema14,
13670
+ outputSchema: OutputSchema14,
13350
13671
  // Auto-create de calendario es bootstrap reversible (volver a llamar usa el
13351
13672
  // calendario existente). NO publica nada externo. modo='generar' tampoco
13352
13673
  // publica — solo prepara contexto.
@@ -13391,7 +13712,7 @@ var rawContract12 = {
13391
13712
  sideEffects: ["reads_firestore", "writes_firestore"]
13392
13713
  };
13393
13714
  var weeklyContentBuilderContract = MartinContractSchema.parse(
13394
- rawContract12
13715
+ rawContract14
13395
13716
  );
13396
13717
  var DEFAULT_TEXT_THRESHOLD = 0.35;
13397
13718
  var DEFAULT_IMAGE_THRESHOLD = 0.08;
@@ -13580,69 +13901,69 @@ async function contentFinder(input) {
13580
13901
  }
13581
13902
  return result;
13582
13903
  }
13583
- var IncludeSchema = import_zod43.z.object({
13584
- products: import_zod43.z.boolean(),
13585
- collections: import_zod43.z.boolean(),
13586
- articles: import_zod43.z.boolean(),
13587
- pages: import_zod43.z.boolean()
13904
+ var IncludeSchema = import_zod45.z.object({
13905
+ products: import_zod45.z.boolean(),
13906
+ collections: import_zod45.z.boolean(),
13907
+ articles: import_zod45.z.boolean(),
13908
+ pages: import_zod45.z.boolean()
13588
13909
  }).describe(
13589
13910
  "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."
13590
13911
  );
13591
- var LimitSchema = import_zod43.z.object({
13592
- products: import_zod43.z.number().int().min(0).max(20),
13593
- collections: import_zod43.z.number().int().min(0).max(10),
13594
- articles: import_zod43.z.number().int().min(0).max(20),
13595
- pages: import_zod43.z.number().int().min(0).max(10)
13912
+ var LimitSchema = import_zod45.z.object({
13913
+ products: import_zod45.z.number().int().min(0).max(20),
13914
+ collections: import_zod45.z.number().int().min(0).max(10),
13915
+ articles: import_zod45.z.number().int().min(0).max(20),
13916
+ pages: import_zod45.z.number().int().min(0).max(10)
13596
13917
  }).describe(
13597
13918
  "Per-category result count caps. Defaults: products 5, collections 3, articles 5, pages 2."
13598
13919
  );
13599
- var ParamsSchema13 = import_zod43.z.object({
13600
- tenantId: import_zod43.z.string().min(1).describe("Tenant identifier (the business account)."),
13601
- brandId: import_zod43.z.string().min(1).describe("Brand identifier within the tenant."),
13602
- contexto: import_zod43.z.string().min(1).describe(
13920
+ var ParamsSchema15 = import_zod45.z.object({
13921
+ tenantId: import_zod45.z.string().min(1).describe("Tenant identifier (the business account)."),
13922
+ brandId: import_zod45.z.string().min(1).describe("Brand identifier within the tenant."),
13923
+ contexto: import_zod45.z.string().min(1).describe(
13603
13924
  "Search context: a paragraph, keyword, or intent string. Embedded for vector search."
13604
13925
  ),
13605
- fecha: import_zod43.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
13926
+ fecha: import_zod45.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
13606
13927
  "Content date in YYYY-MM-DD. Used to detect active season and prioritize matching collections."
13607
13928
  ),
13608
13929
  include: IncludeSchema,
13609
13930
  limit: LimitSchema,
13610
- diversidad: import_zod43.z.boolean().describe(
13931
+ diversidad: import_zod45.z.boolean().describe(
13611
13932
  "Whether to apply Jaccard title diversification + handle dedupe to results. Default true."
13612
13933
  ),
13613
- mode: import_zod43.z.enum(["text", "hybrid"]).optional().describe(
13934
+ mode: import_zod45.z.enum(["text", "hybrid"]).optional().describe(
13614
13935
  "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."
13615
13936
  )
13616
13937
  });
13617
- var VectorResultSchema = import_zod43.z.object({
13618
- id: import_zod43.z.string(),
13619
- similarity: import_zod43.z.number().optional()
13938
+ var VectorResultSchema = import_zod45.z.object({
13939
+ id: import_zod45.z.string(),
13940
+ similarity: import_zod45.z.number().optional()
13620
13941
  }).passthrough();
13621
- var TemporadaSchema2 = import_zod43.z.object({
13622
- coleccion: import_zod43.z.string().nullable(),
13623
- titulo: import_zod43.z.string().nullable(),
13624
- razon: import_zod43.z.string().nullable(),
13625
- fechaInicio: import_zod43.z.string().nullable(),
13626
- fechaFin: import_zod43.z.string().nullable()
13942
+ var TemporadaSchema2 = import_zod45.z.object({
13943
+ coleccion: import_zod45.z.string().nullable(),
13944
+ titulo: import_zod45.z.string().nullable(),
13945
+ razon: import_zod45.z.string().nullable(),
13946
+ fechaInicio: import_zod45.z.string().nullable(),
13947
+ fechaFin: import_zod45.z.string().nullable()
13627
13948
  });
13628
- var SuggestedActionSchema2 = import_zod43.z.record(import_zod43.z.string(), import_zod43.z.unknown());
13629
- var DetectedConflictSchema2 = import_zod43.z.record(import_zod43.z.string(), import_zod43.z.unknown());
13630
- var OutputSchema13 = import_zod43.z.object({
13631
- productos: import_zod43.z.array(VectorResultSchema),
13632
- colecciones: import_zod43.z.array(VectorResultSchema),
13633
- articles: import_zod43.z.array(VectorResultSchema),
13634
- pages: import_zod43.z.array(VectorResultSchema),
13635
- _instrucciones: import_zod43.z.string(),
13636
- _negativePrompt: import_zod43.z.string(),
13949
+ var SuggestedActionSchema2 = import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown());
13950
+ var DetectedConflictSchema2 = import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown());
13951
+ var OutputSchema15 = import_zod45.z.object({
13952
+ productos: import_zod45.z.array(VectorResultSchema),
13953
+ colecciones: import_zod45.z.array(VectorResultSchema),
13954
+ articles: import_zod45.z.array(VectorResultSchema),
13955
+ pages: import_zod45.z.array(VectorResultSchema),
13956
+ _instrucciones: import_zod45.z.string(),
13957
+ _negativePrompt: import_zod45.z.string(),
13637
13958
  _temporadaActiva: TemporadaSchema2.nullable(),
13638
13959
  _detectedConflict: DetectedConflictSchema2.optional(),
13639
- _suggestedActions: import_zod43.z.array(SuggestedActionSchema2)
13960
+ _suggestedActions: import_zod45.z.array(SuggestedActionSchema2)
13640
13961
  });
13641
- var rawContract13 = {
13962
+ var rawContract15 = {
13642
13963
  name: "find_content_for_topic",
13643
13964
  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).",
13644
- paramsSchema: ParamsSchema13,
13645
- outputSchema: OutputSchema13,
13965
+ paramsSchema: ParamsSchema15,
13966
+ outputSchema: OutputSchema15,
13646
13967
  // Lectura pura (vector search). Failures internas en search degradan a
13647
13968
  // arrays vacíos — el helper NO falla.
13648
13969
  requiresConfirmation: false,
@@ -13665,7 +13986,7 @@ var rawContract13 = {
13665
13986
  sideEffects: ["reads_firestore"]
13666
13987
  };
13667
13988
  var contentFinderContract = MartinContractSchema.parse(
13668
- rawContract13
13989
+ rawContract15
13669
13990
  );
13670
13991
  var DEFAULT_PHOTO_THRESHOLD = 0.7;
13671
13992
  var DEFAULT_SHOPIFY_THRESHOLD = 0.65;
@@ -13812,51 +14133,51 @@ async function slotAssetFinder(input) {
13812
14133
  _fuente: fuente
13813
14134
  };
13814
14135
  }
13815
- var ParamsSchema14 = import_zod44.z.object({
13816
- tenantId: import_zod44.z.string().min(1).describe("Tenant identifier (the business account)."),
13817
- brandId: import_zod44.z.string().min(1).describe("Brand identifier within the tenant."),
13818
- keyword: import_zod44.z.string().min(1).describe(
14136
+ var ParamsSchema16 = import_zod46.z.object({
14137
+ tenantId: import_zod46.z.string().min(1).describe("Tenant identifier (the business account)."),
14138
+ brandId: import_zod46.z.string().min(1).describe("Brand identifier within the tenant."),
14139
+ keyword: import_zod46.z.string().min(1).describe(
13819
14140
  "Slot keyword to search photos for. Used as embedding query for multimodal vector search."
13820
14141
  ),
13821
- plataforma: import_zod44.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe(
14142
+ plataforma: import_zod46.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe(
13822
14143
  "Target platform \u2014 determines the variant format to resolve (gbp_4_3, blog_3_2, ig_4_5, ig_1_1)."
13823
14144
  ),
13824
- fecha: import_zod44.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
14145
+ fecha: import_zod46.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
13825
14146
  "Slot date in YYYY-MM-DD. Used to detect active season and apply seasonal catalog overrides."
13826
14147
  ),
13827
- limit: import_zod44.z.number().int().min(1).max(10).optional().describe("Max number of photos to return. Default 5. Max 10.")
14148
+ limit: import_zod46.z.number().int().min(1).max(10).optional().describe("Max number of photos to return. Default 5. Max 10.")
13828
14149
  });
13829
- var TemporadaSchema22 = import_zod44.z.object({
13830
- coleccion: import_zod44.z.string().nullable(),
13831
- titulo: import_zod44.z.string().nullable(),
13832
- razon: import_zod44.z.string().nullable(),
13833
- fechaInicio: import_zod44.z.string().nullable(),
13834
- fechaFin: import_zod44.z.string().nullable()
14150
+ var TemporadaSchema22 = import_zod46.z.object({
14151
+ coleccion: import_zod46.z.string().nullable(),
14152
+ titulo: import_zod46.z.string().nullable(),
14153
+ razon: import_zod46.z.string().nullable(),
14154
+ fechaInicio: import_zod46.z.string().nullable(),
14155
+ fechaFin: import_zod46.z.string().nullable()
13835
14156
  });
13836
- var OutputSchema14 = import_zod44.z.discriminatedUnion("ok", [
14157
+ var OutputSchema16 = import_zod46.z.discriminatedUnion("ok", [
13837
14158
  // Note: success case does not include `ok: true` literal in helper return —
13838
14159
  // helper returns the success shape directly. Adapter to discriminated union
13839
14160
  // happens at wrapper level if needed; here we accept both shapes.
13840
- import_zod44.z.object({
13841
- ok: import_zod44.z.literal(true),
13842
- fotos: import_zod44.z.array(import_zod44.z.record(import_zod44.z.string(), import_zod44.z.unknown())),
13843
- _instrucciones: import_zod44.z.string(),
13844
- _negativePrompt: import_zod44.z.string(),
14161
+ import_zod46.z.object({
14162
+ ok: import_zod46.z.literal(true),
14163
+ fotos: import_zod46.z.array(import_zod46.z.record(import_zod46.z.string(), import_zod46.z.unknown())),
14164
+ _instrucciones: import_zod46.z.string(),
14165
+ _negativePrompt: import_zod46.z.string(),
13845
14166
  _temporadaActiva: TemporadaSchema22.nullable(),
13846
- _bloqueoProducto: import_zod44.z.boolean(),
13847
- _fuente: import_zod44.z.enum(["tenant", "shopify_product"])
14167
+ _bloqueoProducto: import_zod46.z.boolean(),
14168
+ _fuente: import_zod46.z.enum(["tenant", "shopify_product"])
13848
14169
  }),
13849
- import_zod44.z.object({
13850
- ok: import_zod44.z.literal(false),
13851
- error: import_zod44.z.string(),
13852
- code: import_zod44.z.enum(["BRAND_NOT_FOUND"]).optional()
14170
+ import_zod46.z.object({
14171
+ ok: import_zod46.z.literal(false),
14172
+ error: import_zod46.z.string(),
14173
+ code: import_zod46.z.enum(["BRAND_NOT_FOUND"]).optional()
13853
14174
  })
13854
14175
  ]);
13855
- var rawContract14 = {
14176
+ var rawContract16 = {
13856
14177
  name: "get_photos_for_slot",
13857
14178
  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.",
13858
- paramsSchema: ParamsSchema14,
13859
- outputSchema: OutputSchema14,
14179
+ paramsSchema: ParamsSchema16,
14180
+ outputSchema: OutputSchema16,
13860
14181
  requiresConfirmation: false,
13861
14182
  destructive: false,
13862
14183
  affectsPublication: false,
@@ -13883,7 +14204,7 @@ var rawContract14 = {
13883
14204
  sideEffects: ["reads_firestore"]
13884
14205
  };
13885
14206
  var slotAssetFinderContract = MartinContractSchema.parse(
13886
- rawContract14
14207
+ rawContract16
13887
14208
  );
13888
14209
  var DEFAULT_SIMILARITY_THRESHOLD = 0.6;
13889
14210
  function cosineSimilarity(a, b) {
@@ -13960,44 +14281,44 @@ async function canvaTemplateSelector(input) {
13960
14281
  _instrucciones: "Plantilla Canva seleccionada. Llama a marketingDesignWithCanva({contenidoId, plantillaId, fotoVariantePath, textos}) para renderizar la pieza final."
13961
14282
  };
13962
14283
  }
13963
- var ParamsSchema15 = import_zod45.z.object({
13964
- tenantId: import_zod45.z.string().min(1).describe("Tenant identifier (the business account)."),
13965
- brandId: import_zod45.z.string().min(1).describe("Brand identifier within the tenant."),
13966
- plataforma: import_zod45.z.string().min(1).describe(
14284
+ var ParamsSchema17 = import_zod47.z.object({
14285
+ tenantId: import_zod47.z.string().min(1).describe("Tenant identifier (the business account)."),
14286
+ brandId: import_zod47.z.string().min(1).describe("Brand identifier within the tenant."),
14287
+ plataforma: import_zod47.z.string().min(1).describe(
13967
14288
  "Target platform (e.g. 'gbp', 'shopify_blog', 'instagram'). Filters templates that declare this plataforma."
13968
14289
  ),
13969
- tipoContenido: import_zod45.z.string().min(1).describe(
14290
+ tipoContenido: import_zod47.z.string().min(1).describe(
13970
14291
  "Content type (e.g. 'post', 'carousel', 'story', 'blog'). Filters templates that declare this tipoContenido."
13971
14292
  ),
13972
- keyword: import_zod45.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
14293
+ keyword: import_zod47.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
13973
14294
  });
13974
- var OutputSchema15 = import_zod45.z.union([
13975
- import_zod45.z.object({
13976
- plantillaId: import_zod45.z.string(),
13977
- titulo: import_zod45.z.string().nullable(),
13978
- thumbnailUrl: import_zod45.z.string().nullable(),
13979
- similarity: import_zod45.z.number(),
13980
- _instrucciones: import_zod45.z.string()
14295
+ var OutputSchema17 = import_zod47.z.union([
14296
+ import_zod47.z.object({
14297
+ plantillaId: import_zod47.z.string(),
14298
+ titulo: import_zod47.z.string().nullable(),
14299
+ thumbnailUrl: import_zod47.z.string().nullable(),
14300
+ similarity: import_zod47.z.number(),
14301
+ _instrucciones: import_zod47.z.string()
13981
14302
  }),
13982
- import_zod45.z.object({
13983
- plantillaId: import_zod45.z.null(),
13984
- motivo: import_zod45.z.enum([
14303
+ import_zod47.z.object({
14304
+ plantillaId: import_zod47.z.null(),
14305
+ motivo: import_zod47.z.enum([
13985
14306
  "brand_no_encontrada",
13986
14307
  "no_canva",
13987
14308
  "no_match_plataforma",
13988
14309
  "modo_cliente_no_soportado",
13989
14310
  "similarity_baja"
13990
14311
  ]),
13991
- similarity: import_zod45.z.number().optional(),
13992
- _instrucciones: import_zod45.z.string().optional(),
13993
- _todo: import_zod45.z.string().optional()
14312
+ similarity: import_zod47.z.number().optional(),
14313
+ _instrucciones: import_zod47.z.string().optional(),
14314
+ _todo: import_zod47.z.string().optional()
13994
14315
  })
13995
14316
  ]);
13996
- var rawContract15 = {
14317
+ var rawContract17 = {
13997
14318
  name: "select_canva_template",
13998
14319
  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.",
13999
- paramsSchema: ParamsSchema15,
14000
- outputSchema: OutputSchema15,
14320
+ paramsSchema: ParamsSchema17,
14321
+ outputSchema: OutputSchema17,
14001
14322
  // Lectura pura. NO falla — todo "no encontré" es resultado válido del helper.
14002
14323
  requiresConfirmation: false,
14003
14324
  destructive: false,
@@ -14024,7 +14345,7 @@ var rawContract15 = {
14024
14345
  sideEffects: ["reads_firestore"]
14025
14346
  };
14026
14347
  var canvaTemplateSelectorContract = MartinContractSchema.parse(
14027
- rawContract15
14348
+ rawContract17
14028
14349
  );
14029
14350
  function buildDirectorPlanInstrucciones(catalogoVisual) {
14030
14351
  const etiquetas = catalogoVisual.etiquetas || {};
@@ -14248,38 +14569,38 @@ async function photoDirectorExecute(input) {
14248
14569
  _instrucciones: buildDirectorExecuteSuccessInstrucciones(result.balanceAfter)
14249
14570
  };
14250
14571
  }
14251
- var PlanParamsSchema = import_zod46.z.object({
14252
- tenantId: import_zod46.z.string().min(1).describe("Tenant identifier (the business account)."),
14253
- fotoId: import_zod46.z.string().min(1).describe("Photo ID in marketing_fotos. The photo must have an archivoOriginal uploaded.")
14572
+ var PlanParamsSchema = import_zod48.z.object({
14573
+ tenantId: import_zod48.z.string().min(1).describe("Tenant identifier (the business account)."),
14574
+ fotoId: import_zod48.z.string().min(1).describe("Photo ID in marketing_fotos. The photo must have an archivoOriginal uploaded.")
14254
14575
  });
14255
- var PlanContextSchema = import_zod46.z.object({
14256
- fotoId: import_zod46.z.string(),
14257
- archivoOriginal: import_zod46.z.string(),
14258
- estrategia: import_zod46.z.string(),
14259
- brandBrief: import_zod46.z.object({
14260
- segmento: import_zod46.z.string().nullable(),
14261
- personalidad: import_zod46.z.unknown().nullable()
14576
+ var PlanContextSchema = import_zod48.z.object({
14577
+ fotoId: import_zod48.z.string(),
14578
+ archivoOriginal: import_zod48.z.string(),
14579
+ estrategia: import_zod48.z.string(),
14580
+ brandBrief: import_zod48.z.object({
14581
+ segmento: import_zod48.z.string().nullable(),
14582
+ personalidad: import_zod48.z.unknown().nullable()
14262
14583
  }),
14263
- visualRules: import_zod46.z.record(import_zod46.z.string(), import_zod46.z.unknown()),
14264
- catalogoVisual: import_zod46.z.record(import_zod46.z.string(), import_zod46.z.unknown()),
14265
- estiloVisual: import_zod46.z.unknown().nullable(),
14266
- notaTenant: import_zod46.z.unknown().nullable(),
14267
- productoLinkeadoManual: import_zod46.z.unknown().nullable(),
14268
- _instrucciones: import_zod46.z.string()
14584
+ visualRules: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()),
14585
+ catalogoVisual: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()),
14586
+ estiloVisual: import_zod48.z.unknown().nullable(),
14587
+ notaTenant: import_zod48.z.unknown().nullable(),
14588
+ productoLinkeadoManual: import_zod48.z.unknown().nullable(),
14589
+ _instrucciones: import_zod48.z.string()
14269
14590
  });
14270
- var PlanOutputSchema = import_zod46.z.discriminatedUnion("ok", [
14271
- import_zod46.z.object({
14272
- ok: import_zod46.z.literal(true),
14273
- imageBase64: import_zod46.z.string().describe("Compressed photo as base64 for transport to the LLM."),
14591
+ var PlanOutputSchema = import_zod48.z.discriminatedUnion("ok", [
14592
+ import_zod48.z.object({
14593
+ ok: import_zod48.z.literal(true),
14594
+ imageBase64: import_zod48.z.string().describe("Compressed photo as base64 for transport to the LLM."),
14274
14595
  context: PlanContextSchema
14275
14596
  }),
14276
- import_zod46.z.object({
14277
- ok: import_zod46.z.literal(false),
14278
- code: import_zod46.z.string(),
14279
- error: import_zod46.z.string(),
14280
- _instrucciones: import_zod46.z.string().optional(),
14281
- fix: import_zod46.z.string().optional(),
14282
- fotoId: import_zod46.z.string().optional()
14597
+ import_zod48.z.object({
14598
+ ok: import_zod48.z.literal(false),
14599
+ code: import_zod48.z.string(),
14600
+ error: import_zod48.z.string(),
14601
+ _instrucciones: import_zod48.z.string().optional(),
14602
+ fix: import_zod48.z.string().optional(),
14603
+ fotoId: import_zod48.z.string().optional()
14283
14604
  })
14284
14605
  ]);
14285
14606
  var planRawContract = {
@@ -14309,43 +14630,43 @@ var planRawContract = {
14309
14630
  var photoDirectorPlanContract = MartinContractSchema.parse(
14310
14631
  planRawContract
14311
14632
  );
14312
- var ExecuteParamsSchema = import_zod46.z.object({
14633
+ var ExecuteParamsSchema = import_zod48.z.object({
14313
14634
  // tenantId is injected by the wrapper from ctx (A7 module-scope pattern,
14314
14635
  // even though the helper function itself derives tenantId via the foto's
14315
14636
  // brandId). Needed here for extractTargetPath canonical path.
14316
- tenantId: import_zod46.z.string().min(1).describe("Tenant identifier (the business account)."),
14317
- fotoId: import_zod46.z.string().min(1).describe("Photo ID to edit (must have been planned via plan_photo_edit)."),
14318
- prompt: import_zod46.z.string().nullable().describe(
14637
+ tenantId: import_zod48.z.string().min(1).describe("Tenant identifier (the business account)."),
14638
+ fotoId: import_zod48.z.string().min(1).describe("Photo ID to edit (must have been planned via plan_photo_edit)."),
14639
+ prompt: import_zod48.z.string().nullable().describe(
14319
14640
  "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)."
14320
14641
  ),
14321
- acciones: import_zod46.z.array(import_zod46.z.enum(["edit_background", "none"])).describe(
14642
+ acciones: import_zod48.z.array(import_zod48.z.enum(["edit_background", "none"])).describe(
14322
14643
  "Edit operations to perform. Use ['edit_background'] for AI background edit, ['none'] when strategy is 'tal_cual'."
14323
14644
  ),
14324
- descripcion: import_zod46.z.string().describe("Semantic description in Spanish (saved as descripcionVision on the photo)."),
14325
- tipo: import_zod46.z.string().nullable().describe("Photo type from the brand catalogoVisual. null only if catalog has no matching type."),
14326
- tagsPrimarios: import_zod46.z.array(import_zod46.z.string()).describe("Primary tags from the catalogoVisual."),
14327
- tagsSecundarios: import_zod46.z.array(import_zod46.z.string()).describe("Secondary tags from the catalogoVisual."),
14328
- tagsContexto: import_zod46.z.array(import_zod46.z.string()).describe("Contextual tags (occasion, mood, style).")
14645
+ descripcion: import_zod48.z.string().describe("Semantic description in Spanish (saved as descripcionVision on the photo)."),
14646
+ tipo: import_zod48.z.string().nullable().describe("Photo type from the brand catalogoVisual. null only if catalog has no matching type."),
14647
+ tagsPrimarios: import_zod48.z.array(import_zod48.z.string()).describe("Primary tags from the catalogoVisual."),
14648
+ tagsSecundarios: import_zod48.z.array(import_zod48.z.string()).describe("Secondary tags from the catalogoVisual."),
14649
+ tagsContexto: import_zod48.z.array(import_zod48.z.string()).describe("Contextual tags (occasion, mood, style).")
14329
14650
  });
14330
- var ExecuteOutputSchema = import_zod46.z.discriminatedUnion("ok", [
14331
- import_zod46.z.object({
14332
- ok: import_zod46.z.literal(true),
14333
- fotoId: import_zod46.z.string(),
14334
- archivoEditado: import_zod46.z.string().optional(),
14335
- thumbnailUrl: import_zod46.z.string().optional(),
14336
- iteracion: import_zod46.z.number().optional(),
14337
- editCosto: import_zod46.z.number().optional(),
14338
- creditsConsumed: import_zod46.z.number().optional(),
14339
- balanceAfter: import_zod46.z.number().optional(),
14340
- imageBase64: import_zod46.z.string().nullable(),
14341
- _instrucciones: import_zod46.z.string()
14651
+ var ExecuteOutputSchema = import_zod48.z.discriminatedUnion("ok", [
14652
+ import_zod48.z.object({
14653
+ ok: import_zod48.z.literal(true),
14654
+ fotoId: import_zod48.z.string(),
14655
+ archivoEditado: import_zod48.z.string().optional(),
14656
+ thumbnailUrl: import_zod48.z.string().optional(),
14657
+ iteracion: import_zod48.z.number().optional(),
14658
+ editCosto: import_zod48.z.number().optional(),
14659
+ creditsConsumed: import_zod48.z.number().optional(),
14660
+ balanceAfter: import_zod48.z.number().optional(),
14661
+ imageBase64: import_zod48.z.string().nullable(),
14662
+ _instrucciones: import_zod48.z.string()
14342
14663
  }),
14343
- import_zod46.z.object({
14344
- ok: import_zod46.z.literal(false),
14345
- error: import_zod46.z.string(),
14346
- code: import_zod46.z.string(),
14347
- details: import_zod46.z.record(import_zod46.z.string(), import_zod46.z.unknown()).optional(),
14348
- _instrucciones: import_zod46.z.string()
14664
+ import_zod48.z.object({
14665
+ ok: import_zod48.z.literal(false),
14666
+ error: import_zod48.z.string(),
14667
+ code: import_zod48.z.string(),
14668
+ details: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()).optional(),
14669
+ _instrucciones: import_zod48.z.string()
14349
14670
  })
14350
14671
  ]);
14351
14672
  var executeRawContract = {
@@ -15053,6 +15374,12 @@ function callGetCalendar(input) {
15053
15374
  function callAddCalendarSlot(input) {
15054
15375
  return callCF("marketingAddCalendarSlotCallable", input);
15055
15376
  }
15377
+ function callSeoSnapshotReader(input) {
15378
+ return callCF("marketingSeoSnapshotReaderCallable", input);
15379
+ }
15380
+ function callCollectionsReader(input) {
15381
+ return callCF("marketingCollectionsReaderCallable", input);
15382
+ }
15056
15383
  function callBrandBriefWriter(input) {
15057
15384
  return callCF("marketingBrandBriefWriterCallable", input);
15058
15385
  }
@@ -15121,7 +15448,7 @@ function callListarRutinas(input) {
15121
15448
  }
15122
15449
 
15123
15450
  // src/tools/marketing/photos.ts
15124
- var import_zod47 = require("zod");
15451
+ var import_zod49 = require("zod");
15125
15452
 
15126
15453
  // src/services/marketingEmbeddings.ts
15127
15454
  var import_google_auth_library = require("google-auth-library");
@@ -15417,11 +15744,11 @@ REGLAS:
15417
15744
 
15418
15745
  USAR: antes de generar contenido de cualquier slot del calendario.`,
15419
15746
  {
15420
- brandId: import_zod47.z.string().optional().describe("ID de la brand"),
15421
- keyword: import_zod47.z.string().describe("Keyword del slot"),
15422
- plataforma: import_zod47.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
15423
- fecha: import_zod47.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
15424
- limit: import_zod47.z.number().int().min(1).max(10).default(5)
15747
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
15748
+ keyword: import_zod49.z.string().describe("Keyword del slot"),
15749
+ plataforma: import_zod49.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
15750
+ fecha: import_zod49.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
15751
+ limit: import_zod49.z.number().int().min(1).max(10).default(5)
15425
15752
  },
15426
15753
  async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
15427
15754
  const tenantId = session.requireTenant();
@@ -15469,7 +15796,7 @@ DESPUES de ver la foto, decide:
15469
15796
 
15470
15797
  Luego llama execute_photo_edit con tu analisis y prompt.`,
15471
15798
  {
15472
- fotoId: import_zod47.z.string().describe("ID de la foto")
15799
+ fotoId: import_zod49.z.string().describe("ID de la foto")
15473
15800
  },
15474
15801
  async ({ fotoId }) => {
15475
15802
  const tenantId = session.requireTenant();
@@ -15517,14 +15844,14 @@ Retorna la foto editada para que la revises. Si no te gusta:
15517
15844
 
15518
15845
  Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
15519
15846
  {
15520
- fotoId: import_zod47.z.string(),
15521
- prompt: import_zod47.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
15522
- acciones: import_zod47.z.array(import_zod47.z.enum(["edit_background", "none"])),
15523
- descripcion: import_zod47.z.string().describe("Descripcion semantica en espanol"),
15524
- tipo: import_zod47.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
15525
- tagsPrimarios: import_zod47.z.array(import_zod47.z.string()),
15526
- tagsSecundarios: import_zod47.z.array(import_zod47.z.string()),
15527
- tagsContexto: import_zod47.z.array(import_zod47.z.string())
15847
+ fotoId: import_zod49.z.string(),
15848
+ prompt: import_zod49.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
15849
+ acciones: import_zod49.z.array(import_zod49.z.enum(["edit_background", "none"])),
15850
+ descripcion: import_zod49.z.string().describe("Descripcion semantica en espanol"),
15851
+ tipo: import_zod49.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
15852
+ tagsPrimarios: import_zod49.z.array(import_zod49.z.string()),
15853
+ tagsSecundarios: import_zod49.z.array(import_zod49.z.string()),
15854
+ tagsContexto: import_zod49.z.array(import_zod49.z.string())
15528
15855
  },
15529
15856
  async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
15530
15857
  const tenantId = session.requireTenant();
@@ -15600,7 +15927,7 @@ Retorna:
15600
15927
  - creditos: { balance, planId, periodEnd, costoPhotoEdit }
15601
15928
  - _instrucciones: que hacer segun el estado`,
15602
15929
  {
15603
- fotoId: import_zod47.z.string().describe("ID de la foto")
15930
+ fotoId: import_zod49.z.string().describe("ID de la foto")
15604
15931
  },
15605
15932
  async ({ fotoId }) => {
15606
15933
  const tenantId = session.requireTenant();
@@ -15700,11 +16027,11 @@ Retorna:
15700
16027
  "find_products_for_content",
15701
16028
  `[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.`,
15702
16029
  {
15703
- brandId: import_zod47.z.string().optional().describe("ID de la brand"),
15704
- contexto: import_zod47.z.string().describe("Parrafo, keyword o intencion del contenido"),
15705
- fecha: import_zod47.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
15706
- limit: import_zod47.z.number().int().min(1).max(10).default(5),
15707
- diversidad: import_zod47.z.boolean().default(true)
16030
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16031
+ contexto: import_zod49.z.string().describe("Parrafo, keyword o intencion del contenido"),
16032
+ fecha: import_zod49.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
16033
+ limit: import_zod49.z.number().int().min(1).max(10).default(5),
16034
+ diversidad: import_zod49.z.boolean().default(true)
15708
16035
  },
15709
16036
  async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
15710
16037
  console.warn(
@@ -15781,10 +16108,10 @@ RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no
15781
16108
 
15782
16109
  USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
15783
16110
  {
15784
- brandId: import_zod47.z.string().optional().describe("ID de la brand"),
15785
- plataforma: import_zod47.z.string().describe("gbp | instagram | shopify_blog"),
15786
- tipoContenido: import_zod47.z.string().describe("post | carousel | story | blog"),
15787
- keyword: import_zod47.z.string().describe("Keyword del slot")
16111
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16112
+ plataforma: import_zod49.z.string().describe("gbp | instagram | shopify_blog"),
16113
+ tipoContenido: import_zod49.z.string().describe("post | carousel | story | blog"),
16114
+ keyword: import_zod49.z.string().describe("Keyword del slot")
15788
16115
  },
15789
16116
  async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
15790
16117
  const tenantId = session.requireTenant();
@@ -15829,15 +16156,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
15829
16156
 
15830
16157
  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.`,
15831
16158
  {
15832
- brandId: import_zod47.z.string().optional().describe("ID de la brand"),
15833
- semana: import_zod47.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD").describe("Semana en ISO del lunes"),
15834
- necesidades: import_zod47.z.array(
15835
- import_zod47.z.object({
15836
- tema: import_zod47.z.string(),
15837
- keyword: import_zod47.z.string(),
15838
- cantidadSugerida: import_zod47.z.number().int().positive(),
15839
- razon: import_zod47.z.string(),
15840
- slotsAfectados: import_zod47.z.array(import_zod47.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
16159
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16160
+ 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"),
16161
+ necesidades: import_zod49.z.array(
16162
+ import_zod49.z.object({
16163
+ tema: import_zod49.z.string(),
16164
+ keyword: import_zod49.z.string(),
16165
+ cantidadSugerida: import_zod49.z.number().int().positive(),
16166
+ razon: import_zod49.z.string(),
16167
+ slotsAfectados: import_zod49.z.array(import_zod49.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
15841
16168
  })
15842
16169
  ).min(1)
15843
16170
  },
@@ -15900,7 +16227,7 @@ IMPORTANTE \u2014 campo 'razon': Este texto lo ve el tenant en la app. Escribe e
15900
16227
  }
15901
16228
 
15902
16229
  // src/tools/marketing/content.ts
15903
- var import_zod48 = require("zod");
16230
+ var import_zod50 = require("zod");
15904
16231
  var _logOverride = null;
15905
16232
  async function logToMcpLogs(entry) {
15906
16233
  if (_logOverride) return _logOverride(entry);
@@ -15913,22 +16240,22 @@ async function logToMcpLogs(entry) {
15913
16240
  } catch {
15914
16241
  }
15915
16242
  }
15916
- var IncludeSchema2 = import_zod48.z.object({
15917
- products: import_zod48.z.boolean().default(true),
15918
- collections: import_zod48.z.boolean().default(true),
15919
- articles: import_zod48.z.boolean().default(true),
15920
- pages: import_zod48.z.boolean().default(false)
16243
+ var IncludeSchema2 = import_zod50.z.object({
16244
+ products: import_zod50.z.boolean().default(true),
16245
+ collections: import_zod50.z.boolean().default(true),
16246
+ articles: import_zod50.z.boolean().default(true),
16247
+ pages: import_zod50.z.boolean().default(false)
15921
16248
  }).default({
15922
16249
  products: true,
15923
16250
  collections: true,
15924
16251
  articles: true,
15925
16252
  pages: false
15926
16253
  });
15927
- var LimitSchema2 = import_zod48.z.object({
15928
- products: import_zod48.z.number().int().min(0).max(20).default(5),
15929
- collections: import_zod48.z.number().int().min(0).max(10).default(3),
15930
- articles: import_zod48.z.number().int().min(0).max(20).default(5),
15931
- pages: import_zod48.z.number().int().min(0).max(10).default(2)
16254
+ var LimitSchema2 = import_zod50.z.object({
16255
+ products: import_zod50.z.number().int().min(0).max(20).default(5),
16256
+ collections: import_zod50.z.number().int().min(0).max(10).default(3),
16257
+ articles: import_zod50.z.number().int().min(0).max(20).default(5),
16258
+ pages: import_zod50.z.number().int().min(0).max(10).default(2)
15932
16259
  }).default({ products: 5, collections: 3, articles: 5, pages: 2 });
15933
16260
  function registerContentTools(server, session) {
15934
16261
  server.tool(
@@ -15956,13 +16283,13 @@ MODOS (parametro mode):
15956
16283
 
15957
16284
  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.`,
15958
16285
  {
15959
- brandId: import_zod48.z.string().optional().describe("ID de la brand"),
15960
- contexto: import_zod48.z.string().min(1).describe("Parrafo, keyword o intencion"),
15961
- fecha: import_zod48.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
16286
+ brandId: import_zod50.z.string().optional().describe("ID de la brand"),
16287
+ contexto: import_zod50.z.string().min(1).describe("Parrafo, keyword o intencion"),
16288
+ fecha: import_zod50.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
15962
16289
  include: IncludeSchema2.optional(),
15963
16290
  limit: LimitSchema2.optional(),
15964
- diversidad: import_zod48.z.boolean().default(true),
15965
- mode: import_zod48.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)")
16291
+ diversidad: import_zod50.z.boolean().default(true),
16292
+ 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)")
15966
16293
  },
15967
16294
  async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode }) => {
15968
16295
  const tenantId = session.requireTenant();
@@ -16173,8 +16500,8 @@ function registerMarketingTools(server, session) {
16173
16500
  "get_calendar",
16174
16501
  "Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
16175
16502
  {
16176
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16177
- mes: import_zod49.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
16503
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16504
+ mes: import_zod51.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
16178
16505
  },
16179
16506
  async ({ brandId: inputBrandId, mes }) => {
16180
16507
  const tenantId = session.requireTenant();
@@ -16210,29 +16537,46 @@ function registerMarketingTools(server, session) {
16210
16537
  );
16211
16538
  server.tool(
16212
16539
  "get_seo_snapshot",
16213
- "Lee el snapshot SEO de la brand: rank, keywords, oportunidades, competidores.",
16540
+ "Read the latest Semrush SEO snapshot for a brand: rank, top keywords, opportunities, competitors.",
16214
16541
  {
16215
- brandId: import_zod49.z.string().optional().describe("ID de la brand")
16542
+ brandId: import_zod51.z.string().optional().describe("Brand identifier within the tenant")
16216
16543
  },
16217
16544
  async ({ brandId: inputBrandId }) => {
16218
16545
  const tenantId = session.requireTenant();
16219
16546
  const brandId = inputBrandId ?? session.requireBrand();
16220
- const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
16221
- if (!brand) {
16222
- return { content: [{ type: "text", text: JSON.stringify({ error: `Brand "${brandId}" no encontrada` }) }] };
16223
- }
16224
- if (!brand.seoSnapshot) {
16225
- return { content: [{ type: "text", text: JSON.stringify({ error: "No hay snapshot SEO. Ejecuta la Cloud Function marketingSeoSnapshot primero." }) }] };
16547
+ const ctx = await buildContext(session, brandId);
16548
+ const result = await dispatchWithContract({
16549
+ contract: seoSnapshotReaderContract,
16550
+ helper: seoSnapshotReader,
16551
+ callable: callSeoSnapshotReader,
16552
+ input: { tenantId, brandId },
16553
+ ctx
16554
+ });
16555
+ let payload;
16556
+ if (result.state === "success" && result.structuredOutput) {
16557
+ const out = result.structuredOutput;
16558
+ if (out.ok) {
16559
+ payload = out.snapshot;
16560
+ } else {
16561
+ payload = { ok: false, code: out.code, mensaje: result.text };
16562
+ }
16563
+ } else {
16564
+ payload = {
16565
+ ok: false,
16566
+ state: result.state,
16567
+ mensaje: result.text,
16568
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
16569
+ };
16226
16570
  }
16227
- return { content: [{ type: "text", text: JSON.stringify(brand.seoSnapshot, null, 2) }] };
16571
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
16228
16572
  }
16229
16573
  );
16230
16574
  server.tool(
16231
16575
  "get_photo_gallery",
16232
16576
  "Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
16233
16577
  {
16234
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16235
- estado: import_zod49.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
16578
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16579
+ estado: import_zod51.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
16236
16580
  },
16237
16581
  async ({ brandId: inputBrandId, estado }) => {
16238
16582
  session.requireTenant();
@@ -16272,7 +16616,7 @@ function registerMarketingTools(server, session) {
16272
16616
  "generate_marketing_plan",
16273
16617
  "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.",
16274
16618
  {
16275
- brandId: import_zod49.z.string().optional().describe("ID de la brand")
16619
+ brandId: import_zod51.z.string().optional().describe("ID de la brand")
16276
16620
  },
16277
16621
  async ({ brandId: inputBrandId }) => {
16278
16622
  const tenantId = session.requireTenant();
@@ -16286,8 +16630,8 @@ function registerMarketingTools(server, session) {
16286
16630
  "save_marketing_plan",
16287
16631
  "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).",
16288
16632
  {
16289
- brandId: import_zod49.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
16290
- plan: import_zod49.z.record(import_zod49.z.string(), import_zod49.z.unknown()).describe("Full marketing plan object. Common fields: keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, coleccionesPriorizadas, blogStrategy.")
16633
+ brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
16634
+ 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.")
16291
16635
  },
16292
16636
  async ({ brandId: inputBrandId, plan }) => {
16293
16637
  const tenantId = session.requireTenant();
@@ -16315,9 +16659,9 @@ function registerMarketingTools(server, session) {
16315
16659
  "update_marketing_plan_field",
16316
16660
  "Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
16317
16661
  {
16318
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16319
- field: import_zod49.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
16320
- value: import_zod49.z.unknown().describe("Valor del campo")
16662
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16663
+ field: import_zod51.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
16664
+ value: import_zod51.z.unknown().describe("Valor del campo")
16321
16665
  },
16322
16666
  async ({ brandId: inputBrandId, field, value }) => {
16323
16667
  const tenantId = session.requireTenant();
@@ -16330,7 +16674,7 @@ function registerMarketingTools(server, session) {
16330
16674
  "generate_brand_brief",
16331
16675
  "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.",
16332
16676
  {
16333
- brandId: import_zod49.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
16677
+ brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
16334
16678
  },
16335
16679
  async ({ brandId: inputBrandId }) => {
16336
16680
  const tenantId = session.requireTenant();
@@ -16363,8 +16707,8 @@ function registerMarketingTools(server, session) {
16363
16707
  "save_brand_brief",
16364
16708
  "Save the Brand Brief at tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Partial merge \u2014 only the brandBrief field is overwritten.",
16365
16709
  {
16366
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16367
- brandBrief: import_zod49.z.record(import_zod49.z.string(), import_zod49.z.unknown()).describe("Full Brand Brief object.")
16710
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16711
+ brandBrief: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Full Brand Brief object.")
16368
16712
  },
16369
16713
  async ({ brandId: inputBrandId, brandBrief }) => {
16370
16714
  const tenantId = session.requireTenant();
@@ -16390,9 +16734,9 @@ function registerMarketingTools(server, session) {
16390
16734
  "generate_weekly_content",
16391
16735
  "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.",
16392
16736
  {
16393
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16394
- semana: import_zod49.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
16395
- modo: import_zod49.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
16737
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16738
+ semana: import_zod51.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
16739
+ 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'.")
16396
16740
  },
16397
16741
  async ({ brandId: inputBrandId, semana, modo }) => {
16398
16742
  const tenantId = session.requireTenant();
@@ -16420,14 +16764,14 @@ function registerMarketingTools(server, session) {
16420
16764
 
16421
16765
  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.`,
16422
16766
  {
16423
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16424
- plataforma: import_zod49.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
16425
- tipo: import_zod49.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
16426
- keyword: import_zod49.z.string().optional().describe("Keyword target"),
16427
- languageCode: import_zod49.z.string().optional().describe("Idioma (es/en)"),
16428
- fotoId: import_zod49.z.string().optional().describe("ID de la foto a asociar"),
16429
- datos: import_zod49.z.record(import_zod49.z.string(), import_zod49.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
16430
- calendarioItemRef: import_zod49.z.string().optional().describe("Referencia al item del calendario")
16767
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16768
+ plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
16769
+ tipo: import_zod51.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
16770
+ keyword: import_zod51.z.string().optional().describe("Keyword target"),
16771
+ languageCode: import_zod51.z.string().optional().describe("Idioma (es/en)"),
16772
+ fotoId: import_zod51.z.string().optional().describe("ID de la foto a asociar"),
16773
+ datos: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
16774
+ calendarioItemRef: import_zod51.z.string().optional().describe("Referencia al item del calendario")
16431
16775
  },
16432
16776
  async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
16433
16777
  const tenantId = session.requireTenant();
@@ -16463,13 +16807,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
16463
16807
  NO puede cambiar: tenantId, brandId, id (inmutables).
16464
16808
  Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
16465
16809
  {
16466
- contenidoId: import_zod49.z.string().describe("ID del doc en marketing_contenido"),
16467
- datos: import_zod49.z.record(import_zod49.z.string(), import_zod49.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
16468
- fotoId: import_zod49.z.string().nullable().optional().describe("Actualizar foto asociada"),
16469
- keyword: import_zod49.z.string().nullable().optional().describe("Actualizar keyword"),
16470
- languageCode: import_zod49.z.string().optional().describe("Actualizar idioma"),
16471
- estado: import_zod49.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
16472
- calendarioItemRef: import_zod49.z.string().nullable().optional().describe("Vincular a un slot del calendario")
16810
+ contenidoId: import_zod51.z.string().describe("ID del doc en marketing_contenido"),
16811
+ 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: "..." }'),
16812
+ fotoId: import_zod51.z.string().nullable().optional().describe("Actualizar foto asociada"),
16813
+ keyword: import_zod51.z.string().nullable().optional().describe("Actualizar keyword"),
16814
+ languageCode: import_zod51.z.string().optional().describe("Actualizar idioma"),
16815
+ estado: import_zod51.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
16816
+ calendarioItemRef: import_zod51.z.string().nullable().optional().describe("Vincular a un slot del calendario")
16473
16817
  },
16474
16818
  async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
16475
16819
  const tenantId = session.requireTenant();
@@ -16503,19 +16847,19 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
16503
16847
  "add_calendar_slot",
16504
16848
  "Add a NEW slot to the editorial calendar. To modify an existing slot use update_calendar_slot instead.",
16505
16849
  {
16506
- brandId: import_zod49.z.string().describe('Brand ID (e.g. "ponch", "teleglobos").'),
16507
- mes: import_zod49.z.string().describe('Calendar month in YYYY-MM format (e.g. "2026-05").'),
16508
- semana: import_zod49.z.number().describe("Week number within the month (1-5)."),
16509
- slot: import_zod49.z.object({
16510
- dia: import_zod49.z.string().describe("Slot date in YYYY-MM-DD format. Must fall within the week's fechaInicio/fechaFin range."),
16511
- plataforma: import_zod49.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target publishing platform."),
16512
- tipo: import_zod49.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Content type."),
16513
- keyword: import_zod49.z.string().describe("Primary keyword for the content."),
16514
- tema: import_zod49.z.string().optional().describe("Content topic/theme. OMIT this field if it does not apply. Do NOT send empty string or null."),
16515
- productoId: import_zod49.z.string().optional().describe("Linked product ID. OMIT this field if no product is linked."),
16516
- estado: import_zod49.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe('Initial status. OMIT to use default "planificado".'),
16517
- locationId: import_zod49.z.string().optional().describe("GBP location ID \u2014 only when plataforma=gbp AND tenant is multi-location. OMIT otherwise."),
16518
- locationNombre: import_zod49.z.string().optional().describe("Human-readable GBP location name. OMIT if locationId is not provided.")
16850
+ brandId: import_zod51.z.string().describe('Brand ID (e.g. "ponch", "teleglobos").'),
16851
+ mes: import_zod51.z.string().describe('Calendar month in YYYY-MM format (e.g. "2026-05").'),
16852
+ semana: import_zod51.z.number().describe("Week number within the month (1-5)."),
16853
+ slot: import_zod51.z.object({
16854
+ dia: import_zod51.z.string().describe("Slot date in YYYY-MM-DD format. Must fall within the week's fechaInicio/fechaFin range."),
16855
+ plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target publishing platform."),
16856
+ tipo: import_zod51.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Content type."),
16857
+ keyword: import_zod51.z.string().describe("Primary keyword for the content."),
16858
+ 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."),
16859
+ productoId: import_zod51.z.string().optional().describe("Linked product ID. OMIT this field if no product is linked."),
16860
+ estado: import_zod51.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe('Initial status. OMIT to use default "planificado".'),
16861
+ locationId: import_zod51.z.string().optional().describe("GBP location ID \u2014 only when plataforma=gbp AND tenant is multi-location. OMIT otherwise."),
16862
+ locationNombre: import_zod51.z.string().optional().describe("Human-readable GBP location name. OMIT if locationId is not provided.")
16519
16863
  }).describe("New slot data.")
16520
16864
  },
16521
16865
  async ({ brandId, mes, semana, slot }) => {
@@ -16541,27 +16885,27 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
16541
16885
  "update_calendar_slot",
16542
16886
  "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.",
16543
16887
  {
16544
- brandId: import_zod49.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
16545
- mes: import_zod49.z.string().describe("Calendar month in YYYY-MM format."),
16546
- semana: import_zod49.z.number().describe("Week number within the month (1-5)."),
16547
- slotIndex: import_zod49.z.number().describe("Slot index (0-based). Must point to an existing slot \u2014 to add new slots use add_calendar_slot."),
16548
- cambios: import_zod49.z.object({
16549
- dia: import_zod49.z.string().nullable().optional().describe("Slot date in YYYY-MM-DD. OMIT if not changing date. Use null to explicitly clear."),
16550
- plataforma: import_zod49.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional().describe("OMIT if not changing platform."),
16551
- tipo: import_zod49.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional().describe("OMIT if not changing content type."),
16552
- keyword: import_zod49.z.string().nullable().optional().describe("OMIT if not changing keyword."),
16553
- tema: import_zod49.z.string().nullable().optional().describe("OMIT if not changing topic."),
16554
- productoId: import_zod49.z.string().nullable().optional().describe("OMIT if not changing linked product. Use null to unlink."),
16555
- estado: import_zod49.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("OMIT if not changing status manually."),
16556
- contenidoRef: import_zod49.z.string().nullable().optional().describe("OMIT \u2014 managed by the system, not by callers."),
16557
- fotoIdAsignada: import_zod49.z.string().nullable().optional().describe("Use assign_photo_to_content for photos. OMIT here."),
16558
- notas: import_zod49.z.array(NotaCalendarioSchema).optional().describe("Append-only notes for the slot."),
16559
- locationId: import_zod49.z.string().nullable().optional().describe("GBP location ID \u2014 only when plataforma=gbp."),
16560
- locationNombre: import_zod49.z.string().nullable().optional().describe("Human-readable GBP location name (for UI).")
16888
+ brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
16889
+ mes: import_zod51.z.string().describe("Calendar month in YYYY-MM format."),
16890
+ semana: import_zod51.z.number().describe("Week number within the month (1-5)."),
16891
+ 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."),
16892
+ cambios: import_zod51.z.object({
16893
+ dia: import_zod51.z.string().nullable().optional().describe("Slot date in YYYY-MM-DD. OMIT if not changing date. Use null to explicitly clear."),
16894
+ plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional().describe("OMIT if not changing platform."),
16895
+ tipo: import_zod51.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional().describe("OMIT if not changing content type."),
16896
+ keyword: import_zod51.z.string().nullable().optional().describe("OMIT if not changing keyword."),
16897
+ tema: import_zod51.z.string().nullable().optional().describe("OMIT if not changing topic."),
16898
+ productoId: import_zod51.z.string().nullable().optional().describe("OMIT if not changing linked product. Use null to unlink."),
16899
+ estado: import_zod51.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("OMIT if not changing status manually."),
16900
+ contenidoRef: import_zod51.z.string().nullable().optional().describe("OMIT \u2014 managed by the system, not by callers."),
16901
+ fotoIdAsignada: import_zod51.z.string().nullable().optional().describe("Use assign_photo_to_content for photos. OMIT here."),
16902
+ notas: import_zod51.z.array(NotaCalendarioSchema).optional().describe("Append-only notes for the slot."),
16903
+ locationId: import_zod51.z.string().nullable().optional().describe("GBP location ID \u2014 only when plataforma=gbp."),
16904
+ locationNombre: import_zod51.z.string().nullable().optional().describe("Human-readable GBP location name (for UI).")
16561
16905
  }).strict().describe("Fields to update on the slot. Only declared fields accepted; unknown fields are rejected. OMIT any field you are not changing."),
16562
- accionContenidoExistente: import_zod49.z.union([
16563
- import_zod49.z.enum(["descartar", "nuevo_slot", "mantener"]),
16564
- import_zod49.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
16906
+ accionContenidoExistente: import_zod51.z.union([
16907
+ import_zod51.z.enum(["descartar", "nuevo_slot", "mantener"]),
16908
+ import_zod51.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
16565
16909
  ]).optional().describe(
16566
16910
  '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).'
16567
16911
  )
@@ -16606,9 +16950,9 @@ ESCRIBE EN DOS LUGARES:
16606
16950
 
16607
16951
  NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
16608
16952
  {
16609
- contenidoRef: import_zod49.z.string().describe("ID del doc en marketing_contenido"),
16610
- fotoId: import_zod49.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
16611
- calendarioItemRef: import_zod49.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
16953
+ contenidoRef: import_zod51.z.string().describe("ID del doc en marketing_contenido"),
16954
+ fotoId: import_zod51.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
16955
+ calendarioItemRef: import_zod51.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
16612
16956
  },
16613
16957
  async ({ contenidoRef, fotoId, calendarioItemRef }) => {
16614
16958
  const tenantId = session.requireTenant();
@@ -16634,7 +16978,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16634
16978
  "approve_content",
16635
16979
  "Aprueba contenido para publicacion. Valida transicion de estado.",
16636
16980
  {
16637
- contenidoId: import_zod49.z.string().describe("ID del contenido a aprobar")
16981
+ contenidoId: import_zod51.z.string().describe("ID del contenido a aprobar")
16638
16982
  },
16639
16983
  async ({ contenidoId }) => {
16640
16984
  session.requireTenant();
@@ -16658,8 +17002,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16658
17002
  "reject_content",
16659
17003
  "Rechaza contenido con motivo. Valida transicion de estado.",
16660
17004
  {
16661
- contenidoId: import_zod49.z.string().describe("ID del contenido a rechazar"),
16662
- motivo: import_zod49.z.string().describe("Motivo del rechazo")
17005
+ contenidoId: import_zod51.z.string().describe("ID del contenido a rechazar"),
17006
+ motivo: import_zod51.z.string().describe("Motivo del rechazo")
16663
17007
  },
16664
17008
  async ({ contenidoId, motivo }) => {
16665
17009
  session.requireTenant();
@@ -16680,96 +17024,40 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16680
17024
  );
16681
17025
  server.tool(
16682
17026
  "get_collections",
16683
- "Lee todas las colecciones canonicas (Shopify/WordPress/webonly) con sus 6 campos SEO actuales. Incluye best practices 2026.",
17027
+ "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.",
16684
17028
  {
16685
- brandId: import_zod49.z.string().optional().describe("ID de la brand")
17029
+ brandId: import_zod51.z.string().optional().describe("Brand identifier within the tenant")
16686
17030
  },
16687
17031
  async ({ brandId: inputBrandId }) => {
16688
17032
  const tenantId = session.requireTenant();
16689
17033
  const brandId = inputBrandId ?? session.requireBrand();
16690
- const db = getAdminDb();
16691
- const collSnap = await db.collection(`tenants/${tenantId}/marketing_snapshots/${brandId}/collections`).get();
16692
- const items = collSnap.docs.map((d) => d.data());
16693
- if (items.length === 0) {
16694
- return { content: [{ type: "text", text: JSON.stringify({
17034
+ const ctx = await buildContext(session, brandId);
17035
+ const result = await dispatchWithContract({
17036
+ contract: collectionsReaderContract,
17037
+ helper: collectionsReader,
17038
+ callable: callCollectionsReader,
17039
+ input: { tenantId, brandId },
17040
+ ctx
17041
+ });
17042
+ let payload;
17043
+ if (result.state === "success" && result.structuredOutput) {
17044
+ payload = result.structuredOutput;
17045
+ } else {
17046
+ payload = {
16695
17047
  ok: false,
16696
- error: "No hay colecciones nested. El tenant debe sincronizar desde Marketing > Configuraci\xF3n > Shopify > Sincronizar."
16697
- }) }] };
16698
- }
16699
- const brand = await readDoc(`tenants/${tenantId}/marketing_config`, brandId);
16700
- const plan = brand?.plan ?? {};
16701
- const keywords = plan.keywordsPrioritarios ?? [];
16702
- const existingSuggestions = brand?.collectionSuggestions ?? {};
16703
- const collections = items.map((c) => {
16704
- const seo = c.seo || {};
16705
- const featured = c.featuredImage || null;
16706
- return {
16707
- id: c.platformId,
16708
- title: c.title,
16709
- handle: c.handle,
16710
- body_html: typeof c.descriptionHtml === "string" ? c.descriptionHtml.slice(0, 500) : null,
16711
- metaTitle: seo.metaTitle ?? null,
16712
- metaDescription: seo.metaDescription ?? null,
16713
- products_count: null,
16714
- // adapter v2 aun no calcula products_count por collection
16715
- collectionType: "canonical",
16716
- image: featured ? { src: featured.url, alt: featured.altText } : null,
16717
- existingSuggestion: existingSuggestions[String(c.platformId)] ?? null
17048
+ state: result.state,
17049
+ mensaje: result.text,
17050
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
16718
17051
  };
16719
- });
16720
- return { content: [{ type: "text", text: JSON.stringify({
16721
- ok: true,
16722
- totalCollections: collections.length,
16723
- keywordsPrioritarios: keywords,
16724
- collections,
16725
- bestPractices: {
16726
- title: "Keyword principal, claro y corto. H1 de la p\xE1gina.",
16727
- 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.",
16728
- metaTitle: "50-60 chars. Keyword al inicio. Formato: {Keyword} | {Brand}. Max 600px.",
16729
- metaDescription: "120-158 chars. Keyword + CTA + value prop. 120 mobile, 158 desktop.",
16730
- handle: "Keyword en URL. NUNCA cambiar URL indexado sin redirect 301.",
16731
- imageAlt: "Descriptivo con keyword. Accesibilidad + Google Images + AEO.",
16732
- aeo: "AI engines validan im\xE1genes contra schema. Structured data importa."
16733
- },
16734
- schemaParaSave: {
16735
- _instrucciones: "SCHEMA ESTRICTO para save_collection_suggestions. Usa EXACTAMENTE estos nombres de campo. El sistema RECHAZAR\xC1 campos que no coincidan.",
16736
- _ejemplo: {
16737
- collectionId: 123456789,
16738
- suggestedTitle: "Flores Moradas | Env\xEDo CDMX",
16739
- suggestedDescription: "<p>Descubre nuestra colecci\xF3n de flores moradas...</p>",
16740
- suggestedMetaTitle: "Flores Moradas CDMX | Env\xEDo Mismo D\xEDa",
16741
- suggestedMetaDescription: "Env\xEDa flores moradas a domicilio en CDMX. Arreglos frescos con rosas, tulipanes y lirios morados. Entrega el mismo d\xEDa.",
16742
- suggestedHandle: null,
16743
- suggestedImageAlt: "Ramo de flores moradas con rosas y tulipanes - Ponch y Capric\xF3 florer\xEDa CDMX",
16744
- keyword: "flores moradas cdmx",
16745
- notas: "Meta title optimizado con keyword local. Description ampliada con long-tail keywords. Handle no cambia (ya indexado)."
16746
- },
16747
- _reglas: [
16748
- "collectionId: OBLIGATORIO. ID num\xE9rico de Shopify (no GID)",
16749
- "suggestedTitle: string. H1 de la p\xE1gina. Claro con keyword",
16750
- "suggestedDescription: string HTML. 200-400 palabras. Incluir links a colecciones relacionadas",
16751
- "suggestedMetaTitle: string. 50-60 chars. Keyword al inicio. NUNCA m\xE1s de 60",
16752
- "suggestedMetaDescription: string. 120-158 chars. Keyword + CTA + value prop. NUNCA m\xE1s de 158",
16753
- "suggestedHandle: string | null. Solo cambiar si el handle actual es malo. null = no cambiar",
16754
- "suggestedImageAlt: string. Descriptivo con keyword. Para accesibilidad + Google Images",
16755
- "keyword: string. Keyword target de esta colecci\xF3n (del plan de marketing)",
16756
- "notas: string. Explicaci\xF3n breve de qu\xE9 cambiaste y por qu\xE9"
16757
- ],
16758
- _nunca: [
16759
- "NUNCA usar titulo, descripcion, metaTitulo, metaDescripcion \u2014 usar los nombres en ingl\xE9s",
16760
- "NUNCA inventar collectionId \u2014 usar el id del array collections",
16761
- "NUNCA meta title > 60 chars o meta description > 158 chars",
16762
- "NUNCA cambiar handle de colecci\xF3n indexada sin justificaci\xF3n"
16763
- ]
16764
- }
16765
- }) }] };
17052
+ }
17053
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
16766
17054
  }
16767
17055
  );
16768
17056
  server.tool(
16769
17057
  "save_collection_suggestions",
16770
17058
  "Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
16771
17059
  {
16772
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
17060
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16773
17061
  suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
16774
17062
  },
16775
17063
  async ({ brandId: inputBrandId, suggestions }) => {
@@ -16795,7 +17083,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16795
17083
  }
16796
17084
 
16797
17085
  // src/tools/martin.ts
16798
- var import_zod50 = require("zod");
17086
+ var import_zod52 = require("zod");
16799
17087
  function renderResult(result) {
16800
17088
  const payload = {
16801
17089
  state: result.state,
@@ -16811,16 +17099,16 @@ function registerMartinTools(server, session) {
16811
17099
  "recordar_memoria",
16812
17100
  "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.",
16813
17101
  {
16814
- tipo: import_zod50.z.enum(["preferencia", "regla", "patron", "aversion"]).describe(
17102
+ tipo: import_zod52.z.enum(["preferencia", "regla", "patron", "aversion"]).describe(
16815
17103
  "Memory type: 'preferencia' (personal preference), 'regla' (operational rule), 'patron' (observed behavioral pattern), 'aversion' (explicit do-not-do rule)."
16816
17104
  ),
16817
- categoria: import_zod50.z.enum(["compras", "produccion", "dispatch", "ventas", "marketing", "operacion", "personal", "delegacion"]).describe("Business area this memory applies to."),
16818
- contenido: import_zod50.z.string().min(3).max(500).describe("Memory content in the user's own words (3-500 chars)."),
16819
- origen: import_zod50.z.object({
16820
- tipo: import_zod50.z.enum(["conversacion", "feedback_card", "inferido_automatico"]),
16821
- conversacionId: import_zod50.z.string().nullable(),
16822
- cardId: import_zod50.z.string().nullable(),
16823
- rationale: import_zod50.z.array(import_zod50.z.string()).optional()
17105
+ categoria: import_zod52.z.enum(["compras", "produccion", "dispatch", "ventas", "marketing", "operacion", "personal", "delegacion"]).describe("Business area this memory applies to."),
17106
+ contenido: import_zod52.z.string().min(3).max(500).describe("Memory content in the user's own words (3-500 chars)."),
17107
+ origen: import_zod52.z.object({
17108
+ tipo: import_zod52.z.enum(["conversacion", "feedback_card", "inferido_automatico"]),
17109
+ conversacionId: import_zod52.z.string().nullable(),
17110
+ cardId: import_zod52.z.string().nullable(),
17111
+ rationale: import_zod52.z.array(import_zod52.z.string()).optional()
16824
17112
  }).describe("Where this memory was inferred from (conversation, card feedback, or auto-inferred).")
16825
17113
  },
16826
17114
  async ({ tipo, categoria, contenido, origen }) => {
@@ -16839,9 +17127,9 @@ function registerMartinTools(server, session) {
16839
17127
  "olvidar_memoria",
16840
17128
  "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.",
16841
17129
  {
16842
- memoriaId: import_zod50.z.string().min(1).describe("Memory document ID to archive."),
16843
- motivo: import_zod50.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
16844
- confirm: import_zod50.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
17130
+ memoriaId: import_zod52.z.string().min(1).describe("Memory document ID to archive."),
17131
+ motivo: import_zod52.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
17132
+ confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
16845
17133
  },
16846
17134
  async ({ memoriaId, motivo, confirm }) => {
16847
17135
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
@@ -16859,25 +17147,25 @@ function registerMartinTools(server, session) {
16859
17147
  "programar_rutina",
16860
17148
  "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.",
16861
17149
  {
16862
- uidDestinatario: import_zod50.z.string().optional().describe("User ID who should receive the routine output. OMIT to default to the calling user (most common case)."),
16863
- tipo: import_zod50.z.enum(["reporte", "recordatorio", "accion_delegada", "publicacion", "compra_sugerida"]).describe("Routine type from canonical TipoRutinaEnum."),
16864
- frecuencia: import_zod50.z.enum(["diaria", "semanal", "quincenal", "mensual", "trimestral", "puntual"]).describe("Execution cadence."),
16865
- config: import_zod50.z.object({
16866
- diaSemana: import_zod50.z.number().int().min(0).max(6).nullable(),
16867
- diaMes: import_zod50.z.number().int().min(1).max(31).nullable(),
16868
- hora: import_zod50.z.string().regex(/^\d{2}:\d{2}$/),
16869
- fechaPuntual: import_zod50.z.string().datetime({ offset: true }).nullable()
17150
+ 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)."),
17151
+ tipo: import_zod52.z.enum(["reporte", "recordatorio", "accion_delegada", "publicacion", "compra_sugerida"]).describe("Routine type from canonical TipoRutinaEnum."),
17152
+ frecuencia: import_zod52.z.enum(["diaria", "semanal", "quincenal", "mensual", "trimestral", "puntual"]).describe("Execution cadence."),
17153
+ config: import_zod52.z.object({
17154
+ diaSemana: import_zod52.z.number().int().min(0).max(6).nullable(),
17155
+ diaMes: import_zod52.z.number().int().min(1).max(31).nullable(),
17156
+ hora: import_zod52.z.string().regex(/^\d{2}:\d{2}$/),
17157
+ fechaPuntual: import_zod52.z.string().datetime({ offset: true }).nullable()
16870
17158
  }).describe("Schedule configuration. Match fields to the frecuencia (semanal needs diaSemana; mensual needs diaMes; puntual needs fechaPuntual)."),
16871
- accion: import_zod50.z.object({
16872
- tool: import_zod50.z.string().min(1),
16873
- params: import_zod50.z.record(import_zod50.z.string(), import_zod50.z.unknown())
17159
+ accion: import_zod52.z.object({
17160
+ tool: import_zod52.z.string().min(1),
17161
+ params: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.unknown())
16874
17162
  }).describe("Action to execute on each fire (MCP tool name + params)."),
16875
- origen: import_zod50.z.object({
16876
- tipo: import_zod50.z.enum(["conversacion", "feedback_card", "configurada_explicito"]),
16877
- conversacionId: import_zod50.z.string().nullable(),
16878
- cardId: import_zod50.z.string().nullable()
17163
+ origen: import_zod52.z.object({
17164
+ tipo: import_zod52.z.enum(["conversacion", "feedback_card", "configurada_explicito"]),
17165
+ conversacionId: import_zod52.z.string().nullable(),
17166
+ cardId: import_zod52.z.string().nullable()
16879
17167
  }).describe("Where this routine was scheduled from."),
16880
- confirm: import_zod50.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
17168
+ confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
16881
17169
  },
16882
17170
  async ({ uidDestinatario, tipo, frecuencia, config, accion, origen, confirm }) => {
16883
17171
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
@@ -16906,8 +17194,8 @@ function registerMartinTools(server, session) {
16906
17194
  "pausar_rutina",
16907
17195
  "Pause an active routine temporarily. Can be resumed later by re-enabling it.",
16908
17196
  {
16909
- rutinaId: import_zod50.z.string().min(1).describe("Routine document ID to pause."),
16910
- motivo: import_zod50.z.string().optional().describe("Optional reason for pausing (logged for audit).")
17197
+ rutinaId: import_zod52.z.string().min(1).describe("Routine document ID to pause."),
17198
+ motivo: import_zod52.z.string().optional().describe("Optional reason for pausing (logged for audit).")
16911
17199
  },
16912
17200
  async ({ rutinaId, motivo }) => {
16913
17201
  const ctx = await buildContext(session, null);
@@ -16933,9 +17221,9 @@ function registerMartinTools(server, session) {
16933
17221
  "archivar_rutina",
16934
17222
  "Archive a routine that no longer applies. The routine is preserved for audit purposes but will NOT be executed. Requires confirmation.",
16935
17223
  {
16936
- rutinaId: import_zod50.z.string().min(1).describe("Routine document ID to archive (will not execute again)."),
16937
- motivo: import_zod50.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
16938
- confirm: import_zod50.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
17224
+ rutinaId: import_zod52.z.string().min(1).describe("Routine document ID to archive (will not execute again)."),
17225
+ motivo: import_zod52.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
17226
+ confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
16939
17227
  },
16940
17228
  async ({ rutinaId, motivo, confirm }) => {
16941
17229
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });