ajustes
This commit is contained in:
151
mcp-server/tools/docs/_docsReader.js
Normal file
151
mcp-server/tools/docs/_docsReader.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Lectura directa de los markdown del knowledge base desde el filesystem.
|
||||
*
|
||||
* El MCP server corre dentro del container `agentic` junto al FastAPI, asi
|
||||
* que los .md viven en `/app/docs/` (la imagen los copia ahi).
|
||||
*
|
||||
* En caso de override por entorno, respeta `ACAI_DOCS_DIR`. En desarrollo
|
||||
* fuera del container, fallback a paths relativos al cwd.
|
||||
*/
|
||||
|
||||
function resolveDocsDir() {
|
||||
const override = process.env.ACAI_DOCS_DIR;
|
||||
if (override) return override;
|
||||
// Container path
|
||||
return "/app/docs";
|
||||
}
|
||||
|
||||
export async function listDocs() {
|
||||
const dir = resolveDocsDir();
|
||||
let files;
|
||||
try {
|
||||
files = await fs.readdir(dir);
|
||||
} catch (err) {
|
||||
const error = new Error(`No se pudo leer el directorio de docs (${dir}): ${err.message}`);
|
||||
error.code = "DOCS_DIR_NOT_FOUND";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
||||
const docs = [];
|
||||
|
||||
for (const file of mdFiles) {
|
||||
const id = file.replace(/\.md$/i, "");
|
||||
const filePath = path.join(dir, file);
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
const lines = content.split("\n");
|
||||
const titleLine = lines.find((l) => l.trim().startsWith("# ")) || id;
|
||||
const title = titleLine.replace(/^#+/, "").trim();
|
||||
|
||||
// Summary: primeras 30 lineas no-heading, capeado a 500 chars
|
||||
const summaryLines = [];
|
||||
for (const line of lines.slice(0, 30)) {
|
||||
const t = line.trim();
|
||||
if (t && !t.startsWith("#")) summaryLines.push(t);
|
||||
if (summaryLines.join(" ").length > 500) break;
|
||||
}
|
||||
const summary = summaryLines.join(" ").slice(0, 500);
|
||||
|
||||
docs.push({
|
||||
id,
|
||||
title,
|
||||
summary,
|
||||
chars: content.length,
|
||||
});
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
|
||||
export async function readDoc(name, section) {
|
||||
const dir = resolveDocsDir();
|
||||
const safeName = String(name).replace(/\.md$/i, "").replace(/[\/\\]/g, "");
|
||||
const filePath = path.join(dir, `${safeName}.md`);
|
||||
|
||||
let content;
|
||||
try {
|
||||
content = await fs.readFile(filePath, "utf8");
|
||||
} catch (err) {
|
||||
const error = new Error(`Doc '${safeName}' no encontrado en ${dir}`);
|
||||
error.code = "DOC_NOT_FOUND";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const titleLine = content.split("\n").find((l) => l.trim().startsWith("# ")) || safeName;
|
||||
const title = titleLine.replace(/^#+/, "").trim();
|
||||
const availableSections = listSections(content);
|
||||
|
||||
if (section) {
|
||||
const sectionContent = extractSection(content, section);
|
||||
if (sectionContent === null) {
|
||||
return {
|
||||
id: safeName,
|
||||
title,
|
||||
section_requested: section,
|
||||
section_found: false,
|
||||
available_sections: availableSections,
|
||||
chars: 0,
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: safeName,
|
||||
title,
|
||||
section,
|
||||
section_found: true,
|
||||
chars: sectionContent.length,
|
||||
content: sectionContent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: safeName,
|
||||
title,
|
||||
section: null,
|
||||
section_found: true,
|
||||
chars: content.length,
|
||||
available_sections: availableSections,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
function listSections(content) {
|
||||
const sections = [];
|
||||
for (const line of content.split("\n")) {
|
||||
const t = line.trimStart();
|
||||
if (t.startsWith("## ") && !t.startsWith("### ")) {
|
||||
sections.push(t.slice(3).trim());
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function extractSection(content, query) {
|
||||
const q = String(query).toLowerCase().trim();
|
||||
if (!q) return null;
|
||||
const lines = content.split("\n");
|
||||
const captured = [];
|
||||
let capturing = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const t = line.trimStart();
|
||||
const isH2 = t.startsWith("## ") && !t.startsWith("### ");
|
||||
|
||||
if (isH2) {
|
||||
const heading = t.slice(3).trim();
|
||||
if (capturing) break;
|
||||
if (heading.toLowerCase().includes(q)) {
|
||||
capturing = true;
|
||||
captured.push(line);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (capturing) captured.push(line);
|
||||
}
|
||||
|
||||
if (captured.length === 0) return null;
|
||||
return captured.join("\n").trimEnd();
|
||||
}
|
||||
20
mcp-server/tools/docs/index.js
Normal file
20
mcp-server/tools/docs/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { registerListDocsTool } from './list_docs.js';
|
||||
import { registerReadDocTool } from './read_doc.js';
|
||||
|
||||
/**
|
||||
* Tools para consultar la documentación del proyecto bajo demanda.
|
||||
*
|
||||
* El knowledge_base del context engine ya inyecta los docs más relevantes
|
||||
* en cada turno por similitud semántica, pero hay dos casos en los que el
|
||||
* agente necesita pedir un doc explícitamente:
|
||||
* - El doc cayó fuera del top-K y solo aparece en el title-index del KB.
|
||||
* - Se necesita una sección puntual con detalle sin cargar todo el doc.
|
||||
*
|
||||
* Estas tools delegan en endpoints `/api/knowledge/...` del server Python,
|
||||
* que leen los docs del memory store (Redis) — la misma fuente que alimenta
|
||||
* el knowledge_base.
|
||||
*/
|
||||
export function registerDocsTools(server) {
|
||||
registerListDocsTool(server);
|
||||
registerReadDocTool(server);
|
||||
}
|
||||
29
mcp-server/tools/docs/list_docs.js
Normal file
29
mcp-server/tools/docs/list_docs.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { handleToolError } from "../helpers/errorHandler.js";
|
||||
import { listDocs } from "./_docsReader.js";
|
||||
|
||||
export function registerListDocsTool(server) {
|
||||
server.tool(
|
||||
"list_docs",
|
||||
`Lista todos los docs disponibles en el knowledge base con su id, título y un summary corto. Útil cuando necesitas saber qué documentación existe antes de llamar a 'read_doc'. El system prompt y el knowledge_base ya cargan los docs más relevantes a tu tarea — usa 'list_docs' / 'read_doc' solo si necesitas un doc que no apareció completo en el contexto o quieres una sección específica con detalle.`,
|
||||
{},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async () => {
|
||||
try {
|
||||
const docs = await listDocs();
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: docs.length,
|
||||
documents: docs,
|
||||
note: "Usa 'read_doc' con el 'id' (e.g. '05-tables-and-fields') para leer el doc completo o pasa 'section' para una sección concreta.",
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "list_docs", {});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
68
mcp-server/tools/docs/read_doc.js
Normal file
68
mcp-server/tools/docs/read_doc.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { readDoc } from "./_docsReader.js";
|
||||
|
||||
export function registerReadDocTool(server) {
|
||||
server.tool(
|
||||
"read_doc",
|
||||
`Lee un documento del knowledge base de Acai bajo demanda.
|
||||
|
||||
Cuándo usarlo:
|
||||
- El doc no se cargó completo en el contexto (aparece en "Other Available Docs" del knowledge_base).
|
||||
- Necesitas una sección concreta con detalle sin cargar todo el doc.
|
||||
- Vas a hacer una operación delicada y quieres releer las reglas exactas (e.g. crear una tabla, escribir un hook, editar el header).
|
||||
|
||||
Parámetros:
|
||||
- name: id del doc (sin extensión). Ejemplos: '01-builder-fields', '05-tables-and-fields', '09-mcp-tools-reference'. Usa 'list_docs' si dudas del id.
|
||||
- section: opcional. Match case-insensitive y parcial sobre headings H2 ('## ...'). Devuelve desde el heading hasta el siguiente H2.
|
||||
|
||||
Si la sección no existe, la respuesta incluye 'available_sections' para que reintentes con un nombre válido.
|
||||
|
||||
Docs disponibles (resumen):
|
||||
- 01-builder-fields — Campos editables (data-field-type), atributos Acai (c-if, c-for, c-class), c-form, componentes built-in.
|
||||
- 02-twig — Filtros Twig (get, queryDB, hook, module, imagec, translate, raw).
|
||||
- 03-modules-and-sections — Módulos vs secciones generales, thisrecord, multiv2, custom-{tableName}.
|
||||
- 04-pages-and-records — Builder vs Standard, menuType, apartados, reglas sobre enlace/controlador.
|
||||
- 05-tables-and-fields — Schema, create_table, create_field, tipos de campo, casos destructivos.
|
||||
- 06-hooks-and-cmsapi — Hooks PHP, CmsApi/CocoDB, hook middleware.
|
||||
- 07-css-js-conventions — Tailwind+BEM, scoping, Vue 3, componentes nativos.
|
||||
- 08-layout-and-libraries — get/set_layout_field, librerías globales, regla de no editar layout.json.
|
||||
- 09-mcp-tools-reference — Inventario completo + workflows canónicos.
|
||||
- 10-production-patterns — Patrones reales (cabecera, zigzag, FAQ, formulario, detalle).
|
||||
- 11-quick-reference — Cheat sheet con todas las reglas y formatos.`,
|
||||
{
|
||||
name: z.string().describe("ID del doc sin extensión (e.g. '05-tables-and-fields')"),
|
||||
section: z.string().optional().describe("Heading H2 a extraer (case-insensitive, parcial). Omitir para leer el doc completo."),
|
||||
},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async ({ name, section }) => {
|
||||
try {
|
||||
const validationError = validateRequired({ name }, ["name"], "read_doc");
|
||||
if (validationError) return validationError;
|
||||
|
||||
const data = await readDoc(name, section);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.code === "DOC_NOT_FOUND") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: error.message,
|
||||
hint: "Usa 'list_docs' para ver los ids disponibles. Los ids tienen prefijo numérico (e.g. '05-tables-and-fields').",
|
||||
}, null, 2),
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
return handleToolError(error, "read_doc", { name, section });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { registerFileTools } from './files/index.js';
|
||||
import { registerHookTools } from './hooks/index.js';
|
||||
import { registerLibrariesTools } from './libraries/index.js';
|
||||
import { registerLayoutTools } from './layout/index.js';
|
||||
import { registerDocsTools } from './docs/index.js';
|
||||
|
||||
/**
|
||||
* Register all tools on the MCP server
|
||||
@@ -27,4 +28,5 @@ export function registerTools(server) {
|
||||
registerHookTools(server);
|
||||
registerLibrariesTools(server);
|
||||
registerLayoutTools(server);
|
||||
registerDocsTools(server);
|
||||
}
|
||||
|
||||
@@ -6,15 +6,19 @@ import { withAuthParams } from "../helpers/authSchema.js";
|
||||
export function registerSetModuleExampleDataTool(server) {
|
||||
server.tool(
|
||||
"set_module_example_data",
|
||||
`Set example data for a module's editor preview. MANDATORY: call get_module first to get the schema, then fill EVERY variable.
|
||||
`Define datos de ejemplo para el preview del módulo en el editor. Antes de llamar, lee el builder.json del módulo (con 'acai-view') o usa 'get_module_config_vars' para conocer las variables exactas. Rellena TODAS las variables del schema.
|
||||
|
||||
Critical: uploads ALWAYS as [{urlPath: "..."}] (NEVER strings), multiv2 as array with 2+ items, var names from data-field-label (no spaces, lowercase). Use generate_image or placehold.co for image URLs.
|
||||
Reglas críticas:
|
||||
- Uploads SIEMPRE como [{ urlPath: "..." }] (nunca strings ni objetos sueltos).
|
||||
- 'multiv2' como array con al menos 2 items para que el preview se vea representativo.
|
||||
- Los nombres de variables se derivan de 'data-field-label' (minúsculas, sin espacios ni acentos).
|
||||
- Para URLs de imagen usa 'generate_image' o un placeholder (e.g. https://placehold.co/800x600).
|
||||
|
||||
See resource 'acai-cheat-sheet' → "Example Data Formatting" for type-specific value formats.`,
|
||||
Si dudas del formato exacto, lee 'read_doc({ name: "01-builder-fields" })'.`,
|
||||
withAuthParams({
|
||||
moduleId: z.string().describe("Module ID"),
|
||||
moduleSchema: z.object({}).passthrough().describe("Complete module schema (obtained from get_module)"),
|
||||
exampleData: z.object({}).passthrough().describe("Example data for EVERY variable in the module schema. Structure must match the schema exactly. Fill ALL variables without exception."),
|
||||
moduleId: z.string().describe("ID del módulo"),
|
||||
moduleSchema: z.object({}).passthrough().describe("Schema completo del módulo (del builder.json o de 'get_module_config_vars')"),
|
||||
exampleData: z.object({}).passthrough().describe("Datos de ejemplo para TODAS las variables del schema. La estructura debe coincidir exactamente."),
|
||||
}),
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
withAuth(async ({ moduleId, moduleSchema, exampleData }, extra) => {
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
/**
|
||||
* Workflow auto-detection engine.
|
||||
* Keyword-based pattern matching with weighted scoring + contextual adjustments.
|
||||
* No LLM call needed — fast and deterministic.
|
||||
*/
|
||||
|
||||
const WORKFLOW_PATTERNS = {
|
||||
create_section: {
|
||||
keywords: [
|
||||
"crear seccion", "create section", "nueva seccion", "new section",
|
||||
"anadir seccion", "add section", "crear tabla", "create table",
|
||||
"nueva pagina", "new page", "nueva seccion web", "new web section",
|
||||
"montar seccion", "set up section", "configurar seccion",
|
||||
// Additional English patterns
|
||||
"build section", "build page", "make section", "make page",
|
||||
"set up page", "create page", "new table",
|
||||
"section for", "seccion de", "seccion para",
|
||||
// Natural phrasing
|
||||
"want section", "need section", "quiero seccion",
|
||||
"necesito seccion", "hacer seccion", "hacer pagina"
|
||||
],
|
||||
boost: [
|
||||
"categoria", "category", "productos", "products", "blog", "noticias",
|
||||
"news", "equipo", "team", "servicios", "services", "galeria", "gallery",
|
||||
"portfolio", "testimonios", "testimonials", "faq", "preguntas",
|
||||
"clientes", "clients", "proyectos", "projects",
|
||||
"restaurante", "restaurant", "tienda", "store", "shop",
|
||||
"eventos", "events", "cursos", "courses"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
populate_content: {
|
||||
keywords: [
|
||||
"anadir contenido", "add content", "crear registros", "create records",
|
||||
"poblar", "populate", "rellenar", "fill", "bulk", "masivo",
|
||||
"insertar datos", "insert data", "meter datos", "cargar contenido",
|
||||
"load content", "contenido de ejemplo", "sample content",
|
||||
"crear entradas", "create entries", "anadir registros", "add records",
|
||||
"registros de ejemplo", "sample records", "meter registros",
|
||||
"fill with data", "fill with content", "add sample", "add examples",
|
||||
"anadir ejemplos", "contenido de prueba", "test content"
|
||||
],
|
||||
boost: [
|
||||
"imagenes", "images", "fotos", "photos", "stock", "ejemplo", "sample",
|
||||
"demo", "placeholder", "varios", "multiple", "lote", "batch"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
create_module: {
|
||||
keywords: [
|
||||
"crear modulo", "create module", "nuevo modulo", "new module",
|
||||
"disenar modulo", "design module", "hacer modulo", "make module",
|
||||
"componente", "component", "crear componente", "create component",
|
||||
"nuevo componente", "new component", "montar modulo",
|
||||
"build module", "build component", "make component"
|
||||
],
|
||||
boost: [
|
||||
"hero", "slider", "card", "grid", "lista", "list", "banner",
|
||||
"footer", "header", "navbar", "cta", "call to action",
|
||||
"carousel", "accordion", "tabs", "pricing", "features"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
edit_module: {
|
||||
keywords: [
|
||||
"editar modulo", "edit module", "modificar modulo", "modify module",
|
||||
"cambiar modulo", "change module", "actualizar modulo", "update module",
|
||||
"arreglar modulo", "fix module", "mejorar modulo", "improve module",
|
||||
"corregir modulo", "ajustar modulo", "adjust module"
|
||||
],
|
||||
boost: [
|
||||
"css", "html", "javascript", "js", "estilo", "style", "variable",
|
||||
"campo", "field", "diseno", "design", "responsive", "movil", "mobile",
|
||||
"color", "fuente", "font", "espaciado", "spacing",
|
||||
"hero", "slider", "card", "grid", "banner", "footer", "header",
|
||||
"navbar", "cta", "carousel", "accordion", "tabs", "pricing"
|
||||
],
|
||||
weight: 10
|
||||
},
|
||||
manage_records: {
|
||||
keywords: [
|
||||
"editar registro", "edit record", "actualizar registro", "update record",
|
||||
"borrar registro", "delete record", "buscar registro", "search record",
|
||||
"listar registros", "list records", "modificar registro", "modify record",
|
||||
"ver registros", "view records", "consultar registros", "query records",
|
||||
"cambiar datos", "change data", "eliminar registro", "remove record",
|
||||
// CRUD-oriented English patterns
|
||||
"update data", "delete data", "edit data", "modify data",
|
||||
"update field", "change field", "edit entry", "delete entry",
|
||||
"update price", "change price", "update name", "change name",
|
||||
"remove records", "remove entries", "crud",
|
||||
"insert record", "insert entry", "create record", "add entry",
|
||||
"find record", "find records", "search records", "search data"
|
||||
],
|
||||
boost: [
|
||||
"filtrar", "filter", "where", "campo", "field", "valor", "value",
|
||||
"pagina", "page", "ordenar", "sort", "buscar", "search",
|
||||
"precio", "price", "nombre", "name", "fecha", "date",
|
||||
"estado", "status", "activo", "active"
|
||||
],
|
||||
weight: 8
|
||||
},
|
||||
manage_media: {
|
||||
// Only specific action phrases — generic words like "image/foto" are in boost, not keywords
|
||||
keywords: [
|
||||
"subir imagen", "upload image", "subir foto", "upload photo",
|
||||
"buscar imagen stock", "search stock image", "buscar fotos stock",
|
||||
"generar imagen", "generate image", "generar foto",
|
||||
"reemplazar imagen", "replace image", "cambiar imagen", "change image",
|
||||
"borrar imagen", "delete image", "eliminar imagen", "remove image",
|
||||
"gestionar media", "manage media", "gestionar imagenes", "manage images",
|
||||
"buscar stock", "search stock", "stock photos", "fotos stock",
|
||||
"subir archivo", "upload file"
|
||||
],
|
||||
boost: [
|
||||
"stock", "pixabay", "pexels", "ai", "inteligencia artificial",
|
||||
"resize", "thumbnail", "miniatura", "s3", "assets",
|
||||
"comprimir", "compress", "optimizar", "optimize",
|
||||
// Generic image words are boosts, NOT keywords
|
||||
"imagen", "image", "foto", "photo", "galeria", "gallery", "media"
|
||||
],
|
||||
weight: 5 // Reduced from 8 — media is usually a step, not a workflow
|
||||
},
|
||||
seo_setup: {
|
||||
keywords: [
|
||||
"seo", "meta tags", "meta descripcion", "meta description",
|
||||
"enlace", "slug", "url amigable", "friendly url", "sitemap",
|
||||
"schema markup", "posicionamiento", "ranking",
|
||||
"meta titulo", "meta title", "configurar seo", "setup seo",
|
||||
"set up seo", "configure seo"
|
||||
],
|
||||
boost: [
|
||||
"google", "keywords", "palabras clave", "busqueda", "search",
|
||||
"indexar", "index", "robots", "canonical", "og:image"
|
||||
],
|
||||
weight: 6
|
||||
},
|
||||
explore_site: {
|
||||
keywords: [
|
||||
"explorar", "explore", "que tiene", "what's in", "listar todo",
|
||||
"list all", "mostrar", "show me", "overview", "resumen",
|
||||
"que hay", "que secciones", "what sections", "ver todo",
|
||||
"show everything", "estructura", "structure", "inventario",
|
||||
"mapa del sitio", "site map", "what modules", "que modulos"
|
||||
],
|
||||
boost: [
|
||||
"estructura", "structure", "mapa", "map", "resumen", "summary",
|
||||
"completo", "complete", "todas", "all"
|
||||
],
|
||||
weight: 5
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize text for matching: lowercase, remove accents, strip common articles, trim.
|
||||
*/
|
||||
function normalizeText(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare task text for matching: normalize + strip common filler words (articles, prepositions)
|
||||
* that break keyword matching (e.g., "editar el módulo" should match "editar módulo").
|
||||
*/
|
||||
function prepareTaskForMatching(text) {
|
||||
const normalized = normalizeText(text);
|
||||
// Strip common Spanish/English articles and short prepositions that break adjacent keyword matching
|
||||
return normalized.replace(/\b(el|la|los|las|un|una|unos|unas|del|al|the|a|an)\b/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// ── Contextual adjustment patterns ──────────────────────────────────────────
|
||||
// These use regex word matching to detect intent combinations that substring
|
||||
// matching misses (e.g., "create a new products section" has words separated).
|
||||
|
||||
const CREATION_VERBS = /\b(crear|create|nueva?o?|new|build|make|set up|montar|anadir|add|disenar|design|hacer)\b/;
|
||||
const EDIT_VERBS = /\b(editar|edit|modificar|modify|cambiar|change|actualizar|update|arreglar|fix|mejorar|improve|ajustar|adjust|corregir)\b/;
|
||||
const CRUD_VERBS = /\b(editar|edit|borrar|delete|eliminar|remove|actualizar|update|crear|create|insertar|insert|modificar|modify|buscar|search|listar|list|consultar|query|cambiar|change|find|get|ver|view)\b/;
|
||||
const SECTION_WORDS = /\b(seccion|section|pagina|page|tabla|table|web|sitio|site)\b/;
|
||||
const MODULE_WORDS = /\b(modulo|module|componente|component)\b/;
|
||||
const RECORD_WORDS = /\b(registro|registros|record|records|datos|data|entrada|entradas|entry|entries|contenido|content|precio|price|campo|field)\b/;
|
||||
const MEDIA_ONLY_WORDS = /\b(subir|upload|reemplazar|replace|descargar|download)\b/;
|
||||
const IMAGE_WORDS = /\b(imagen|imagenes|image|images|foto|fotos|photo|photos|galeria|gallery)\b/;
|
||||
// Words that indicate the task is about content/records, not creating a new section
|
||||
const CONTENT_INTENT_WORDS = /\b(contenido|content|rellenar|fill|poblar|populate|registros|records|sample|ejemplo|articulos|articles|entradas|entries|anadir contenido|add content)\b/;
|
||||
// Words that indicate the task is about SEO, not creating a new section
|
||||
const SEO_INTENT_WORDS = /\b(seo|meta tags?|meta descripcion|meta description|meta titulo|meta title|sitemap|slug|posicionamiento|ranking|canonical)\b/;
|
||||
|
||||
/**
|
||||
* Post-scoring contextual adjustments.
|
||||
* Uses regex word matching (not substring) to detect intent patterns the keyword
|
||||
* phase may miss due to non-adjacent words.
|
||||
*/
|
||||
function applyContextAdjustments(scores, normalizedTask) {
|
||||
const hasCreationVerb = CREATION_VERBS.test(normalizedTask);
|
||||
const hasEditVerb = EDIT_VERBS.test(normalizedTask);
|
||||
const hasCrudVerb = CRUD_VERBS.test(normalizedTask);
|
||||
const hasSection = SECTION_WORDS.test(normalizedTask);
|
||||
const hasModule = MODULE_WORDS.test(normalizedTask);
|
||||
const hasRecord = RECORD_WORDS.test(normalizedTask);
|
||||
const hasMediaAction = MEDIA_ONLY_WORDS.test(normalizedTask);
|
||||
const hasImageWord = IMAGE_WORDS.test(normalizedTask);
|
||||
const hasContentIntent = CONTENT_INTENT_WORDS.test(normalizedTask);
|
||||
const hasSeoIntent = SEO_INTENT_WORDS.test(normalizedTask);
|
||||
|
||||
// ── Section creation intent ──
|
||||
// "create" + "section/page/table" = strong signal for create_section
|
||||
// BUT NOT when the real intent is populating content or configuring SEO
|
||||
if (hasCreationVerb && hasSection && !hasContentIntent && !hasSeoIntent) {
|
||||
scores.create_section = scores.create_section || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.create_section.score += 20;
|
||||
}
|
||||
|
||||
// ── Module creation intent ──
|
||||
// "create/new" + "module/component" = strong signal for create_module
|
||||
if (hasCreationVerb && hasModule) {
|
||||
scores.create_module = scores.create_module || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.create_module.score += 20;
|
||||
}
|
||||
|
||||
// ── Module edit intent ──
|
||||
// "edit/modify/change" + "module/component" = strong signal for edit_module
|
||||
if (hasEditVerb && hasModule) {
|
||||
scores.edit_module = scores.edit_module || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.edit_module.score += 20;
|
||||
}
|
||||
|
||||
// ── Decisive create vs edit for modules ──
|
||||
// When both create_module and edit_module have scores, apply decisive differentiation
|
||||
if (hasModule && scores.create_module && scores.edit_module) {
|
||||
if (hasCreationVerb && !hasEditVerb) {
|
||||
// Clearly creation intent → penalize edit
|
||||
scores.edit_module.score = Math.max(0, scores.edit_module.score - 15);
|
||||
} else if (hasEditVerb && !hasCreationVerb) {
|
||||
// Clearly edit intent → penalize create
|
||||
scores.create_module.score = Math.max(0, scores.create_module.score - 15);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Record CRUD intent ──
|
||||
// Any CRUD verb + "record/data/entry" = signal for manage_records
|
||||
if (hasCrudVerb && hasRecord) {
|
||||
scores.manage_records = scores.manage_records || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.manage_records.score += 15;
|
||||
}
|
||||
|
||||
// ── Penalize manage_media when context is clearly about something else ──
|
||||
// If the task mentions section/module/record context, media is a step not the workflow
|
||||
if (scores.manage_media && (hasSection || hasModule || hasRecord)) {
|
||||
// Only keep media score if there's an explicit media action verb ("upload", "replace")
|
||||
if (!hasMediaAction) {
|
||||
scores.manage_media.score = Math.max(0, Math.floor(scores.manage_media.score * 0.3));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Boost manage_media only when it's the clear primary intent ──
|
||||
// "upload/replace" + "image/photo" WITHOUT section/module/record context
|
||||
if (hasMediaAction && hasImageWord && !hasSection && !hasModule && !hasRecord) {
|
||||
scores.manage_media = scores.manage_media || { score: 0, keywordHits: 0, boostHits: 0 };
|
||||
scores.manage_media.score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the best workflow for a given task description.
|
||||
* Returns the top match with confidence, or suggestions if ambiguous.
|
||||
*
|
||||
* @param {string} task - The user's task description
|
||||
* @returns {{ workflow: string, confidence: number, alternatives: Array }}
|
||||
*/
|
||||
export function detectWorkflow(task) {
|
||||
const normalizedTask = prepareTaskForMatching(task);
|
||||
const scores = {};
|
||||
|
||||
// ── Phase 1: Keyword + boost scoring ──
|
||||
for (const [workflowId, pattern] of Object.entries(WORKFLOW_PATTERNS)) {
|
||||
let score = 0;
|
||||
let keywordHits = 0;
|
||||
let boostHits = 0;
|
||||
|
||||
// Check keyword matches
|
||||
for (const keyword of pattern.keywords) {
|
||||
if (normalizedTask.includes(normalizeText(keyword))) {
|
||||
keywordHits++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check boost matches
|
||||
for (const boost of pattern.boost) {
|
||||
if (normalizedTask.includes(normalizeText(boost))) {
|
||||
boostHits++;
|
||||
}
|
||||
}
|
||||
|
||||
score = (keywordHits * pattern.weight) + (boostHits * 3);
|
||||
scores[workflowId] = { score, keywordHits, boostHits };
|
||||
}
|
||||
|
||||
// ── Phase 2: Contextual adjustments ──
|
||||
// Uses regex word matching to catch intent patterns that substring matching misses
|
||||
applyContextAdjustments(scores, normalizedTask);
|
||||
|
||||
// Sort by score descending
|
||||
const ranked = Object.entries(scores)
|
||||
.filter(([, data]) => data.score > 0)
|
||||
.sort(([, a], [, b]) => b.score - a.score);
|
||||
|
||||
if (ranked.length === 0) {
|
||||
return {
|
||||
workflow: null,
|
||||
confidence: 0,
|
||||
alternatives: []
|
||||
};
|
||||
}
|
||||
|
||||
const [topId, topData] = ranked[0];
|
||||
const maxPossibleScore = WORKFLOW_PATTERNS[topId].keywords.length * WORKFLOW_PATTERNS[topId].weight
|
||||
+ WORKFLOW_PATTERNS[topId].boost.length * 3;
|
||||
const confidence = Math.min(topData.score / Math.max(maxPossibleScore * 0.15, 1), 1);
|
||||
|
||||
// Check if top 2 are close (ambiguous)
|
||||
const alternatives = ranked.slice(1, 3).map(([id, data]) => ({
|
||||
workflow: id,
|
||||
score: data.score,
|
||||
confidence: Math.min(data.score / Math.max(
|
||||
WORKFLOW_PATTERNS[id].keywords.length * WORKFLOW_PATTERNS[id].weight * 0.15, 1
|
||||
), 1)
|
||||
}));
|
||||
|
||||
const isAmbiguous = alternatives.length > 0
|
||||
&& alternatives[0].score > 0
|
||||
&& (topData.score - alternatives[0].score) < (topData.score * 0.2);
|
||||
|
||||
return {
|
||||
workflow: topId,
|
||||
confidence: Math.round(confidence * 100) / 100,
|
||||
ambiguous: isAmbiguous,
|
||||
alternatives
|
||||
};
|
||||
}
|
||||
|
||||
export { WORKFLOW_PATTERNS };
|
||||
@@ -1,5 +0,0 @@
|
||||
import { registerOrchestrateTool } from "./orchestrate.js";
|
||||
|
||||
export function registerOrchestratorTools(server) {
|
||||
registerOrchestrateTool(server);
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { detectWorkflow } from "./detector.js";
|
||||
import { getWorkflow, listWorkflows } from "./workflows/index.js";
|
||||
|
||||
/**
|
||||
* Register the orchestrate_task tool on the MCP server.
|
||||
*/
|
||||
export function registerOrchestrateTool(server) {
|
||||
server.tool(
|
||||
"orchestrate_task",
|
||||
"Provides workflow context, domain rules, and step-by-step guidance for Acai CMS tasks. " +
|
||||
"Returns relevant warnings, resource pointers, and suggested tool order. " +
|
||||
"Optional but recommended for multi-step tasks — helps avoid common mistakes. " +
|
||||
"Available workflows: create_section, populate_content, create_module, edit_module, " +
|
||||
"manage_records, manage_media, seo_setup, explore_site.",
|
||||
{
|
||||
task: z.string().describe(
|
||||
"The user's task or request in their own words. " +
|
||||
"Example: 'Crear una sección de productos con categorías e imágenes'"
|
||||
),
|
||||
forceWorkflow: z.string().optional().describe(
|
||||
"Optional: force a specific workflow instead of auto-detecting. " +
|
||||
"Use when auto-detection is wrong or you know exactly which workflow to use. " +
|
||||
"Values: create_section, populate_content, create_module, edit_module, " +
|
||||
"manage_records, manage_media, seo_setup, explore_site"
|
||||
)
|
||||
},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async ({ task, forceWorkflow }) => {
|
||||
try {
|
||||
let workflowId;
|
||||
let confidence;
|
||||
let detectionInfo;
|
||||
|
||||
if (forceWorkflow) {
|
||||
// Forced workflow — skip detection
|
||||
workflowId = forceWorkflow;
|
||||
confidence = 1.0;
|
||||
detectionInfo = { method: "forced", forceWorkflow };
|
||||
} else {
|
||||
// Auto-detect workflow from task description
|
||||
const detection = detectWorkflow(task);
|
||||
|
||||
if (!detection.workflow) {
|
||||
// No workflow matched — return general orientation
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
workflow: "none_detected",
|
||||
message: "Could not determine a specific workflow for this task. " +
|
||||
"You can proceed freely using available tools, or specify a workflow with forceWorkflow.",
|
||||
availableWorkflows: listWorkflows(),
|
||||
generalRules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields are ALWAYS arrays of objects with urlPath property",
|
||||
"Use ONLY Twig FILTERS (pipe syntax), not Twig functions",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)"
|
||||
]
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
if (detection.ambiguous) {
|
||||
// Ambiguous — return top suggestions
|
||||
const topWorkflow = getWorkflow(detection.workflow);
|
||||
const altWorkflows = detection.alternatives
|
||||
.map(a => getWorkflow(a.workflow))
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
workflow: "ambiguous",
|
||||
message: "Multiple workflows could match this task. " +
|
||||
"Pick the most appropriate one using forceWorkflow, or proceed with the top match.",
|
||||
topMatch: {
|
||||
id: topWorkflow.id,
|
||||
name: topWorkflow.name,
|
||||
description: topWorkflow.description,
|
||||
confidence: detection.confidence
|
||||
},
|
||||
alternatives: altWorkflows.map((w, i) => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
description: w.description,
|
||||
confidence: detection.alternatives[i].confidence
|
||||
}))
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
workflowId = detection.workflow;
|
||||
confidence = detection.confidence;
|
||||
detectionInfo = {
|
||||
method: "auto",
|
||||
confidence: detection.confidence,
|
||||
alternatives: detection.alternatives
|
||||
};
|
||||
}
|
||||
|
||||
// Load the workflow
|
||||
const workflow = getWorkflow(workflowId);
|
||||
if (!workflow) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: `Unknown workflow: '${workflowId}'`,
|
||||
availableWorkflows: listWorkflows()
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Build the response
|
||||
const response = {
|
||||
success: true,
|
||||
workflow: workflow.id,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
confidence,
|
||||
detection: detectionInfo,
|
||||
totalSteps: workflow.steps.length,
|
||||
steps: workflow.steps,
|
||||
context: workflow.context,
|
||||
rules: workflow.rules,
|
||||
warnings: workflow.warnings,
|
||||
resources: workflow.resources
|
||||
};
|
||||
|
||||
console.error(`[Orchestrator] Detected workflow: ${workflow.id} (confidence: ${confidence}) for task: "${task.substring(0, 80)}..."`);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}]
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Orchestrator] Error:", error);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
export const createModuleWorkflow = {
|
||||
id: "create_module",
|
||||
name: "Create Module",
|
||||
description: "Design and create an HTML module by writing project files directly, then compile it in the CMS.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Understand the design",
|
||||
description: "Clarify with user: what does the module show? Is it a hero, grid, list, slider, CTA, form?",
|
||||
tool: null,
|
||||
critical: "Get clear requirements before writing code. Ask about: layout, colors, responsive behavior, editable fields."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Review project styling and patterns",
|
||||
description: "Use the saved project styles and nearby modules as reference before writing code.",
|
||||
tool: "save_project_styles",
|
||||
critical: "Align typography, spacing, colors, and component patterns with the existing project."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Create the module files",
|
||||
description: "Write index-base.tpl, style.css, script.js, and optional hook.php directly in the module folder.",
|
||||
tool: "acai-write",
|
||||
critical: "Use project-relative paths. Create complete files. Keep variable names lowercase, descriptive, and stable."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Refine targeted blocks if needed",
|
||||
description: "Use incremental replacements for small fixes instead of rewriting whole files.",
|
||||
tool: "acai-line-replace",
|
||||
critical: "Prefer block edits for existing files to reduce token usage and avoid accidental rewrites."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Compile the module",
|
||||
description: "Compile after editing index-base.tpl so the CMS syncs index.tpl and builder metadata.",
|
||||
tool: "compile_module",
|
||||
critical: "This is mandatory after every index-base.tpl change."
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
action: "Set example data",
|
||||
description: "Set example/static data for module preview. MUST call get_module first to discover the variable schema.",
|
||||
tool: "set_module_example_data",
|
||||
critical: "Call get_module first to get ALL variable names. Then fill EVERY variable with realistic example data. Missing variables = blank preview."
|
||||
},
|
||||
{
|
||||
step: 7,
|
||||
action: "Check module rendering",
|
||||
description: "Test the module with specific variable values to verify it renders correctly.",
|
||||
tool: "check_module",
|
||||
critical: "Test with realistic values. Check for Twig syntax errors, broken images, layout issues."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
builder_vars: "data-field-type attribute on elements creates editable fields. Types: textfield (single line text), headfield (heading), textbox (multiline), wysiwyg (rich HTML), link (URL), upload (single image), uploadBackground (background image), uploadMulti (gallery), list (dropdown options), multiv2 (repeatable block).",
|
||||
component_syntax: "c-if='varname' shows/hides element based on variable. c-for='item in items' loops over array. c-hidden='true' makes element invisible (for config vars). c-else after c-if for alternative content.",
|
||||
module_structure: "Create index-base.tpl, style.css, script.js, and optional hook.php in the module directory. Compile to generate builder.json and the public templates.",
|
||||
css_conventions: "Use TailwindCSS by default. For custom CSS: BEM naming with kebab-case. Root class should match module name. Avoid !important.",
|
||||
upload_in_modules: "Upload fields are arrays. Single image: {{ varname[0].urlPath | imagec(WIDTH) }}. Background: style=\"background-image: url('{{ varname[0].urlPath | imagec(1920) }}')\". Gallery: {% for img in varname %}{{ img.urlPath }}{% endfor %}."
|
||||
},
|
||||
rules: [
|
||||
"Variable names: lowercase, no spaces, no accents, no special characters",
|
||||
"Labels must be UNIQUE — duplicate labels create shared fields",
|
||||
"Upload fields are ALWAYS arrays — access with [0].urlPath",
|
||||
"Use ONLY Twig FILTERS (pipe syntax), not Twig functions",
|
||||
"c-if='varname' for conditional rendering of optional fields",
|
||||
"c-hidden='true' for configuration variables not shown to end user",
|
||||
"data-field-width on upload elements to set image optimization width",
|
||||
"For multiv2 (repeatable): parent element needs data-field-type='multiv2', children are the repeated fields"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT use duplicate labels — they create shared/linked fields",
|
||||
"DO NOT forget to set example data — the module will appear blank in the editor",
|
||||
"DO NOT use Twig functions (range, random, etc.) — only filters work",
|
||||
"DO NOT access upload vars as strings — always use varname[0].urlPath (array)",
|
||||
"DO NOT mix React/Vue syntax — use Twig for templating, vanilla JS for interactivity"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-builder-vars",
|
||||
"acai://resources/guia-atributos-acai",
|
||||
"acai://resources/guia-programacion-acai"
|
||||
]
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
export const createSectionWorkflow = {
|
||||
id: "create_section",
|
||||
name: "Create New Section",
|
||||
description: "Full workflow for creating a new website section: table + fields + module + template + content.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Understand requirements",
|
||||
description: "Clarify with user: section name, type (multi/single/category), fields needed, whether it needs URL (enlace), SEO meta tags.",
|
||||
tool: null,
|
||||
critical: "Ask before acting. Multi = multiple records (blog, products). Single = one record (about page). Category = grouping for other sections."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Check existing tables",
|
||||
description: "List current tables to avoid naming conflicts and understand existing structure.",
|
||||
tool: "list_tables",
|
||||
critical: "Table names must be unique. Check if a similar section already exists."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Create the table",
|
||||
description: "Create the database table with correct type and configuration. Pass enlace=true if records need public URLs; pass seoMetas=true if records need SEO meta fields. Those flags are enough — there is no update_table_schema step afterwards.",
|
||||
tool: "create_table",
|
||||
critical: "menuType must be: 'multi' (multiple records), 'single' (one record), 'category' (grouping), or 'separador' (menu separator). Set enlace=true if records need their own URL page. Set seoMetas=true if you want the SEO meta fields added from the start."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Add fields to the table",
|
||||
description: "Create each necessary field with the correct type. create_field is a single-field operation — call it once per field.",
|
||||
tool: "create_field",
|
||||
critical: "One call per field. Field types: textfield, textbox, wysiwyg, date, checkbox, list, upload, multitext, codigo, separator. Pass isRequired / maxLength / listType / etc. via initialProps."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Verify table schema",
|
||||
description: "Get the complete schema to confirm all fields were created correctly.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Verify all fields exist with correct types before proceeding to module creation."
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
action: "Design and create the listing module",
|
||||
description: "Create the listing module that displays a list/grid of records. Use create_module to scaffold the folder, then acai-write on index-base.tpl with the Twig (compile runs automatically).",
|
||||
tool: "create_module",
|
||||
critical: "Use Twig syntax. Access records with the 'get' filter. Primary key is 'num' not 'id'. Upload fields are ALWAYS arrays: use record.field[0].urlPath | imagec(width). After create_module, use acai-write on index-base.tpl to set the actual template."
|
||||
},
|
||||
{
|
||||
step: 7,
|
||||
action: "Set module example data",
|
||||
description: "Set example/static data for module preview. MUST call get_module_config_vars first to discover ALL variables.",
|
||||
tool: "set_module_example_data",
|
||||
critical: "Every builder variable must have example data. Missing variables cause blank previews."
|
||||
},
|
||||
{
|
||||
step: 8,
|
||||
action: "Add sample content",
|
||||
description: "Create 2-3 sample records with realistic content and images. If table has enlace=true, include the 'enlace' field with a URL slug.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "Date format: YYYY-MM-DD HH:mm:ss. Checkbox: 1 or 0. Upload fields: use upload_record_image separately. For sections with enlace, create records BEFORE creating the general section to ensure directory structure is ready."
|
||||
},
|
||||
{
|
||||
step: 9,
|
||||
action: "Create the general section (detail template) — if enlace=true",
|
||||
description: "If the table has enlace enabled, create a module named literally 'custom-{tableName}' in template/estandar/modulos/. This module renders every record's URL automatically; there is NO _detailPage field to configure. Use acai-write on template/estandar/modulos/custom-{tableName}/index-base.tpl — it creates the folder and triggers the compile.",
|
||||
tool: "acai-write",
|
||||
critical: "Folder name must be EXACTLY 'custom-' + tableName (e.g. table 'vacantes' → 'custom-vacantes'). The CMS routes by this convention. Inside the template access the current record via `thisrecord.fieldname`. Do NOT create a separate listing page in 'apartados' for details, do NOT use query params, do NOT use hooks to fetch the record."
|
||||
},
|
||||
{
|
||||
step: 10,
|
||||
action: "Verify the result",
|
||||
description: "Check module rendering with actual variable values.",
|
||||
tool: "check_module",
|
||||
critical: "Test with actual variable values to ensure no rendering errors."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
twig_filters: "Use 'get' filter for DB queries: {% set items = 'tablename' | get('WHERE active=1', 'ORDER BY num DESC', 10) %}. Use 'imagec' for image resize: {{ path | imagec(400) }}. Use 'module' to include other modules: {{ 'modulename' | module(vars) }}.",
|
||||
field_types: "textfield (single line), textbox (multiline), wysiwyg (rich HTML), date (YYYY-MM-DD), checkbox (0/1), list (dropdown/radio/checkbox), upload (files/images), multitext (key-value pairs), codigo (code editor), separator (visual divider).",
|
||||
list_field_config: "Static options: optionsType='text', optionsText='value1|Label 1\\nvalue2|Label 2'. Table relation: optionsType='table', optionsTablename='tablename', optionsValueField='num', optionsLabelField='name'. SQL: optionsType='query', optionsQuery='SELECT num,name FROM cms_tablename'.",
|
||||
builder_vars: "data-field-type attribute on HTML elements creates editable fields. Types: textfield, headfield, textbox, wysiwyg, link, upload, uploadBackground, uploadMulti, list, multiv2. Variable names derived from labels (lowercase, no spaces/accents).",
|
||||
upload_rules: "Upload fields ALWAYS return arrays. Single image: {{ record.imagen[0].urlPath | imagec(WIDTH) }}. Gallery loop: {% for img in record.galeria %}{{ img.urlPath }}{% endfor %}. Check existence: {% if record.imagen and record.imagen|length > 0 %}.",
|
||||
general_section_convention: "For any table with enlace=true, the detail template is a module at template/estandar/modulos/custom-{tableName}/. The CMS binds it automatically — no configuration required. Use `thisrecord.field` inside the Twig to access the record being rendered."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields are ALWAYS arrays of objects with urlPath property",
|
||||
"Use ONLY Twig FILTERS (pipe syntax), not Twig functions",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)",
|
||||
"Enlace (URL slug): auto-formatted to /path/ with slashes",
|
||||
"Variable names in modules: lowercase, no spaces, no accents, no special chars",
|
||||
"c-if='varname' for conditional rendering, c-hidden='true' for invisible config vars",
|
||||
"When using 'get' filter: SQL string syntax, NOT objects. Example: 'WHERE num > 5'",
|
||||
"The general section (record detail) is ALWAYS a module named 'custom-{tableName}' — never a separate page in 'apartados'."
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT use record.imagen.urlPath — it's record.imagen[0].urlPath (array!)",
|
||||
"DO NOT use 'id' as primary key — Acai uses 'num'",
|
||||
"DO NOT forget to set example data after creating a module — it will look blank",
|
||||
"DO NOT create a detail template if enlace is false — there's no URL to navigate to",
|
||||
"DO NOT use Twig functions like range() — only filters (pipe syntax) are available",
|
||||
"DO NOT configure '_detailPage' or any similar field — it does not exist; routing is by the 'custom-{tableName}' convention.",
|
||||
"DO NOT create individual pages in 'apartados' for each record — the general section handles all records of the table automatically.",
|
||||
"For best results with new enlace sections, create records BEFORE creating the general section so the directory structure exists."
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-builder-vars",
|
||||
"acai://resources/guia-twig-filters",
|
||||
"acai://resources/guia-atributos-acai",
|
||||
"acai://resources/guia-registros"
|
||||
]
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
export const editModuleWorkflow = {
|
||||
id: "edit_module",
|
||||
name: "Edit Module",
|
||||
description: "Modify an existing HTML module: update code, styles, variables, or structure.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get current module code",
|
||||
description: "Read the current HTML, CSS, JS, and PHP of the module.",
|
||||
tool: "get_module",
|
||||
critical: "ALWAYS read the current code before modifying. Understand existing variables, structure, and styling."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Check where it's used",
|
||||
description: "Find all pages and records using this module to understand impact.",
|
||||
tool: "check_module_usage",
|
||||
critical: "Know the blast radius of your changes — how many live pages will be affected."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Make changes",
|
||||
description: "Update the module code with the required modifications.",
|
||||
tool: "save_module",
|
||||
critical: "Pass the module 'id' parameter to update (not create). save_module REPLACES the entire module — include ALL html/css/js, not just the changed parts."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Update example data if needed",
|
||||
description: "If you added or renamed variables, update the example data to match.",
|
||||
tool: "set_module_example_data",
|
||||
critical: "Call get_module first to discover new variable names. Fill ALL variables, including new ones."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Verify rendering",
|
||||
description: "Test the modified module with variable values to confirm changes work.",
|
||||
tool: "check_module",
|
||||
critical: "Test with realistic values. Compare rendering before and after changes."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
builder_vars: "data-field-type attribute on elements creates editable fields. Types: textfield, headfield, textbox, wysiwyg, link, upload, uploadBackground, uploadMulti, list, multiv2.",
|
||||
component_syntax: "c-if='varname' shows/hides element. c-for='item in items' loops. c-hidden='true' invisible config. c-else after c-if.",
|
||||
save_behavior: "save_module with 'id' parameter = UPDATE. Without 'id' = CREATE new. The tool replaces the ENTIRE module code, not a diff."
|
||||
},
|
||||
rules: [
|
||||
"ALWAYS include the full html/css/js when saving — save_module replaces everything",
|
||||
"Pass the 'id' parameter to update an existing module",
|
||||
"Variable names: lowercase, no spaces, no accents",
|
||||
"Labels must be UNIQUE across the module",
|
||||
"Upload fields are ALWAYS arrays — access with [0].urlPath"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT remove existing variables without checking usage — they may have data on live pages",
|
||||
"DO NOT rename variables — it breaks existing data bindings. Add new ones instead if needed",
|
||||
"DO NOT save partial code (just HTML without CSS) — save_module replaces ALL sections",
|
||||
"DO NOT forget to update example data when adding new variables"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-builder-vars",
|
||||
"acai://resources/guia-atributos-acai"
|
||||
]
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
export const exploreSiteWorkflow = {
|
||||
id: "explore_site",
|
||||
name: "Explore Site",
|
||||
description: "Get an overview of the current Acai site: sections, modules, content.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "List all tables/sections",
|
||||
description: "Get the complete site structure with all sections, their types, and menu order.",
|
||||
tool: "list_tables",
|
||||
critical: "This returns the site's skeleton: all sections with type (multi/single/category/separador), menu name, and order."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Inspect sections of interest",
|
||||
description: "Get the full schema of specific sections to understand their fields and configuration.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Look at field types, required fields, list configurations, and upload fields."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "List all modules",
|
||||
description: "See all available design components/modules.",
|
||||
tool: "list_modules",
|
||||
critical: "Modules are the visual building blocks. Each has HTML, CSS, JS, and builder variables."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Sample content",
|
||||
description: "Preview records in key sections to understand what content exists.",
|
||||
tool: "list_table_records",
|
||||
critical: "Use limit=5 to get a representative sample without overwhelming the response."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
orientation: "list_tables returns all sections with their type: 'multi' (multiple records like blog/products), 'single' (one record like about page), 'category' (grouping for other sections), 'separador' (menu separator). This is the site's architecture.",
|
||||
modules_overview: "list_modules shows all components. Use get_module on specific ones to see their HTML/CSS/JS code and builder variables."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix",
|
||||
"Primary key is 'num', never 'id'"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT modify anything during exploration — this workflow is read-only",
|
||||
"DO NOT assume field names — always check the schema first"
|
||||
],
|
||||
resources: []
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import { createSectionWorkflow } from "./createSection.js";
|
||||
import { populateContentWorkflow } from "./populateContent.js";
|
||||
import { createModuleWorkflow } from "./createModule.js";
|
||||
import { editModuleWorkflow } from "./editModule.js";
|
||||
import { manageRecordsWorkflow } from "./manageRecords.js";
|
||||
import { manageMediaWorkflow } from "./manageMedia.js";
|
||||
import { seoSetupWorkflow } from "./seoSetup.js";
|
||||
import { exploreSiteWorkflow } from "./exploreSite.js";
|
||||
|
||||
/**
|
||||
* Registry of all available workflows.
|
||||
* Keyed by workflow ID for fast lookup.
|
||||
*/
|
||||
export const WORKFLOWS = {
|
||||
create_section: createSectionWorkflow,
|
||||
populate_content: populateContentWorkflow,
|
||||
create_module: createModuleWorkflow,
|
||||
edit_module: editModuleWorkflow,
|
||||
manage_records: manageRecordsWorkflow,
|
||||
manage_media: manageMediaWorkflow,
|
||||
seo_setup: seoSetupWorkflow,
|
||||
explore_site: exploreSiteWorkflow,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a workflow by ID.
|
||||
* @param {string} id - Workflow identifier
|
||||
* @returns {object|null} The workflow definition or null
|
||||
*/
|
||||
export function getWorkflow(id) {
|
||||
return WORKFLOWS[id] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary list of all available workflows (for help/listing).
|
||||
*/
|
||||
export function listWorkflows() {
|
||||
return Object.values(WORKFLOWS).map((w) => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
description: w.description,
|
||||
totalSteps: w.steps.length,
|
||||
}));
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
export const manageMediaWorkflow = {
|
||||
id: "manage_media",
|
||||
name: "Manage Media",
|
||||
description: "Image upload, generation, replacement, and management.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Prepare or generate images",
|
||||
description: "Use an existing image URL/asset or generate an AI image for the content.",
|
||||
tool: "generate_image",
|
||||
critical: "generate_image uses Nano Banana AI. Existing remote image URLs can also be passed directly to upload tools."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Upload to record",
|
||||
description: "Attach images to a record's upload field.",
|
||||
tool: "upload_record_image",
|
||||
critical: "Requires: tableName, recordId, fieldName, imageUrl. The image is downloaded server-side and attached to the record."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "List current uploads",
|
||||
description: "Check what's already uploaded in a field to know if replacing or adding.",
|
||||
tool: "list_record_uploads",
|
||||
critical: "Returns array of upload objects with uploadId needed for replace/delete operations."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Replace or delete if needed",
|
||||
description: "Replace an existing image or delete an upload.",
|
||||
tool: "replace_record_image OR delete_record_upload",
|
||||
critical: "Both require the uploadId from list_record_uploads. replace_record_image downloads new image and swaps it."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
upload_structure: "Upload fields store arrays of objects: [{urlPath, fileName, fileSize, mimeType, uploadDate}]. Access in Twig templates: record.field[0].urlPath | imagec(width).",
|
||||
image_sources: "Use existing remote image URLs, project assets, or Nano Banana AI image generation.",
|
||||
assets_upload: "upload_image_to_assets: uploads to website /images/ folder (not tied to a record). Accepts base64, data URI, or URL. Can resize and compress.",
|
||||
s3_upload: "upload_image_to_s3: uploads to Amazon S3. Returns public S3 URL. Accepts URL, local path, base64, or data URI."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix",
|
||||
"Primary key is 'num', never 'id'",
|
||||
"Upload fields are ALWAYS arrays of objects with urlPath property",
|
||||
"Use imagec filter for resizing: {{ path | imagec(width_in_pixels) }}"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT try to upload before creating the record — the record must exist first",
|
||||
"DO NOT confuse upload_record_image (attaches to record) with upload_image_to_assets (saves to /images/ folder)",
|
||||
"DO NOT delete uploads without confirming — the image will be removed from the live page"
|
||||
],
|
||||
resources: []
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
export const manageRecordsWorkflow = {
|
||||
id: "manage_records",
|
||||
name: "Manage Records",
|
||||
description: "CRUD operations on existing records: query, create, update, and delete data.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get table schema",
|
||||
description: "Understand field names, types, and constraints before querying or modifying.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Know the exact field names and types. Upload fields require special handling."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Query records",
|
||||
description: "List or search records to find the ones to work with.",
|
||||
tool: "list_table_records",
|
||||
critical: "Use 'where' param for SQL WHERE filtering. Use 'limit' for pagination. Use 'page' for page navigation."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Create or update records",
|
||||
description: "Create new records or update existing ones with correct field values.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "Pass 'recordId' for update, omit for create. Only included fields are modified on update. Field values must match field types."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Handle uploads if needed",
|
||||
description: "Upload images or files to record fields.",
|
||||
tool: "upload_record_image",
|
||||
critical: "Separate call per image per field per record. Cannot set upload fields via create_or_update_record."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Verify changes",
|
||||
description: "Query the records again to confirm changes were applied correctly.",
|
||||
tool: "list_table_records",
|
||||
critical: "Confirm all fields have the expected values, including upload fields."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
querying: "list_table_records supports: where='campo = \"valor\"' (SQL WHERE), page=1 (pagination), limit=20 (records per page). WHERE clause uses SQL string syntax.",
|
||||
updating: "Pass recordId + fields object to update. Only the fields included in the object are modified — other fields are left unchanged.",
|
||||
creating: "Omit recordId to create. Can batch insert by passing fields as an array of objects.",
|
||||
deleting: "delete_table_records requires tableName and recordIds (array of IDs). Use deleteAll=true to delete everything (DANGEROUS)."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields CANNOT be set via create_or_update_record — use upload_record_image",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)",
|
||||
"WHERE clauses use SQL string syntax: where='nombre = \"valor\"'"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT use 'id' to reference records — use 'num'",
|
||||
"DO NOT set upload fields via create_or_update_record — it will not work",
|
||||
"DO NOT delete records without confirming with the user first"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-registros"
|
||||
]
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
export const populateContentWorkflow = {
|
||||
id: "populate_content",
|
||||
name: "Populate Content",
|
||||
description: "Bulk record creation with images for an existing section.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get table schema",
|
||||
description: "Understand all fields and their types before creating records.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Know the exact field names and types. Upload fields cannot be set via create_or_update_record."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "List existing records",
|
||||
description: "Check what already exists to avoid duplicates.",
|
||||
tool: "list_table_records",
|
||||
critical: "Review existing content before adding new records."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Generate images if needed",
|
||||
description: "Create AI images for the content being created when existing assets are not available.",
|
||||
tool: "generate_image",
|
||||
critical: "Generate the image first and use the returned URL for upload later."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Create records",
|
||||
description: "Create all records with text content. Can batch insert multiple records in one call.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "Batch insert: pass an array of objects in 'fields' parameter. Date format: YYYY-MM-DD HH:mm:ss. Checkbox: 1 or 0."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Upload images to records",
|
||||
description: "Attach images to each record's upload fields.",
|
||||
tool: "upload_record_image",
|
||||
critical: "Must call SEPARATELY for each record+field combination. Cannot batch image uploads. Need the record's num/ID from step 4."
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
action: "Verify records",
|
||||
description: "Confirm all records were created with correct data.",
|
||||
tool: "list_table_records",
|
||||
critical: "Check that all fields are populated correctly including upload fields."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
batch_insert: "create_or_update_record supports batch: pass fields as an array of objects instead of a single object. Each object is one record. Returns an array of created record IDs.",
|
||||
image_sources: "Use existing project/client assets when available, or generate_image for AI-generated images via Nano Banana.",
|
||||
upload_flow: "1. Create record first (get its num/ID). 2. Then call upload_record_image with tableName, recordId, fieldName, imageUrl. 3. The image is downloaded server-side and attached to the record."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix in all tool calls",
|
||||
"Primary key is ALWAYS 'num', never 'id'",
|
||||
"Upload fields CANNOT be set via create_or_update_record — use upload_record_image",
|
||||
"Date format: YYYY-MM-DD HH:mm:ss",
|
||||
"Checkbox values: 1 or 0 (number, not boolean)",
|
||||
"Enlace field: auto-formatted to /path/ with slashes if not provided"
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT try to set upload field values in create_or_update_record — use upload_record_image after creation",
|
||||
"DO NOT forget that batch insert returns an array of created record IDs — you need these for image uploads",
|
||||
"DO NOT upload images before creating the record — the record must exist first"
|
||||
],
|
||||
resources: [
|
||||
"acai://resources/guia-registros"
|
||||
]
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
export const seoSetupWorkflow = {
|
||||
id: "seo_setup",
|
||||
name: "SEO Setup",
|
||||
description: "Configure SEO for a section: meta tags, URL slugs, and detail template.",
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
action: "Get current table schema",
|
||||
description: "Check which SEO fields already exist and whether enlace is enabled.",
|
||||
tool: "get_table_schema",
|
||||
critical: "Look for seo_title / seo_description / seo_keywords fields and the enlace field in the schema response."
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
action: "Add SEO meta fields if missing",
|
||||
description: "If seo_title / seo_description / seo_keywords are not present, add them as regular fields. Note: for NEW tables you can instead pass seoMetas=true to create_table and they get added up front.",
|
||||
tool: "create_field",
|
||||
critical: "One create_field call per SEO field. Typical set: seo_title (textfield), seo_description (textbox), seo_keywords (textfield)."
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
action: "Add enlace field if missing",
|
||||
description: "If the table has no enlace field and records need public URLs, add one. For NEW tables pass enlace=true to create_table instead.",
|
||||
tool: "create_field",
|
||||
critical: "fieldName='enlace', type='textfield'. Acai auto-formats the value to /section/slug/. Existing records then get URLs based on this field."
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
action: "Update records with SEO data",
|
||||
description: "Fill in SEO fields for each record: meta title, meta description, keywords.",
|
||||
tool: "create_or_update_record",
|
||||
critical: "SEO fields are: seo_title, seo_description, seo_keywords. Check the schema for exact field names before writing."
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
action: "Create or update the general section (detail template)",
|
||||
description: "Ensure the detail page template at template/estandar/modulos/custom-{tableName}/index-base.tpl exists and includes the SEO meta tags. The CMS renders this module automatically on each record URL.",
|
||||
tool: "acai-write",
|
||||
critical: "Folder must be EXACTLY 'custom-' + tableName. Inside the Twig, access record data via `thisrecord.seo_title`, `thisrecord.seo_description`, etc. Include these in the <head> via the layout's SEO slot, or inline if the project uses per-section heads."
|
||||
}
|
||||
],
|
||||
context: {
|
||||
enlace_behavior: "When the table has an 'enlace' field, Acai auto-generates URL slugs in /tableName/record-slug/ format. The value is auto-formatted with slashes.",
|
||||
seo_fields: "SEO meta fields are just regular textfield/textbox fields named seo_title, seo_description, seo_keywords. For new tables you can skip this step by passing seoMetas=true to create_table.",
|
||||
detail_template: "For any table with enlace, the record URL is rendered by the module 'custom-{tableName}' (convention — not configurable). The module accesses the current record via `thisrecord`. There is no '_detailPage' field."
|
||||
},
|
||||
rules: [
|
||||
"Table names WITHOUT 'cms_' prefix",
|
||||
"Enlace values are auto-formatted to /path/ format",
|
||||
"SEO fields are regular fields, not a special flag on the schema",
|
||||
"The general section (detail template) is ALWAYS a module named 'custom-{tableName}' — never a separate page in 'apartados'.",
|
||||
"There is no update_table_schema / _detailPage — routing is by convention on the module folder name."
|
||||
],
|
||||
warnings: [
|
||||
"DO NOT enable enlace on a 'single' type table — single tables have one record and usually don't need individual URLs",
|
||||
"DO NOT forget to create the 'custom-{tableName}' module after enabling enlace — without it, record URLs show blank pages",
|
||||
"DO NOT configure '_detailPage' — it does not exist."
|
||||
],
|
||||
resources: []
|
||||
};
|
||||
@@ -9,16 +9,16 @@ import { canAccessTable } from "../helpers/accessControl.js";
|
||||
export function registerCreateOrUpdateRecordTool(server) {
|
||||
server.tool(
|
||||
"create_or_update_record",
|
||||
`Create or update records in a database table. Before using: read resource 'acai-cheat-sheet' for domain rules, then check table schema with get_table_schema.
|
||||
`Crea o actualiza registros en una tabla. Antes de usar: consulta el schema con 'get_table_schema' (sin 'cms_'); si dudas del formato lee 'read_doc({ name: "11-quick-reference" })' o '06-hooks-and-cmsapi'.
|
||||
|
||||
Key rules: tables without 'cms_' prefix, primary key is 'num', uploads are arrays (use upload_record_image after creating record), dates as YYYY-MM-DD HH:mm:ss, checkboxes as 1/0, enlace as /path/.
|
||||
Reglas clave: tablas sin prefijo 'cms_'; PK es 'num' (nunca 'id'); foreign keys con sufijo '_num'; uploads son arrays — NO los envíes en 'fields', sube después con 'upload_record_image'; fechas en formato YYYY-MM-DD HH:mm:ss; checkboxes como 1/0 (números).
|
||||
|
||||
For builder tables (e.g. 'apartados'): must include num:null, builder:"[]", controlador, precontrolador, breadcrumb, enlace fields. See resource 'guia-registros' for full field type reference.`,
|
||||
Para tablas builder (e.g. 'apartados') al crear nuevo registro: incluye num:null, builder:"[]", controlador, precontrolador, breadcrumb, enlace. NUNCA modifiques 'enlace' ni 'controlador' de un registro existente — los stripeo automáticamente en updates.`,
|
||||
withAuthParams({
|
||||
tableName: z.string().describe("Name of the table (without 'cms_' prefix, e.g., 'productos', 'equipo')"),
|
||||
recordId: z.any().optional().describe("Record ID for updating. Leave empty to create new record. NOT USED when records is an array."),
|
||||
fields: z.any().describe("Single record object OR array of record objects for batch insert. Example: { nombre: 'Product 1' } or [{ nombre: 'Product 1' }, { nombre: 'Product 2' }]. IMPORTANT: Always consult 'guia-registros' for field types and formats and check if is table with builder fields."),
|
||||
tableSchema: z.any().describe("Provide the table schema object to validate field types before sending to API. If not provided, schema will not be validated."),
|
||||
tableName: z.string().describe("Nombre de la tabla sin prefijo 'cms_' (e.g. 'productos', 'apartados')"),
|
||||
recordId: z.any().optional().describe("'num' del registro a actualizar. Omitir para crear nuevo. NO se usa cuando 'fields' es array."),
|
||||
fields: z.any().describe("Objeto único o array de objetos para inserción batch. Ejemplo: { nombre: 'Producto 1' } o [{ nombre: 'A' }, { nombre: 'B' }]. Antes consulta el schema y, si dudas, lee 'read_doc({ name: \"11-quick-reference\" })'."),
|
||||
tableSchema: z.any().describe("Schema de la tabla para validar tipos antes de enviar (opcional)."),
|
||||
}),
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
withAuth(async ({ tableName, recordId, fields }, extra) => {
|
||||
|
||||
Reference in New Issue
Block a user