01-builder-fields.md documentaba `checkbox` y `colorpicker` como tipos validos
—y usaba colorpicker en un ejemplo— pero el parser no los reconocia. Verificado
contra el parser real: ambos salian con `type` indefinido, y en ese caso
funciones.php hace `continue` y **descarta la variable en silencio**. O sea que
el doc mandaba al agente a escribir campos que nunca llegaban al builder.json.
En cambio no documentaba `colors`, `corners` ni `ratio`, que si funcionan.
- Se documentan los cuatro tipos reales, con el formato de valor de cada uno.
`colors` es una paleta de N colores en una sola variable (JSON de pares
nombre/color, nombres declarados en data-field-colors); `colorpicker` es un
color suelto (hex plano).
- `colorpicker` pasa a existir de verdad: se anade al parser y al compilador
(companion en el plugin maestro). Forge ya tenia widget para ambos —
ColorsField y ColorField— asi que solo faltaba la pieza del parser.
- `checkbox` se marca explicitamente como inexistente, con la alternativa
(`list` de dos opciones).
- set_module_example_data documenta el formato de ambos, que el agente no podia
adivinar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La tool llamaba a `action_ws=setStaticVars`, que hace un file_put_contents
del builder.json ENTERO en la web para cambiar una sola clave. Eso se
saltaba las dos garantias que protegen ese fichero: el bloqueo de escritura
de la API de ficheros (BLOCKED_GENERATED_FILENAMES en handlers/files.py) y
la allowlist de /api/modules/update-metadata.
El riesgo no era teorico. El builder.json es la unica memoria de que
variable vive en que columna de builder_custom: si una compilacion caia
entre la lectura y la escritura de setStaticVars, esta devolvia el mapeo
var->columna anterior encima del recien generado y el contenido guardado
quedaba colgado de la variable equivocada en TODAS las paginas que usan el
modulo.
Ahora delega en /api/modules/update-metadata, que ya es la via quirurgica
para la metadata y acepta staticVars en su allowlist (objeto, <=64KB,
<=200 claves).
Se aprovecha para quitar el volcado del schema y del payload completos por
consola en cada llamada.
Companion en el repo de Forge: compiler.js reenvia staticVars en cada
compilacion. Antes no lo mandaba, asi que el CMS lo tiraba al regenerar el
builder.json — solo 5 de 4870 modulos lo conservaban.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
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.
Una sesion real del agente guardo title3/title6/title2 (columnas de
builder_custom segun varsMeta.fieldName, como decian las docs) y el front
no pintaba nada: el runtime traduce vars con t($record, $var) por nombre
de var. Corregidas descripciones de set/get_record_translations, docs
03/09/11b y ACAI_ENDPOINTS; ademas el puente PHP ahora rechaza titleN/
textN sobre builder_custom con error explicativo (guardrail).
Path absoluto con prefijo (/en/...); CocoEnlace regenera las filas al
cambiar el enlace base. Actualizados tool description, ACAI_ENDPOINTS y
docs 04/09/11b.
- Nueva categoria tools/languages: list_web_languages,
get_record_translations y set_record_translations (registros, config,
textos_generales y vars de modulo via builder_custom).
- Param lang opcional en list_table_records y get_record
(options.translates de CocoDB).
- Docs actualizados (03/04/06/09/11b + ACAI_ENDPOINTS) con el modelo de
cms_traducciones y los workflows de traduccion.
Para una imagen local/pegada desde vscode: guardarla en una carpeta
sincronizada NO truncada (cms/uploads/chat/ o cms/uploads/generated/),
dejar que el sync la suba a test y pasar su RUTA RELATIVA como imageUrl.
El server lee los bytes de disco vía resolve_image_source — cero base64
por el contexto del modelo, cero URLs localhost inalcanzables.
- Validación relajada: además de http(s) y ruta absoluta, se acepta ruta
relativa del proyecto (sin esquema, sin "..", <=512 chars, charset de
ruta) → sigue rechazando data-URI/base64 crudo.
- Descripciones de upload_record_image / replace_record_image actualizadas
con el flujo correcto.
- resolve_image_source y el aislamiento de entorno: sin cambios (la ruta
relativa la resuelve por modo+stub, igual para chat y vscode).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Imágenes:
- analyze_image y upload resuelven los bytes por el endpoint Python
/api/image-bytes (pythonGetBinary). analyze_image enruta los dominios
forge (env ACAI_FORGE_DOMAIN) al endpoint en vez de fetch directo (que
daba ECONNREFUSED 127.0.0.1 dentro del container).
Aislamiento de entorno (vscode = solo test):
- resolveCurrentModeOverride(): sesión MCP HTTP (mcpSessionId presente) →
"local"; stdio (chat/cron) → ACAI_MODE_OVERRIDE de entorno. Lo usan los
builders de headers (pythonServerClient, files/helpers) → toda tool del
MCP HTTP manda X-Acai-Mode: local.
- httpServer.resolveProjectCredentials fuerza forceMode:"local" al resolver
project-info → la sesión obtiene web_url/api_web_url forge-local y opera
siempre contra test, nunca producción.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Resolución dinámica del modelo por sesión (model_resolver): override de
usuario (metadata) → default global (Redis db 0 acai:config:ai:*) → fallback.
Mapea a string litellm; LiteLLMAdapter respeta el modelo por request y
enruta openrouter/* con OPENROUTER_API_KEY del entorno.
- Razonamiento: reasoning_effort por sesión en ModelConfig/AgentProfile,
aplicado al agente y al planner.
- Coste: cost.py calcula por modelo (catálogo OpenRouter/DeepSeek en Redis →
litellm → fijo) y emite tarifas + modelo usado en EXECUTION_COMPLETED.
- Visión nativa: imágenes como bloques image_url en el turno del usuario
(TaskState.image_attachments → Context Engine → adapter), con persistencia
en recent_messages y conteo de tokens de imagen (~1500).
- El turno no se pierde al cancelar: se persiste el mensaje del usuario + marca
de interrupción para que un "vuelve a intentarlo" tenga contexto.
- Fix analyze_image: preservar el subdirectorio de usuario del chat-upload
(basename descartaba "<user>/" → not found).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- saveFileBuilder (fileBuilder.js) hacía POST directo a viewer_functions.php
sin header Host -> en Forge (api_web_url interno http://web:80) Apache
servía el vhost por defecto -> 404. Ahora delega en
AcaiHttpClient.postViewerAction, que resuelve api_web_url + Host:
forge_host (igual que el resto de tools). Pasa credentials completo.
- upload_record_image: rechaza data-URI/base64 con error claro (antes
derivaba el nombre del base64 -> "File name too long" en mcp_respond.php).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- mcp-server _docsReader.js: resolveDocsDir → ACAI_DOCS_DIR /
$ACAI_PROJECT_DIR/docs / /app/docs. Arregla DOC_NOT_FOUND en VSCode
(HTTP MCP) y local; el .mcp.json ya inyecta ACAI_PROJECT_DIR
- routes.py: /knowledge/load idempotente — salta embeddings si el hash
de contenido no cambió (clave Redis kbhash), para dispararlo libremente
desde el botón de scaffold sin re-embeber
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>