Files
agenticSystem/mcp-server/tools/docs/_docsReader.js
Jordan Diaz 9277862e56 read_doc: resolver docs por ACAI_PROJECT_DIR + knowledge load idempotente
- 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>
2026-06-11 17:23:53 +00:00

173 lines
5.0 KiB
JavaScript

import fs from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
/**
* Lectura directa de los markdown del knowledge base desde el filesystem.
*
* Orden de resolucion del directorio de docs:
* 1. `ACAI_DOCS_DIR` — override explicito por entorno (si esta definido y no vacio).
* 2. `<ACAI_PROJECT_DIR>/docs` — caso principal: cada proyecto/web tiene su
* propio `docs/`. El `.mcp.json` inyecta `ACAI_PROJECT_DIR` (p.ej.
* `/opt/acai/webs/<user>/<site>`), funciona tanto en local (VSCode) como
* en cloud (agentic).
* 3. `/app/docs` — fallback final: container `agentic` donde esta horneada la
* copia canonica de los .md.
*/
function dirExists(p) {
try {
return existsSync(p);
} catch {
return false;
}
}
function resolveDocsDir() {
// 1. Override explicito
const override = process.env.ACAI_DOCS_DIR;
if (override && override.trim() !== "") return override;
// 2. Docs del proyecto/web
const projectDir = process.env.ACAI_PROJECT_DIR;
if (projectDir && projectDir.trim() !== "") {
const projectDocs = path.join(projectDir, "docs");
if (dirExists(projectDocs)) return projectDocs;
}
// 3. Fallback al container agentic
return "/app/docs";
}
export async function listDocs() {
const dir = resolveDocsDir();
let files;
try {
files = await fs.readdir(dir);
} catch (err) {
const error = new Error(`No se pudo leer el directorio de docs (${dir}): ${err.message}`);
error.code = "DOCS_DIR_NOT_FOUND";
throw error;
}
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
const docs = [];
for (const file of mdFiles) {
const id = file.replace(/\.md$/i, "");
const filePath = path.join(dir, file);
const content = await fs.readFile(filePath, "utf8");
const lines = content.split("\n");
const titleLine = lines.find((l) => l.trim().startsWith("# ")) || id;
const title = titleLine.replace(/^#+/, "").trim();
// Summary: primeras 30 lineas no-heading, capeado a 500 chars
const summaryLines = [];
for (const line of lines.slice(0, 30)) {
const t = line.trim();
if (t && !t.startsWith("#")) summaryLines.push(t);
if (summaryLines.join(" ").length > 500) break;
}
const summary = summaryLines.join(" ").slice(0, 500);
docs.push({
id,
title,
summary,
chars: content.length,
});
}
return docs;
}
export async function readDoc(name, section) {
const dir = resolveDocsDir();
const safeName = String(name).replace(/\.md$/i, "").replace(/[\/\\]/g, "");
const filePath = path.join(dir, `${safeName}.md`);
let content;
try {
content = await fs.readFile(filePath, "utf8");
} catch (err) {
const error = new Error(`Doc '${safeName}' no encontrado en ${dir}`);
error.code = "DOC_NOT_FOUND";
throw error;
}
const titleLine = content.split("\n").find((l) => l.trim().startsWith("# ")) || safeName;
const title = titleLine.replace(/^#+/, "").trim();
const availableSections = listSections(content);
if (section) {
const sectionContent = extractSection(content, section);
if (sectionContent === null) {
return {
id: safeName,
title,
section_requested: section,
section_found: false,
available_sections: availableSections,
chars: 0,
content: "",
};
}
return {
id: safeName,
title,
section,
section_found: true,
chars: sectionContent.length,
content: sectionContent,
};
}
return {
id: safeName,
title,
section: null,
section_found: true,
chars: content.length,
available_sections: availableSections,
content,
};
}
function listSections(content) {
const sections = [];
for (const line of content.split("\n")) {
const t = line.trimStart();
if (t.startsWith("## ") && !t.startsWith("### ")) {
sections.push(t.slice(3).trim());
}
}
return sections;
}
function extractSection(content, query) {
const q = String(query).toLowerCase().trim();
if (!q) return null;
const lines = content.split("\n");
const captured = [];
let capturing = false;
for (const line of lines) {
const t = line.trimStart();
const isH2 = t.startsWith("## ") && !t.startsWith("### ");
if (isH2) {
const heading = t.slice(3).trim();
if (capturing) break;
if (heading.toLowerCase().includes(q)) {
capturing = true;
captured.push(line);
continue;
}
}
if (capturing) captured.push(line);
}
if (captured.length === 0) return null;
return captured.join("\n").trimEnd();
}