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/index.js CHANGED
@@ -2,390 +2,53 @@
2
2
  // El shebang es lo que vuelve ejecutable este archivo como `bin` del paquete. Sin él,
3
3
  // `npx mcp-phase-gate` falla en Linux y macOS: el sistema no sabe con qué interpretarlo.
4
4
  //
5
- // Servidor MCP: define las tools y las conecta con la lógica de los demás módulos.
6
- // Aquí no se calcula nada — las rutas viven en paths.ts, las fases en phases.ts y las
7
- // tareas en tasks.ts. Este archivo solo traduce entre el protocolo y esas funciones.
8
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ // El punto de entrada: con argumentos, la línea de comandos (cli.ts); sin ellos, el servidor MCP
6
+ // (servidor.ts) conectado por stdio. Aquí no se define ninguna tool.
9
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
- import * as fs from "fs";
12
- import * as path from "path";
13
- import { DOCS, REPOS, SERVER_NAME, SERVER_VERSION, requireBasePath, stateFile } from "./config.js";
14
- import { describeProjects, discoverProjects, estaDentroReal, findProjectFolder, leerTextoAcotado, requireProjectFolder, resolveDocPath, resolveRepoPath, taskFolderPath, validarSegmento, } from "./paths.js";
15
- import { describePhases, discoverPhases, loadPhasePrompt, nombreContextoInicial, } from "./phases.js";
16
- import { avisoDeCompuerta, describeEstado, guardarPuntero, limpiarEstadoViejo, listarTareas, tareaActiva, } from "./tasks.js";
17
- import { validarLlamada } from "./schemas.js";
18
- const server = new Server({
19
- name: SERVER_NAME,
20
- version: SERVER_VERSION,
21
- }, {
22
- capabilities: {
23
- tools: {},
24
- },
25
- });
26
- // La lista de tools es estática, así que estas descripciones se arman una sola vez al
27
- // arrancar. La de proyectos puede quedarse vieja si creas una carpeta con el cliente abierto:
28
- // es orientativa, y la lista que manda es la que devuelven las tools al rechazar un proyecto.
29
- const PHASES_DESCRIPTION = describePhases();
30
- const PROJECTS_DESCRIPTION = describeProjects();
31
- // ─── Formato de las respuestas de tarea ─────────────────────────────────────
32
- function lineaDeTarea(estado, phases) {
33
- return `- ${estado.taskName} — ${describeEstado(estado, phases)}`;
34
- }
35
- server.setRequestHandler(ListToolsRequestSchema, async () => {
36
- return {
37
- tools: [
38
- {
39
- name: "start_task",
40
- description: "Inicia una tarea NUEVA creando su carpeta en la Documentación Central y guardando el contexto inicial. Para volver a una tarea que ya existe usa 'switch_task': start_task reescribiría su contexto inicial.",
41
- inputSchema: {
42
- type: "object",
43
- properties: {
44
- project: { type: "string", description: `Proyecto. Tiene que ser uno de los que ya existen: ${PROJECTS_DESCRIPTION}. Si no lo es, la tool te devuelve la lista actualizada.` },
45
- task_name: { type: "string", description: "Nombre de la tarea (creará una carpeta con este nombre)" },
46
- initial_context: { type: "string", description: "El contexto, correo o requerimiento inicial completo" },
47
- crear_proyecto: { type: "boolean", description: "Solo para ESTRENAR un proyecto que todavía no existe. Déjalo sin poner salvo que el usuario te haya dicho explícitamente que quiere crear un proyecto nuevo: sirve para que un nombre mal escrito no cree una carpeta suelta en la Documentación." }
48
- },
49
- required: ["project", "task_name", "initial_context"],
50
- },
51
- },
52
- {
53
- name: "get_phase_prompt",
54
- description: `Obtiene el prompt maestro de comportamiento según la fase. Fases disponibles — ${PHASES_DESCRIPTION}. Comprueba además que no te estés saltando una fase: si faltan documentos anteriores, el prompt llega con un aviso al principio.`,
55
- inputSchema: {
56
- type: "object",
57
- properties: {
58
- phase: { type: "number", description: `Número de la fase. Disponibles: ${PHASES_DESCRIPTION}.` },
59
- project: { type: "string", description: `Proyecto en el que estás trabajando: ${PROJECTS_DESCRIPTION}. Pásalo siempre que lo sepas —lo sabes por el repositorio que tienes abierto— para que se pueda comprobar que no te saltas una fase. La tarea se toma de la activa de ese proyecto.` },
60
- task_name: { type: "string", description: "Solo si quieres comprobar contra una tarea distinta de la activa del proyecto. Normalmente se omite." }
61
- },
62
- required: ["phase"],
63
- },
64
- },
65
- {
66
- name: "get_active_task",
67
- description: "Devuelve la tarea activa y en qué fase va, deducida de los documentos que ya tiene. Úsalo al iniciar un chat nuevo o si perdiste el contexto. Sin 'project' devuelve la activa de CADA proyecto.",
68
- inputSchema: {
69
- type: "object",
70
- properties: {
71
- project: { type: "string", description: `Proyecto del que quieres la tarea activa: ${PROJECTS_DESCRIPTION}. Si lo omites, se listan las de todos los proyectos.` }
72
- },
73
- },
74
- },
75
- {
76
- name: "list_tasks",
77
- description: "Lista las tareas que existen y la fase en la que va cada una. Úsalo para retomar una tarea vieja o para saber qué hay pendiente.",
78
- inputSchema: {
79
- type: "object",
80
- properties: {
81
- project: { type: "string", description: `Proyecto a listar: ${PROJECTS_DESCRIPTION}. Si lo omites, se listan todos.` }
82
- },
83
- },
84
- },
85
- {
86
- name: "switch_task",
87
- description: "Cambia cuál es la tarea activa de un proyecto, sin tocar ningún archivo. Es la forma correcta de retomar una tarea que ya existe.",
88
- inputSchema: {
89
- type: "object",
90
- properties: {
91
- project: { type: "string", description: `Proyecto de la tarea: ${PROJECTS_DESCRIPTION}.` },
92
- task_name: { type: "string", description: "Nombre de la carpeta de la tarea a activar. Si no existe, la tool te devuelve las que sí." }
93
- },
94
- required: ["project", "task_name"],
95
- },
96
- },
97
- {
98
- name: "read_central_doc",
99
- description: "Lee un archivo de la Documentación Central. RECOMENDADO: pasa 'project' + 'task_name' + 'file_name' y el servidor arma la ruta correcta automáticamente (<PROYECTO>/Proyectos/<tarea>/<archivo>). Alternativamente puedes pasar 'file_path' relativo a DOCUMENTACIÓN.",
100
- inputSchema: {
101
- type: "object",
102
- properties: {
103
- project: { type: "string", description: `Proyecto, uno de los existentes: ${PROJECTS_DESCRIPTION}. Úsalo junto con task_name y file_name.` },
104
- task_name: { type: "string", description: "Nombre de la carpeta de la tarea." },
105
- file_name: { type: "string", description: "Nombre (o ruta relativa) del archivo dentro de la carpeta de la tarea (ej. '01 - Análisis Técnico.md')." },
106
- file_path: { type: "string", description: "ALTERNATIVA: ruta relativa dentro de DOCUMENTACIÓN (ej. 'PROYECTO-A/Proyectos/tarea/01-analisis.md'). Solo si no usas project+task_name." }
107
- },
108
- },
109
- },
110
- {
111
- name: "write_central_doc",
112
- description: "Escribe o sobrescribe un archivo en la Documentación Central. RECOMENDADO: pasa 'project' + 'task_name' + 'file_name' y el servidor arma la ruta correcta automáticamente (<PROYECTO>/Proyectos/<tarea>/<archivo>). Alternativamente puedes pasar 'file_path' relativo a DOCUMENTACIÓN.",
113
- inputSchema: {
114
- type: "object",
115
- properties: {
116
- project: { type: "string", description: `Proyecto, uno de los existentes: ${PROJECTS_DESCRIPTION}. Úsalo junto con task_name y file_name.` },
117
- task_name: { type: "string", description: "Nombre de la carpeta de la tarea." },
118
- file_name: { type: "string", description: "Nombre (o ruta relativa) del archivo dentro de la carpeta de la tarea (ej. '01 - Análisis Técnico.md')." },
119
- file_path: { type: "string", description: "ALTERNATIVA: ruta relativa dentro de DOCUMENTACIÓN. Solo si no usas project+task_name." },
120
- content: { type: "string", description: "Contenido a escribir" }
121
- },
122
- required: ["content"],
123
- },
124
- },
125
- {
126
- name: "read_cross_repo",
127
- description: "Lee archivos de otros repositorios locales, dentro de la carpeta configurada en ORQUESTADOR_REPOS_PATH.",
128
- inputSchema: {
129
- type: "object",
130
- properties: {
131
- repo_name: { type: "string", description: "Nombre del repositorio (ej. 'repo-a', 'repo-b')" },
132
- file_path: { type: "string", description: "Ruta relativa del archivo dentro del repositorio (ej. 'src/main.ts')" }
133
- },
134
- required: ["repo_name", "file_path"],
135
- },
136
- }
137
- ],
138
- };
139
- });
140
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
141
- const { name, arguments: args } = request.params;
142
- try {
143
- // Todo argumento se valida aquí, antes de que ninguna tool lo use. Un campo ausente o de
144
- // otro tipo devuelve un error que lo nombra, en vez de convertirse en "undefined".
145
- const llamada = validarLlamada(name, args);
146
- if (llamada.tool === "start_task") {
147
- const proj = llamada.args.project; // el schema ya le quitó los espacios
148
- const taskName = llamada.args.task_name;
149
- const context = llamada.args.initial_context;
150
- // Estrenar proyecto tiene que ser deliberado: sin esta puerta, un nombre mal escrito
151
- // creaba una carpeta suelta en la Documentación y nadie se enteraba hasta buscarla.
152
- let carpetaProyecto = findProjectFolder(proj);
153
- let estrenado = false;
154
- if (!carpetaProyecto) {
155
- if (llamada.args.crear_proyecto !== true) {
156
- throw new Error(`No existe el proyecto "${proj}". Los proyectos disponibles son: ${describeProjects()}. ` +
157
- `Si el usuario te confirmó que quiere estrenar el proyecto "${proj.toUpperCase()}", ` +
158
- `vuelve a llamar a start_task con crear_proyecto: true.`);
159
- }
160
- // El nombre se vuelve carpeta tal cual: "../evil" la creaba fuera de la Documentación.
161
- validarSegmento(proj, "project");
162
- carpetaProyecto = proj.toUpperCase();
163
- estrenado = true;
164
- }
165
- const carpetaTarea = taskFolderPath(carpetaProyecto, taskName);
166
- const contextoInicial = path.join(carpetaTarea, nombreContextoInicial());
167
- const docsBase = requireBasePath(DOCS);
168
- if (!estaDentroReal(docsBase, carpetaTarea)) {
169
- throw new Error(`Acceso denegado: la carpeta de la tarea queda fuera de ${docsBase}.`);
170
- }
171
- // start_task escribe el contexto inicial sin preguntar, así que llamarlo sobre una tarea
172
- // que ya existe borraba el requerimiento original — el único documento que no se puede
173
- // regenerar leyendo el código. Retomar una tarea es switch_task, que no toca archivos.
174
- if (fs.existsSync(contextoInicial)) {
175
- throw new Error(`La tarea "${taskName}" ya existe en ${carpetaProyecto} y start_task reescribiría su ` +
176
- `contexto inicial. Si quieres retomarla, usa switch_task. Si de verdad quieres ` +
177
- `empezar de cero, borra o renombra la carpeta primero: ${carpetaTarea}`);
178
- }
179
- if (!fs.existsSync(carpetaTarea)) {
180
- fs.mkdirSync(carpetaTarea, { recursive: true });
181
- }
182
- fs.writeFileSync(contextoInicial, context, "utf-8");
183
- // Queda como activa de SU proyecto. Otra ventana trabajando en otro proyecto conserva
184
- // la suya: los punteros son independientes.
185
- guardarPuntero(carpetaProyecto, taskName);
186
- const aviso = estrenado ? `\nProyecto "${carpetaProyecto}" estrenado.` : "";
187
- return {
188
- content: [{ type: "text", text: `Tarea '${taskName}' inicializada correctamente. Archivo guardado en: ${contextoInicial}\nTarea activa de ${carpetaProyecto}: ${taskName}.${aviso}` }],
189
- };
190
- }
191
- if (llamada.tool === "get_phase_prompt") {
192
- // Que sea un entero ya lo garantiza el schema; si la fase no existe, lo dice loadPhasePrompt.
193
- const { phase, project, task_name } = llamada.args;
194
- const prompt = loadPhasePrompt(phase);
195
- // La compuerta avisa, no bloquea: el prompt se entrega siempre. Si comprobarla fallara
196
- // por lo que sea, entregarlo igual es mejor que dejar al usuario sin fase por un aviso.
197
- let aviso = null;
198
- try {
199
- aviso = avisoDeCompuerta(phase, discoverPhases(), {
200
- ...(project ? { project } : {}),
201
- ...(task_name ? { taskName: task_name } : {}),
202
- });
203
- }
204
- catch (error) {
205
- console.error(`[orquestador] No se pudo comprobar la compuerta: ${error.message}`);
206
- }
207
- return {
208
- content: [{ type: "text", text: aviso ? `${aviso}\n\n${prompt}` : prompt }],
209
- };
210
- }
211
- if (llamada.tool === "get_active_task") {
212
- const phases = discoverPhases();
213
- const pedido = llamada.args.project ?? "";
214
- // Con proyecto: la suya. Sin proyecto: la de cada uno. Antes había un único puntero
215
- // global y responder "la activa" a secas obligaba a adivinar cuál de dos ventanas
216
- // preguntaba; listar todas es la respuesta honesta cuando no se sabe.
217
- const proyectos = pedido
218
- ? [requireProjectFolder(pedido)]
219
- : [...discoverProjects().values()].sort();
220
- if (proyectos.length === 0) {
221
- return { content: [{ type: "text", text: `No hay ningún proyecto en la Documentación Central todavía.` }] };
222
- }
223
- const bloques = [];
224
- for (const carpetaProyecto of proyectos) {
225
- const activa = tareaActiva(carpetaProyecto, phases);
226
- if (!activa) {
227
- bloques.push(`${carpetaProyecto}: sin tareas todavía.`);
228
- continue;
229
- }
230
- const nota = activa.origen === "deducida"
231
- ? " (deducida por ser la más reciente: no había puntero guardado, confírmalo antes de escribir)"
232
- : "";
233
- bloques.push(`${carpetaProyecto}: ${activa.estado.taskName}${nota}\n` +
234
- ` Fase: ${describeEstado(activa.estado, phases)}\n` +
235
- ` Carpeta: ${activa.estado.folderPath}`);
236
- }
237
- return {
238
- content: [{ type: "text", text: `${bloques.join("\n\n")}\n\nPunteros guardados en: ${stateFile()}` }],
239
- };
240
- }
241
- if (llamada.tool === "list_tasks") {
242
- const phases = discoverPhases();
243
- const pedido = llamada.args.project ?? "";
244
- const proyectos = pedido
245
- ? [requireProjectFolder(pedido)]
246
- : [...discoverProjects().values()].sort();
247
- const bloques = [];
248
- for (const carpetaProyecto of proyectos) {
249
- const tareas = listarTareas(carpetaProyecto, phases);
250
- const activa = tareaActiva(carpetaProyecto, phases);
251
- if (tareas.length === 0) {
252
- bloques.push(`${carpetaProyecto}: sin tareas.`);
253
- continue;
254
- }
255
- const lineas = tareas.map((tarea) => {
256
- const marca = tarea.taskName === activa?.estado.taskName ? " [activa]" : "";
257
- return `${lineaDeTarea(tarea, phases)}${marca}`;
258
- });
259
- bloques.push(`${carpetaProyecto} (${tareas.length}), de la más reciente a la más antigua:\n${lineas.join("\n")}`);
260
- }
261
- return {
262
- content: [{ type: "text", text: bloques.join("\n\n") || "No hay proyectos todavía." }],
263
- };
264
- }
265
- if (llamada.tool === "switch_task") {
266
- const phases = discoverPhases();
267
- const carpetaProyecto = requireProjectFolder(llamada.args.project);
268
- const taskName = llamada.args.task_name;
269
- const tareas = listarTareas(carpetaProyecto, phases);
270
- const encontrada = tareas.find((tarea) => tarea.taskName === taskName);
271
- if (!encontrada) {
272
- const disponibles = tareas.length
273
- ? tareas.map((tarea) => `"${tarea.taskName}"`).join(", ")
274
- : "(ninguna)";
275
- throw new Error(`No existe la tarea "${taskName}" en ${carpetaProyecto}. Las que hay son: ${disponibles}. ` +
276
- `Si es una tarea nueva, créala con start_task.`);
277
- }
278
- guardarPuntero(carpetaProyecto, taskName);
279
- return {
280
- content: [{ type: "text", text: `Tarea activa de ${carpetaProyecto}: ${taskName}.\nFase: ${describeEstado(encontrada, phases)}\nCarpeta: ${encontrada.folderPath}` }],
281
- };
282
- }
283
- if (llamada.tool === "read_central_doc") {
284
- const docsBase = requireBasePath(DOCS);
285
- const fullPath = resolveDocPath(llamada.args);
286
- if (!estaDentroReal(docsBase, fullPath)) {
287
- throw new Error(`Acceso denegado: la ruta queda fuera de ${docsBase}.`);
288
- }
289
- const content = leerTextoAcotado(fullPath);
290
- return {
291
- content: [{ type: "text", text: content }],
292
- };
293
- }
294
- if (llamada.tool === "write_central_doc") {
295
- const content = llamada.args.content;
296
- const docsBase = requireBasePath(DOCS);
297
- const fullPath = resolveDocPath(llamada.args);
298
- if (!estaDentroReal(docsBase, fullPath)) {
299
- throw new Error(`Acceso denegado: la ruta queda fuera de ${docsBase}.`);
300
- }
301
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
302
- fs.writeFileSync(fullPath, content, "utf-8");
303
- return {
304
- content: [{ type: "text", text: `Archivo escrito exitosamente en ${fullPath}` }],
305
- };
306
- }
307
- if (llamada.tool === "read_cross_repo") {
308
- const fullPath = resolveRepoPath(llamada.args.repo_name, llamada.args.file_path);
309
- const content = leerTextoAcotado(fullPath);
310
- return {
311
- content: [{ type: "text", text: content }],
312
- };
313
- }
314
- throw new Error(`Unknown tool: ${name}`);
315
- }
316
- catch (error) {
317
- return {
318
- content: [{ type: "text", text: `Error ejecutando tool ${name}: ${error.message}` }],
319
- isError: true,
320
- };
321
- }
322
- });
8
+ import { DOCS, NOMBRES_LOGICOS, REPOS, SERVER_NAME, errorDeEnv, leerVariable, requireBasePath } from "./config.js";
9
+ import { ejecutarCli } from "./cli.js";
10
+ import { log } from "./log.js";
11
+ import { crearServidor } from "./servidor.js";
12
+ import { limpiarEstadoViejo } from "./tasks.js";
323
13
  // Deja en el log el estado de cada ruta al arrancar. No aborta: el servidor sigue en pie y
