Compare commits

...

2 Commits

Author SHA1 Message Date
Jordan Diaz
f7e950694e feat: tools get_hook_entryparams / set_hook_entryparams
Completan la feature del endpoint /api/creator/hook-entryparams, que ya
estaba desplegado en el server (commit fe81765 del repo de Forge) pero sin
tools que lo usaran desde el agente.

Los entryParams son los parametros de entrada declarados de un hook global
({variable, value?, valueType?}), que se le pasan al invocarlo.
2026-08-12 11:32:17 +00:00
Jordan Diaz
bdee7665ec feat: el agente puede declarar y leer las cmsTables de un modulo
Las cmsTables (tablas del CMS cuyo contenido MUESTRA un modulo) ya se
definen desde Forge y desde el CMS legacy; faltaba que el agente las
entendiera.

- update_module_metadata acepta cmsTables. El endpoint del server ya la
  validaba, asi que solo habia que declararla en el schema Zod.
- get_module_config_vars la devuelve: sale gratis porque el handler ya
  resolvia el schema del modulo para uploadFields/varsMeta.
- Docs (01, 03, 09) con el matiz que mas se puede confundir: `tables` es
  donde viven los VALORES de las vars (siempre builder_custom, lo pone el
  compilador) y `cmsTables` es que contenido MUESTRA el modulo.

Para el agente lo util no es solo escribirlas: al recibirlas sabe donde
esta de verdad el contenido visible. Si le piden cambiar lo que muestra un
listado de noticias, los registros estan en esa tabla, no en las vars del
modulo.

Se documenta ademas que la metadata del builder.json es la UNICA parte
editable del fichero (y solo con esta tool): el resto lo regenera el
compilador en cada compilacion.
2026-08-12 11:32:07 +00:00
6 changed files with 162 additions and 12 deletions

View File

@@ -500,3 +500,4 @@ Valores comunes: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `zoom-in`, `
8. Checkbox guarda `1` o `0` (número), nunca `true`/`false`.
9. Evita Tailwind arbitrary-value en `index-base.tpl` — muévelos a `style.css`.
10. `script.js` y `style.css` son estáticos: NO uses sintaxis Twig dentro. Pasa valores dinámicos vía `data-*`.
11. Si el módulo lista registros de una tabla del CMS (`c-for` sobre ella, o su `hook.php` la consulta), declara esa tabla con `update_module_metadata({ cmsTables: [...] })`. No es un `data-field-*`: no se deduce del HTML, hay que declararla. Ver `03-modules-and-sections.md`.

View File

@@ -28,6 +28,7 @@ Componentes visuales reutilizables. Viven en `template/estandar/modulos/<module-
Reglas duras:
- **Solo se edita `index-base.tpl`.** `index.tpl`, `index-twig.tpl` y `builder.json` los genera el compilador y se sobrescriben automáticamente.
- **Excepción del `builder.json`: la metadata SÍ se edita, con `update_module_metadata`** (`label`, `description`, `onlyAdminModule`, `MJMLModule`, `cmsTables`). Nunca la escribas con tools de archivo: el compilador regenera el fichero entero y perderías el cambio.
- Editar `index-base.tpl` con `acai-write` o `acai-line-replace` **dispara la compilación automática**.
- `script.js` y `style.css` son **estáticos** — NO uses sintaxis Twig dentro. Pasa valores dinámicos vía atributos `data-*`.
- `index-base.tpl` solo contiene HTML/Twig. **Nunca** embebas etiquetas `<script>` con lógica del módulo, **nunca** PHP.
@@ -203,6 +204,32 @@ Acceso en Twig:
Las variables son **propiedades del objeto iterado**, no variables sueltas.
## `cmsTables` — qué tablas del CMS muestra el módulo
Un módulo puede pintar contenido que NO vive en sus variables: un listado de noticias, una parrilla de productos, un carrusel del blog. Esos registros están en tablas del CMS, y `cmsTables` es donde el módulo declara cuáles.
```json
{ "label": "Listado de noticias", "cmsTables": ["noticias"] }
```
**No confundir con `tables`**, que también está en el `builder.json` y se parece demasiado:
| Clave | Qué es | Quién la pone |
|-------|--------|---------------|
| `tables` | Dónde se guardan los VALORES de las variables del módulo. Siempre `["builder_custom"]` | El compilador. No la toques |
| `cmsTables` | Qué contenido MUESTRA el módulo | Una persona desde el editor, o tú con `update_module_metadata` |
Para qué sirve: el editor pinta un acceso directo al CMS de cada tabla declarada desde cualquier página que incluya el módulo, para que quien edita esa página llegue al contenido sin buscarlo.
Cuándo declararla:
- **Sí**: el módulo hace `c-for` sobre registros de una tabla, o su `hook.php` los consulta.
- **No**: el módulo solo muestra sus propias variables (un banner con título e imagen). Deja la lista vacía.
Cómo usarla tú:
- Al crear un módulo que lista contenido, decláralas: `update_module_metadata({ module, cmsTables: ["noticias"] })`.
- Al recibirlas en `get_module_config_vars`, te dicen **dónde está de verdad el contenido visible**. Si el usuario pide cambiar lo que muestra un listado de noticias, los registros están en `noticias` — no en las variables del módulo. Ve a esa tabla con `list_table_records` / `create_or_update_record`.
- Pasar `cmsTables` reemplaza la lista entera: incluye las que quieras conservar. `[]` la vacía.
## Traducir las variables de un módulo
Los valores textuales de las variables de un módulo (títulos, descripciones, wysiwyg, etc.) NO se guardan en la fila de la página, sino en la tabla `builder_custom`. Por eso una traducción de módulo apunta siempre a `builder_custom`, no a `apartados` ni a la tabla de la página.

