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,483 @@
import { z } from "zod";
import { withAuth, getApiClient, getCommonParams } from "../../auth/index.js";
import { handleToolError, handleApiResponse, validateRequired } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
const COMMIT_HASH_REGEX = /\b[a-f0-9]{40}\b/i;
const MAX_LOG_LIMIT = 20;
const lastModulePathBySession = new Map();
const LAYOUT_CONTEXT_PATH = "/modulos/layout/";
const LAYOUT_ALIASES = new Set([
"layout",
"header",
"footer",
"hookglobal",
"hooksglobal",
"globalhook",
"globalhooks",
"hooksglobales"
]);
function normalizePath(path) {
const trimmedPath = path.trim();
if (!trimmedPath) {
return trimmedPath;
}
const cleanPath = trimmedPath.replace(/^\/+|\/+$/g, "");
if (!cleanPath) {
return "/";
}
const normalizedToken = cleanPath.toLowerCase().replace(/[\s_-]+/g, "");
if (LAYOUT_ALIASES.has(normalizedToken) || cleanPath.toLowerCase() === "modulos/layout") {
return LAYOUT_CONTEXT_PATH;
}
// Common case: only module id/name provided (e.g. nameofthecustommodule_aeq9kl)
if (!cleanPath.includes("/")) {
return `/modulos/${cleanPath}/`;
}
// If "modulos/xxx" is provided, normalize with leading/trailing slash.
if (cleanPath.startsWith("modulos/")) {
return `/${cleanPath}/`;
}
// Fallback: normalize any other path preserving its segments.
return `/${cleanPath}/`;
}
function parseGitLogResponse(logData) {
const rawLog = logData?.log;
if (typeof rawLog === "string") {
return {
entries: [],
hasNoPreviousVersions: true,
noPreviousVersionsMessage: rawLog
};
}
if (Array.isArray(rawLog)) {
return {
entries: rawLog,
hasNoPreviousVersions: false,
noPreviousVersionsMessage: null
};
}
return {
entries: [],
hasNoPreviousVersions: false,
noPreviousVersionsMessage: null
};
}
function getWsPayload(response) {
if (!response || typeof response !== "object") {
return response;
}
// Prefer direct WS payload shape: { log: [...], result: ... }
if ("log" in response || "result" in response || "error" in response || "success" in response) {
return response;
}
// Fallback for axios shape: { data: {...} }
if (response.data && typeof response.data === "object") {
return response.data;
}
return response;
}
function extractCommitIdFromEntry(entry) {
if (!entry) {
return null;
}
if (Array.isArray(entry)) {
const id = entry[1];
if (typeof id === "string" && id.trim()) {
return id.trim();
}
if (typeof id === "number") {
return String(id);
}
return null;
}
if (typeof entry === "string") {
return entry.trim();
}
if (typeof entry !== "object") {
return null;
}
const directId = entry.id || entry.commitId || entry.hash || entry.sha || entry.commit;
if (typeof directId === "string" && directId.trim()) {
return directId.trim();
}
const serializedEntry = JSON.stringify(entry);
const hashMatch = serializedEntry.match(COMMIT_HASH_REGEX);
return hashMatch ? hashMatch[0] : null;
}
function formatLogEntries(logData, limit = MAX_LOG_LIMIT) {
const { entries } = parseGitLogResponse(logData);
const slicedEntries = entries.slice(0, limit);
return slicedEntries.map((entry, index) => {
const commitId = extractCommitIdFromEntry(entry);
const date = Array.isArray(entry) ? entry[0] : (entry?.date || entry?.fecha || null);
if (Array.isArray(entry)) {
return {
index: index + 1,
date,
id: commitId,
raw: entry
};
}
if (typeof entry === "object" && entry !== null) {
return {
index: index + 1,
date,
id: commitId,
...entry
};
}
return {
index: index + 1,
id: commitId,
raw: entry
};
});
}
function getTargetText(path) {
return path ? `este módulo ${path}` : "todo";
}
export function registerRecoverGitTools(server) {
server.tool(
"list_git_log",
`List the latest git history entries (up to 20) so the user can choose the rollback id.
Routing guidance:
- If user says "haz rollback" or "recupera" without a commit id, use this tool first.
- Show the returned commits and ask which id to use.
- Do NOT choose an id automatically in that scenario.`,
withAuthParams({
path: z.string().optional().describe("Optional module path OR module id/name. Example path: /modulos/nameofthecustommodule_aeq9kl/. Example id/name: nameofthecustommodule_aeq9kl. Global hooks/header/footer/layout are in /modulos/layout/ (aliases header/footer/layout/hooks globales auto-map). If omitted, returns global git history."),
limit: z.number().int().min(1).max(MAX_LOG_LIMIT).optional().describe("Maximum number of entries to return (default 20, max 20)."),
}),
withAuth(async ({ path, limit = MAX_LOG_LIMIT }, extra) => {
try {
const normalizedPath = path ? normalizePath(path) : '';
if (normalizedPath) {
lastModulePathBySession.set(extra.sessionId, normalizedPath);
}
const client = await getApiClient(extra.sessionId);
const logResponse = await client.post(
"/cms/lib/viewer_functions.php",
await getCommonParams(extra.sessionId, {
action_ws: "getGitLog",
...(normalizedPath ? { path: normalizedPath } : {})
})
);
const logPayload = getWsPayload(logResponse);
const logError = handleApiResponse(logPayload, 'list_git_log:getGitLog');
if (logError) return logError;
const parsedLog = parseGitLogResponse(logPayload);
if (parsedLog.hasNoPreviousVersions) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: parsedLog.noPreviousVersionsMessage || "No hay versiones anteriores.",
target: getTargetText(normalizedPath),
path: normalizedPath,
count: 0,
commits: []
}, null, 2)
}],
};
}
const list = formatLogEntries(logPayload, limit);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: "Selecciona un id y llama a recover_git con ese id. Nota: el primer commit es la versión actual; el segundo es la versión anterior (último rollback sugerido).",
target: getTargetText(normalizedPath),
path: normalizedPath,
count: list.length,
commits: list
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'list_git_log', { path });
}
})
);
server.tool(
"recover_git",
`Execute recoverGit using an explicit commit id chosen by the user.
Routing guidance:
- Use this only when a specific commit id is already provided by the user.
- If user did not provide id, call list_git_log first.
SAFETY: You must pass confirm=true to execute recovery.`,
withAuthParams({
id: z.string().describe("Commit id selected by the user from list_git_log."),
path: z.string().optional().describe("Optional module path OR module id/name. Example path: /modulos/nameofthecustommodule_aeq9kl/. Example id/name: nameofthecustommodule_aeq9kl. Global hooks/header/footer/layout are in /modulos/layout/ (aliases header/footer/layout/hooks globales auto-map). If omitted, rollback applies to todo."),
confirm: z.boolean().optional().describe("Set true to confirm execution. If false/omitted, tool will not execute recoverGit."),
}),
withAuth(async ({ id, path, confirm = false }, extra) => {
try {
const validationError = validateRequired({ id }, ['id'], 'recover_git');
if (validationError) return validationError;
const normalizedPath = path ? normalizePath(path) : '';
if (normalizedPath) {
lastModulePathBySession.set(extra.sessionId, normalizedPath);
}
const target = getTargetText(normalizedPath);
if (!confirm) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
requiresConfirmation: true,
message: `Confirmación requerida para hacer rollback de ${target}. Ejecuta de nuevo con confirm=true.`,
id,
path: normalizedPath,
target
}, null, 2)
}],
isError: true,
};
}
const client = await getApiClient(extra.sessionId);
const response = await client.post(
"/cms/lib/viewer_functions.php",
await getCommonParams(extra.sessionId, {
action_ws: "recoverGit",
...(normalizedPath ? { path: normalizedPath } : {}),
id
})
);
const recoverPayload = getWsPayload(response);
const apiError = handleApiResponse(recoverPayload, 'recover_git');
if (apiError) return apiError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Rollback ejecutado sobre ${target}.`,
id,
path: normalizedPath,
target,
response: recoverPayload
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'recover_git');
}
})
);
server.tool(
"recover_previous_git",
`Rollback to the previous version (second commit in git log).
Routing guidance:
- Use this when user says "haz rollback a la versión anterior" or "regresa a la versión anterior".
- This tool proposes the previous version id (second entry in getGitLog), but the user must either select an id or confirm using that suggested id.
Rules:
- If path is provided, use that module.
- If path is omitted, use the last module path used in this session.
- If there is no last module path, apply to todo (without path).
- Always asks confirmation before executing rollback.`,
withAuthParams({
path: z.string().optional().describe("Optional module path OR module id/name. Example path: /modulos/nameofthecustommodule_aeq9kl/. Example id/name: nameofthecustommodule_aeq9kl. Global hooks/header/footer/layout are in /modulos/layout/ (aliases header/footer/layout/hooks globales auto-map). If omitted, uses the last module path from this session; if none, applies to todo."),
selectedId: z.string().optional().describe("Commit id selected by the user. If omitted, you must set confirmLatest=true to use the suggested previous version id."),
confirmLatest: z.boolean().optional().describe("Set true to confirm using the suggested 'último' id (second commit in log)."),
confirm: z.boolean().optional().describe("Set true to confirm execution. If false/omitted, tool returns selected id and target."),
}),
withAuth(async ({ path, selectedId, confirmLatest = false, confirm = false }, extra) => {
try {
const normalizedPath = path
? normalizePath(path)
: (lastModulePathBySession.get(extra.sessionId) || null);
if (normalizedPath) {
lastModulePathBySession.set(extra.sessionId, normalizedPath);
}
const client = await getApiClient(extra.sessionId);
const logResponse = await client.post(
"/cms/lib/viewer_functions.php",
await getCommonParams(extra.sessionId, {
action_ws: "getGitLog",
...(normalizedPath ? { path: normalizedPath } : {})
})
);
const logPayload = getWsPayload(logResponse);
const logError = handleApiResponse(logPayload, 'recover_previous_git:getGitLog');
if (logError) return logError;
const parsedLog = parseGitLogResponse(logPayload);
if (parsedLog.hasNoPreviousVersions) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
message: parsedLog.noPreviousVersionsMessage || "No hay versiones anteriores.",
target: getTargetText(normalizedPath),
path: normalizedPath
}, null, 2)
}],
isError: true,
};
}
const entries = parsedLog.entries;
if (entries.length < 2) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
message: "No hay suficiente historial para volver a la versión anterior. Se requieren al menos 2 commits.",
target: getTargetText(normalizedPath),
path: normalizedPath,
entriesFound: entries.length
}, null, 2)
}],
isError: true,
};
}
const target = getTargetText(normalizedPath);
const suggestedPreviousId = extractCommitIdFromEntry(entries[1]);
if (!suggestedPreviousId) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
requiresConfirmation: true,
message: "No se pudo resolver el id sugerido de la versión anterior (segundo commit).",
target,
path: normalizedPath
}, null, 2)
}],
isError: true,
};
}
const finalId = confirmLatest ? suggestedPreviousId : selectedId;
if (!finalId) {
const commits = formatLogEntries(logPayload, MAX_LOG_LIMIT);
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
requiresUserSelection: true,
message: `Debes indicar selectedId o confirmar confirmLatest=true para usar el 'último' (segundo commit).`,
note: "El primer commit es la versión actual; el segundo es la versión anterior.",
target,
path: normalizedPath,
suggestedPreviousId,
commits
}, null, 2)
}],
isError: true,
};
}
if (!confirm) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
requiresConfirmation: true,
message: `Confirmación requerida para hacer rollback en ${target}.`,
target,
path: normalizedPath,
suggestedPreviousId,
finalId,
usedSuggestedPrevious: confirmLatest
}, null, 2)
}],
isError: true,
};
}
const recoverResponse = await client.post(
"/cms/lib/viewer_functions.php",
await getCommonParams(extra.sessionId, {
action_ws: "recoverGit",
...(normalizedPath ? { path: normalizedPath } : {}),
id: finalId
})
);
const recoverPayload = getWsPayload(recoverResponse);
const recoverError = handleApiResponse(recoverPayload, 'recover_previous_git:recoverGit');
if (recoverError) return recoverError;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Rollback ejecutado en ${target}.`,
target,
id: finalId,
path: normalizedPath,
suggestedPreviousId,
usedSuggestedPrevious: confirmLatest,
response: recoverPayload
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'recover_previous_git');
}
})
);
}