324
14
  // el error real se entrega al llamar la tool. Esto es solo para que, cuando alguien acabe
325
15
  // abriendo el log, el motivo ya esté ahí escrito.
326
16
  function reportarRutas() {
327
17
  for (const requerida of [DOCS, REPOS]) {
328
18
  try {
329
- console.error(`[orquestador] ${requerida.variable} = ${requireBasePath(requerida)}`);
19
+ const ruta = requireBasePath(requerida);
20
+ log("info", `${leerVariable(requerida.logica).usada} = ${ruta}`);
330
21
  }
331
22
  catch (error) {
332
- console.error(`[orquestador] SIN CONFIGURAR: ${error.message}`);
23
+ log("error", `SIN CONFIGURAR: ${error instanceof Error ? error.message : String(error)}`);
333
24
  }
334
25
  }
335
- }
336
- // Un servidor MCP habla JSON-RPC por stdin y no imprime nada por stdout. Ejecutado a mano en
337
- // una terminal —que es exactamente lo que hace quien acaba de leer `npx mcp-phase-gate` en el
338
- // README— se queda esperando entrada, sin señal de vida, y parece roto. Estas dos banderas
339
- // existen para que ese momento devuelva una explicación en vez de un cursor parpadeando.
340
- const AYUDA = `${SERVER_NAME} v${SERVER_VERSION}
341
-
342
- Servidor MCP que parte cada requerimiento en cinco fases con compuerta y obliga a que cada
343
- una deje su documento en una documentación central.
344
-
345
- Esto NO se ejecuta a mano: lo lanza tu cliente MCP. Ejecutado directamente se queda esperando
346
- mensajes JSON-RPC por stdin, que es lo que parece que se colgó.
347
-
348
- Regístralo en tu cliente (Cursor, VS Code, Claude Code) así:
349
-
350
- {
351
- "mcpServers": {
352
- "${SERVER_NAME}": {
353
- "command": "npx",
354
- "args": ["-y", "mcp-phase-gate"],
355
- "env": {
356
- "ORQUESTADOR_DOCS_PATH": "<tu carpeta de documentación>",
357
- "ORQUESTADOR_REPOS_PATH": "<tu carpeta de repositorios>"
358
- }
359
- }
26
+ // Las dos variables de un mismo nombre lógico, con valores distintos: se avisa una vez.
27
+ for (const logica of NOMBRES_LOGICOS) {
28
+ const { discrepancia } = leerVariable(logica);
29
+ if (discrepancia)
30
+ log("warning", discrepancia);
360
31
  }
361
- }
362
-
363
- Variables de entorno:
364
- ORQUESTADOR_DOCS_PATH Obligatoria. Raíz de la Documentación Central.
365
- ORQUESTADOR_REPOS_PATH Obligatoria. Carpeta con tus repositorios de código.
366
- ORQUESTADOR_TASKS_SUBDIR Subcarpeta de tareas dentro de cada proyecto. Por defecto "Proyectos".
367
- ORQUESTADOR_PROMPTS_PATH Carpeta de prompts propios, que se superponen a los del paquete.
368
- ORQUESTADOR_STATE_PATH Dónde guardar los punteros de tarea activa.
369
-
370
- Documentación: https://github.com/Montse2308/mcp-phase-gate`;
32
+ const delEnv = errorDeEnv();
33
+ if (delEnv)
34
+ log("error", delEnv);
35
+ }
371
36
  async function main() {
372
- const bandera = process.argv[2];
373
- if (bandera === "--version" || bandera === "-v") {
374
- console.log(SERVER_VERSION);
375
- return;
376
- }
377
- if (bandera === "--help" || bandera === "-h") {
378
- console.log(AYUDA);
37
+ // Con argumentos, el binario es la herramienta de la persona (ayuda, versión, doctor); sin
38
+ // ellos, el servidor que lanza el cliente. exitCode y no exit(): exit cortaría la salida de
39
+ // doctor a medio escribir cuando stdout es una tubería.
40
+ const resultado = ejecutarCli(process.argv.slice(2));
41
+ if (resultado.accion === "salir") {
42
+ process.exitCode = resultado.codigo;
379
43
  return;
380
44
  }
381
45
  limpiarEstadoViejo();
382
46
  reportarRutas();
383
- const transport = new StdioServerTransport();
384
- await server.connect(transport);
385
- console.error(`${SERVER_NAME} MCP server running on stdio`);
47
+ await crearServidor().connect(new StdioServerTransport());
48
+ log("info", `${SERVER_NAME} MCP server running on stdio`);
386
49
  }
387
50
  main().catch((error) => {
388
- console.error("Server error:", error);
51
+ log("error", `Server error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
389
52
  process.exit(1);
390
53
  });
391
54
  //# sourceMappingURL=index.js.map
package/build/log.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
3
+ export declare const PREFIJO_LOG = "[mcp-phase-gate]";
4
+ export declare function conectarLog(mcp: McpServer | null): void;
5
+ export declare function log(nivel: LoggingLevel, texto: string): void;
6
+ //# sourceMappingURL=log.d.ts.map
package/build/log.js ADDED
@@ -0,0 +1,27 @@
1
+ // El log del servidor. Va siempre a stderr, que es lo que guardan los clientes en su archivo de
2
+ // log, y además, cuando hay un cliente conectado, como notificación MCP (`notifications/message`),
3
+ // que algunos clientes muestran en su panel. stdout nunca: es el canal del JSON-RPC.
4
+ //
5
+ // Vive en su propio módulo y no en servidor.ts porque lo usan también phases.ts y tasks.ts, que
6
+ // servidor.ts importa: ponerlo ahí haría un import circular.
7
+ import { SERVER_NAME } from "./config.js";
8
+ export const PREFIJO_LOG = `[${SERVER_NAME}]`;
9
+ // Un solo servidor por proceso, que es lo que hay en producción. Si se crean varios (los tests),
10
+ // las notificaciones van al último que se creó; stderr sigue recibiendo todo.
11
+ let conectado = null;
12
+ // Lo llama crearServidor. Desde ese momento, si el servidor está conectado, cada línea del log
13
+ // también sale como notificación.
14
+ export function conectarLog(mcp) {
15
+ conectado = mcp;
16
+ }
17
+ export function log(nivel, texto) {
18
+ console.error(`${PREFIJO_LOG} ${texto}`);
19
+ // Solo después del initialize: antes, el cliente no espera notificaciones, y en una terminal la
20
+ // línea JSON aparecía cruda en stdout al arrancar.
21
+ if (!conectado?.isConnected() || !conectado.server.getClientVersion())
22
+ return;
23
+ // El SDK descarta solo lo que esté por debajo del nivel que pidió el cliente. Un fallo al
24
+ // notificar no puede tumbar la operación que estaba registrando: ya quedó en stderr.
25
+ conectado.server.sendLoggingMessage({ level: nivel, logger: SERVER_NAME, data: texto }).catch(() => { });
26
+ }
27
+ //# sourceMappingURL=log.js.map
package/build/paths.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export declare function estaDentro(base: string, ruta: string): boolean;
2
2
  export declare function rutaReal(ruta: string): string;
3
3
  export declare function estaDentroReal(base: string, ruta: string): boolean;
4
+ export declare function claveDeNombre(nombre: string): string;
4
5
  export declare function validarSegmento(valor: string, campo: string): string;
6
+ export declare function validarNombreVisible(valor: string, campo: string): string;
5
7
  export declare function discoverProjects(): Map<string, string>;
6
8
  export declare function describeProjects(): string;
7
9
  export declare function findProjectFolder(project: string): string | null;
@@ -16,6 +18,13 @@ export type ArgumentosDeDocumento = {
16
18
  file_path?: string | undefined;
17
19
  };
18
20
  export declare function resolveDocPath(args: ArgumentosDeDocumento): string;
21
+ export type UbicacionEnTarea = {
22
+ carpetaProyecto: string;
23
+ taskName: string;
24
+ carpetaTarea: string;
25
+ relativo: string;
26
+ };
27
+ export declare function ubicarEnTarea(rutaAbsoluta: string): UbicacionEnTarea | null;
19
28
  export declare function resolveRepoPath(repoName: string, filePath: string): string;
20
29
  export declare const LIMITE_LECTURA_BYTES = 1048576;
21
30
  export declare function leerTextoAcotado(ruta: string): string;
package/build/paths.js CHANGED
@@ -38,6 +38,13 @@ export function rutaReal(ruta) {
38
38
  export function estaDentroReal(base, ruta) {
39
39
  return estaDentro(rutaReal(base), rutaReal(ruta));
40
40
  }
41
+ // Clave para comparar nombres de documento: dos nombres con la misma clave se ven iguales, y en
42
+ // Windows y macOS abren el mismo archivo. rutaReal solo corrige las mayúsculas de lo que ya
43
+ // existe, así que lo que todavía no existe se compara por aquí. Sin distinguir mayúsculas en
44
+ // todos los sistemas, para que el comportamiento no dependa del equipo.
45
+ export function claveDeNombre(nombre) {
46
+ return nombre.normalize("NFC").toLowerCase();
47
+ }
41
48
  // Un nombre de proyecto, tarea o repo es UN segmento de ruta, portable entre sistemas:
42
49
  // la Documentación viaja entre equipos con Windows, macOS y Linux. Lo que se rechaza es lo
43
50
  // que hoy ya era peligroso (separadores, "..") o lo que Windows no puede guardar y rompe la
@@ -69,6 +76,17 @@ export function validarSegmento(valor, campo) {
69
76
  }
70
77
  return valor;
71
78
  }
79
+ // Un proyecto o una tarea que start_task va a crear tampoco puede empezar por punto: las carpetas
80
+ // con punto no son proyecto ni tarea, así que no aparecería en list_tasks y write_central_doc no
81
+ // podría escribir en ella. No va en validarSegmento porque read_central_doc también lo usa, y la
82
+ // lectura no cambia.
83
+ export function validarNombreVisible(valor, campo) {
84
+ if (valor.startsWith(".")) {
85
+ throw new Error(`${campo} ${JSON.stringify(valor)} no es válido: no puede empezar por punto. Las carpetas ` +
86
+ `con punto no cuentan como proyecto ni como tarea, así que no se vería después.`);
87
+ }
88
+ return valor;
89
+ }
72
90
  // ─── Proyectos ──────────────────────────────────────────────────────────────
73
91
  // Los proyectos no se declaran en ninguna parte: son las carpetas que existen. Antes había
74
92
  // una tabla fija con los proyectos de quien escribió el código, que no restringía nada
@@ -78,12 +96,13 @@ export function validarSegmento(valor, campo) {
78
96
  // Es proyecto la carpeta que contenga la subcarpeta de tareas. Esa regla deja fuera sola a
79
97
  // las carpetas que no son proyectos, y da de alta uno nuevo con solo crearle su subcarpeta.
80
98
  // Si la subcarpeta está vacía la regla no se puede aplicar y vale cualquier carpeta.
99
+ // Las carpetas con punto nunca son proyecto: .obsidian o .git en la raíz de la Documentación.
81
100
  export function discoverProjects() {
82
101
  const proyectos = new Map(); // clave en minúsculas -> nombre real de la carpeta
83
102
  const base = requireBasePath(DOCS);
84
103
  const subdir = tasksSubdir();
85
104
  for (const entrada of fs.readdirSync(base, { withFileTypes: true })) {
86
- if (!entrada.isDirectory())
105
+ if (!entrada.isDirectory() || entrada.name.startsWith("."))
87
106
  continue;
88
107
  if (subdir && !fs.existsSync(path.join(base, entrada.name, subdir)))
89
108
  continue;
@@ -97,8 +116,8 @@ export function describeProjects() {
97
116
  return nombres.length ? nombres.join(", ") : "(ninguno todavía)";
98
117
  }
99
118
  catch {
100
- // Sin ORQUESTADOR_DOCS_PATH configurada no se pueden listar; el error real lo da la tool.
101
- return "(no se pudieron leer: revisa ORQUESTADOR_DOCS_PATH)";
119
+ // Sin DOCS_PATH configurada no se pueden listar; el error real lo da la tool.
120
+ return "(no se pudieron leer: revisa PHASE_GATE_DOCS_PATH)";
102
121
  }
103
122
  }
104
123
  // Devuelve el nombre real de la carpeta, respetando mayúsculas, o null si no existe.
@@ -114,7 +133,7 @@ export function requireProjectFolder(project) {
114
133
  }
115
134
  // ─── Carpetas de tarea ──────────────────────────────────────────────────────
116
135
  // La ruta de una tarea SIEMPRE se arma aquí, en un solo lugar.
117
- // Estructura: <ORQUESTADOR_DOCS_PATH>/<PROYECTO>/<subcarpeta de tareas>/<tarea>
136
+ // Estructura: <PHASE_GATE_DOCS_PATH>/<PROYECTO>/<subcarpeta de tareas>/<tarea>
118
137
  export function taskFolderPath(carpetaProyecto, taskName) {
119
138
  validarSegmento(taskName, "task_name");
120
139
  return path.join(requireBasePath(DOCS), carpetaProyecto, tasksSubdir(), taskName);
@@ -143,10 +162,10 @@ function validarRutaRelativa(valor, campo, relativaA) {
143
162
  // la base: antes "../otra-tarea/x.md" escribía en otra tarea y la comprobación de la tool,
144
163
  // que solo miraba la base, lo dejaba pasar.
145
164
  //
146
- // Con file_path basta con no salir de la base (la restricción a carpetas de tarea llega en la
147
- // Etapa 1), pero sus segmentos también tienen que ser portables: antes creaba dentro de la
148
- // Documentación un "CON", un "nul.md" o un flujo alterno "nota.md:secreto", que el Explorador
149
- // y OneDrive no saben manejar.
165
+ // Con file_path aquí basta con no salir de la base (write_central_doc además exige que caiga en
166
+ // una tarea: ubicarEnTarea), pero sus segmentos también tienen que ser portables: antes creaba
167
+ // dentro de la Documentación un "CON", un "nul.md" o un flujo alterno "nota.md:secreto", que el
168
+ // Explorador y OneDrive no saben manejar.
150
169
  export function resolveDocPath(args) {
151
170
  const { project = "", task_name: taskName = "", file_name: fileName = "", file_path: filePath = "" } = args;
152
171
  if (project && taskName && fileName) {
@@ -169,6 +188,50 @@ export function resolveDocPath(args) {
169
188
  }
170
189
  throw new Error("Debes proporcionar (project + task_name + file_name) o bien file_path.");
171
190
  }
191
+ // Segmentos de `ruta` dentro de `base`, o null si no cae dentro. Las dos ya vienen resueltas.
192
+ function segmentosDentro(base, ruta) {
193
+ const relativa = path.relative(base, ruta);
194
+ if (relativa === "")
195
+ return [];
196
+ if (relativa.startsWith("..") || path.isAbsolute(relativa))
197
+ return null;
198
+ return relativa.split(/[\\/]/);
199
+ }
200
+ // write_central_doc solo escribe dentro de la carpeta de una tarea que ya existe. Con file_path
201
+ // se podía escribir en cualquier lugar de la Documentación, incluido el phase-gate.json del
202
+ // proyecto, y con una tarea mal escrita el mkdir recursivo creaba una carpeta suelta.
203
+ //
204
+ // Se mide sobre rutas reales, como la contención: un enlace interno que lleva a una tarea vale,
205
+ // porque lo que se escribe acaba dentro de ella. La subcarpeta de tareas se compara con la que
206
+ // hay en disco y no con la variable: en Windows y macOS pueden diferir en mayúsculas, y
207
+ // discoverProjects ya da el proyecto por bueno.
208
+ //
209
+ // Devuelve null si la ruta no cae dentro de la carpeta de una tarea existente, o si algún
210
+ // segmento dentro de la tarea empieza por punto (.git, .phase-gate no son documentos).
211
+ export function ubicarEnTarea(rutaAbsoluta) {
212
+ const docsReal = rutaReal(requireBasePath(DOCS));
213
+ const segmentos = segmentosDentro(docsReal, rutaReal(rutaAbsoluta));
214
+ if (!segmentos || segmentos.length === 0)
215
+ return null;
216
+ // El nombre que da el disco tiene que ser el del proyecto, no solo parecerse en mayúsculas.
217
+ const [proyecto = "", ...resto] = segmentos;
218
+ const carpetaProyecto = findProjectFolder(proyecto);
219
+ if (carpetaProyecto !== proyecto)
220
+ return null;
221
+ const raizProyecto = path.join(docsReal, carpetaProyecto);
222
+ const subdir = segmentosDentro(raizProyecto, rutaReal(tasksRootPath(carpetaProyecto)));
223
+ if (!subdir || subdir.some((segmento, i) => resto[i] !== segmento))
224
+ return null;
225
+ const [taskName, ...archivo] = resto.slice(subdir.length);
226
+ if (!taskName || taskName.startsWith(".") || archivo.length === 0)
227
+ return null;
228
+ if (archivo.some((segmento) => segmento.startsWith(".")))
229
+ return null;
230
+ const carpetaTarea = path.join(raizProyecto, ...subdir, taskName);
231
+ if (!fs.statSync(carpetaTarea, { throwIfNoEntry: false })?.isDirectory())
232
+ return null;
233
+ return { carpetaProyecto, taskName, carpetaTarea, relativo: archivo.join("/") };
234
+ }
172
235
  // ─── Repos ──────────────────────────────────────────────────────────────────
173
236
  // Ruta de un archivo dentro de UN repo. Antes la comprobación miraba la carpeta de repos
174
237
  // entera, y desde repoA "../repoB/.env" pasaba: estaba dentro de la carpeta de repos. El repo
package/build/phases.d.ts CHANGED
@@ -1,19 +1,31 @@
1
+ export declare const GLOBAL_RULES_FILE = "global-rules.md";
2
+ export declare const CARPETA_COMANDOS = "comandos";
3
+ export declare function resolveCommandFile(numero: number, capas?: string[]): string | null;
4
+ export declare function esCarpeta(ruta: string): boolean;
1
5
  export declare const PHASE_FILE_PATTERN: RegExp;
2
6
  export type PhaseFile = {
3
7
  file: string;
4
8
  title: string;
5
9
  documento?: string | undefined;
6
10
  documentoInicial?: string | undefined;
11
+ recibos?: string | undefined;
7
12
  };
13
+ export declare const MARCADOR_RANURA: RegExp;
14
+ export declare function quitarMarcadores(texto: string): string;
8
15
  export declare function parsePhaseFile(fullPath: string): {
9
16
  meta: Record<string, string>;
10
17
  cuerpo: string;
11
18
  };
19
+ export declare function separarFrontmatter(texto: string): {
20
+ meta: Record<string, string>;
21
+ cuerpo: string;
22
+ };
12
23
  export declare function discoverPhases(capas?: string[]): Map<number, PhaseFile>;
13
24
  export declare function readPhaseTitle(cuerpo: string, fileName: string): string;
14
25
  export declare const CONTEXTO_INICIAL_POR_DEFECTO = "00 - Contexto Inicial.md";
15
26
  export declare function nombreContextoInicial(capas?: string[]): string;
16
27
  export declare function loadPhasePrompt(phase: number, capas?: string[]): string;
28
+ export declare function resolveGlobalRules(capas?: string[]): string | null;
29
+ export declare function leerPromptNormalizado(ruta: string): string;
17
30
  export declare function describePhaseFiles(available: Map<number, PhaseFile>, phase: number): string;
18
- export declare function describePhases(): string;
19
31
  //# sourceMappingURL=phases.d.ts.map