View File

@@ -37,6 +37,7 @@ Reglas:
| `check_module_usage` | Lista páginas que usan el módulo | **OBLIGATORIO antes de `delete_module`** |
| `delete_module` | Elimina la carpeta del módulo | Destructivo. Si `inUse=true`, deniega — el usuario debe quitarlo de las páginas primero |
| `set_module_example_data` | Define datos de ejemplo para preview en el editor | Pasar valores para TODAS las variables del schema |
| `update_module_metadata` | Edita metadata del `builder.json` | `label`, `description`, `onlyAdminModule`, `MJMLModule`, `cmsTables`. NO renombra el módulo. Rechaza los módulos de layout (`custom-header/footer[-twig]`) |
### Registros (records)
@@ -51,7 +52,7 @@ Reglas:
| `remove_module_from_record` | Quita módulo de la página | Por `sectionId` (preferido) o `modulePosition` |
| `reorder_module` | Mueve módulo a otra posición | `fromPosition``toPosition` |
| `toggle_module_visibility` | Muestra/oculta sin borrar | Por `sectionId` |
| `get_module_config_vars` | Lee valores actuales de las variables | Por `tableName` + `recordNum` + `sectionId` |
| `get_module_config_vars` | Lee valores actuales de las variables | Por `tableName` + `recordNum` + `sectionId`. Devuelve además `cmsTables`: las tablas cuyo contenido MUESTRA el módulo (ahí están sus registros, no en las vars) |
| `set_module_config_vars` | Escribe variables del módulo | Devuelve `uploadFields` con `recordNum`+`fieldName` listos para subir imágenes |
### Tablas y campos (schema)

View File

