ponch-mcp-server 1.0.83 → 1.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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,96 @@ async function getCurrentUsage(_tenantId, _quotaName) {
9753
9753
  function getWrapperMessage(key, locale) {
9754
9754
  return getMessage(`marketing.wrapper.${key}`, locale);
9755
9755
  }
9756
+ function _extractReceived(issue) {
9757
+ if (issue.received !== void 0 && issue.received !== null) {
9758
+ return String(issue.received);
9759
+ }
9760
+ const m = issue.message?.match(/received\s+(\w+)/i);
9761
+ return m ? m[1] : "unknown";
9762
+ }
9763
+ function _placeholderForType(expected, locale) {
9764
+ switch (expected) {
9765
+ case "string":
9766
+ return locale === "en" ? '"a-string"' : '"un-texto"';
9767
+ case "number":
9768
+ return "42";
9769
+ case "integer":
9770
+ return "42";
9771
+ case "boolean":
9772
+ return "true";
9773
+ case "array":
9774
+ return "[]";
9775
+ case "object":
9776
+ return "{}";
9777
+ case "null":
9778
+ return "null";
9779
+ default:
9780
+ return locale === "en" ? '"a-value"' : '"un-valor"';
9781
+ }
9782
+ }
9783
+ function _unitForOrigin(origin, locale) {
9784
+ if (origin === "string") return locale === "en" ? "characters" : "caracteres";
9785
+ if (origin === "array") return locale === "en" ? "items" : "elementos";
9786
+ if (origin === "set") return locale === "en" ? "items" : "elementos";
9787
+ if (origin === "number" || origin === "bigint" || origin === "int") return "";
9788
+ if (origin === "date") return locale === "en" ? "date" : "fecha";
9789
+ return locale === "en" ? "units" : "unidades";
9790
+ }
9791
+ function getIssueHumanText(issue, locale) {
9792
+ const field = issue.path.length > 0 ? issue.path.join(".") : "(root)";
9793
+ switch (issue.code) {
9794
+ case "invalid_type": {
9795
+ const expected = String(issue.expected ?? "unknown");
9796
+ const received = _extractReceived(issue);
9797
+ const example = _placeholderForType(issue.expected, locale);
9798
+ return getMessage("marketing.wrapper.issue_invalid_type", locale, {
9799
+ field,
9800
+ expected,
9801
+ received,
9802
+ example
9803
+ });
9804
+ }
9805
+ case "too_small": {
9806
+ const minimum = String(issue.minimum ?? "");
9807
+ const unit = _unitForOrigin(issue.origin, locale);
9808
+ return getMessage("marketing.wrapper.issue_too_small", locale, {
9809
+ field,
9810
+ minimum,
9811
+ unit
9812
+ });
9813
+ }
9814
+ case "too_big": {
9815
+ const maximum = String(issue.maximum ?? "");
9816
+ const unit = _unitForOrigin(issue.origin, locale);
9817
+ return getMessage("marketing.wrapper.issue_too_big", locale, {
9818
+ field,
9819
+ maximum,
9820
+ unit
9821
+ });
9822
+ }
9823
+ case "invalid_enum_value":
9824
+ case "invalid_value": {
9825
+ const opts = (issue.options ?? []).map((o) => JSON.stringify(o)).join(", ");
9826
+ return getMessage("marketing.wrapper.issue_invalid_enum", locale, {
9827
+ field,
9828
+ options: opts
9829
+ });
9830
+ }
9831
+ case "invalid_string":
9832
+ case "invalid_format": {
9833
+ const hint = issue.validation ? `(${issue.validation})` : "";
9834
+ return getMessage("marketing.wrapper.issue_invalid_string", locale, {
9835
+ field,
9836
+ hint
9837
+ });
9838
+ }
9839
+ default:
9840
+ return getMessage("marketing.wrapper.issue_generic", locale, {
9841
+ field,
9842
+ message: issue.message
9843
+ });
9844
+ }
9845
+ }
9756
9846
  var NIVELES_ORDER = ["ninguno", "ver", "editar", "completo"];
9757
9847
  function nivelAlcanza(nivel, accion) {
9758
9848
  return NIVELES_ORDER.indexOf(nivel) >= NIVELES_ORDER.indexOf(accion);
@@ -9855,9 +9945,16 @@ function wrapWithContract(contract, helper, options = {}) {
9855
9945
  expected: issue.expected
9856
9946
  };
9857
9947
  });
