Initial commit

This commit is contained in:
Jordan
2026-04-01 23:16:45 +01:00
commit 91cfdaee72
200 changed files with 25589 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
import { z } from "zod";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, handleApiResponse, validateRequired } from "../helpers/errorHandler.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { withAuthParams } from "../helpers/authSchema.js";
export function registerCheckModuleTool(server) {
server.tool(
"check_module",
"Preview how a module renders with sample data. Returns a preview (first 50 lines + summary) by default — use fullRender=true for complete output. Always shows errors in full.",
withAuthParams({
moduleName: z.string().describe("Module ID/name to check"),
vars: z.record(z.string(), z.any()).describe("Object with builder variable values. Keys should match the variable names from data-field-label (without spaces/special chars)"),
fullRender: z.boolean().optional().describe("If true, returns complete rendered HTML. Default: false (preview — first 50 lines + summary, saves tokens)."),
}),
{ readOnlyHint: true, destructiveHint: false },
withAuth(async ({ moduleName, vars, fullRender }, extra) => {
const startTime = Date.now();
console.error(`[Tool] check_module - START: moduleName=${moduleName}, varsCount=${Object.keys(vars || {}).length}, sessionId=${extra.sessionId}`);
try {
// Validate required parameters
const validationError = validateRequired(
{ moduleName, vars },
['moduleName', 'vars'],
'check_module'
);
if (validationError) {
console.error(`[Tool] check_module - VALIDATION ERROR: ${validationError.content[0].text}`);
return validationError;
}
const credentials = await getSessionCredentials(extra.sessionId);
const payload = {
moduleName: moduleName,
vars: vars
};
console.error(`[Tool] check_module - Calling AcaiHttpClient.checkModuleCode...`);
const response = await AcaiHttpClient.checkModuleCode(credentials, credentials.token, payload);
// Check for API errors in response
/*const apiError = handleApiResponse(response.data, 'check_module');
if (apiError) {
console.error(`[Tool] check_module - API ERROR: ${apiError.content[0].text}`);
return apiError;
}*/
const elapsedTime = Date.now() - startTime;
console.error(`[Tool] check_module - SUCCESS: completed in ${elapsedTime}ms`);
let outputText = `Module Preview for "${moduleName}":\n\n${JSON.stringify(response.data, null, 2)}`;
// Preview mode (default): truncate to first 50 lines + summary
if (!fullRender) {
const lines = outputText.split('\n');
const PREVIEW_LINES = 50;
if (lines.length > PREVIEW_LINES) {
const preview = lines.slice(0, PREVIEW_LINES).join('\n');
outputText = `${preview}\n\n--- PREVIEW: showing ${PREVIEW_LINES} of ${lines.length} lines. Use fullRender=true for complete output. ---`;
}
}
return {
content: [{
type: "text",
text: outputText
}],
};
} catch (error) {
const elapsedTime = Date.now() - startTime;
console.error(`[Tool] check_module - ERROR after ${elapsedTime}ms: ${error.message}`);
return handleToolError(error, 'check_module', { moduleName, varsCount: Object.keys(vars || {}).length });
}
})
);
}

View File