@@ -129,16 +129,130 @@ Examples:
);
}
function registerGetHookEntryParamsTool(server) {
server.tool(
"get_hook_entryparams",
`Read the declared entry parameters (entryParams) of a global hook. entryParams are the input parameters a hook expects — each has a required 'variable' name plus optional 'value' and 'valueType'. They are passed to the hook when it is invoked.
Use this when the user asks about a hook's inputs, or to inspect the current params before editing them.
hookEndPoint format: starts and ends with '/', with '/' as separator. E.g. "/hooks/appListado/".
Returns:
- entryParams: array of { variable, value?, valueType? }.
Example: [{ "variable": "action" }, { "variable": "data", "value": "", "valueType": "Integer" }].`,
withAuthParams({
hookEndPoint: z.string().describe('Hook endpoint path, e.g. "/hooks/appListado/"'),
}),
{ readOnlyHint: true, destructiveHint: false },
withAuth(async ({ hookEndPoint }, extra) => {
try {
const { projectSlug } = getCurrentProjectInfo();
const result = await pythonGet("/api/creator/hook-entryparams", {
project: projectSlug,
endPoint: hookEndPoint,
});
if (!result?.success) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: result?.error || "No se pudieron leer los entryParams",
}),
}],
isError: true,
};
}
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
exists: !!result.exists,
entryParams: result.entryParams || [],
hookEndPoint,
}, null, 2),
}],
};
} catch (error) {
return handleToolError(error, "get_hook_entryparams", { hookEndPoint });
}
})
);
}
function registerSetHookEntryParamsTool(server) {
server.tool(
"set_hook_entryparams",
`Set the declared entry parameters (entryParams) of a global hook. entryParams describe the inputs a hook expects — each has a required 'variable' name plus optional 'value' and 'valueType'.
IMPORTANT: this OVERWRITES the entire entryParams list of the hook (it does NOT merge). You must pass the COMPLETE set of params every time, because the whole array is replaced.
Use this AFTER creating or editing the hook file (via acai-write) to declare which inputs it accepts.
entryParams format: array of { variable, value?, valueType? }.
Example: [{ "variable": "action" }, { "variable": "data", "value": "", "valueType": "Integer" }].`,
withAuthParams({
hookEndPoint: z.string().describe('Hook endpoint path, e.g. "/hooks/appListado/"'),
entryParams: z.array(z.object({
variable: z.string(),
value: z.string().optional(),
valueType: z.string().optional(),
})).describe('Complete list of entry params. Each item: { variable (required), value? (string), valueType? (string) }. Replaces the whole array. E.g. [{"variable":"action"},{"variable":"data","value":"","valueType":"Integer"}]'),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ hookEndPoint, entryParams }, extra) => {
try {
const { projectSlug } = getCurrentProjectInfo();
const result = await pythonPost("/api/creator/hook-entryparams", {
project: projectSlug,
endPoint: hookEndPoint,
entryParams,
});
if (!result?.success) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: result?.error || "No se pudo guardar",
}),
}],
isError: true,
};
}
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: result.message || "entryParams actualizados",
entryParams: result.entryParams || [],
hookEndPoint,
}, null, 2),
}],
};
} catch (error) {
return handleToolError(error, "set_hook_entryparams", { hookEndPoint, entryParams });
}
})
);
}
/**
* Registra las tools de configuracion de hooks globales.
*
* `get_hook_middleware` es de solo lectura y se registra siempre. El set
* modifica el layout y solo se expone si el rol puede editar codigo — sigue
* el mismo criterio que otras tools de escritura (ver project/index.js).
* Las tools de solo lectura (`get_hook_middleware`, `get_hook_entryparams`) se
* registran siempre. Las de escritura modifican el layout y solo se exponen si
* el rol puede editar codigo — sigue el mismo criterio que otras tools de
* escritura (ver project/index.js).
*/
export function registerHookTools(server) {
registerGetHookMiddlewareTool(server);
registerGetHookEntryParamsTool(server);
if (canEditCode()) {
registerSetHookMiddlewareTool(server);
registerSetHookEntryParamsTool(server);
}
}

View File

