Initial commit

This commit is contained in:
Jordan
2026-04-01 23:16:45 +01:00
commit bc4199aed2
201 changed files with 25612 additions and 0 deletions

View File

@@ -0,0 +1,345 @@
/**
* 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 };

View File

@@ -0,0 +1,5 @@
import { registerOrchestrateTool } from "./orchestrate.js";
export function registerOrchestratorTools(server) {
registerOrchestrateTool(server);
}

View File

@@ -0,0 +1,165 @@
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
};
}
}
);
}

View File

@@ -0,0 +1,85 @@
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"
]
};

View File

@@ -0,0 +1,110 @@
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.",
tool: "create_table",
critical: "type must be: 'multi' (multiple records), 'single' (one record), 'category' (grouping), or 'separador' (menu separator). Set enlace=true if records need their own URL page."
},
{
step: 4,
action: "Add fields to the table",
description: "Create all necessary fields with correct types and configuration.",
tool: "edit_table_field",
critical: "Can batch multiple fields in one call. Field types: textfield, textbox, wysiwyg, date, checkbox, list, upload, multitext, codigo, separator."
},
{
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 an HTML module that displays a list/grid of records from this section.",
tool: "save_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)."
},
{
step: 7,
action: "Set module example data",
description: "Set example/static data for module preview. MUST call get_module 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, creating records first ensures directory structure is ready."
},
{
step: 9,
action: "Create detail template (if enlace=true)",
description: "If the section has enlace enabled, create the detail page template that shows when navigating to a record's URL.",
tool: "save_general_section",
critical: "Use 'thisrecord' variable to access the current record. Same Twig rules apply. Note: save_general_section will auto-initialize the directory if needed."
},
{
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', optionsText='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 %}."
},
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'"
],
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",
"For best results with new enlace sections, create records BEFORE calling save_general_section to ensure directory structure exists"
],
resources: [
"acai://resources/guia-builder-vars",
"acai://resources/guia-twig-filters",
"acai://resources/guia-atributos-acai",
"acai://resources/guia-registros"
]
};

View File

@@ -0,0 +1,64 @@
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"
]
};

View File

@@ -0,0 +1,48 @@
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: []
};

View File

@@ -0,0 +1,44 @@
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,
}));
}

View File

@@ -0,0 +1,53 @@
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: []
};

View File

@@ -0,0 +1,64 @@
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"
]
};

View File

@@ -0,0 +1,70 @@
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"
]
};

View File

@@ -0,0 +1,58 @@
export const seoSetupWorkflow = {
id: "seo_setup",
name: "SEO Setup",
description: "Configure SEO for a section: meta tags, URL slugs, and structured data.",
steps: [
{
step: 1,
action: "Get current table schema",
description: "Check if seo_metas is already enabled and if enlace (URL slug) exists.",
tool: "get_table_schema",
critical: "Look for seo_metas flag and enlace configuration in the schema response."
},
{
step: 2,
action: "Enable SEO meta tags",
description: "Turn on seo_metas in the table schema to add meta title/description fields.",
tool: "update_table_schema",
critical: "Set seo_metas=true in the schema. This adds SEO fields to each record."
},
{
step: 3,
action: "Enable enlace for URL slugs",
description: "Enable enlace so records get their own URL-friendly pages.",
tool: "update_table_schema",
critical: "Set enlace=true. This auto-generates /section/record-name/ URLs for each record."
},
{
step: 4,
action: "Update records with SEO data",
description: "Fill in SEO fields for each record: meta title, meta description.",
tool: "create_or_update_record",
critical: "SEO fields are typically: seo_title, seo_description. Check the schema for exact field names."
},
{
step: 5,
action: "Create or update detail template",
description: "Ensure the detail page template includes proper meta tags and structured data.",
tool: "save_general_section",
critical: "The template uses 'thisrecord' variable. Include meta tags in the template for SEO."
}
],
context: {
enlace_behavior: "When enlace is enabled, Acai auto-generates URL slugs in /section/record-name/ format. The enlace field value is auto-formatted with slashes.",
seo_fields: "Enabling seo_metas adds meta title and description fields to the record editor. These are used in the <head> of the detail page.",
detail_template: "The general section template (save_general_section) defines what renders when a user visits a record's URL. Uses 'thisrecord' to access the current record's data."
},
rules: [
"Table names WITHOUT 'cms_' prefix",
"update_table_schema requires both tableName and the schema object",
"Enlace values are auto-formatted to /path/ format",
"SEO meta fields are only available after enabling seo_metas on the table"
],
warnings: [
"DO NOT enable enlace on a 'single' type table — single tables have only one record and usually don't need individual URLs",
"DO NOT forget to create a detail template after enabling enlace — without it, record URLs show blank pages"
],
resources: []
};