@@ -0,0 +1,62 @@
import { z } from "zod";
import { withAuth, getSessionCredentials, getApiClient } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
export function registerCheckModuleUsageTool(server) {
server.tool(
"check_module_usage",
"Check which pages/URLs use a module. Call BEFORE delete_module to verify it's safe to remove.",
withAuthParams({
id: z.string().describe("Module ID to check usage for"),
}),
{ readOnlyHint: true, destructiveHint: false },
withAuth(async ({ id }, extra) => {
try {
// Validate required parameters
const validationError = validateRequired({ id }, ['id'], 'check_module_usage');
if (validationError) return validationError;
const credentials = await getSessionCredentials(extra.sessionId);
// Build the request payload
const payload = {
action_ws: "checkModuleInWeb",
module: id,
token: credentials.token,
tokenHash: credentials.tokenHash
};
// Make the request to the client's website
const response = await AcaiHttpClient.postViewerFunctions(
await getApiClient(extra.sessionId),
payload
);
// Check for API errors in response
const apiError = handleApiResponse(response.data, 'check_module_usage');
if (apiError) return apiError;
// Extract usage information
const usageData = response.data.data || response.data;
return {
content: [{
type: "text", text: JSON.stringify({
success: true,
moduleId: id,
usage: usageData,
canDelete: !usageData || Object.keys(usageData).length === 0,
message: Object.keys(usageData || {}).length === 0
? "Module is not used anywhere - safe to delete"
: `Module is used in ${Object.keys(usageData || {}).length} location(s)`
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'check_module_usage', { id });
}
})
);
}

View File

@@ -0,0 +1,82 @@
import { z } from "zod";
import axios from "axios";
import path from "path";
import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { LOCAL_SERVER_URL } from "../../config/index.js";
export function registerCompileModuleTool(server) {
server.tool(
"compile_module",
`Manually recompile a module or general section when generated files may be out of sync and you need to force compilation without editing index-base.tpl.
Do not use this as part of the normal editing flow: most index-base.tpl edits made through the Acai file tools compile automatically.
This is a recovery / resync tool, not a required step after routine changes.
It parses the HTML into Twig, generates builder vars, and syncs with the Docker CMS.
Pass the full path to the index-base.tpl file and the project directory.`,
withAuthParams({
filePath: z.string().describe("Full absolute path to the index-base.tpl file that was edited"),
projectDir: z.string().describe("Full absolute path to the project root directory"),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ filePath, projectDir }, extra) => {
try {
const validationError = validateRequired(
{ filePath, projectDir },
['filePath', 'projectDir'],
'compile_module'
);
if (validationError) return validationError;
const normalizedProjectDir = path.resolve(projectDir);
const normalizedFilePath = path.resolve(filePath);
const projectSlug = path.basename(normalizedProjectDir);
const relativePath = path.relative(normalizedProjectDir, normalizedFilePath);
const canUseSlugMode =
!!projectSlug &&
!!relativePath &&
relativePath !== "" &&
!relativePath.startsWith("..") &&
!path.isAbsolute(relativePath);
const payload = canUseSlugMode
? { project: projectSlug, relativePath, project_dir: projectDir }
: { file: filePath, project_dir: projectDir };
// Call the Python server compile endpoint
const response = await axios.post(
`${LOCAL_SERVER_URL}/api/compile-module`,
payload,
{ headers: { "Content-Type": "application/json" }, timeout: 30000 }
);
if (response.data?.ok) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: "Module compiled successfully",
output: response.data.output || "",
}, null, 2)
}],
};
} else {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: response.data?.error || "Compilation failed",
}, null, 2)
}],
isError: true,
};
}
} catch (error) {
return handleToolError(error, 'compile_module', { filePath, projectDir });
}
})
);
}

View File

@@ -0,0 +1,75 @@
import { z } from "zod";
import axios from "axios";
import { withAuth } from "../../auth/index.js";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { LOCAL_SERVER_URL } from "../../config/index.js";
export function registerCreateModuleTool(server) {
server.tool(
"create_module",
`Create a new builder module in the project. This creates the module directory with index-base.tpl, style.css, and script.js, then compiles it automatically.
After creating the module, use add_module_to_record to place it on a page, then set_module_config_vars to fill its variables with content.
Parameters:
- moduleId: unique identifier (lowercase, underscores, e.g. "hero_banner")
- html: the Twig/HTML content for index-base.tpl
- css: optional CSS for style.css
- js: optional JavaScript for script.js
- php: optional PHP code for module hook file .php
- label: human-readable name (e.g. "Hero Banner V2")
- description: brief description of what the module does`,
withAuthParams({
moduleId: z.string().describe("Module identifier (lowercase, underscores, e.g. 'hero_banner')"),
html: z.string().describe("HTML/Twig content for index-base.tpl ( needed for compile module )"),
css: z.string().optional().default("").describe("CSS content for style.css ( optional, you can also add CSS later on file )"),
js: z.string().optional().default("").describe("JavaScript content for script.js ( optional, you can also add CSS later on file )"),
php: z.string().optional().default("").describe("PHP code for module hook file .php ( optional, you can also add CSS later on file )"),
label: z.string().optional().default("").describe("Human-readable module name"),
description: z.string().optional().default("").describe("Brief description"),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ moduleId, html, css, js, php, label, description }, extra) => {
try {
const validationError = validateRequired({ moduleId, html }, ['moduleId', 'html'], 'create_module');
if (validationError) return validationError;
const projectDir = process.env.ACAI_PROJECT_DIR || "";
if (!projectDir) {
return { content: [{ type: "text", text: "Error: ACAI_PROJECT_DIR not set" }], isError: true };
}
moduleId = moduleId.toLowerCase().replace(/\s+/g, '_'); // Ensure moduleId is lowercase and uses underscores
moduleId = moduleId + "_" + (Math.random().toString(36).substring(2, 8).toUpperCase());
const response = await axios.post(
`${LOCAL_SERVER_URL}/api/create-module`,
{ project_dir: projectDir, module_id: moduleId, html, css: css || "", js: js || "", label, description, php: php || "" },
{ headers: { "Content-Type": "application/json" }, timeout: 30000 }
);
if (response.data?.success) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
moduleId,
path: response.data.path,
compiled: response.data.compiled,
note: response.data.compiled
? "Module created and compiled. Use add_module_to_record to place it on a page, then set_module_config_vars to fill its variables."
: "Module created but compilation failed: " + (response.data.compile_output || "unknown error"),
}, null, 2),
}],
};
} else {
return { content: [{ type: "text", text: JSON.stringify(response.data) }], isError: true };
}
} catch (error) {
return handleToolError(error, 'create_module', { moduleId });
}
})
);
}