@@ -7,22 +7,27 @@ import { getCurrentProjectInfo } from "../files/helpers.js";
// Tool: update_module_metadata
// Edita la metadata de builder.json de un modulo (label, description,
// onlyAdminModule, MJMLModule) delegando en /api/modules/update-metadata.
// El id/carpeta del modulo NO se puede renombrar y el endpoint rechaza los
// modulos generados del layout (custom-header/footer[-twig]).
// onlyAdminModule, MJMLModule, cmsTables) delegando en
// /api/modules/update-metadata. El id/carpeta del modulo NO se puede renombrar
// y el endpoint rechaza los modulos generados del layout
// (custom-header/footer[-twig]).
// Claves editables del builder.json. Se envian solo las que llegan definidas,
// de modo que el endpoint hace un merge parcial y no pisa el resto.
const EDITABLE_KEYS = ["label", "description", "onlyAdminModule", "MJMLModule"];
const EDITABLE_KEYS = ["label", "description", "onlyAdminModule", "MJMLModule", "cmsTables"];
export function registerUpdateModuleMetadataTool(server) {
server.tool(
"update_module_metadata",
`Edit a module's metadata in its builder.json: 'label' (display name shown in the visual builder), 'description' (short help text for editors), 'onlyAdminModule' (true = the module is only visible to admin users in the builder), 'MJMLModule' (true = the module is an email/MJML module).
`Edit a module's metadata in its builder.json: 'label' (display name shown in the visual builder), 'description' (short help text for editors), 'onlyAdminModule' (true = the module is only visible to admin users in the builder), 'MJMLModule' (true = the module is an email/MJML module), 'cmsTables' (CMS tables whose content this module displays).
Use 'cmsTables' when the module renders records from a CMS table — a news list, a product grid, a blog carousel. Declare the tables it reads (e.g. ["noticias"]). The editor then shows a direct link to each of those tables from any page that includes the module, so whoever edits that page can reach the content without hunting for it. A module that only shows its own variables (a banner with a title and an image) needs no cmsTables.
Do NOT confuse 'cmsTables' with the 'tables' key of builder.json: 'tables' is where the module's VARIABLE VALUES are stored (always builder_custom) and is managed by the compiler. 'cmsTables' is what content the module DISPLAYS, and is chosen by a human or by you.
The module id (its folder name under template/estandar/modulos/) CANNOT be renamed with this tool — there is no rename option at all. Only the fields above change; everything else in builder.json is preserved.
At least one of label, description, onlyAdminModule or MJMLModule is required. Fields you omit are left untouched.
At least one of label, description, onlyAdminModule, MJMLModule or cmsTables is required. Fields you omit are left untouched. Passing cmsTables replaces the whole list, so include the tables you want to keep; pass [] to clear it.
Not applicable to the generated layout modules (custom-header, custom-footer, custom-header-twig, custom-footer-twig): those are artifacts of the global layout and the request will be rejected — use set_layout_field for them.`,
withAuthParams({
@@ -31,11 +36,12 @@ Not applicable to the generated layout modules (custom-header, custom-footer, cu
description: z.string().optional().describe("Short description shown to editors in the builder"),
onlyAdminModule: z.boolean().optional().describe("If true, the module is only visible to admin users in the builder"),
MJMLModule: z.boolean().optional().describe("If true, the module is treated as an email (MJML) module"),
cmsTables: z.array(z.string()).optional().describe("CMS tables whose records this module displays, e.g. [\"noticias\"]. Replaces the whole list; [] clears it. Only real tables of the project — they become direct links to the CMS."),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ module, label, description, onlyAdminModule, MJMLModule }, _extra) => {
withAuth(async ({ module, label, description, onlyAdminModule, MJMLModule, cmsTables }, _extra) => {
try {
const provided = { label, description, onlyAdminModule, MJMLModule };
const provided = { label, description, onlyAdminModule, MJMLModule, cmsTables };
// Validacion temprana: sin ninguna clave editable no tiene
// sentido llamar al endpoint. Ojo con los booleanos false —

View File

@@ -22,6 +22,7 @@ export function registerGetModuleConfigVarsTool(server) {
- vars: resolved values (text, HTML, etc.) for simple vars and arrays for multi/repeater vars
- varsMeta: per-var physical location { tableName: 'builder_custom', recordNum, fieldName, type }. USE THIS to know exactly which row + column to update with create_or_update_record. The variable's display name (e.g. 'titulo') is NOT the same as the physical column name (e.g. 'title2'). Uploads inside a 'multi' var carry their labels in subFields.<subVar>.infoLabels.
- uploadFields: per-var upload location for upload_record_image / replace_record_image / set_upload_info. Each entry includes infoLabels when the module defines them (array; position 0 = info1).
- cmsTables: CMS tables whose records this module DISPLAYS (e.g. ["noticias"]), or [] if it only shows its own variables. This is where the module's visible content really lives: if the user asks to change what a news-list module shows, the records are in those tables, NOT in the module's vars. Use list_table_records / create_or_update_record against them. Empty list means nothing to look up elsewhere.
- moduleId, sectionId
Required params: