mcp-phase-gate 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/config.js CHANGED
@@ -8,40 +8,166 @@ import * as fs from "fs";
8
8
  import * as os from "os";
9
9
  import * as path from "path";
10
10
  import { fileURLToPath } from "url";
11
- import dotenv from "dotenv";
11
+ import { parseEnv } from "util";
12
12
  // Carpeta de este módulo. Sirve para localizar el .env y el package.json de la raíz del repo.
13
13
  export const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
14
- // Carga el archivo .env de la raíz del repo (un nivel arriba de build/ o src/).
15
- // Así cada dispositivo define sus propias rutas sin tocar el mcp.json.
16
- // quiet: true evita que dotenv v17 imprima su banner en stdout, que es el canal del JSON-RPC.
17
- dotenv.config({ path: path.join(MODULE_DIR, "..", ".env"), quiet: true });
14
+ // ─── El .env ────────────────────────────────────────────────────────────────
15
+ // El .env de la raíz del paquete (un nivel arriba de build/ o src/) deja que cada dispositivo
16
+ // defina sus rutas sin tocar el mcp.json. Se lee con un lector propio y no con dotenv ni con
17
+ // process.loadEnvFile: loadEnvFile no quita el BOM de UTF-8, y con un .env guardado con BOM la
18
+ // primera clave llegaba como "\uFEFFORQUESTADOR_DOCS_PATH" y la variable desaparecía. Y un .env
19
+ // en UTF-16 —lo que sale de PowerShell con ">"— hoy solo se adivinaba por los acentos rotos.
20
+ //
21
+ // El bloque "env" del cliente sigue ganando: solo se asignan las claves que no estén ya en el
22
+ // entorno, igual que hacía dotenv.
23
+ const clavesCargadas = new Set();
24
+ let errorDeCarga = null;
25
+ export function envPorDefecto() {
26
+ return path.join(MODULE_DIR, "..", ".env");
27
+ }
28
+ // Claves que vinieron del .env; las demás, del entorno. doctor usa esto para decir la fuente.
29
+ export function clavesDeEnv() {
30
+ return clavesCargadas;
31
+ }
32
+ // Error guardado de la última carga (por ejemplo, UTF-16), o null.
33
+ export function errorDeEnv() {
34
+ return errorDeCarga;
35
+ }
36
+ // Solo para tests: borra del entorno las claves que vinieron del .env, vacía clavesDeEnv() y el
37
+ // error guardado. Sin esto, un .env cargado en un test contaminaría al siguiente.
38
+ export function reiniciarEnv() {
39
+ for (const clave of clavesCargadas)
40
+ delete process.env[clave];
41
+ clavesCargadas.clear();
42
+ errorDeCarga = null;
43
+ }
44
+ // Carga el .env. Sin argumento, el de la raíz del paquete. Se llama una vez al importar este
45
+ // módulo. Volver a llamarla sustituye lo que cargó la vez anterior.
46
+ export function cargarEnv(archivo = envPorDefecto()) {
47
+ reiniciarEnv();
48
+ let bytes;
49
+ try {
50
+ bytes = fs.readFileSync(archivo);
51
+ }
52
+ catch (error) {
53
+ if (error?.code === "ENOENT")
54
+ return; // sin .env, todo sale del entorno
55
+ errorDeCarga = `No se pudo leer el .env (${archivo}): ${error instanceof Error ? error.message : String(error)}`;
56
+ return;
57
+ }
58
+ // UTF-16 con BOM en cualquiera de sus dos órdenes, o sin BOM pero con bytes NUL: en un .env de
59
+ // texto en UTF-8 no hay ningún NUL, y en UTF-16 los hay en casi cada carácter.
60
+ const conBom16 = bytes.length >= 2 && ((bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0xfe && bytes[1] === 0xff));
61
+ if (conBom16 || bytes.subarray(0, 512).includes(0)) {
62
+ errorDeCarga = `El .env está en UTF-16 y no se cargó (${archivo}). Guárdalo como UTF-8 (sin BOM).`;
63
+ return;
64
+ }
65
+ const texto = bytes.toString("utf-8").replace(/^\uFEFF/, "");
66
+ let valores;
67
+ try {
68
+ valores = parseEnv(texto);
69
+ }
70
+ catch (error) {
71
+ errorDeCarga = `No se pudo interpretar el .env (${archivo}): ${error instanceof Error ? error.message : String(error)}`;
72
+ return;
73
+ }
74
+ for (const [clave, valor] of Object.entries(valores)) {
75
+ if (process.env[clave] !== undefined)
76
+ continue;
77
+ process.env[clave] = valor;
78
+ clavesCargadas.add(clave);
79
+ }
80
+ }
81
+ cargarEnv();
82
+ export const VARIABLES = {
83
+ DOCS_PATH: { principal: "PHASE_GATE_DOCS_PATH", alias: "ORQUESTADOR_DOCS_PATH", esRuta: true },
84
+ REPOS_PATH: { principal: "PHASE_GATE_REPOS_PATH", alias: "ORQUESTADOR_REPOS_PATH", esRuta: true },
85
+ TASKS_SUBDIR: { principal: "PHASE_GATE_TASKS_SUBDIR", alias: "ORQUESTADOR_TASKS_SUBDIR", esRuta: false },
86
+ PROMPTS_PATH: { principal: "PHASE_GATE_PROMPTS_PATH", alias: "ORQUESTADOR_PROMPTS_PATH", esRuta: true },
87
+ STATE_PATH: { principal: "PHASE_GATE_STATE_PATH", alias: "ORQUESTADOR_STATE_PATH", esRuta: true },
88
+ ELICITATION: { principal: "PHASE_GATE_ELICITATION", alias: null, esRuta: false },
89
+ };
90
+ export const NOMBRES_LOGICOS = Object.keys(VARIABLES);
91
+ // Qué nombre gana, en este orden:
92
+ // 1. La fuente: el entorno (el bloque "env" del cliente) gana al .env, también cuando cada
93
+ // nombre llega por una vía distinta. Si no, un .env con PHASE_GATE_X le ganaría a un bloque
94
+ // env con ORQUESTADOR_X, y "el cliente gana al .env" dejaría de ser cierto.
95
+ // 2. Dentro de la misma fuente, PHASE_GATE_* gana a ORQUESTADOR_*.
96
+ //
97
+ // "Definida" es que exista, aunque esté vacía: TASKS_SUBDIR vacía a propósito significa que las
98
+ // tareas cuelgan directo del proyecto, y eso tiene que poder decirse también con el nombre nuevo.
99
+ // En las variables de RUTA, en cambio, vacía cuenta como ausente: una ruta vacía no significa
100
+ // nada, y no debe tapar un alias que sí apunta a una carpeta.
101
+ export function leerVariable(logica) {
102
+ const { principal, alias, esRuta } = VARIABLES[logica];
103
+ const definida = (nombre) => {
104
+ const valor = process.env[nombre];
105
+ return valor !== undefined && (!esRuta || valor.trim() !== "");
106
+ };
107
+ const fuenteDe = (nombre) => (clavesCargadas.has(nombre) ? ".env" : "entorno");
108
+ const candidatos = (alias ? [principal, alias] : [principal]).filter(definida);
109
+ // sort es estable: con la misma fuente se conserva el orden principal, alias.
110
+ candidatos.sort((a, b) => Number(fuenteDe(a) === ".env") - Number(fuenteDe(b) === ".env"));
111
+ const usada = candidatos[0] ?? null;
112
+ const ignorada = candidatos[1] ?? null;
113
+ const crudo = usada ? process.env[usada] : undefined;
114
+ let discrepancia = null;
115
+ if (usada && ignorada) {
116
+ const comparable = (valor) => {
117
+ const recortado = valor.trim();
118
+ return esRuta && recortado ? path.resolve(recortado) : recortado;
119
+ };
120
+ if (comparable(process.env[usada]) !== comparable(process.env[ignorada])) {
121
+ const motivo = fuenteDe(usada) !== fuenteDe(ignorada) ? " (el entorno del cliente gana al .env)" : "";
122
+ discrepancia = `${ignorada} difiere de ${usada}; se usa ${usada}${motivo}.`;
123
+ }
124
+ }
125
+ else if (esRuta && alias && usada === alias && process.env[principal] !== undefined) {
126
+ discrepancia = `${principal} está vacía; se usa ${alias}.`;
127
+ }
128
+ return {
129
+ logica,
130
+ valor: crudo?.trim(),
131
+ usada,
132
+ fuente: usada === null ? "sin definir" : fuenteDe(usada),
133
+ discrepancia,
134
+ };
135
+ }
136
+ // Cómo se nombra una variable en un mensaje: la principal, y el alias entre paréntesis.
137
+ export function nombrarVariable(logica) {
138
+ const { principal, alias } = VARIABLES[logica];
139
+ return alias ? `${principal} (o su alias ${alias})` : principal;
140
+ }
18
141
  export const DOCS = {
19
- variable: "ORQUESTADOR_DOCS_PATH",
142
+ logica: "DOCS_PATH",
20
143
  apuntaA: "la carpeta raíz de la Documentación Central",
21
144
  };