View File

@@ -0,0 +1,9 @@
import { registerCheckModuleTool } from './check.js';
import { registerCheckModuleUsageTool } from './checkUsage.js';
import { registerCompileModuleTool } from './compile.js';
export function registerModuleTools(server) {
registerCheckModuleTool(server);
registerCheckModuleUsageTool(server);
registerCompileModuleTool(server);
}

View File

@@ -0,0 +1,93 @@
import { z } from "zod";
import { withAuth, getSessionCredentials, getApiClient, getCommonParams } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
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.
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.
See resource 'acai-cheat-sheet' → "Example Data Formatting" for type-specific value formats.`,
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."),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ moduleId, moduleSchema, exampleData }, extra) => {
try {
// Validate required parameters
const validationError = validateRequired({ moduleId, exampleData }, ['moduleId', 'exampleData'], 'set_module_example_data');
if (validationError) return validationError;
// Validate that all schema variables are present in exampleData
if (moduleSchema && moduleSchema.codeVars) {
const schemaVars = Object.keys(moduleSchema.codeVars);
const dataVars = Object.keys(exampleData);
const missingVars = schemaVars.filter(v => !dataVars.includes(v));
if (missingVars.length > 0) {
console.warn(`[set_module_example_data] WARNING: Missing variables in exampleData: ${missingVars.join(', ')}`);
}
// Check for upload fields that are not arrays
for (const [varName, varInfo] of Object.entries(moduleSchema.codeVars)) {
if (varInfo.type === 'upload' && exampleData[varName]) {
if (!Array.isArray(exampleData[varName])) {
console.error(`[set_module_example_data] ERROR: Upload field '${varName}' is not an array! Current value: ${JSON.stringify(exampleData[varName])}`);
console.error(`[set_module_example_data] Upload fields MUST be arrays with urlPath objects: [{"urlPath": "..."}]`);
} else if (exampleData[varName].length > 0 && !exampleData[varName][0].urlPath) {
console.error(`[set_module_example_data] ERROR: Upload field '${varName}' items missing 'urlPath' property!`);
}
}
}
}
const credentials = await getSessionCredentials(extra.sessionId);
const client = await getApiClient(extra.sessionId);
// Log data for debugging
console.error(`[set_module_example_data] Module ID: ${moduleId}`);
console.error(`[set_module_example_data] Module Schema:`, JSON.stringify(moduleSchema, null, 2));
console.error(`[set_module_example_data] Example Data:`, JSON.stringify(exampleData, null, 2));
// Prepare payload for setStaticVars action
const payload = await getCommonParams(extra.sessionId, {
action_ws: "setStaticVars",
moduleId: moduleId,
staticVars: exampleData,
schema: moduleSchema
});
console.error(`[set_module_example_data] Full Payload:`, JSON.stringify(payload, null, 2));
// Send to viewer_functions
const response = await client.post("/cms/lib/viewer_functions.php", payload);
console.error(`[set_module_example_data] Response:`, JSON.stringify(response.data, null, 2));
// Check for API errors in response
const apiError = handleApiResponse(response.data, 'set_module_example_data');
if (apiError) return apiError;
return {
content: [{
type: "text", text: JSON.stringify({
success: true,
message: `Example data set successfully for module '${moduleId}'`,
moduleId: moduleId,
dataCount: Object.keys(exampleData).length,
schemaVarsCount: moduleSchema?.codeVars ? Object.keys(moduleSchema.codeVars).length : 0,
response: response.data
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'set_module_example_data', { moduleId });
}
})
);
}