9858
- 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;
9948
+ let text;
9949
+ if (parseResult.error.issues.length === 0) {
9950
+ text = getWrapperMessage("input_invalido", locale);
9951
+ } else {
9952
+ const intro = getWrapperMessage("input_invalido_intro", locale);
9953
+ const lines = parseResult.error.issues.map(
9954
+ (i) => getIssueHumanText(i, locale)
9955
+ );
9956
+ text = `${intro} ${lines.join(" ")}`;
9957
+ }
9861
9958
  await writeAuditLog({
9862
9959
  tenantId: ctx.tenantId,
9863
9960
  brandId: ctx.brandId ?? null,
@@ -10736,18 +10833,20 @@ var import_zod35 = require("zod");
10736
10833
  var import_firebase_admin7 = require("firebase-admin");
10737
10834
  var import_zod36 = require("zod");
10738
10835
  var import_zod37 = require("zod");
10739
- var import_firebase_admin8 = require("firebase-admin");
10740
10836
  var import_zod38 = require("zod");
10741
10837
  var import_zod39 = require("zod");
10838
+ var import_firebase_admin8 = require("firebase-admin");
10742
10839
  var import_zod40 = require("zod");
10743
- var import_firebase_admin9 = require("firebase-admin");
10744
10840
  var import_zod41 = require("zod");
10745
- var import_firebase_admin10 = require("firebase-admin");
10746
10841
  var import_zod42 = require("zod");
10842
+ var import_firebase_admin9 = require("firebase-admin");
10747
10843
  var import_zod43 = require("zod");
10844
+ var import_firebase_admin10 = require("firebase-admin");
10748
10845
  var import_zod44 = require("zod");
10749
10846
  var import_zod45 = require("zod");
10750
10847
  var import_zod46 = require("zod");
10848
+ var import_zod47 = require("zod");
10849
+ var import_zod48 = require("zod");
10751
10850
  var import_firestore6 = require("firebase-admin/firestore");
10752
10851
  var RULE_NEGATIVES = {
10753
10852
  allowFaces: "no people, no faces, no hands",
@@ -12062,6 +12161,229 @@ var rawContract7 = {
12062
12161
  var getCalendarContract = MartinContractSchema.parse(
12063
12162
  rawContract7
12064
12163
  );
12164
+ async function seoSnapshotReader(input) {
12165
+ const { db, tenantId, brandId } = input;
12166
+ const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
12167
+ const brandSnap = await brandRef.get();
12168
+ if (!brandSnap.exists) {
12169
+ return { ok: false, code: "BRAND_NOT_FOUND" };
12170
+ }
12171
+ const snap = await db.collection("tenants").doc(tenantId).collection("marketing_snapshots_semrush").where("brandId", "==", brandId).orderBy("mes", "desc").limit(1).get();
12172
+ if (snap.empty) {
12173
+ return { ok: false, code: "SEO_SNAPSHOT_MISSING" };
12174
+ }
12175
+ const doc = snap.docs[0];
12176
+ const data = doc.data();
12177
+ const mes = typeof data.mes === "string" ? data.mes : "";
12178
+ return {
12179
+ ok: true,
12180
+ brandId,
12181
+ mes,
12182
+ snapshot: { id: doc.id, ...data }
12183
+ };
12184
+ }
12185
+ var ParamsSchema8 = import_zod38.z.object({
12186
+ tenantId: import_zod38.z.string().min(1).describe("Tenant identifier (the business account)."),
12187
+ brandId: import_zod38.z.string().min(1).describe("Brand identifier within the tenant.")
12188
+ });
12189
+ var SuccessSchema = import_zod38.z.object({
12190
+ ok: import_zod38.z.literal(true),
12191
+ brandId: import_zod38.z.string(),
12192
+ mes: import_zod38.z.string(),
12193
+ snapshot: import_zod38.z.record(import_zod38.z.string(), import_zod38.z.unknown())
12194
+ });
12195
+ var FailureSchema = import_zod38.z.object({
12196
+ ok: import_zod38.z.literal(false),
12197
+ code: import_zod38.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"])
12198
+ });
12199
+ var OutputSchema8 = import_zod38.z.discriminatedUnion("ok", [SuccessSchema, FailureSchema]);
12200
+ var rawContract8 = {
12201
+ name: "get_seo_snapshot",
12202
+ description: "Read the latest Semrush SEO snapshot for a brand. Returns rank, top keywords, opportunities, and competitors.",
12203
+ paramsSchema: ParamsSchema8,
12204
+ outputSchema: OutputSchema8,
12205
+ requiresConfirmation: false,
12206
+ destructive: false,
12207
+ affectsPublication: false,
12208
+ affectsExternal: false,
12209
+ martinSummaryTemplate: (input, output, locale) => {
12210
+ if (!output.ok) {
12211
+ if (output.code === "BRAND_NOT_FOUND") {
12212
+ return locale === "en" ? `Brand ${input.brandId} not found.` : `No encontr\xE9 la brand ${input.brandId}.`;
12213
+ }
12214
+ return locale === "en" ? `No SEO snapshot for brand ${input.brandId} yet.` : `A\xFAn no hay snapshot SEO de ${input.brandId}.`;
12215
+ }
12216
+ return locale === "en" ? `Latest SEO snapshot for ${input.brandId} (${output.mes}).` : `Snapshot SEO m\xE1s reciente de ${input.brandId} (${output.mes}).`;
12217
+ },
12218
+ // AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
12219
+ auditAction: "marketing.seo_snapshot.leer",
12220
+ quotasConsumed: [],
12221
+ permissionScope: "module",
12222
+ permissionKey: "marketing",
12223
+ permissionAction: "ver",
12224
+ sideEffects: ["reads_firestore"]
12225
+ };
12226
+ var seoSnapshotReaderContract = MartinContractSchema.parse(
12227
+ rawContract8
12228
+ );
12229
+ var BEST_PRACTICES_EN = {
12230
+ title: "Primary keyword, clear and short. Page H1.",
12231
+ description: "Above grid: 2-3 sentences (50-70 words). Below grid: 200-400 words with long-tail keywords. Pages with descriptions rank 2.7x higher.",
12232
+ metaTitle: "50-60 chars. Keyword first. Format: {Keyword} | {Brand}. Max 600px.",
12233
+ metaDescription: "120-158 chars. Keyword + CTA + value prop. 120 mobile, 158 desktop.",
12234
+ handle: "Keyword in URL. NEVER change an indexed URL without a 301 redirect.",
12235
+ imageAlt: "Descriptive with keyword. Accessibility + Google Images + AEO.",
12236
+ aeo: "AI engines validate images against schema. Structured data matters."
12237
+ };
12238
+ var SCHEMA_FOR_SAVE_EN = {
12239
+ _instructions: "STRICT SCHEMA for save_collection_suggestions. Use EXACTLY these field names. The system WILL REJECT mismatched fields.",
12240
+ _example: {
12241
+ collectionId: 123456789,
12242
+ suggestedTitle: "Purple Flowers | Same-Day CDMX Delivery",
12243
+ suggestedDescription: "<p>Discover our purple flowers collection...</p>",
12244
+ suggestedMetaTitle: "Purple Flowers CDMX | Same-Day Delivery",
12245
+ suggestedMetaDescription: "Send purple flowers to your door in CDMX. Fresh arrangements with purple roses, tulips, and lilies. Same-day delivery.",
12246
+ suggestedHandle: null,
12247
+ suggestedImageAlt: "Bouquet of purple flowers with roses and tulips \u2014 local florist CDMX",
12248
+ keyword: "purple flowers cdmx",
12249
+ notas: "Meta title optimized with local keyword. Description expanded with long-tail keywords. Handle unchanged (already indexed)."
12250
+ },
12251
+ _rules: [
12252
+ "collectionId: REQUIRED. Numeric Shopify ID (not GID).",
12253
+ "suggestedTitle: string. Page H1. Clear with keyword.",
12254
+ "suggestedDescription: HTML string. 200-400 words. Include links to related collections.",
12255
+ "suggestedMetaTitle: string. 50-60 chars. Keyword first. NEVER over 60.",
12256
+ "suggestedMetaDescription: string. 120-158 chars. Keyword + CTA + value prop. NEVER over 158.",
12257
+ "suggestedHandle: string | null. Only change if the current handle is bad. null = no change.",
12258
+ "suggestedImageAlt: string. Descriptive with keyword. For accessibility + Google Images.",
12259
+ "keyword: string. Target keyword for this collection (from the marketing plan).",
12260
+ "notas: string. Brief explanation of what changed and why."
12261
+ ],
12262
+ _never: [
12263
+ "NEVER use Spanish field names (titulo, descripcion, etc.) \u2014 use English names.",
12264
+ "NEVER fabricate collectionId \u2014 use the id from the collections array.",
12265
+ "NEVER exceed 60 chars in metaTitle or 158 chars in metaDescription.",
12266
+ "NEVER change the handle of an indexed collection without justification."
12267
+ ]
12268
+ };
12269
+ async function collectionsReader(input) {
12270
+ const { db, tenantId, brandId } = input;
12271
+ const brandRef = db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId);
12272
+ const brandSnap = await brandRef.get();
12273
+ if (!brandSnap.exists) {
12274
+ return { ok: false, code: "BRAND_NOT_FOUND" };
12275
+ }
12276
+ const collSnap = await db.collection("tenants").doc(tenantId).collection("marketing_snapshots").doc(brandId).collection("collections").get();
12277
+ if (collSnap.empty) {
12278
+ return { ok: false, code: "NO_COLLECTIONS_NESTED" };
12279
+ }
12280
+ const items = collSnap.docs.map((d) => d.data());
12281
+ const brand = brandSnap.data() ?? {};
12282
+ const plan = brand.plan ?? {};
12283
+ const keywordsPrioritarios = Array.isArray(plan.keywordsPrioritarios) ? plan.keywordsPrioritarios : [];
12284
+ const existingSuggestions = brand.collectionSuggestions ?? {};
12285
+ const collections = items.map((c) => {
12286
+ const seo = c.seo || {};
12287
+ const featured = c.featuredImage || null;
12288
+ return {
12289
+ id: c.platformId,
12290
+ title: c.title,
12291
+ handle: c.handle,
12292
+ body_html: typeof c.descriptionHtml === "string" ? c.descriptionHtml.slice(0, 500) : null,
12293
+ metaTitle: seo.metaTitle ?? null,
12294
+ metaDescription: seo.metaDescription ?? null,
12295
+ products_count: null,
12296
+ collectionType: "canonical",
12297
+ image: featured ? { src: featured.url, alt: featured.altText } : null,
12298
+ existingSuggestion: existingSuggestions[String(c.platformId)] ?? null
12299
+ };
12300
+ });
12301
+ return {
12302
+ ok: true,
12303
+ brandId,
12304
+ totalCollections: collections.length,
12305
+ keywordsPrioritarios,
12306
+ collections,
12307
+ bestPractices: BEST_PRACTICES_EN,
12308
+ schemaParaSave: SCHEMA_FOR_SAVE_EN
12309
+ };
12310
+ }
12311
+ var ParamsSchema9 = import_zod39.z.object({
12312
+ tenantId: import_zod39.z.string().min(1).describe("Tenant identifier (the business account)."),
12313
+ brandId: import_zod39.z.string().min(1).describe("Brand identifier within the tenant.")
12314
+ });
12315
+ var CollectionItemSchema = import_zod39.z.object({
12316
+ id: import_zod39.z.unknown(),
12317
+ title: import_zod39.z.unknown(),
12318
+ handle: import_zod39.z.unknown(),
12319
+ body_html: import_zod39.z.string().nullable(),
12320
+ metaTitle: import_zod39.z.unknown(),
12321
+ metaDescription: import_zod39.z.unknown(),
12322
+ products_count: import_zod39.z.null(),
12323
+ collectionType: import_zod39.z.literal("canonical"),
12324
+ image: import_zod39.z.object({ src: import_zod39.z.unknown(), alt: import_zod39.z.unknown() }).nullable(),
12325
+ existingSuggestion: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown()).nullable()
12326
+ });
12327
+ var BestPracticesSchema = import_zod39.z.object({
12328
+ title: import_zod39.z.string(),
12329
+ description: import_zod39.z.string(),
12330
+ metaTitle: import_zod39.z.string(),
12331
+ metaDescription: import_zod39.z.string(),
12332
+ handle: import_zod39.z.string(),
12333
+ imageAlt: import_zod39.z.string(),
12334
+ aeo: import_zod39.z.string()
12335
+ });
12336
+ var SchemaForSaveSchema = import_zod39.z.object({
12337
+ _instructions: import_zod39.z.string(),
12338
+ _example: import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown()),
12339
+ _rules: import_zod39.z.array(import_zod39.z.string()),
12340
+ _never: import_zod39.z.array(import_zod39.z.string())
12341
+ });
12342
+ var SuccessSchema2 = import_zod39.z.object({
12343
+ ok: import_zod39.z.literal(true),
12344
+ brandId: import_zod39.z.string(),
12345
+ totalCollections: import_zod39.z.number(),
12346
+ // Shape real: Array<{ keyword, posicion, volumen, dificultad, prioridad, accion }>.
12347
+ // Passthrough loose para no acoplar el reader al schema interno del plan.
12348
+ keywordsPrioritarios: import_zod39.z.array(import_zod39.z.record(import_zod39.z.string(), import_zod39.z.unknown())),
12349
+ collections: import_zod39.z.array(CollectionItemSchema),
12350
+ bestPractices: BestPracticesSchema,
12351
+ schemaParaSave: SchemaForSaveSchema
12352
+ });
12353
+ var FailureSchema2 = import_zod39.z.object({
12354
+ ok: import_zod39.z.literal(false),
12355
+ code: import_zod39.z.enum(["BRAND_NOT_FOUND", "NO_COLLECTIONS_NESTED"])
12356
+ });
12357
+ var OutputSchema9 = import_zod39.z.discriminatedUnion("ok", [SuccessSchema2, FailureSchema2]);
12358
+ var rawContract9 = {
12359
+ name: "get_collections",
12360
+ description: "Read all canonical collections (Shopify/WordPress/web-only) for a brand with their 6 SEO fields, plus best practices and the strict schema for generating SEO suggestions.",
12361
+ paramsSchema: ParamsSchema9,
12362
+ outputSchema: OutputSchema9,
12363
+ requiresConfirmation: false,
12364
+ destructive: false,
12365
+ affectsPublication: false,
12366
+ affectsExternal: false,
12367
+ martinSummaryTemplate: (input, output, locale) => {
12368
+ if (!output.ok) {
12369
+ if (output.code === "BRAND_NOT_FOUND") {
12370
+ return locale === "en" ? `Brand ${input.brandId} not found.` : `No encontr\xE9 la brand ${input.brandId}.`;
12371
+ }
12372
+ return locale === "en" ? `No collections synced for ${input.brandId} yet.` : `A\xFAn no hay colecciones sincronizadas para ${input.brandId}.`;
12373
+ }
12374
+ return locale === "en" ? `${output.totalCollections} collections for ${input.brandId}.` : `${output.totalCollections} colecciones para ${input.brandId}.`;
12375
+ },
12376
+ // AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
12377
+ auditAction: "marketing.colecciones.listar",
12378
+ quotasConsumed: [],
12379
+ permissionScope: "module",
12380
+ permissionKey: "marketing",
12381
+ permissionAction: "ver",
12382
+ sideEffects: ["reads_firestore"]
12383
+ };
12384
+ var collectionsReaderContract = MartinContractSchema.parse(
12385
+ rawContract9
12386
+ );
12065
12387
  var PLATAFORMA_A_FORMATO = {
12066
12388
  gbp: "gbp_4_3",
12067
12389
  shopify_blog: "blog_3_2",
@@ -12173,35 +12495,35 @@ async function photoAssigner(input) {
12173
12495
  formato
12174
12496
  };
12175
12497
  }
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(
12498
+ var ParamsSchema10 = import_zod40.z.object({
12499
+ tenantId: import_zod40.z.string().min(1).describe("Tenant identifier (the business account)."),
12500
+ brandId: import_zod40.z.string().min(1).describe("Brand identifier within the tenant."),
12501
+ contenidoRef: import_zod40.z.string().min(1).describe("Document ID in marketing_contenido (the post receiving the photo)."),
12502
+ fotoId: import_zod40.z.string().min(1).describe("Photo ID in marketing_fotos. Photo must be in state='editada'."),
12503
+ calendarioItemRef: import_zod40.z.string().regex(/^semana:\d+:slot:\d+$/).optional().describe(
12182
12504
  'Calendar slot reference in format "semana:N:slot:M". Optional \u2014 if provided, syncs the slot.fotoIdAsignada UI snapshot atomically with the content update. OMIT if there is no calendar slot to sync.'
12183
12505
  )
12184
12506
  });
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()
12507
+ var OutputSchema10 = import_zod40.z.discriminatedUnion("ok", [
12508
+ import_zod40.z.object({
12509
+ ok: import_zod40.z.literal(true),
12510
+ fotoId: import_zod40.z.string(),
12511
+ contenidoRef: import_zod40.z.string(),
12512
+ plataforma: import_zod40.z.string(),
12513
+ varianteUrl: import_zod40.z.string().nullable(),
12514
+ formato: import_zod40.z.string()
12193
12515
  }),
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()
12516
+ import_zod40.z.object({
12517
+ ok: import_zod40.z.literal(false),
12518
+ error: import_zod40.z.string(),
12519
+ code: import_zod40.z.enum(["CONTENT_NOT_FOUND", "CONTENT_TENANT_MISMATCH", "PHOTO_NOT_FOUND", "PHOTO_NOT_READY"]).optional()
12198
12520
  })
12199
12521
  ]);
12200
- var rawContract8 = {
12522
+ var rawContract10 = {
12201
12523
  name: "assign_photo_to_content",
12202
12524
  description: 'Assign an edited photo to an existing content. Writes atomically to BOTH marketing_contenido.fotoId+mediaVariante (canonical for publish) AND marketing_calendario slot.fotoIdAsignada (UI snapshot, only if calendarioItemRef provided). NEVER use update_calendar_slot for this \u2014 that one writes to the agenda, not the post. The photo must be in state="editada".',
12203
- paramsSchema: ParamsSchema8,
12204
- outputSchema: OutputSchema8,
12525
+ paramsSchema: ParamsSchema10,
12526
+ outputSchema: OutputSchema10,
12205
12527
  // Cambia la foto vinculada al contenido — reversible (volver a llamar con
12206
12528
  // otra fotoId), no publica nada externo. La foto editada queda intacta;
12207
12529
  // solo se actualiza la referencia.
@@ -12243,7 +12565,7 @@ var rawContract8 = {
12243
12565
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
12244
12566
  };
12245
12567
  var photoAssignerContract = MartinContractSchema.parse(
12246
- rawContract8
12568
+ rawContract10
12247
12569
  );
12248
12570
  function findPageByHeuristic(pages, pattern) {
12249
12571
  return pages.find((p) => pattern.test(p.title || "")) || pages.find((p) => pattern.test(p.handle || "")) || null;
@@ -12432,27 +12754,27 @@ var BRAND_BRIEF_SCHEMA_HINT = {
12432
12754
  escenas: [{ id: string, nombre: string, promptHint: string }]
12433
12755
  }`
12434
12756
  };
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.")
12757
+ var ParamsSchema11 = import_zod41.z.object({
12758
+ tenantId: import_zod41.z.string().min(1).describe("Tenant identifier (the business account)."),
12759
+ brandId: import_zod41.z.string().min(1).describe("Brand identifier within the tenant.")
12438
12760
  });
12439
- var OutputSchema9 = import_zod39.z.discriminatedUnion("ok", [
12440
- import_zod39.z.object({
12441
- ok: import_zod39.z.literal(true),
12761
+ var OutputSchema11 = import_zod41.z.discriminatedUnion("ok", [
12762
+ import_zod41.z.object({
12763
+ ok: import_zod41.z.literal(true),
12442
12764
  /** 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())
12765
+ payload: import_zod41.z.record(import_zod41.z.string(), import_zod41.z.unknown())
12444
12766
  }),
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()
12767
+ import_zod41.z.object({
12768
+ ok: import_zod41.z.literal(false),
12769
+ error: import_zod41.z.string(),
12770
+ code: import_zod41.z.enum(["BRAND_NOT_FOUND"]).optional()
12449
12771
  })
12450
12772
  ]);
12451
- var rawContract9 = {
12773
+ var rawContract11 = {
12452
12774
  name: "generate_brand_brief",
12453
12775
  description: "Aggregate business data (Shopify, SEO, GBP, scraped site_content, brand config, tenant locations) into a payload to generate a pre-filled Brand Brief. Read-only \u2014 does NOT write the brief; the caller saves it via save_brand_brief.",
12454
- paramsSchema: ParamsSchema9,
12455
- outputSchema: OutputSchema9,
12776
+ paramsSchema: ParamsSchema11,
12777
+ outputSchema: OutputSchema11,
12456
12778
  // Lectura pura, sin side effects de escritura ni publicación.
12457
12779
  requiresConfirmation: false,
12458
12780
  destructive: false,
@@ -12477,7 +12799,7 @@ var rawContract9 = {
12477
12799
  sideEffects: ["reads_firestore"]
12478
12800
  };
12479
12801
  var brandBriefBuilderContract = MartinContractSchema.parse(
12480
- rawContract9
12802
+ rawContract11
12481
12803
  );
12482
12804
  async function resolveLastImportId(db, tenantId, brandId) {
12483
12805
  const configSnap = await db.collection("tenants").doc(tenantId).collection("marketing_config").doc(brandId).get();
@@ -12665,28 +12987,28 @@ var BLOG_STRATEGY_REGLAS = [
12665
12987
  "blogId, handle, title, ultimoPostFecha, ultimoPostKeyword, totalArticulos \u2014 copiar del shopifyBlogs (datos reales del import)",
12666
12988
  "defaultBlogId y defaultBlogHandle \u2014 usar el primer blog de la lista"
12667
12989
  ];
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.")
12990
+ var ParamsSchema12 = import_zod42.z.object({
12991
+ tenantId: import_zod42.z.string().min(1).describe("Tenant identifier (the business account)."),
12992
+ brandId: import_zod42.z.string().min(1).describe("Brand identifier within the tenant.")
12671
12993
  });
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(
12994
+ var OutputSchema12 = import_zod42.z.discriminatedUnion("ok", [
12995
+ import_zod42.z.object({
12996
+ ok: import_zod42.z.literal(true),
12997
+ payload: import_zod42.z.record(import_zod42.z.string(), import_zod42.z.unknown()).describe(
12676
12998
  "Aggregated business data + instructions + schema hints for the LLM to generate a marketing plan. Includes seoSnapshot, productos, historialReciente, shopifyColecciones, shopifyBlogs, brandBrief, planActual, and schema/rules hints for coleccionesPriorizadas and blogStrategy."
12677
12999
  )
12678
13000
  }),
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()
13001
+ import_zod42.z.object({
13002
+ ok: import_zod42.z.literal(false),
13003
+ error: import_zod42.z.string(),
13004
+ code: import_zod42.z.enum(["BRAND_NOT_FOUND", "SEO_SNAPSHOT_MISSING"]).optional()
12683
13005
  })
12684
13006
  ]);
12685
- var rawContract10 = {
13007
+ var rawContract12 = {
12686
13008
  name: "generate_marketing_plan",
12687
13009
  description: "Aggregate business data (seoSnapshot, productos, brand config, brandBrief, historial, Shopify collections + blogs) into a payload for the LLM to generate a marketing plan. Read-only \u2014 does NOT save the plan. The caller saves via save_marketing_plan or update_marketing_plan_field after generating. Requires an existing seoSnapshot on the brand (returns SEO_SNAPSHOT_MISSING otherwise).",
12688
- paramsSchema: ParamsSchema10,
12689
- outputSchema: OutputSchema10,
13010
+ paramsSchema: ParamsSchema12,
13011
+ outputSchema: OutputSchema12,
12690
13012
  // Lectura pura, no muta nada.
12691
13013
  requiresConfirmation: false,
12692
13014
  destructive: false,
@@ -12711,7 +13033,7 @@ var rawContract10 = {
12711
13033
  sideEffects: ["reads_firestore"]
12712
13034
  };
12713
13035
  var marketingPlanBuilderContract = MartinContractSchema.parse(
12714
- rawContract10
13036
+ rawContract12
12715
13037
  );
12716
13038
  function buildGenId() {
12717
13039
  return `mkt_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
@@ -12952,54 +13274,54 @@ async function contenidoWriter(input) {
12952
13274
  pipelineLinked
12953
13275
  };
12954
13276
  }
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(
13277
+ var ParamsSchema13 = import_zod43.z.object({
13278
+ tenantId: import_zod43.z.string().min(1).describe("Tenant identifier (the business account)."),
13279
+ brandId: import_zod43.z.string().min(1).describe("Brand identifier within the tenant."),
13280
+ plataforma: import_zod43.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target platform for the content."),
13281
+ tipo: import_zod43.z.string().optional().describe(
12960
13282
  "Content type ('post', 'blog', 'carousel', 'reel', 'story', 'review_response'). Match to the platform."
12961
13283
  ),
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(
13284
+ keyword: import_zod43.z.string().optional().describe("Primary keyword for the content. OMIT if not applicable."),
13285
+ languageCode: import_zod43.z.string().optional().describe(
12964
13286
  "Content language code (e.g. 'es', 'en'). For shopify_blog auto-injects to datos.languageCode if not present."
12965
13287
  ),
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(
13288
+ fotoId: import_zod43.z.string().optional().describe("Linked photo ID. OMIT if no photo associated yet (use assign_photo_to_content later)."),
13289
+ datos: import_zod43.z.record(import_zod43.z.string(), import_zod43.z.unknown()).describe(
12968
13290
  "Platform-specific content payload. Validated against Blog/GBP/IG/Review schemas. Use buildDatosBlog/GBP/IG/Review helpers to construct safely."
12969
13291
  ),
12970
- calendarioItemRef: import_zod41.z.string().optional().describe(
13292
+ calendarioItemRef: import_zod43.z.string().optional().describe(
12971
13293
  'Calendar slot reference (format "semana:N:slot:M"). If provided, links content to that slot AND discards prior non-discarded content of the same slot.'
12972
13294
  )
12973
13295
  });
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()
13296
+ var PipelineLinkResultSchema = import_zod43.z.object({
13297
+ linked: import_zod43.z.boolean(),
13298
+ paso: import_zod43.z.string().nullable(),
13299
+ motivo: import_zod43.z.string().optional(),
13300
+ code: import_zod43.z.string().optional(),
13301
+ _instrucciones: import_zod43.z.string().optional()
12980
13302
  });
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(),
13303
+ var OutputSchema13 = import_zod43.z.discriminatedUnion("ok", [
13304
+ import_zod43.z.object({
13305
+ ok: import_zod43.z.literal(true),
13306
+ contenidoId: import_zod43.z.string(),
13307
+ estado: import_zod43.z.string(),
13308
+ plataforma: import_zod43.z.enum(["gbp", "shopify_blog", "instagram", "review"]),
13309
+ descartados: import_zod43.z.number().int().nonnegative(),
12988
13310
  pipelineLinked: PipelineLinkResultSchema
12989
13311
  }),
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()
13312
+ import_zod43.z.object({
13313
+ ok: import_zod43.z.literal(false),
13314
+ error: import_zod43.z.string(),
13315
+ code: import_zod43.z.enum(["CONTENT_FREQUENCY_LIMIT", "CONTENT_DATA_VALIDATION_FAILED"]).optional(),
13316
+ activosEstaSemana: import_zod43.z.number().int().optional(),
13317
+ limite: import_zod43.z.number().int().optional()
12996
13318
  })
12997
13319
  ]);
12998
- var rawContract11 = {
13320
+ var rawContract13 = {
12999
13321
  name: "save_generated_content",
13000
13322
  description: "Save newly generated marketing content to marketing_contenido. The content is saved as pendiente_aprobacion \u2014 it is NOT published until the tenant approves it. Enforces weekly frequency limits per platform. If calendarioItemRef is provided, links to the slot and discards any prior content of the same slot. Validates datos against the platform schema.",
13001
- paramsSchema: ParamsSchema11,
13002
- outputSchema: OutputSchema11,
13323
+ paramsSchema: ParamsSchema13,
13324
+ outputSchema: OutputSchema13,
13003
13325
  // Crea borrador + descarta borrador previo del mismo slot. Reversible
13004
13326
  // (volver a llamar con datos corregidos crea un nuevo borrador y descarta
13005
13327
  // el actual). NO publica nada externo — la CF de publish se encarga
@@ -13044,7 +13366,7 @@ var rawContract11 = {
13044
13366
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
13045
13367
  };
13046
13368
  var contenidoWriterContract = MartinContractSchema.parse(
13047
- rawContract11
13369
+ rawContract13
13048
13370
  );
13049
13371
  function fmtDate(d) {
13050
13372
  const y = d.getFullYear();
@@ -13319,34 +13641,34 @@ OBLIGATORIO: calendarioItemRef con formato EXACTO "semana:N:slot:M" (ej: "semana
13319
13641
  OBLIGATORIO: fotoId \u2014 SIEMPRE pasa el ID de la foto que elegiste con get_photos_for_slot.
13320
13642
  Si el slot tiene notas[], LEERLAS y usarlas como contexto. Si estado es revisar, regenerar adaptando a las notas.
13321
13643
  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(
13644
+ var ParamsSchema14 = import_zod44.z.object({
13645
+ tenantId: import_zod44.z.string().min(1).describe("Tenant identifier (the business account)."),
13646
+ brandId: import_zod44.z.string().min(1).describe("Brand identifier within the tenant."),
13647
+ semana: import_zod44.z.number().int().min(1).max(5).optional().describe(
13326
13648
  "Week number within the current month (1-5). OMIT to default to the current week inferred from today."
13327
13649
  ),
13328
- modo: import_zod42.z.enum(["planificar", "generar"]).optional().describe(
13650
+ modo: import_zod44.z.enum(["planificar", "generar"]).optional().describe(
13329
13651
  "Operation mode. 'planificar' (default): propose distribution platform+keyword+tipo per day. 'generar': requires slots in pre_aprobado/revisar \u2014 generate actual content. OMIT to default to 'planificar'."
13330
13652
  )
13331
13653
  });
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(
13654
+ var OutputSchema14 = import_zod44.z.discriminatedUnion("ok", [
13655
+ import_zod44.z.object({
13656
+ ok: import_zod44.z.literal(true),
13657
+ payload: import_zod44.z.record(import_zod44.z.string(), import_zod44.z.unknown()).describe(
13336
13658
  "Aggregated context for the LLM. Shape varies by modo: 'planificar' returns brand skeleton + slotsYaExistentes + historial; 'generar' returns slotsParaGenerar + fotosDisponibles + brand.blogStrategy + blogJIT (if blog slots present)."
13337
13659
  )
13338
13660
  }),
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()
13661
+ import_zod44.z.object({
13662
+ ok: import_zod44.z.literal(false),
13663
+ error: import_zod44.z.string(),
13664
+ code: import_zod44.z.enum(["BRAND_NOT_FOUND", "WEEK_HAS_NO_SLOTS"]).optional()
13343
13665
  })
13344
13666
  ]);
13345
- var rawContract12 = {
13667
+ var rawContract14 = {
13346
13668
  name: "generate_weekly_content",
13347
13669
  description: "Build the week's content. Two modes: 'planificar' aggregates context for the LLM to propose distribution (LLM then uses add_calendar_slot/update_calendar_slot per day); 'generar' returns pre-approved slots + photos + blog JIT context for the LLM to generate actual content (LLM then calls save_generated_content per slot). AUTO-CREATES the monthly marketing_calendario if it does not exist. NEVER generates content directly \u2014 always returns a payload for the LLM consumer.",
13348
- paramsSchema: ParamsSchema12,
13349
- outputSchema: OutputSchema12,
13670
+ paramsSchema: ParamsSchema14,
13671
+ outputSchema: OutputSchema14,
13350
13672
  // Auto-create de calendario es bootstrap reversible (volver a llamar usa el
13351
13673
  // calendario existente). NO publica nada externo. modo='generar' tampoco
13352
13674
  // publica — solo prepara contexto.
@@ -13391,7 +13713,7 @@ var rawContract12 = {
13391
13713
  sideEffects: ["reads_firestore", "writes_firestore"]
13392
13714
  };
13393
13715
  var weeklyContentBuilderContract = MartinContractSchema.parse(
13394
- rawContract12
13716
+ rawContract14
13395
13717
  );
13396
13718
  var DEFAULT_TEXT_THRESHOLD = 0.35;
13397
13719
  var DEFAULT_IMAGE_THRESHOLD = 0.08;
@@ -13580,69 +13902,69 @@ async function contentFinder(input) {
13580
13902
  }
13581
13903
  return result;
13582
13904
  }
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()
13905
+ var IncludeSchema = import_zod45.z.object({
13906
+ products: import_zod45.z.boolean(),
13907
+ collections: import_zod45.z.boolean(),
13908
+ articles: import_zod45.z.boolean(),
13909
+ pages: import_zod45.z.boolean()
13588
13910
  }).describe(
13589
13911
  "Toggles for which Shopify content types to search. Default truthy for products/collections/articles, false for pages. Set false to skip a category for performance."
13590
13912
  );
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)
13913
+ var LimitSchema = import_zod45.z.object({
13914
+ products: import_zod45.z.number().int().min(0).max(20),
13915
+ collections: import_zod45.z.number().int().min(0).max(10),
13916
+ articles: import_zod45.z.number().int().min(0).max(20),
13917
+ pages: import_zod45.z.number().int().min(0).max(10)
13596
13918
  }).describe(
13597
13919
  "Per-category result count caps. Defaults: products 5, collections 3, articles 5, pages 2."
13598
13920
  );
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(
13921
+ var ParamsSchema15 = import_zod45.z.object({
13922
+ tenantId: import_zod45.z.string().min(1).describe("Tenant identifier (the business account)."),
13923
+ brandId: import_zod45.z.string().min(1).describe("Brand identifier within the tenant."),
13924
+ contexto: import_zod45.z.string().min(1).describe(
13603
13925
  "Search context: a paragraph, keyword, or intent string. Embedded for vector search."
13604
13926
  ),
13605
- fecha: import_zod43.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
13927
+ fecha: import_zod45.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
13606
13928
  "Content date in YYYY-MM-DD. Used to detect active season and prioritize matching collections."
13607
13929
  ),
13608
13930
  include: IncludeSchema,
13609
13931
  limit: LimitSchema,
13610
- diversidad: import_zod43.z.boolean().describe(
13932
+ diversidad: import_zod45.z.boolean().describe(
13611
13933
  "Whether to apply Jaccard title diversification + handle dedupe to results. Default true."
13612
13934
  ),
13613
- mode: import_zod43.z.enum(["text", "hybrid"]).optional().describe(
13935
+ mode: import_zod45.z.enum(["text", "hybrid"]).optional().describe(
13614
13936
  "Search mode. 'text' (default): single query per collection against embeddingText, fast. 'hybrid': parallel text + image queries with Reciprocal Rank Fusion, 2x queries \u2014 useful when text mode returns few results or to surface visually-similar items with weak SEO."
13615
13937
  )
13616
13938
  });
13617
- var VectorResultSchema = import_zod43.z.object({
13618
- id: import_zod43.z.string(),
13619
- similarity: import_zod43.z.number().optional()
13939
+ var VectorResultSchema = import_zod45.z.object({
13940
+ id: import_zod45.z.string(),
13941
+ similarity: import_zod45.z.number().optional()
13620
13942
  }).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()
13943
+ var TemporadaSchema2 = import_zod45.z.object({
13944
+ coleccion: import_zod45.z.string().nullable(),
13945
+ titulo: import_zod45.z.string().nullable(),
13946
+ razon: import_zod45.z.string().nullable(),
13947
+ fechaInicio: import_zod45.z.string().nullable(),
13948
+ fechaFin: import_zod45.z.string().nullable()
13627
13949
  });
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(),
13950
+ var SuggestedActionSchema2 = import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown());
13951
+ var DetectedConflictSchema2 = import_zod45.z.record(import_zod45.z.string(), import_zod45.z.unknown());
13952
+ var OutputSchema15 = import_zod45.z.object({
13953
+ productos: import_zod45.z.array(VectorResultSchema),
13954
+ colecciones: import_zod45.z.array(VectorResultSchema),
13955
+ articles: import_zod45.z.array(VectorResultSchema),
13956
+ pages: import_zod45.z.array(VectorResultSchema),
13957
+ _instrucciones: import_zod45.z.string(),
13958
+ _negativePrompt: import_zod45.z.string(),
13637
13959
  _temporadaActiva: TemporadaSchema2.nullable(),
13638
13960
  _detectedConflict: DetectedConflictSchema2.optional(),
13639
- _suggestedActions: import_zod43.z.array(SuggestedActionSchema2)
13961
+ _suggestedActions: import_zod45.z.array(SuggestedActionSchema2)
13640
13962
  });
13641
- var rawContract13 = {
13963
+ var rawContract15 = {
13642
13964
  name: "find_content_for_topic",
13643
13965
  description: "Find Shopify content (products, collections, articles, pages) semantically related to a context/keyword + date. Uses multimodal vector search (1408d Vertex). Mode 'text' (default) queries embeddingText (fast); 'hybrid' merges text + image results via Reciprocal Rank Fusion (rescues items with weak text SEO). Auto-injects JIT context: linking instructions, brand visual negatives, active season collection, and conflict detection (>85% similar article).",
13644
- paramsSchema: ParamsSchema13,
13645
- outputSchema: OutputSchema13,
13966
+ paramsSchema: ParamsSchema15,
13967
+ outputSchema: OutputSchema15,
13646
13968
  // Lectura pura (vector search). Failures internas en search degradan a
13647
13969
  // arrays vacíos — el helper NO falla.
13648
13970
  requiresConfirmation: false,
@@ -13665,7 +13987,7 @@ var rawContract13 = {
13665
13987
  sideEffects: ["reads_firestore"]
13666
13988
  };
13667
13989
  var contentFinderContract = MartinContractSchema.parse(
13668
- rawContract13
13990
+ rawContract15
13669
13991
  );
13670
13992
  var DEFAULT_PHOTO_THRESHOLD = 0.7;
13671
13993
  var DEFAULT_SHOPIFY_THRESHOLD = 0.65;
@@ -13812,51 +14134,51 @@ async function slotAssetFinder(input) {
13812
14134
  _fuente: fuente
13813
14135
  };
13814
14136
  }
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(
14137
+ var ParamsSchema16 = import_zod46.z.object({
14138
+ tenantId: import_zod46.z.string().min(1).describe("Tenant identifier (the business account)."),
14139
+ brandId: import_zod46.z.string().min(1).describe("Brand identifier within the tenant."),
14140
+ keyword: import_zod46.z.string().min(1).describe(
13819
14141
  "Slot keyword to search photos for. Used as embedding query for multimodal vector search."
13820
14142
  ),
13821
- plataforma: import_zod44.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe(
14143
+ plataforma: import_zod46.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe(
13822
14144
  "Target platform \u2014 determines the variant format to resolve (gbp_4_3, blog_3_2, ig_4_5, ig_1_1)."
13823
14145
  ),
13824
- fecha: import_zod44.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
14146
+ fecha: import_zod46.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe(
13825
14147
  "Slot date in YYYY-MM-DD. Used to detect active season and apply seasonal catalog overrides."
13826
14148
  ),
13827
- limit: import_zod44.z.number().int().min(1).max(10).optional().describe("Max number of photos to return. Default 5. Max 10.")
14149
+ limit: import_zod46.z.number().int().min(1).max(10).optional().describe("Max number of photos to return. Default 5. Max 10.")
13828
14150
  });
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()
14151
+ var TemporadaSchema22 = import_zod46.z.object({
14152
+ coleccion: import_zod46.z.string().nullable(),
14153
+ titulo: import_zod46.z.string().nullable(),
14154
+ razon: import_zod46.z.string().nullable(),
14155
+ fechaInicio: import_zod46.z.string().nullable(),
14156
+ fechaFin: import_zod46.z.string().nullable()
13835
14157
  });
13836
- var OutputSchema14 = import_zod44.z.discriminatedUnion("ok", [
14158
+ var OutputSchema16 = import_zod46.z.discriminatedUnion("ok", [
13837
14159
  // Note: success case does not include `ok: true` literal in helper return —
13838
14160
  // helper returns the success shape directly. Adapter to discriminated union
13839
14161
  // 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(),
14162
+ import_zod46.z.object({
14163
+ ok: import_zod46.z.literal(true),
14164
+ fotos: import_zod46.z.array(import_zod46.z.record(import_zod46.z.string(), import_zod46.z.unknown())),
14165
+ _instrucciones: import_zod46.z.string(),
14166
+ _negativePrompt: import_zod46.z.string(),
13845
14167
  _temporadaActiva: TemporadaSchema22.nullable(),
13846
- _bloqueoProducto: import_zod44.z.boolean(),
13847
- _fuente: import_zod44.z.enum(["tenant", "shopify_product"])
14168
+ _bloqueoProducto: import_zod46.z.boolean(),
14169
+ _fuente: import_zod46.z.enum(["tenant", "shopify_product"])
13848
14170
  }),
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()
14171
+ import_zod46.z.object({
14172
+ ok: import_zod46.z.literal(false),
14173
+ error: import_zod46.z.string(),
14174
+ code: import_zod46.z.enum(["BRAND_NOT_FOUND"]).optional()
13853
14175
  })
13854
14176
  ]);
13855
- var rawContract14 = {
14177
+ var rawContract16 = {
13856
14178
  name: "get_photos_for_slot",
13857
14179
  description: "Find tenant photos for a calendar slot (keyword + platform + date) via multimodal vector search (1408d Vertex). Resolves the platform-specific variant (gbp_4_3/blog_3_2/ig_4_5/ig_1_1). Auto-injects JIT context: how to choose, brand visual negatives, active season overrides, and source layer (tenant photos vs Shopify product fallback). If <3 photos returned, consider calling request_photo_shoot to alert the tenant.",
13858
- paramsSchema: ParamsSchema14,
13859
- outputSchema: OutputSchema14,
14180
+ paramsSchema: ParamsSchema16,
14181
+ outputSchema: OutputSchema16,
13860
14182
  requiresConfirmation: false,
13861
14183
  destructive: false,
13862
14184
  affectsPublication: false,
@@ -13883,7 +14205,7 @@ var rawContract14 = {
13883
14205
  sideEffects: ["reads_firestore"]
13884
14206
  };
13885
14207
  var slotAssetFinderContract = MartinContractSchema.parse(
13886
- rawContract14
14208
+ rawContract16
13887
14209
  );
13888
14210
  var DEFAULT_SIMILARITY_THRESHOLD = 0.6;
13889
14211
  function cosineSimilarity(a, b) {
@@ -13960,44 +14282,44 @@ async function canvaTemplateSelector(input) {
13960
14282
  _instrucciones: "Plantilla Canva seleccionada. Llama a marketingDesignWithCanva({contenidoId, plantillaId, fotoVariantePath, textos}) para renderizar la pieza final."
13961
14283
  };
13962
14284
  }
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(
14285
+ var ParamsSchema17 = import_zod47.z.object({
14286
+ tenantId: import_zod47.z.string().min(1).describe("Tenant identifier (the business account)."),
14287
+ brandId: import_zod47.z.string().min(1).describe("Brand identifier within the tenant."),
14288
+ plataforma: import_zod47.z.string().min(1).describe(
13967
14289
  "Target platform (e.g. 'gbp', 'shopify_blog', 'instagram'). Filters templates that declare this plataforma."
13968
14290
  ),
13969
- tipoContenido: import_zod45.z.string().min(1).describe(
14291
+ tipoContenido: import_zod47.z.string().min(1).describe(
13970
14292
  "Content type (e.g. 'post', 'carousel', 'story', 'blog'). Filters templates that declare this tipoContenido."
13971
14293
  ),
13972
- keyword: import_zod45.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
14294
+ keyword: import_zod47.z.string().min(1).describe("Slot keyword. Used as embedding query for cosine similarity match.")
13973
14295
  });
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()
14296
+ var OutputSchema17 = import_zod47.z.union([
14297
+ import_zod47.z.object({
14298
+ plantillaId: import_zod47.z.string(),
14299
+ titulo: import_zod47.z.string().nullable(),
14300
+ thumbnailUrl: import_zod47.z.string().nullable(),
14301
+ similarity: import_zod47.z.number(),
14302
+ _instrucciones: import_zod47.z.string()
13981
14303
  }),
13982
- import_zod45.z.object({
13983
- plantillaId: import_zod45.z.null(),
13984
- motivo: import_zod45.z.enum([
14304
+ import_zod47.z.object({
14305
+ plantillaId: import_zod47.z.null(),
14306
+ motivo: import_zod47.z.enum([
13985
14307
  "brand_no_encontrada",
13986
14308
  "no_canva",
13987
14309
  "no_match_plataforma",
13988
14310
  "modo_cliente_no_soportado",
13989
14311
  "similarity_baja"
13990
14312
  ]),
13991
- similarity: import_zod45.z.number().optional(),
13992
- _instrucciones: import_zod45.z.string().optional(),
13993
- _todo: import_zod45.z.string().optional()
14313
+ similarity: import_zod47.z.number().optional(),
14314
+ _instrucciones: import_zod47.z.string().optional(),
14315
+ _todo: import_zod47.z.string().optional()
13994
14316
  })
13995
14317
  ]);
13996
- var rawContract15 = {
14318
+ var rawContract17 = {
13997
14319
  name: "select_canva_template",
13998
14320
  description: "Select the best Canva template from the tenant for a slot. Filters templates by plataforma+tipoContenido, then cosine-similarity matches embeddings vs the keyword query. Returns plantillaId on match (similarity >= 0.60). On miss returns plantillaId=null with motivo enum (no_canva, no_match_plataforma, modo_cliente_no_soportado, similarity_baja, brand_no_encontrada). The LLM should degrade gracefully (generate without Canva template) on any miss.",
13999
- paramsSchema: ParamsSchema15,
14000
- outputSchema: OutputSchema15,
14321
+ paramsSchema: ParamsSchema17,
14322
+ outputSchema: OutputSchema17,
14001
14323
  // Lectura pura. NO falla — todo "no encontré" es resultado válido del helper.
14002
14324
  requiresConfirmation: false,
14003
14325
  destructive: false,
@@ -14024,7 +14346,7 @@ var rawContract15 = {
14024
14346
  sideEffects: ["reads_firestore"]
14025
14347
  };
14026
14348
  var canvaTemplateSelectorContract = MartinContractSchema.parse(
14027
- rawContract15
14349
+ rawContract17
14028
14350
  );
14029
14351
  function buildDirectorPlanInstrucciones(catalogoVisual) {
14030
14352
  const etiquetas = catalogoVisual.etiquetas || {};
@@ -14248,38 +14570,38 @@ async function photoDirectorExecute(input) {
14248
14570
  _instrucciones: buildDirectorExecuteSuccessInstrucciones(result.balanceAfter)
14249
14571
  };
14250
14572
  }
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.")
14573
+ var PlanParamsSchema = import_zod48.z.object({
14574
+ tenantId: import_zod48.z.string().min(1).describe("Tenant identifier (the business account)."),
14575
+ fotoId: import_zod48.z.string().min(1).describe("Photo ID in marketing_fotos. The photo must have an archivoOriginal uploaded.")
14254
14576
  });
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()
14577
+ var PlanContextSchema = import_zod48.z.object({
14578
+ fotoId: import_zod48.z.string(),
14579
+ archivoOriginal: import_zod48.z.string(),
14580
+ estrategia: import_zod48.z.string(),
14581
+ brandBrief: import_zod48.z.object({
14582
+ segmento: import_zod48.z.string().nullable(),
14583
+ personalidad: import_zod48.z.unknown().nullable()
14262
14584
  }),
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()
14585
+ visualRules: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()),
14586
+ catalogoVisual: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()),
14587
+ estiloVisual: import_zod48.z.unknown().nullable(),
14588
+ notaTenant: import_zod48.z.unknown().nullable(),
14589
+ productoLinkeadoManual: import_zod48.z.unknown().nullable(),
14590
+ _instrucciones: import_zod48.z.string()
14269
14591
  });
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."),
14592
+ var PlanOutputSchema = import_zod48.z.discriminatedUnion("ok", [
14593
+ import_zod48.z.object({
14594
+ ok: import_zod48.z.literal(true),
14595
+ imageBase64: import_zod48.z.string().describe("Compressed photo as base64 for transport to the LLM."),
14274
14596
  context: PlanContextSchema
14275
14597
  }),
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()
14598
+ import_zod48.z.object({
14599
+ ok: import_zod48.z.literal(false),
14600
+ code: import_zod48.z.string(),
14601
+ error: import_zod48.z.string(),
14602
+ _instrucciones: import_zod48.z.string().optional(),
14603
+ fix: import_zod48.z.string().optional(),
14604
+ fotoId: import_zod48.z.string().optional()
14283
14605
  })
14284
14606
  ]);
14285
14607
  var planRawContract = {
@@ -14309,43 +14631,43 @@ var planRawContract = {
14309
14631
  var photoDirectorPlanContract = MartinContractSchema.parse(
14310
14632
  planRawContract
14311
14633
  );
14312
- var ExecuteParamsSchema = import_zod46.z.object({
14634
+ var ExecuteParamsSchema = import_zod48.z.object({
14313
14635
  // tenantId is injected by the wrapper from ctx (A7 module-scope pattern,
14314
14636
  // even though the helper function itself derives tenantId via the foto's
14315
14637
  // 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(
14638
+ tenantId: import_zod48.z.string().min(1).describe("Tenant identifier (the business account)."),
14639
+ fotoId: import_zod48.z.string().min(1).describe("Photo ID to edit (must have been planned via plan_photo_edit)."),
14640
+ prompt: import_zod48.z.string().nullable().describe(
14319
14641
  "English prompt for Gemini Image Edit when acciones includes edit_background. Pass null when strategy is 'tal_cual' (no editing \u2014 just regenerate thumbnail + embedding with new tags)."
14320
14642
  ),
14321
- acciones: import_zod46.z.array(import_zod46.z.enum(["edit_background", "none"])).describe(
14643
+ acciones: import_zod48.z.array(import_zod48.z.enum(["edit_background", "none"])).describe(
14322
14644
  "Edit operations to perform. Use ['edit_background'] for AI background edit, ['none'] when strategy is 'tal_cual'."
14323
14645
  ),
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).")
14646
+ descripcion: import_zod48.z.string().describe("Semantic description in Spanish (saved as descripcionVision on the photo)."),
14647
+ tipo: import_zod48.z.string().nullable().describe("Photo type from the brand catalogoVisual. null only if catalog has no matching type."),
14648
+ tagsPrimarios: import_zod48.z.array(import_zod48.z.string()).describe("Primary tags from the catalogoVisual."),
14649
+ tagsSecundarios: import_zod48.z.array(import_zod48.z.string()).describe("Secondary tags from the catalogoVisual."),
14650
+ tagsContexto: import_zod48.z.array(import_zod48.z.string()).describe("Contextual tags (occasion, mood, style).")
14329
14651
  });
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()
14652
+ var ExecuteOutputSchema = import_zod48.z.discriminatedUnion("ok", [
14653
+ import_zod48.z.object({
14654
+ ok: import_zod48.z.literal(true),
14655
+ fotoId: import_zod48.z.string(),
14656
+ archivoEditado: import_zod48.z.string().optional(),
14657
+ thumbnailUrl: import_zod48.z.string().optional(),
14658
+ iteracion: import_zod48.z.number().optional(),
14659
+ editCosto: import_zod48.z.number().optional(),
14660
+ creditsConsumed: import_zod48.z.number().optional(),
14661
+ balanceAfter: import_zod48.z.number().optional(),
14662
+ imageBase64: import_zod48.z.string().nullable(),
14663
+ _instrucciones: import_zod48.z.string()
14342
14664
  }),
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()
14665
+ import_zod48.z.object({
14666
+ ok: import_zod48.z.literal(false),
14667
+ error: import_zod48.z.string(),
14668
+ code: import_zod48.z.string(),
14669
+ details: import_zod48.z.record(import_zod48.z.string(), import_zod48.z.unknown()).optional(),
14670
+ _instrucciones: import_zod48.z.string()
14349
14671
  })
14350
14672
  ]);
14351
14673
  var executeRawContract = {
@@ -15053,6 +15375,12 @@ function callGetCalendar(input) {
15053
15375
  function callAddCalendarSlot(input) {
15054
15376
  return callCF("marketingAddCalendarSlotCallable", input);
15055
15377
  }
15378
+ function callSeoSnapshotReader(input) {
15379
+ return callCF("marketingSeoSnapshotReaderCallable", input);
15380
+ }
15381
+ function callCollectionsReader(input) {
15382
+ return callCF("marketingCollectionsReaderCallable", input);
15383
+ }
15056
15384
  function callBrandBriefWriter(input) {
15057
15385
  return callCF("marketingBrandBriefWriterCallable", input);
15058
15386
  }
@@ -15121,7 +15449,7 @@ function callListarRutinas(input) {
15121
15449
  }
15122
15450
 
15123
15451
  // src/tools/marketing/photos.ts
15124
- var import_zod47 = require("zod");
15452
+ var import_zod49 = require("zod");
15125
15453
 
15126
15454
  // src/services/marketingEmbeddings.ts
15127
15455
  var import_google_auth_library = require("google-auth-library");
@@ -15417,11 +15745,11 @@ REGLAS:
15417
15745
 
15418
15746
  USAR: antes de generar contenido de cualquier slot del calendario.`,
15419
15747
  {
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)
15748
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
15749
+ keyword: import_zod49.z.string().describe("Keyword del slot"),
15750
+ plataforma: import_zod49.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino del slot"),
15751
+ fecha: import_zod49.z.string().describe("Fecha del slot en ISO YYYY-MM-DD"),
15752
+ limit: import_zod49.z.number().int().min(1).max(10).default(5)
15425
15753
  },
15426
15754
  async ({ brandId: inputBrandId, keyword, plataforma, fecha, limit }) => {
15427
15755
  const tenantId = session.requireTenant();
@@ -15469,7 +15797,7 @@ DESPUES de ver la foto, decide:
15469
15797
 
15470
15798
  Luego llama execute_photo_edit con tu analisis y prompt.`,
15471
15799
  {
15472
- fotoId: import_zod47.z.string().describe("ID de la foto")
15800
+ fotoId: import_zod49.z.string().describe("ID de la foto")
15473
15801
  },
15474
15802
  async ({ fotoId }) => {
15475
15803
  const tenantId = session.requireTenant();
@@ -15517,14 +15845,14 @@ Retorna la foto editada para que la revises. Si no te gusta:
15517
15845
 
15518
15846
  Si estrategia era 'tal_cual', pasa acciones=['none'] \u2014 no se edita pero si se genera thumbnail y embedding con tus tags.`,
15519
15847
  {
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())
15848
+ fotoId: import_zod49.z.string(),
15849
+ prompt: import_zod49.z.string().nullable().describe("Prompt en ingles para Gemini Image Edit. null si tal_cual"),
15850
+ acciones: import_zod49.z.array(import_zod49.z.enum(["edit_background", "none"])),
15851
+ descripcion: import_zod49.z.string().describe("Descripcion semantica en espanol"),
15852
+ tipo: import_zod49.z.string().nullable().describe("Tipo del catalogoVisual del tenant"),
15853
+ tagsPrimarios: import_zod49.z.array(import_zod49.z.string()),
15854
+ tagsSecundarios: import_zod49.z.array(import_zod49.z.string()),
15855
+ tagsContexto: import_zod49.z.array(import_zod49.z.string())
15528
15856
  },
15529
15857
  async ({ fotoId, prompt, acciones, descripcion, tipo, tagsPrimarios, tagsSecundarios, tagsContexto }) => {
15530
15858
  const tenantId = session.requireTenant();
@@ -15600,7 +15928,7 @@ Retorna:
15600
15928
  - creditos: { balance, planId, periodEnd, costoPhotoEdit }
15601
15929
  - _instrucciones: que hacer segun el estado`,
15602
15930
  {
15603
- fotoId: import_zod47.z.string().describe("ID de la foto")
15931
+ fotoId: import_zod49.z.string().describe("ID de la foto")
15604
15932
  },
15605
15933
  async ({ fotoId }) => {
15606
15934
  const tenantId = session.requireTenant();
@@ -15700,11 +16028,11 @@ Retorna:
15700
16028
  "find_products_for_content",
15701
16029
  `[DEPRECATED 179.5] Wrapper interno de find_content_for_topic. Para nuevas integraciones, usa find_content_for_topic con include.products=true directamente. Este wrapper existe solo por compat hacia atras.`,
15702
16030
  {
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)
16031
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16032
+ contexto: import_zod49.z.string().describe("Parrafo, keyword o intencion del contenido"),
16033
+ fecha: import_zod49.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
16034
+ limit: import_zod49.z.number().int().min(1).max(10).default(5),
16035
+ diversidad: import_zod49.z.boolean().default(true)
15708
16036
  },
15709
16037
  async ({ brandId: inputBrandId, contexto, fecha, limit, diversidad }) => {
15710
16038
  console.warn(
@@ -15781,10 +16109,10 @@ RETORNA { plantillaId, titulo, thumbnailUrl } o { plantillaId: null, motivo: 'no
15781
16109
 
15782
16110
  USAR: solo si tenant tiene Canva conectado (tenants/{tenantId}/marketing_config/{brandId}.canva.connected=true).`,
15783
16111
  {
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")
16112
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16113
+ plataforma: import_zod49.z.string().describe("gbp | instagram | shopify_blog"),
16114
+ tipoContenido: import_zod49.z.string().describe("post | carousel | story | blog"),
16115
+ keyword: import_zod49.z.string().describe("Keyword del slot")
15788
16116
  },
15789
16117
  async ({ brandId: inputBrandId, plataforma, tipoContenido, keyword }) => {
15790
16118
  const tenantId = session.requireTenant();
@@ -15829,15 +16157,15 @@ USAR: cuando get_photos_for_slot retorna pocas fotos y el slot aun no esta cubie
15829
16157
 
15830
16158
  IMPORTANTE \u2014 campo 'razon': Este texto lo ve el tenant en la app. Escribe en lenguaje amigable y claro, NO tecnico. Ejemplo MALO: "get_photos_for_slot retorno 0 fotos para shopify_blog". Ejemplo BUENO: "No hay fotos de alcatraz para el blog del 8 de abril". Siempre en espanol si el tenant habla espanol.`,
15831
16159
  {
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")')
16160
+ brandId: import_zod49.z.string().optional().describe("ID de la brand"),
16161
+ semana: import_zod49.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD").describe("Semana en ISO del lunes"),
16162
+ necesidades: import_zod49.z.array(
16163
+ import_zod49.z.object({
16164
+ tema: import_zod49.z.string(),
16165
+ keyword: import_zod49.z.string(),
16166
+ cantidadSugerida: import_zod49.z.number().int().positive(),
16167
+ razon: import_zod49.z.string(),
16168
+ slotsAfectados: import_zod49.z.array(import_zod49.z.string()).optional().describe('Refs de slots afectados (formato "semana:N:slot:M")')
15841
16169
  })
15842
16170
  ).min(1)
15843
16171
  },
@@ -15900,7 +16228,7 @@ IMPORTANTE \u2014 campo 'razon': Este texto lo ve el tenant en la app. Escribe e
15900
16228
  }
15901
16229
 
15902
16230
  // src/tools/marketing/content.ts
15903
- var import_zod48 = require("zod");
16231
+ var import_zod50 = require("zod");
15904
16232
  var _logOverride = null;
15905
16233
  async function logToMcpLogs(entry) {
15906
16234
  if (_logOverride) return _logOverride(entry);
@@ -15913,22 +16241,22 @@ async function logToMcpLogs(entry) {
15913
16241
  } catch {
15914
16242
  }
15915
16243
  }
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)
16244
+ var IncludeSchema2 = import_zod50.z.object({
16245
+ products: import_zod50.z.boolean().default(true),
16246
+ collections: import_zod50.z.boolean().default(true),
16247
+ articles: import_zod50.z.boolean().default(true),
16248
+ pages: import_zod50.z.boolean().default(false)
15921
16249
  }).default({
15922
16250
  products: true,
15923
16251
  collections: true,
15924
16252
  articles: true,
15925
16253
  pages: false
15926
16254
  });
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)
16255
+ var LimitSchema2 = import_zod50.z.object({
16256
+ products: import_zod50.z.number().int().min(0).max(20).default(5),
16257
+ collections: import_zod50.z.number().int().min(0).max(10).default(3),
16258
+ articles: import_zod50.z.number().int().min(0).max(20).default(5),
16259
+ pages: import_zod50.z.number().int().min(0).max(10).default(2)
15932
16260
  }).default({ products: 5, collections: 3, articles: 5, pages: 2 });
15933
16261
  function registerContentTools(server, session) {
15934
16262
  server.tool(
@@ -15956,13 +16284,13 @@ MODOS (parametro mode):
15956
16284
 
15957
16285
  Cuando uses hybrid: inspecciona el campo \`modesFound\` en los resultados. 2 = el doc aparecio en ambos modos (alta confianza). 1 = aparecio solo en text (problema visual) o solo en image (problema SEO). Esos "solo image" son candidatos perfectos para recomendar optimizacion al tenant.`,
15958
16286
  {
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"),
16287
+ brandId: import_zod50.z.string().optional().describe("ID de la brand"),
16288
+ contexto: import_zod50.z.string().min(1).describe("Parrafo, keyword o intencion"),
16289
+ fecha: import_zod50.z.string().describe("Fecha del contenido en ISO YYYY-MM-DD"),
15962
16290
  include: IncludeSchema2.optional(),
15963
16291
  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)")
16292
+ diversidad: import_zod50.z.boolean().default(true),
16293
+ mode: import_zod50.z.enum(["text", "hybrid"]).default("text").describe("Modo de busqueda: 'text' (rapido, 1 query) o 'hybrid' (text+image via RRF, 2x queries, rescata SEO debil)")
15966
16294
  },
15967
16295
  async ({ brandId: inputBrandId, contexto, fecha, include, limit, diversidad, mode }) => {
15968
16296
  const tenantId = session.requireTenant();
@@ -16173,8 +16501,8 @@ function registerMarketingTools(server, session) {
16173
16501
  "get_calendar",
16174
16502
  "Lee el calendario editorial del mes. Muestra semanas con items planificados por plataforma.",
16175
16503
  {
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)")
16504
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16505
+ mes: import_zod51.z.string().optional().describe("Mes en formato YYYY-MM (default: mes actual)")
16178
16506
  },
16179
16507
  async ({ brandId: inputBrandId, mes }) => {
16180
16508
  const tenantId = session.requireTenant();
@@ -16210,29 +16538,46 @@ function registerMarketingTools(server, session) {
16210
16538
  );
16211
16539
  server.tool(
16212
16540
  "get_seo_snapshot",
16213
- "Lee el snapshot SEO de la brand: rank, keywords, oportunidades, competidores.",
16541
+ "Read the latest Semrush SEO snapshot for a brand: rank, top keywords, opportunities, competitors.",
16214
16542
  {
16215
- brandId: import_zod49.z.string().optional().describe("ID de la brand")
16543
+ brandId: import_zod51.z.string().optional().describe("Brand identifier within the tenant")
16216
16544
  },
16217
16545
  async ({ brandId: inputBrandId }) => {
16218
16546
  const tenantId = session.requireTenant();
16219
16547
  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." }) }] };
16548
+ const ctx = await buildContext(session, brandId);
16549
+ const result = await dispatchWithContract({
16550
+ contract: seoSnapshotReaderContract,
16551
+ helper: seoSnapshotReader,
16552
+ callable: callSeoSnapshotReader,
16553
+ input: { tenantId, brandId },
16554
+ ctx
16555
+ });
16556
+ let payload;
16557
+ if (result.state === "success" && result.structuredOutput) {
16558
+ const out = result.structuredOutput;
16559
+ if (out.ok) {
16560
+ payload = out.snapshot;
16561
+ } else {
16562
+ payload = { ok: false, code: out.code, mensaje: result.text };
16563
+ }
16564
+ } else {
16565
+ payload = {
16566
+ ok: false,
16567
+ state: result.state,
16568
+ mensaje: result.text,
16569
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
16570
+ };
16226
16571
  }
16227
- return { content: [{ type: "text", text: JSON.stringify(brand.seoSnapshot, null, 2) }] };
16572
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
16228
16573
  }
16229
16574
  );
16230
16575
  server.tool(
16231
16576
  "get_photo_gallery",
16232
16577
  "Fotos disponibles filtradas por estado. Muestra fotos con metadata y conteo.",
16233
16578
  {
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)")
16579
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16580
+ estado: import_zod51.z.string().optional().describe("Filtrar por estado (nueva, procesando, editada, usada, descartada, error)")
16236
16581
  },
16237
16582
  async ({ brandId: inputBrandId, estado }) => {
16238
16583
  session.requireTenant();
@@ -16272,7 +16617,7 @@ function registerMarketingTools(server, session) {
16272
16617
  "generate_marketing_plan",
16273
16618
  "Aggregate the data needed to generate a strategic marketing plan: SEO snapshot + products. The LLM uses the system prompt to generate the plan from this payload.",
16274
16619
  {
16275
- brandId: import_zod49.z.string().optional().describe("ID de la brand")
16620
+ brandId: import_zod51.z.string().optional().describe("ID de la brand")
16276
16621
  },
16277
16622
  async ({ brandId: inputBrandId }) => {
16278
16623
  const tenantId = session.requireTenant();
@@ -16286,8 +16631,8 @@ function registerMarketingTools(server, session) {
16286
16631
  "save_marketing_plan",
16287
16632
  "Save a marketing plan to the brand configuration. Writes to tenants/{tenantId}/marketing_config/{brandId}.plan. If the plan object includes a blogStrategy field, it is extracted and stored at brand level (not nested inside plan).",
16288
16633
  {
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.")
16634
+ brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
16635
+ plan: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Full marketing plan object. Common fields: keywordsPrioritarios, gapsCompetencia, temporadas, tonoMarca, quickWins, coleccionesPriorizadas, blogStrategy.")
16291
16636
  },
16292
16637
  async ({ brandId: inputBrandId, plan }) => {
16293
16638
  const tenantId = session.requireTenant();
@@ -16315,9 +16660,9 @@ function registerMarketingTools(server, session) {
16315
16660
  "update_marketing_plan_field",
16316
16661
  "Actualiza UN campo del plan de marketing sin sobreescribir el resto. Merge parcial. Ideal para agregar coleccionesPriorizadas, actualizar quickWins, etc.",
16317
16662
  {
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")
16663
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16664
+ field: import_zod51.z.string().describe('Nombre del campo a actualizar (ej: "coleccionesPriorizadas", "quickWins", "temporadas")'),
16665
+ value: import_zod51.z.unknown().describe("Valor del campo")
16321
16666
  },
16322
16667
  async ({ brandId: inputBrandId, field, value }) => {
16323
16668
  const tenantId = session.requireTenant();
@@ -16330,7 +16675,7 @@ function registerMarketingTools(server, session) {
16330
16675
  "generate_brand_brief",
16331
16676
  "Aggregate all business data needed to generate a Brand Brief: Shopify (products, collections, orders, shop info), SEO snapshot, GBP profiles, scraped site_content, brand config, tenant locations. Read-only \u2014 does NOT write the brief. After receiving the payload, generate the brief content and save it via save_brand_brief.",
16332
16677
  {
16333
- brandId: import_zod49.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
16678
+ brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context.")
16334
16679
  },
16335
16680
  async ({ brandId: inputBrandId }) => {
16336
16681
  const tenantId = session.requireTenant();
@@ -16363,8 +16708,8 @@ function registerMarketingTools(server, session) {
16363
16708
  "save_brand_brief",
16364
16709
  "Save the Brand Brief at tenants/{tenantId}/marketing_config/{brandId}.brandBrief. Partial merge \u2014 only the brandBrief field is overwritten.",
16365
16710
  {
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.")
16711
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16712
+ brandBrief: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Full Brand Brief object.")
16368
16713
  },
16369
16714
  async ({ brandId: inputBrandId, brandBrief }) => {
16370
16715
  const tenantId = session.requireTenant();
@@ -16390,9 +16735,9 @@ function registerMarketingTools(server, session) {
16390
16735
  "generate_weekly_content",
16391
16736
  "Aggregate calendar + photos + plan data to generate the week's content. The LLM generates with the system prompt, then uses save_generated_content to persist.",
16392
16737
  {
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'.")
16738
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16739
+ semana: import_zod51.z.number().optional().describe("Numero de semana (1-5). Default: semana actual del mes."),
16740
+ modo: import_zod51.z.enum(["planificar", "generar"]).optional().describe("'planificar' = proponer distribucion (plataforma+keyword por dia). 'generar' = generar contenido real para slots ya planificados. Default: 'planificar'.")
16396
16741
  },
16397
16742
  async ({ brandId: inputBrandId, semana, modo }) => {
16398
16743
  const tenantId = session.requireTenant();
@@ -16420,14 +16765,14 @@ function registerMarketingTools(server, session) {
16420
16765
 
16421
16766
  IMPORTANT: If you selected a photo with get_photos_for_slot, ALWAYS pass fotoId here. Without fotoId the post will be published without an image.`,
16422
16767
  {
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")
16768
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16769
+ plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Plataforma destino"),
16770
+ tipo: import_zod51.z.string().optional().describe("Tipo de contenido (post, blog, carousel, etc.)"),
16771
+ keyword: import_zod51.z.string().optional().describe("Keyword target"),
16772
+ languageCode: import_zod51.z.string().optional().describe("Idioma (es/en)"),
16773
+ fotoId: import_zod51.z.string().optional().describe("ID de la foto a asociar"),
16774
+ datos: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).describe("Datos especificos de la plataforma (output de buildDatos*)"),
16775
+ calendarioItemRef: import_zod51.z.string().optional().describe("Referencia al item del calendario")
16431
16776
  },
16432
16777
  async ({ brandId: inputBrandId, plataforma, tipo, keyword, languageCode, fotoId, datos, calendarioItemRef }) => {
16433
16778
  const tenantId = session.requireTenant();
@@ -16463,13 +16808,13 @@ Usa para: corregir body, metaTitle, tags, fotoId, o cualquier campo sin tener qu
16463
16808
  NO puede cambiar: tenantId, brandId, id (inmutables).
16464
16809
  Si pasas campos dentro de "datos", se hace merge con los datos existentes (no los reemplaza entero).`,
16465
16810
  {
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")
16811
+ contenidoId: import_zod51.z.string().describe("ID del doc en marketing_contenido"),
16812
+ datos: import_zod51.z.record(import_zod51.z.string(), import_zod51.z.unknown()).optional().describe('Campos de datos a actualizar (merge parcial sobre datos existentes). Ej: { body: "...", metaTitle: "..." }'),
16813
+ fotoId: import_zod51.z.string().nullable().optional().describe("Actualizar foto asociada"),
16814
+ keyword: import_zod51.z.string().nullable().optional().describe("Actualizar keyword"),
16815
+ languageCode: import_zod51.z.string().optional().describe("Actualizar idioma"),
16816
+ estado: import_zod51.z.enum(["borrador", "generado", "pendiente_aprobacion", "aprobado", "rechazado"]).optional().describe("Cambiar estado manualmente"),
16817
+ calendarioItemRef: import_zod51.z.string().nullable().optional().describe("Vincular a un slot del calendario")
16473
16818
  },
16474
16819
  async ({ contenidoId, datos: newDatos, fotoId, keyword, languageCode, estado, calendarioItemRef }) => {
16475
16820
  const tenantId = session.requireTenant();
@@ -16503,19 +16848,19 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
16503
16848
  "add_calendar_slot",
16504
16849
  "Add a NEW slot to the editorial calendar. To modify an existing slot use update_calendar_slot instead.",
16505
16850
  {
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.")
16851
+ brandId: import_zod51.z.string().describe('Brand ID (e.g. "ponch", "teleglobos").'),
16852
+ mes: import_zod51.z.string().describe('Calendar month in YYYY-MM format (e.g. "2026-05").'),
16853
+ semana: import_zod51.z.number().describe("Week number within the month (1-5)."),
16854
+ slot: import_zod51.z.object({
16855
+ dia: import_zod51.z.string().describe("Slot date in YYYY-MM-DD format. Must fall within the week's fechaInicio/fechaFin range."),
16856
+ plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).describe("Target publishing platform."),
16857
+ tipo: import_zod51.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).describe("Content type."),
16858
+ keyword: import_zod51.z.string().describe("Primary keyword for the content."),
16859
+ tema: import_zod51.z.string().optional().describe("Content topic/theme. OMIT this field if it does not apply. Do NOT send empty string or null."),
16860
+ productoId: import_zod51.z.string().optional().describe("Linked product ID. OMIT this field if no product is linked."),
16861
+ estado: import_zod51.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe('Initial status. OMIT to use default "planificado".'),
16862
+ locationId: import_zod51.z.string().optional().describe("GBP location ID \u2014 only when plataforma=gbp AND tenant is multi-location. OMIT otherwise."),
16863
+ locationNombre: import_zod51.z.string().optional().describe("Human-readable GBP location name. OMIT if locationId is not provided.")
16519
16864
  }).describe("New slot data.")
16520
16865
  },
16521
16866
  async ({ brandId, mes, semana, slot }) => {
@@ -16541,27 +16886,27 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
16541
16886
  "update_calendar_slot",
16542
16887
  "MODIFY an EXISTING slot in the editorial calendar. To create a new slot use add_calendar_slot. If slotIndex does not exist, returns error SLOT_NOT_FOUND.",
16543
16888
  {
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).")
16889
+ brandId: import_zod51.z.string().optional().describe("Brand ID. If omitted, uses the active brand from session context."),
16890
+ mes: import_zod51.z.string().describe("Calendar month in YYYY-MM format."),
16891
+ semana: import_zod51.z.number().describe("Week number within the month (1-5)."),
16892
+ slotIndex: import_zod51.z.number().describe("Slot index (0-based). Must point to an existing slot \u2014 to add new slots use add_calendar_slot."),
16893
+ cambios: import_zod51.z.object({
16894
+ dia: import_zod51.z.string().nullable().optional().describe("Slot date in YYYY-MM-DD. OMIT if not changing date. Use null to explicitly clear."),
16895
+ plataforma: import_zod51.z.enum(["gbp", "shopify_blog", "instagram", "review"]).nullable().optional().describe("OMIT if not changing platform."),
16896
+ tipo: import_zod51.z.enum(["post", "blog", "carousel", "reel", "story", "review_response"]).nullable().optional().describe("OMIT if not changing content type."),
16897
+ keyword: import_zod51.z.string().nullable().optional().describe("OMIT if not changing keyword."),
16898
+ tema: import_zod51.z.string().nullable().optional().describe("OMIT if not changing topic."),
16899
+ productoId: import_zod51.z.string().nullable().optional().describe("OMIT if not changing linked product. Use null to unlink."),
16900
+ estado: import_zod51.z.enum(["planificado", "pre_aprobado", "generado", "revisar", "aprobado", "publicado", "rechazado"]).optional().describe("OMIT if not changing status manually."),
16901
+ contenidoRef: import_zod51.z.string().nullable().optional().describe("OMIT \u2014 managed by the system, not by callers."),
16902
+ fotoIdAsignada: import_zod51.z.string().nullable().optional().describe("Use assign_photo_to_content for photos. OMIT here."),
16903
+ notas: import_zod51.z.array(NotaCalendarioSchema).optional().describe("Append-only notes for the slot."),
16904
+ locationId: import_zod51.z.string().nullable().optional().describe("GBP location ID \u2014 only when plataforma=gbp."),
16905
+ locationNombre: import_zod51.z.string().nullable().optional().describe("Human-readable GBP location name (for UI).")
16561
16906
  }).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")
16907
+ accionContenidoExistente: import_zod51.z.union([
16908
+ import_zod51.z.enum(["descartar", "nuevo_slot", "mantener"]),
16909
+ import_zod51.z.string().regex(/^mover:semana:\d+:slot:\d+$/, "Format: mover:semana:N:slot:M")
16565
16910
  ]).optional().describe(
16566
16911
  'Required ONLY when the slot already has a contenidoRef AND the cambios touch semantic fields (keyword/tema/plataforma/tipo). In that case, the helper returns ACCION_CONTENIDO_EXISTENTE_REQUIRED with the 4 options listed below \u2014 ask the tenant which one to apply. OMIT this field in any other case. Do NOT send null. Valid values: "descartar" (mark existing content as discarded and apply changes to this slot) | "mover:semana:N:slot:M" (move existing content to target empty slot N/M and apply changes to origin slot) | "nuevo_slot" (do NOT touch this slot or its content; create a new slot same day with the changes \u2014 useful for multiple posts per day) | "mantener" (keep existing content here and apply changes anyway \u2014 typo-fix case where old content stays valid).'
16567
16912
  )
@@ -16606,9 +16951,9 @@ ESCRIBE EN DOS LUGARES:
16606
16951
 
16607
16952
  NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agenda, no en el post.`,
16608
16953
  {
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')
16954
+ contenidoRef: import_zod51.z.string().describe("ID del doc en marketing_contenido"),
16955
+ fotoId: import_zod51.z.string().describe("ID de la foto en marketing_fotos (estado editada)"),
16956
+ calendarioItemRef: import_zod51.z.string().optional().describe('Ref del slot (formato "semana:N:slot:M") para actualizar fotoIdAsignada')
16612
16957
  },
16613
16958
  async ({ contenidoRef, fotoId, calendarioItemRef }) => {
16614
16959
  const tenantId = session.requireTenant();
@@ -16634,7 +16979,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16634
16979
  "approve_content",
16635
16980
  "Aprueba contenido para publicacion. Valida transicion de estado.",
16636
16981
  {
16637
- contenidoId: import_zod49.z.string().describe("ID del contenido a aprobar")
16982
+ contenidoId: import_zod51.z.string().describe("ID del contenido a aprobar")
16638
16983
  },
16639
16984
  async ({ contenidoId }) => {
16640
16985
  session.requireTenant();
@@ -16658,8 +17003,8 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16658
17003
  "reject_content",
16659
17004
  "Rechaza contenido con motivo. Valida transicion de estado.",
16660
17005
  {
16661
- contenidoId: import_zod49.z.string().describe("ID del contenido a rechazar"),
16662
- motivo: import_zod49.z.string().describe("Motivo del rechazo")
17006
+ contenidoId: import_zod51.z.string().describe("ID del contenido a rechazar"),
17007
+ motivo: import_zod51.z.string().describe("Motivo del rechazo")
16663
17008
  },
16664
17009
  async ({ contenidoId, motivo }) => {
16665
17010
  session.requireTenant();
@@ -16680,96 +17025,40 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16680
17025
  );
16681
17026
  server.tool(
16682
17027
  "get_collections",
16683
- "Lee todas las colecciones canonicas (Shopify/WordPress/webonly) con sus 6 campos SEO actuales. Incluye best practices 2026.",
17028
+ "Read all canonical collections (Shopify/WordPress/web-only) for a brand with their 6 SEO fields, plus best practices and the strict schema for generating SEO suggestions.",
16684
17029
  {
16685
- brandId: import_zod49.z.string().optional().describe("ID de la brand")
17030
+ brandId: import_zod51.z.string().optional().describe("Brand identifier within the tenant")
16686
17031
  },
16687
17032
  async ({ brandId: inputBrandId }) => {
16688
17033
  const tenantId = session.requireTenant();
16689
17034
  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({
17035
+ const ctx = await buildContext(session, brandId);
17036
+ const result = await dispatchWithContract({
17037
+ contract: collectionsReaderContract,
17038
+ helper: collectionsReader,
17039
+ callable: callCollectionsReader,
17040
+ input: { tenantId, brandId },
17041
+ ctx
17042
+ });
17043
+ let payload;
17044
+ if (result.state === "success" && result.structuredOutput) {
17045
+ payload = result.structuredOutput;
17046
+ } else {
17047
+ payload = {
16695
17048
  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
17049
+ state: result.state,
17050
+ mensaje: result.text,
17051
+ ...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
16718
17052
  };
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
- }) }] };
17053
+ }
17054
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
16766
17055
  }
16767
17056
  );
16768
17057
  server.tool(
16769
17058
  "save_collection_suggestions",
16770
17059
  "Guarda sugerencias SEO para colecciones de Shopify. El tenant las aprueba en la UI.",
16771
17060
  {
16772
- brandId: import_zod49.z.string().optional().describe("ID de la brand"),
17061
+ brandId: import_zod51.z.string().optional().describe("ID de la brand"),
16773
17062
  suggestions: CollectionSuggestionsInputArraySchema.describe("Array de sugerencias SEO. Cada una con max 60 chars en metaTitle, max 158 en metaDescription, max 125 en imageAlt")
16774
17063
  },
16775
17064
  async ({ brandId: inputBrandId, suggestions }) => {
@@ -16795,7 +17084,7 @@ NUNCA uses update_calendar_slot para asignar fotos \u2014 eso escribe en la agen
16795
17084
  }
16796
17085
 
16797
17086
  // src/tools/martin.ts
16798
- var import_zod50 = require("zod");
17087
+ var import_zod52 = require("zod");
16799
17088
  function renderResult(result) {
16800
17089
  const payload = {
16801
17090
  state: result.state,
@@ -16811,16 +17100,16 @@ function registerMartinTools(server, session) {
16811
17100
  "recordar_memoria",
16812
17101
  "Save a user preference, rule, pattern or aversion that the system will respect in future interactions. Personal to the calling user (scope: self). Applied automatically in Martin's prompt for subsequent conversations.",
16813
17102
  {
16814
- tipo: import_zod50.z.enum(["preferencia", "regla", "patron", "aversion"]).describe(
17103
+ tipo: import_zod52.z.enum(["preferencia", "regla", "patron", "aversion"]).describe(
16815
17104
  "Memory type: 'preferencia' (personal preference), 'regla' (operational rule), 'patron' (observed behavioral pattern), 'aversion' (explicit do-not-do rule)."
16816
17105
  ),
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()
17106
+ categoria: import_zod52.z.enum(["compras", "produccion", "dispatch", "ventas", "marketing", "operacion", "personal", "delegacion"]).describe("Business area this memory applies to."),
17107
+ contenido: import_zod52.z.string().min(3).max(500).describe("Memory content in the user's own words (3-500 chars)."),
17108
+ origen: import_zod52.z.object({
17109
+ tipo: import_zod52.z.enum(["conversacion", "feedback_card", "inferido_automatico"]),
17110
+ conversacionId: import_zod52.z.string().nullable(),
17111
+ cardId: import_zod52.z.string().nullable(),
17112
+ rationale: import_zod52.z.array(import_zod52.z.string()).optional()
16824
17113
  }).describe("Where this memory was inferred from (conversation, card feedback, or auto-inferred).")
16825
17114
  },
16826
17115
  async ({ tipo, categoria, contenido, origen }) => {
@@ -16839,9 +17128,9 @@ function registerMartinTools(server, session) {
16839
17128
  "olvidar_memoria",
16840
17129
  "Archive a memory that no longer applies. The memory remains visible in settings but is no longer enforced in future interactions. Requires confirmation: the first call returns a confirmation prompt; pass confirm=true on the second call to execute.",
16841
17130
  {
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.")
17131
+ memoriaId: import_zod52.z.string().min(1).describe("Memory document ID to archive."),
17132
+ motivo: import_zod52.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
17133
+ confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
16845
17134
  },
16846
17135
  async ({ memoriaId, motivo, confirm }) => {
16847
17136
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
@@ -16859,25 +17148,25 @@ function registerMartinTools(server, session) {
16859
17148
  "programar_rutina",
16860
17149
  "Schedule a recurring or one-time task. Useful for monthly reports, reminders, scheduled posts, suggested purchases. Personal to the calling user (scope: self). Requires confirmation.",
16861
17150
  {
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()
17151
+ uidDestinatario: import_zod52.z.string().optional().describe("User ID who should receive the routine output. OMIT to default to the calling user (most common case)."),
17152
+ tipo: import_zod52.z.enum(["reporte", "recordatorio", "accion_delegada", "publicacion", "compra_sugerida"]).describe("Routine type from canonical TipoRutinaEnum."),
17153
+ frecuencia: import_zod52.z.enum(["diaria", "semanal", "quincenal", "mensual", "trimestral", "puntual"]).describe("Execution cadence."),
17154
+ config: import_zod52.z.object({
17155
+ diaSemana: import_zod52.z.number().int().min(0).max(6).nullable(),
17156
+ diaMes: import_zod52.z.number().int().min(1).max(31).nullable(),
17157
+ hora: import_zod52.z.string().regex(/^\d{2}:\d{2}$/),
17158
+ fechaPuntual: import_zod52.z.string().datetime({ offset: true }).nullable()
16870
17159
  }).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())
17160
+ accion: import_zod52.z.object({
17161
+ tool: import_zod52.z.string().min(1),
17162
+ params: import_zod52.z.record(import_zod52.z.string(), import_zod52.z.unknown())
16874
17163
  }).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()
17164
+ origen: import_zod52.z.object({
17165
+ tipo: import_zod52.z.enum(["conversacion", "feedback_card", "configurada_explicito"]),
17166
+ conversacionId: import_zod52.z.string().nullable(),
17167
+ cardId: import_zod52.z.string().nullable()
16879
17168
  }).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.")
17169
+ confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
16881
17170
  },
16882
17171
  async ({ uidDestinatario, tipo, frecuencia, config, accion, origen, confirm }) => {
16883
17172
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });
@@ -16906,8 +17195,8 @@ function registerMartinTools(server, session) {
16906
17195
  "pausar_rutina",
16907
17196
  "Pause an active routine temporarily. Can be resumed later by re-enabling it.",
16908
17197
  {
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).")
17198
+ rutinaId: import_zod52.z.string().min(1).describe("Routine document ID to pause."),
17199
+ motivo: import_zod52.z.string().optional().describe("Optional reason for pausing (logged for audit).")
16911
17200
  },
16912
17201
  async ({ rutinaId, motivo }) => {
16913
17202
  const ctx = await buildContext(session, null);
@@ -16933,9 +17222,9 @@ function registerMartinTools(server, session) {
16933
17222
  "archivar_rutina",
16934
17223
  "Archive a routine that no longer applies. The routine is preserved for audit purposes but will NOT be executed. Requires confirmation.",
16935
17224
  {
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.")
17225
+ rutinaId: import_zod52.z.string().min(1).describe("Routine document ID to archive (will not execute again)."),
17226
+ motivo: import_zod52.z.string().optional().describe("Optional reason for archiving (logged for audit)."),
17227
+ confirm: import_zod52.z.boolean().optional().describe("Set to true on the re-invocation after the user confirms. Default false.")
16939
17228
  },
16940
17229
  async ({ rutinaId, motivo, confirm }) => {
16941
17230
  const ctx = await buildContext(session, null, { confirmationGranted: confirm === true });