22
145
  export const REPOS = {
23
- variable: "ORQUESTADOR_REPOS_PATH",
146
+ logica: "REPOS_PATH",
24
147
  apuntaA: "la carpeta que contiene tus repositorios de código",
25
148
  };
26
149
  // Se resuelve y valida en cada llamada, no una vez al arrancar. Así el servidor se queda en
27
150
  // pie aunque falte configuración, las tools que no dependen de estas rutas siguen sirviendo,
28
151
  // y el error llega al chat —donde está el usuario— en vez de quedarse enterrado en un log.
29
- export function requireBasePath({ variable, apuntaA }) {
30
- const valor = process.env[variable]?.trim();
31
- if (!valor) {
32
- throw new Error(`Falta la variable ${variable}, que debe apuntar a ${apuntaA}. Defínela en el archivo ` +
33
- `.env de la raíz del repo, o en el bloque "env" del mcp.json de tu cliente.`);
152
+ export function requireBasePath({ logica, apuntaA }) {
153
+ const { valor, usada } = leerVariable(logica);
154
+ if (!valor || !usada) {
155
+ // Si el .env no se pudo cargar, esa es casi seguro la causa: se dice antes que nada.
156
+ const delEnv = errorDeCarga ? ` ${errorDeCarga}` : "";
157
+ throw new Error(`Falta ${nombrarVariable(logica)}, que debe apuntar a ${apuntaA}. Defínela en el archivo ` +
158
+ `.env de la raíz del repo, o en el bloque "env" del mcp.json de tu cliente.${delEnv}`);
34
159
  }
35
160
  const ruta = path.resolve(valor);
36
161
  // Una variable definida pero incorrecta falla igual de tarde que una ausente, así que la
37
- // existencia se comprueba aquí. El caso del encoding ya mordió una vez y no es adivinable.
162
+ // existencia se comprueba aquí. El BOM y el UTF-16 ya los resuelve cargarEnv; un .env guardado
163
+ // en ANSI todavía rompe los acentos sin que se pueda detectar con certeza.
38
164
  if (!fs.existsSync(ruta)) {
39
- throw new Error(`${variable} apunta a "${ruta}", que no existe en este equipo. Corrige la ruta en el .env. ` +
40
- `Si lleva acentos, revisa que el .env esté guardado en UTF-8 sin BOM: en UTF-16 los ` +
165
+ throw new Error(`${usada} apunta a "${ruta}", que no existe en este equipo. Corrige la ruta en el .env. ` +
166
+ `Si lleva acentos, revisa que el .env esté guardado en UTF-8: en otra codificación los ` +
41
167
  `acentos llegan rotos y la carpeta parece no existir.`);
42
168
  }
43
169
  if (!fs.statSync(ruta).isDirectory()) {
44
- throw new Error(`${variable} apunta a "${ruta}", que es un archivo y no una carpeta.`);
170
+ throw new Error(`${usada} apunta a "${ruta}", que es un archivo y no una carpeta.`);
45
171
  }
46
172
  return ruta;
47
173
  }
@@ -49,8 +175,9 @@ export function requireBasePath({ variable, apuntaA }) {
49
175
  // Es una convención de organización, no una ruta de dispositivo, pero vive en el entorno
50
176
  // para no tener que introducir un archivo de configuración por una sola cadena.
51
177
  // Vacía a propósito significa que las tareas cuelgan directo del proyecto.
178
+ export const TASKS_SUBDIR_POR_DEFECTO = "Proyectos";
52
179
  export function tasksSubdir() {
53
- return process.env.ORQUESTADOR_TASKS_SUBDIR?.trim() ?? "Proyectos";
180
+ return leerVariable("TASKS_SUBDIR").valor ?? TASKS_SUBDIR_POR_DEFECTO;
54
181
  }
55
182
  // ─── Prompts, en capas ──────────────────────────────────────────────────────
56
183
  // Los prompts de las fases viven en archivos .md sueltos. Vivir fuera del código permite
@@ -60,7 +187,7 @@ export function tasksSubdir() {
60
187
  // Se cargan en CAPAS, no por reemplazo: primero los del paquete y encima, si existe, la
61
188
  // carpeta que declare el usuario. La última capa que declare una fase es la que queda.
62
189
  //
63
- // Antes `ORQUESTADOR_PROMPTS_PATH` sustituía la carpeta entera, y eso solo funcionaba con un
190
+ // Antes `PROMPTS_PATH` sustituía la carpeta entera, y eso solo funcionaba con un
64
191
  // clon del repo alrededor, donde editar `prompts/` es lo natural. Instalado desde npm los de
65
192
  // fábrica caen dentro de node_modules, donde editarlos no sirve porque la siguiente
66
193
  // instalación se los lleva. Reemplazar obligaba entonces a copiar las cinco fases para
@@ -72,7 +199,7 @@ export function defaultPromptsPath() {
72
199
  // De menor a mayor prioridad. La del usuario va al final: gana.
73
200
  export function promptsLayers() {
74
201
  const capas = [defaultPromptsPath()];
75
- const propia = process.env.ORQUESTADOR_PROMPTS_PATH?.trim();
202
+ const propia = leerVariable("PROMPTS_PATH").valor;
76
203
  if (propia)
77
204
  capas.push(path.resolve(propia));
78
205
  return capas;
@@ -88,10 +215,12 @@ export function promptsLayers() {
88
215
  // No se migran: el puntero se recalcula solo con un `switch_task`, y arrastrar código de
89
216
  // migración para un dato que cuesta un comando en recuperarse es más caro que el problema.
90
217
  export const APP_DIR_NAME = "mcp-phase-gate";
218
+ // La variable se recorta y se resuelve, como las demás rutas: antes se usaba tal cual, y un
219
+ // espacio pegado al final del valor creaba otra carpeta de estado.
91
220
  export function stateDir() {
92
- if (process.env.ORQUESTADOR_STATE_PATH) {
93
- return process.env.ORQUESTADOR_STATE_PATH;
94
- }
221
+ const propia = leerVariable("STATE_PATH").valor;
222
+ if (propia)
223
+ return path.resolve(propia);
95
224
  const home = os.homedir();
96
225
  if (process.platform === "win32") {
97
226
  const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
@@ -0,0 +1,15 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
3
+ import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js";
4
+ export type Confirmacion = "confirmada" | "rechazada" | "no-disponible";
5
+ export type ResultadoConfirmacion = {
6
+ estado: Confirmacion;
7
+ nota?: string;
8
+ };
9
+ export declare const ESPERA_POR_DEFECTO_MS = 180000;
10
+ export declare function fijarEsperaDeConfirmacion(ms: number | null): void;
11
+ export declare function pedirConfirmacion(mcp: McpServer, extra: RequestHandlerExtra<ServerRequest, ServerNotification>, pregunta: {
12
+ mensaje: string;
13
+ etiqueta: string;
14
+ }): Promise<ResultadoConfirmacion>;
15
+ //# sourceMappingURL=confirmacion.d.ts.map
@@ -0,0 +1,100 @@
1
+ // Confirmaciones pedidas directamente a la persona, por elicitation de MCP.
2
+ //
3
+ // Hasta ahora, reescribir un documento protegido o estrenar un proyecto dependía de una bandera
4
+ // (`force`, `crear_proyecto`) que pone el modelo cuando "el usuario lo confirmó en el chat". Es
5
+ // justo lo que se quiere evitar: la bandera la escribe quien no debe decidir. Con elicitation, el
6
+ // servidor le pregunta a la persona en un formulario de su cliente, y la respuesta no pasa por el
7
+ // modelo.
8
+ //
9
+ // Si el cliente no lo soporta, o lo anuncia pero no lo muestra, se vuelve al comportamiento
10
+ // anterior: deciden las banderas. Una persona que dice que no, o que cierra el formulario, nunca
11
+ // se convierte en un sí.
12
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
13
+ import { leerVariable } from "./config.js";
14
+ import { log } from "./log.js";
15
+ // El SDK espera un minuto por defecto a que el cliente responda; para que una persona lea y
16
+ // decida es poco. Los tests lo acortan.
17
+ export const ESPERA_POR_DEFECTO_MS = 180_000;
18
+ let esperaMs = ESPERA_POR_DEFECTO_MS;
19
+ export function fijarEsperaDeConfirmacion(ms) {
20
+ esperaMs = ms ?? ESPERA_POR_DEFECTO_MS;
21
+ }
22
+ const sesiones = new WeakMap();
23
+ function sesionDe(mcp) {
24
+ let sesion = sesiones.get(mcp);
25
+ if (!sesion) {
26
+ sesion = { noDisponible: false, vencimientosSeguidos: 0 };
27
+ sesiones.set(mcp, sesion);
28
+ }
29
+ return sesion;
30
+ }
31
+ const NOTA_OFF = "Si tu cliente no te mostró la confirmación, regístralo con PHASE_GATE_ELICITATION=off y la confirmación " +
32
+ "volverá a pedirse en el chat.";
33
+ const NOTA_SIN_RESPUESTA = "Tu cliente no respondió a dos confirmaciones seguidas, así que esta vez tampoco se escribe nada; durante el " +
34
+ "resto de la sesión la confirmación vuelve a pedirse en el chat (con force).";
35
+ const NOTA_CANCELADA = "El cliente canceló la llamada antes de que el usuario respondiera.";
36
+ // Valores de PHASE_GATE_ELICITATION que apagan las confirmaciones directas.
37
+ const APAGADO = new Set(["off", "false", "0", "no"]);
38
+ const NOTA_NO_IMPLEMENTA = "Tu cliente anuncia confirmaciones directas pero no las implementa: durante el resto de la sesión la " +
39
+ "confirmación vuelve a pedirse en el chat (con force).";
40
+ export async function pedirConfirmacion(mcp, extra, pregunta) {
41
+ // La escotilla para un cliente cuya implementación falle.
42
+ if (APAGADO.has(leerVariable("ELICITATION").valor?.toLowerCase() ?? ""))
43
+ return { estado: "no-disponible" };
44
+ if (!mcp.server.getClientCapabilities()?.elicitation?.form)
45
+ return { estado: "no-disponible" };
46
+ const sesion = sesionDe(mcp);
47
+ if (sesion.noDisponible)
48
+ return { estado: "no-disponible" };
49
+ try {
50
+ const respuesta = await mcp.server.elicitInput({
51
+ mode: "form",
52
+ message: pregunta.mensaje,
53
+ requestedSchema: {
54
+ type: "object",
55
+ properties: {
56
+ confirmar: {
57
+ type: "boolean",
58
+ title: pregunta.etiqueta,
59
+ description: "Marca la casilla y acepta para confirmar.",
60
+ default: false,
61
+ },
62
+ },
63
+ required: ["confirmar"],
64
+ },
65
+ },
66
+ // Con la señal de la llamada: si el cliente cancela la tool (por su propio plazo, o porque la
67
+ // persona la detuvo), la pregunta se cancela también y no se escribe nada a sus espaldas.
68
+ { timeout: esperaMs, relatedRequestId: extra.requestId, signal: extra.signal });
69
+ sesion.vencimientosSeguidos = 0;
70
+ return { estado: respuesta.action === "accept" && respuesta.content?.confirmar === true ? "confirmada" : "rechazada" };
71
+ }
72
+ catch (error) {
73
+ const mensaje = error instanceof Error ? error.message : String(error);
74
+ // Una cancelación del cliente no dice nada de si muestra o no el formulario: no cuenta como
75
+ // vencimiento ni cambia la sesión.
76
+ if (extra.signal.aborted)
77
+ return { estado: "rechazada", nota: NOTA_CANCELADA };
78
+ if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) {
79
+ sesion.vencimientosSeguidos++;
80
+ if (sesion.vencimientosSeguidos >= 2) {
81
+ // Desde la siguiente llamada decide force; ESTA falla cerrada. Si no, una persona ausente
82
+ // dos veces más un force puesto por el modelo acabarían escribiendo sin que nadie confirmara.
83
+ sesion.noDisponible = true;
84
+ log("warning", "El cliente dejó vencer dos confirmaciones seguidas; se desactivan para el resto de la sesión.");
85
+ return { estado: "rechazada", nota: NOTA_SIN_RESPUESTA };
86
+ }
87
+ return { estado: "rechazada", nota: NOTA_OFF };
88
+ }
89
+ sesion.vencimientosSeguidos = 0;
90
+ if ((error instanceof McpError && error.code === ErrorCode.MethodNotFound) || /does not support/i.test(mensaje)) {
91
+ sesion.noDisponible = true;
92
+ log("warning", `El cliente anuncia elicitation pero no la implementa (${mensaje}); se desactiva para la sesión.`);
93
+ return { estado: "no-disponible", nota: NOTA_NO_IMPLEMENTA };
94
+ }
95
+ // Ante lo desconocido se falla cerrado: no se escribe nada y la persona puede repetirlo.
96
+ log("warning", `La confirmación falló y se trata como un no: ${mensaje}`);
97
+ return { estado: "rechazada" };
98
+ }
99
+ }
100
+ //# sourceMappingURL=confirmacion.js.map
@@ -0,0 +1,83 @@
1
+ import type { LecturaVariable } from "./config.js";
2
+ import type { PhaseFile } from "./phases.js";
3
+ export type Severidad = "error" | "aviso" | "info";
4
+ export type Hallazgo = {
5
+ severidad: Severidad;
6
+ codigo: string;
7
+ texto: string;
8
+ };
9
+ export type EstadoRuta = {
10
+ ruta?: string;
11
+ existe: boolean;
12
+ error?: string;
13
+ };
14
+ export type EtiquetaCapa = "fábrica" | "propia";
15
+ export type Via = "npx" | "global" | "dependencia" | "clon";
16
+ export type Diagnostico = {
17
+ paquete: {
18
+ version: string;
19
+ ruta: string;
20
+ via: Via;
21
+ };
22
+ node: {
23
+ version: string;
24
+ plataforma: NodeJS.Platform;
25
+ };
26
+ variables: LecturaVariable[];
27
+ rutas: {
28
+ docs: EstadoRuta;
29
+ repos: EstadoRuta;
30
+ };
31
+ estado: {
32
+ archivo: string;
33
+ existe: boolean;
34
+ punteros: number;
35
+ };
36
+ capas: {
37
+ ruta: string;
38
+ etiqueta: EtiquetaCapa;
39
+ existe: boolean;
40
+ }[];
41
+ reglasGlobales: {
42
+ archivo: string | null;
43
+ capa: string | null;
44
+ faltan: string[];
45
+ };
46
+ fases: {
47
+ numero: number;
48
+ titulo: string;
49
+ archivo: string;
50
+ capa: string;
51
+ documento?: string;
52
+ recibos?: string;
53
+ comando: string | null;
54
+ }[];
55
+ proyectos: {
56
+ nombre: string;
57
+ tareas: number;
58
+ activa: {
59
+ tarea: string;
60
+ origen: "puntero" | "deducida";
61
+ fase: string;
62
+ } | null;
63
+ }[];
64
+ cliente?: {
65
+ nombre: string;
66
+ version: string;
67
+ capacidades: {
68
+ elicitation: boolean;
69
+ roots: boolean;
70
+ };
71
+ };
72
+ hallazgos: Hallazgo[];
73
+ };
74
+ export declare const SECCIONES_GLOBALES: string[];
75
+ export declare function deducirVia(ruta: string, entorno?: NodeJS.ProcessEnv): Via;
76
+ export declare function capasDePrompts(): Diagnostico["capas"];
77
+ export declare function capaDeArchivo(archivo: string, capas: Diagnostico["capas"]): EtiquetaCapa | null;
78
+ export declare function fasesConCapa(capas?: Diagnostico["capas"], phases?: Map<number, PhaseFile>): Diagnostico["fases"];
79
+ export declare function diagnosticar(opciones?: {
80
+ cliente?: Diagnostico["cliente"];
81
+ }): Diagnostico;
82
+ export declare function formatearDiagnostico(d: Diagnostico, formato: "texto" | "markdown"): string;
83
+ //# sourceMappingURL=diagnostico.d.ts.map