xonecode 0.9.0 → 0.12.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.
Files changed (120) hide show
  1. package/THIRD_PARTY_NOTICES.md +56 -0
  2. package/apps/web/dist/assets/index-BCO9A9PF.js +485 -0
  3. package/apps/web/dist/assets/index-CBbdNsze.css +1 -0
  4. package/apps/web/dist/assets/{php-DjRQ1K-w.js → php-DOl4aWua.js} +1 -1
  5. package/apps/web/dist/assets/{ruby-BcLyZ3qi.js → ruby-DFYuLGa1.js} +1 -1
  6. package/apps/web/dist/iconos/conectores/deepwiki.png +0 -0
  7. package/apps/web/dist/iconos/conectores/jira.png +0 -0
  8. package/apps/web/dist/index.html +2 -2
  9. package/apps/web/dist/openui/visor.css +1 -0
  10. package/apps/web/dist/openui/visor.js +9113 -0
  11. package/dist/agent/conectores/conectoresEnDisco.js +188 -0
  12. package/dist/agent/conectores/conectoresEnDisco.js.map +1 -0
  13. package/dist/agent/conectores/proveedorDeConector.js +56 -0
  14. package/dist/agent/conectores/proveedorDeConector.js.map +1 -0
  15. package/dist/agent/conectores/servicioDeConectores.js +496 -0
  16. package/dist/agent/conectores/servicioDeConectores.js.map +1 -0
  17. package/dist/agent/config/catalogoModelos.js +2 -0
  18. package/dist/agent/config/catalogoModelos.js.map +1 -1
  19. package/dist/agent/config/modelos.js +27 -0
  20. package/dist/agent/config/modelos.js.map +1 -1
  21. package/dist/agent/exportarPdf.js +5 -1
  22. package/dist/agent/exportarPdf.js.map +1 -1
  23. package/dist/agent/grafo/arbolDeProyecto.js +66 -1
  24. package/dist/agent/grafo/arbolDeProyecto.js.map +1 -1
  25. package/dist/agent/grafo/memoriaDeProyecto.js +3 -2
  26. package/dist/agent/grafo/memoriaDeProyecto.js.map +1 -1
  27. package/dist/agent/grafo/navegacionXone.js +8 -3
  28. package/dist/agent/grafo/navegacionXone.js.map +1 -1
  29. package/dist/agent/grafo/proyecto.js +31 -15
  30. package/dist/agent/grafo/proyecto.js.map +1 -1
  31. package/dist/agent/grafo/unirSecciones.js +128 -0
  32. package/dist/agent/grafo/unirSecciones.js.map +1 -0
  33. package/dist/agent/grafo/xoneAgent.js +7 -0
  34. package/dist/agent/grafo/xoneAgent.js.map +1 -1
  35. package/dist/agent/motores/trueforge/capacidades.js +42 -1
  36. package/dist/agent/motores/trueforge/capacidades.js.map +1 -1
  37. package/dist/agent/motores/trueforge/eventosTrueforge.js +52 -7
  38. package/dist/agent/motores/trueforge/eventosTrueforge.js.map +1 -1
  39. package/dist/agent/motores/trueforge/modeloLangchain.js +5 -2
  40. package/dist/agent/motores/trueforge/modeloLangchain.js.map +1 -1
  41. package/dist/agent/motores/trueforge/sesionTrueforge.js +19 -2
  42. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  43. package/dist/agent/motores/trueforge/toolsDeFichero.js +3 -2
  44. package/dist/agent/motores/trueforge/toolsDeFichero.js.map +1 -1
  45. package/dist/agent/motores/trueforge/trueforge.js +1 -1
  46. package/dist/agent/motores/trueforge/trueforge.js.map +1 -1
  47. package/dist/agent/navegacion/indiceEnDisco.js +10 -1
  48. package/dist/agent/navegacion/indiceEnDisco.js.map +1 -1
  49. package/dist/agent/planesEnDisco.js +64 -0
  50. package/dist/agent/planesEnDisco.js.map +1 -0
  51. package/dist/agent/sesiones/modeloDelCambio.js +70 -0
  52. package/dist/agent/sesiones/modeloDelCambio.js.map +1 -0
  53. package/dist/agent/sesiones/sesionGit.js +14 -0
  54. package/dist/agent/sesiones/sesionGit.js.map +1 -1
  55. package/dist/agent/subagentes/agentesEnDisco.js +95 -5
  56. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  57. package/dist/agent/subagentes/escrituraExterna.js +8 -2
  58. package/dist/agent/subagentes/escrituraExterna.js.map +1 -1
  59. package/dist/agent/subagentes/subagenteCodex.js +1 -1
  60. package/dist/agent/subagentes/subagenteCodex.js.map +1 -1
  61. package/dist/agent/subagentes/subagenteExterno.js +1 -1
  62. package/dist/agent/subagentes/subagenteExterno.js.map +1 -1
  63. package/dist/agent/subagentes/subagenteOpencode.js +1 -1
  64. package/dist/agent/subagentes/subagenteOpencode.js.map +1 -1
  65. package/dist/agent/turno/puente.js +17 -1
  66. package/dist/agent/turno/puente.js.map +1 -1
  67. package/dist/agent/turno/resumenDeContexto.js +17 -4
  68. package/dist/agent/turno/resumenDeContexto.js.map +1 -1
  69. package/dist/agent/turno/turnoReal.js +16 -2
  70. package/dist/agent/turno/turnoReal.js.map +1 -1
  71. package/dist/cli/tui/transcript.js +4 -0
  72. package/dist/cli/tui/transcript.js.map +1 -1
  73. package/dist/core/actos.js.map +1 -1
  74. package/dist/core/agentes.js +17 -0
  75. package/dist/core/agentes.js.map +1 -1
  76. package/dist/core/artefactos.js +22 -0
  77. package/dist/core/artefactos.js.map +1 -1
  78. package/dist/core/conectores.js +191 -0
  79. package/dist/core/conectores.js.map +1 -0
  80. package/dist/core/diffDeColecciones.js +84 -0
  81. package/dist/core/diffDeColecciones.js.map +1 -0
  82. package/dist/core/exportacion.js +6 -1
  83. package/dist/core/exportacion.js.map +1 -1
  84. package/dist/core/fotoDeColecciones.js +48 -0
  85. package/dist/core/fotoDeColecciones.js.map +1 -0
  86. package/dist/core/imagenesDeDocumento.js +88 -0
  87. package/dist/core/imagenesDeDocumento.js.map +1 -0
  88. package/dist/core/memoria.js +24 -0
  89. package/dist/core/memoria.js.map +1 -0
  90. package/dist/core/modelos.js +23 -1
  91. package/dist/core/modelos.js.map +1 -1
  92. package/dist/core/navegacion.js.map +1 -1
  93. package/dist/core/notify.js +23 -5
  94. package/dist/core/notify.js.map +1 -1
  95. package/dist/core/ports.js.map +1 -1
  96. package/dist/core/serieDeEscrituras.js +13 -0
  97. package/dist/core/serieDeEscrituras.js.map +1 -1
  98. package/dist/core/settings.js +62 -1
  99. package/dist/core/settings.js.map +1 -1
  100. package/dist/core/tareasDelPlan.js +103 -0
  101. package/dist/core/tareasDelPlan.js.map +1 -0
  102. package/dist/core/trazaDeErrores.js +42 -9
  103. package/dist/core/trazaDeErrores.js.map +1 -1
  104. package/dist/core/turno.js +34 -4
  105. package/dist/core/turno.js.map +1 -1
  106. package/dist/web/servidor/arranque.js +445 -2
  107. package/dist/web/servidor/arranque.js.map +1 -1
  108. package/dist/web/servidor/pielWeb.js +63 -4
  109. package/dist/web/servidor/pielWeb.js.map +1 -1
  110. package/dist/web/servidor/servidor.js +16 -1
  111. package/dist/web/servidor/servidor.js.map +1 -1
  112. package/dist/web/servidor/transporte.js.map +1 -1
  113. package/dist/web/servidor/vestibulo.js +45 -7
  114. package/dist/web/servidor/vestibulo.js.map +1 -1
  115. package/dist/web/servidor/visorOpenui.js +63 -0
  116. package/dist/web/servidor/visorOpenui.js.map +1 -0
  117. package/package.json +1 -1
  118. package/skills/openui-builder/SKILL.md +48 -0
  119. package/apps/web/dist/assets/index-CtfT1BjI.css +0 -1
  120. package/apps/web/dist/assets/index-DqcZ7_Rx.js +0 -478
@@ -66,7 +66,7 @@ import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedore
66
66
  import { crearCheckpointerDeProyecto, mantenimientoDelCheckpointer, } from "../../agent/sesiones/checkpointer.js";
67
67
  import { resumenDePoda } from "../../core/podaDeCheckpoint.js";
68
68
  import { cargarSettings, guardarConcurrenciaDeTareas, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarEntorno as guardarEntornoEnDisco, olvidarEntornoDeSettings, copiasDeEntorno, borrarCopiasDeEntorno, } from "../../agent/config/settingsEnDisco.js";
69
- import { dentroDelWorkspace, expandirConCasa, motivoParaNoOlvidarEntorno, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
69
+ import { dentroDelWorkspace, expandirConCasa, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
70
70
  import { esModoDeEscritura, MODO_POR_OMISION, } from "../../core/modoDeEscritura.js";
71
71
  import { mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
72
72
  import { elegirCarpetaEnMaquina, haySelectorDeCarpeta } from "../../agent/config/selectorEnMaquina.js";
@@ -77,7 +77,14 @@ import { cambiosDeSesion, fotoDeApertura, olvidarSesion, parcheDeSesion } from "
77
77
  import { commitDeTurno, cambiosPendientes, trabajoSinCommitear } from "../../agent/sesiones/gitSync.js";
78
78
  import { marcarTareaDeSesion, sembrarConsumosPendientes } from "./sesiones.js";
79
79
  import { RUTA_ARTEFACTOS, esRutaDeArtefacto } from "../../core/artefactos.js";
80
- import { arbolDeProyecto, leerFicheroDeProyecto, motivoDeRutaInaceptable, } from "../../agent/grafo/arbolDeProyecto.js";
80
+ import { arbolDeProyecto, leerFicheroDeProyecto, mimeDeImagen, motivoDeRutaInaceptable, } from "../../agent/grafo/arbolDeProyecto.js";
81
+ import { modeloEnDisco } from "../../agent/navegacion/indiceEnDisco.js";
82
+ import { ficherosDelProyecto } from "../../agent/turno/ficherosDelProyecto.js";
83
+ import { fotoDeColecciones } from "../../core/fotoDeColecciones.js";
84
+ import { modeloDelCambio } from "../../agent/sesiones/modeloDelCambio.js";
85
+ import { planesDelProyecto } from "../../agent/planesEnDisco.js";
86
+ import { CSP_DE_OPENUI, documentoDeOpenui, esArtefactoOpenui } from "./visorOpenui.js";
87
+ import { RUTA_IMAGEN_DEL_PROYECTO } from "../../core/imagenesDeDocumento.js";
81
88
  import { leerArtefactoCrudo, leerArtefactoDeSesion, } from "../../agent/grafo/artefactosEnDisco.js";
82
89
  import { CatalogoModelos, baseUrlDeOllama, baseUrlDeOllamaCloud, capacidadesDeOllama, } from "../../agent/config/catalogoModelos.js";
83
90
  import { Modelos } from "../../agent/config/modelos.js";
@@ -99,6 +106,8 @@ import { baseDeWorkspaceLegada, baseDeWorkspacePorOmision, conexionDeVestibulo,
99
106
  // doblan.
100
107
  import { filaDeTarea } from "./transporte.js";
101
108
  import { esEsfuerzo, nivelesDeEsfuerzo } from "../../core/esfuerzo.js";
109
+ import { definicionDelCable, RUTA_CALLBACK_MCP } from "../../core/conectores.js";
110
+ import { servicioDeConectoresCableado } from "../../agent/conectores/servicioDeConectores.js";
102
111
  /** Las dos rutas del cable. El cliente las tiene escritas en `apps/web/src/conexion.ts`. */
103
112
  /**
104
113
  * Cuánto se espera al aviso de «lo que ya había sin commitear» antes de anunciar sin él.
@@ -163,6 +172,21 @@ export const MS_ENTRE_PROGRESOS = 250;
163
172
  export function raizDelClientePorOmision() {
164
173
  return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "apps", "web", "dist");
165
174
  }
175
+ /**
176
+ * El visor de OpenUI del build del cliente, o `undefined` si no está. Se lee en cada petición, como
177
+ * el resto del cliente: reconstruirlo se ve recargando, sin reiniciar el servidor.
178
+ */
179
+ export function leerVisorOpenui(raizDelCliente) {
180
+ try {
181
+ return {
182
+ js: readFileSync(join(raizDelCliente, "openui", "visor.js"), "utf8"),
183
+ css: readFileSync(join(raizDelCliente, "openui", "visor.css"), "utf8"),
184
+ };
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ }
166
190
  export const FALTA_EL_BUILD = "falta el build del cliente: ejecuta «npm run build:web»";
167
191
  /** Tope del cuerpo de `POST /accion`. Generoso para una prosa larga, finito porque el
168
192
  * cuerpo se acumula en memoria y un cliente roto no puede llenarla. */
@@ -175,6 +199,14 @@ function tipoServido(mime) {
175
199
  ? `${mime}; charset=utf-8`
176
200
  : mime;
177
201
  }
202
+ /**
203
+ * Las cinco entidades de HTML, para la página del callback OAuth de un conector. El
204
+ * `mensaje` que escapa es siempre una frase NUESTRA (`ServicioDeConectores.completar`), pero
205
+ * se escapa igual: defensa en profundidad, no confianza en el origen.
206
+ */
207
+ function escaparHtml(texto) {
208
+ return texto.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
209
+ }
178
210
  /**
179
211
  * Monta `/eventos` y `/accion` sobre un servidor ya levantado.
180
212
  *
@@ -944,6 +976,9 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
944
976
  // El workspace, por lo mismo que los subagentes y las skills: Ajustes se puede abrir en
945
977
  // cuanto conecta, y un campo en blanco se lee como «no hay ninguno puesto».
946
978
  const workspace = mensajeDeWorkspace();
979
+ // Y los conectores MCP, tras el workspace y por la misma regla: solo si esta ejecución
980
+ // los monta, y entonces la ventana de Ajustes no se abre con la sección en blanco.
981
+ const conectores = mensajeDeConectores();
947
982
  /**
948
983
  * Lo consumido por la sesión que se va a pintar. Va en la ráfaga por lo mismo que los
949
984
  * modelos: una pestaña que conecta a mitad de sesión no vio los cambios anteriores, y
@@ -963,6 +998,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
963
998
  cliente(tareas);
964
999
  if (workspace !== undefined)
965
1000
  cliente(workspace);
1001
+ if (conectores !== undefined)
1002
+ cliente(conectores);
966
1003
  if (consumoDeLaSesion !== undefined) {
967
1004
  cliente({
968
1005
  clase: "consumo",
@@ -1107,6 +1144,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1107
1144
  });
1108
1145
  /** Los cuatro nombres conocidos y nada más: lo que llega por el cable no elige binario. */
1109
1146
  const esNombreDeHerramienta = (v) => v === "adb" || v === "emulator" || v === "xcrun" || v === "devicectl";
1147
+ /** Las seis acciones conocidas sobre UN conector, y nada más: lo demás es un 400. */
1148
+ const esAccionDeConector = (v) => v === "anadir" || v === "quitar" || v === "probar" || v === "autorizar" || v === "desconectar" || v === "crear";
1110
1149
  const mensajeDeWorkspace = () => {
1111
1150
  const ruta = opciones.workspace?.();
1112
1151
  if (ruta === undefined)
@@ -1122,6 +1161,76 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1122
1161
  if (m !== undefined)
1123
1162
  emitir(m);
1124
1163
  };
1164
+ /**
1165
+ * El servicio de conectores, construido UNA VEZ al montar — no en cada mensaje, porque es
1166
+ * quien guarda en memoria las pruebas y las autorizaciones pendientes (`servicioDeConectores.ts`).
1167
+ * `alCambiar` es lo que hace que un cambio que ocurre en segundo plano (autorizar, probar)
1168
+ * llegue por el cable sin que nadie lo pida — la misma costura que `emitirCambioDeTareas`.
1169
+ * Ausente la OPCIÓN = esta ejecución no monta conectores.
1170
+ */
1171
+ const servicioConectores = opciones.conectores?.(() => emitirConectores());
1172
+ const mensajeDeConectores = () => {
1173
+ if (servicioConectores === undefined)
1174
+ return undefined;
1175
+ // El catálogo lo compone el SERVICIO y no se arma aquí: es `catálogo ∪ definiciones`, y una
1176
+ // segunda composición en este fichero solo traería las filas de código — dejando sin nombre
1177
+ // en pantalla a todo conector escrito a mano.
1178
+ const { catalogo, conectores, desconocidos, ilegible, error } = servicioConectores.lista();
1179
+ return {
1180
+ clase: "conectores",
1181
+ catalogo,
1182
+ conectores,
1183
+ desconocidos,
1184
+ ...(ilegible ? { ilegible } : {}),
1185
+ ...(error === undefined ? {} : { error }),
1186
+ };
1187
+ };
1188
+ const emitirConectores = () => {
1189
+ const m = mensajeDeConectores();
1190
+ if (m !== undefined)
1191
+ emitir(m);
1192
+ };
1193
+ /**
1194
+ * Autoriza un conector POR DONDE ÉL PIDA: OAuth abre el navegador del sistema y `api-key` pide
1195
+ * una clave por la consola. `ninguna` no se autoriza —por eso no llega a hacer nada— y un id
1196
+ * que no resuelve tampoco.
1197
+ *
1198
+ * Es UNA función porque la usan DOS sitios —el «Conectar» de la fila y el encadenado de
1199
+ * `crear`—: dos copias serían dos sitios donde el carril puede divergir del que dice la fila, y
1200
+ * el carril lo contesta el SERVIDOR (`autenticacionDe`), nunca el cliente, que manda la
1201
+ * intención («autoriza este id») y no por dónde. Así un conector escrito a mano con `api-key`
1202
+ * no puede acabar abriendo un navegador por una pestaña que se equivocó: `autorizar` lo
1203
+ * rechazaría, y esto ni lo intenta.
1204
+ */
1205
+ const autorizarConector = async (id) => {
1206
+ if (servicioConectores === undefined)
1207
+ return;
1208
+ const autenticacion = servicioConectores.autenticacionDe(id);
1209
+ if (autenticacion === "oauth") {
1210
+ // El puerto REAL: con `--puerto 0` el sistema elige uno, y es el mismo que ya usa
1211
+ // `servidor.url` para la ráfaga inicial — nunca uno construido a mano.
1212
+ await servicioConectores.autorizar(id, `http://127.0.0.1:${servidor.puerto}${RUTA_CALLBACK_MCP}`);
1213
+ return;
1214
+ }
1215
+ if (autenticacion !== "api-key")
1216
+ return;
1217
+ // La MISMA resolución de consola que `pedirCredencial`: la del proyecto si hay uno abierto y
1218
+ // la del vestíbulo si no. La clave vuelve por `{clase: "secreto"}`, el único mensaje del
1219
+ // cable que la lleva.
1220
+ const consola = vestibulo.proyectoAbierto()?.consola.consola ?? vestibulo.consola.consola;
1221
+ // La pregunta DICE que no hace falta el prefijo, y no es un adorno: la criba
1222
+ // (`motivoDeClaveInaceptable`) rechaza cualquier valor con espacios —o sea justo el
1223
+ // «Bearer abc» que suele ser lo que uno copia del servidor— y el `Bearer` lo pone
1224
+ // `credencialDe` al componer la cabecera. Sin decirlo, el rechazo llega sin explicación.
1225
+ const clave = (await consola.leerSecreto(`clave de ${id} (sin el «Bearer»): `)).trim();
1226
+ // Cadena vacía es la consola sin nadie al otro lado y también el Enter a secas: en los dos
1227
+ // casos no se guarda nada y no se dice nada más — quien canceló no necesita un sermón.
1228
+ if (clave === "")
1229
+ return;
1230
+ // Una clave que no pasa la criba deja su motivo en `error`, que ya viaja por el cable: aquí
1231
+ // no hay nada que informar por consola, que es donde `pedirCredencial` sí lo hace.
1232
+ servicioConectores.guardarClave(id, clave);
1233
+ };
1125
1234
  const mensajeDeTareas = () => {
1126
1235
  if (opciones.colaDeTareas === undefined)
1127
1236
  return undefined;
@@ -2744,6 +2853,73 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2744
2853
  emitir({ clase: "arbol", rutas: [], recortado: false, error: "no se pudo listar el proyecto" });
2745
2854
  }
2746
2855
  };
2856
+ /**
2857
+ * El diff semántico de un `.xne` de Revisión. Siempre CONTESTA —la fila abierta espera, y un
2858
+ * silencio la dejaría cargando para siempre—, y la ruta pasa por la MISMA barrera que la del
2859
+ * lector de Ficheros antes de llegar a git.
2860
+ */
2861
+ const atenderModeloDelCambio = async (ruta) => {
2862
+ const abierto = vestibulo.proyectoAbierto();
2863
+ const sesion = abierto?.sesion;
2864
+ const sinNada = (error) => emitir({ clase: "modeloDelCambio", ruta, error });
2865
+ if (abierto === undefined || opciones.modeloDelCambio === undefined)
2866
+ return sinNada("esta ejecución no sabe comparar el modelo");
2867
+ if (sesion === undefined)
2868
+ return sinNada("la sesión todavía no ha empezado");
2869
+ if (motivoDeRutaInaceptable(ruta) !== undefined)
2870
+ return sinNada("esa ruta no se enseña");
2871
+ try {
2872
+ const cambios = await opciones.modeloDelCambio(abierto.raiz, sesion, ruta);
2873
+ if (cambios === undefined)
2874
+ return sinNada("no hay con qué comparar");
2875
+ emitir({ clase: "modeloDelCambio", ruta, cambios });
2876
+ }
2877
+ catch (error) {
2878
+ informar(`no se pudo comparar el modelo (${codigoDe(error)})`);
2879
+ sinNada("no se pudo comparar el modelo");
2880
+ }
2881
+ };
2882
+ /**
2883
+ * Los planes del proyecto abierto. Mismas reglas que el árbol: sin proyecto no se contesta,
2884
+ * sin puerto se contesta con error, y el fallo viaja sin la ruta de la máquina.
2885
+ */
2886
+ const atenderPlanes = async () => {
2887
+ const abierto = vestibulo.proyectoAbierto();
2888
+ if (abierto === undefined)
2889
+ return;
2890
+ if (opciones.planesDelProyecto === undefined) {
2891
+ emitir({ clase: "planes", error: "esta ejecución no puede leer los planes" });
2892
+ return;
2893
+ }
2894
+ try {
2895
+ emitir({ clase: "planes", planes: opciones.planesDelProyecto(abierto.raiz) });
2896
+ }
2897
+ catch (error) {
2898
+ informar(`no se pudieron leer los planes (${codigoDe(error)})`);
2899
+ emitir({ clase: "planes", error: "no se pudieron leer los planes" });
2900
+ }
2901
+ };
2902
+ /**
2903
+ * La foto del modelo XOne del proyecto abierto. Mismas reglas que el árbol: sin proyecto no
2904
+ * se contesta, sin puerto se contesta con error —un «consultando…» eterno es un fallo mudo—,
2905
+ * y el fallo viaja sin la ruta de la máquina.
2906
+ */
2907
+ const atenderColecciones = async () => {
2908
+ const abierto = vestibulo.proyectoAbierto();
2909
+ if (abierto === undefined)
2910
+ return;
2911
+ if (opciones.coleccionesDelProyecto === undefined) {
2912
+ emitir({ clase: "colecciones", error: "esta ejecución no puede leer el modelo del proyecto" });
2913
+ return;
2914
+ }
2915
+ try {
2916
+ emitir({ clase: "colecciones", foto: await opciones.coleccionesDelProyecto(abierto.raiz) });
2917
+ }
2918
+ catch (error) {
2919
+ informar(`no se pudo leer el modelo del proyecto (${codigoDe(error)})`);
2920
+ emitir({ clase: "colecciones", error: "no se pudo leer el modelo del proyecto" });
2921
+ }
2922
+ };
2747
2923
  /**
2748
2924
  * El contenido de una ruta del proyecto abierto. El lector decide si se puede enseñar.
2749
2925
  *
@@ -3341,6 +3517,25 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3341
3517
  return;
3342
3518
  }
3343
3519
  const descargar = query.get("descargar") !== null;
3520
+ // Un programa de OpenUI se VE dentro de su visor, en un documento autocontenido y sin red
3521
+ // (`visorOpenui.ts`). Descargarlo sigue dando el fichero tal cual.
3522
+ if (!descargar && esArtefactoOpenui(leido.nombre)) {
3523
+ const visor = opciones.visorOpenui?.();
3524
+ if (visor === undefined) {
3525
+ responder(503, "el visor de OpenUI no está construido: ejecuta «npm run build:web»");
3526
+ return;
3527
+ }
3528
+ const documento = Buffer.from(documentoDeOpenui(leido.datos.toString("utf8"), visor, leido.nombre), "utf8");
3529
+ respuesta.writeHead(200, {
3530
+ "Content-Type": "text/html; charset=utf-8",
3531
+ "Content-Length": documento.length,
3532
+ "Content-Security-Policy": CSP_DE_OPENUI,
3533
+ "X-Content-Type-Options": "nosniff",
3534
+ "Cache-Control": "no-store",
3535
+ });
3536
+ respuesta.end(documento);
3537
+ return;
3538
+ }
3344
3539
  // Sin mime conocido se descarga en vez de adivinar. El nombre ya pasó la barrera de
3345
3540
  // segmento llano, así que entre comillas no puede romper la cabecera.
3346
3541
  const inline = !descargar && leido.mime !== undefined;
@@ -3356,6 +3551,106 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3356
3551
  });
3357
3552
  respuesta.end(leido.datos);
3358
3553
  });
3554
+ /**
3555
+ * `GET /imagen-del-proyecto?ruta=<relativa>` — una IMAGEN del proyecto abierto, para que un `.md`
3556
+ * de la pestaña Ficheros enseñe las suyas.
3557
+ *
3558
+ * Existe porque el visor de markdown solo pinta imágenes `http(s)` absolutas: un enlace relativo o
3559
+ * una `data:` se quedaban en su texto alternativo. La vista del documento reescribe cada enlace a
3560
+ * esta ruta (`arbolDeProyecto.ts#vistaDeMarkdown`), y aquí se sirve con la barrera de siempre:
3561
+ * - **Solo imágenes, por su extensión**, antes de leer nada: esto no es un lector de ficheros por
3562
+ * HTTP, y sin esa criba cualquier ruta que se enseñe se podría pedir por aquí.
3563
+ * - **Leída con `leerFichero`** —las dos barreras de la pestaña Ficheros, sobre el texto y sobre
3564
+ * el camino REAL, y su tope—, no con una copia de ellas.
3565
+ * - `Content-Security-Policy: sandbox`, `nosniff` y `no-store`: un `.svg` abierto a pelo en una
3566
+ * pestaña no ejecuta nada, el navegador no adivina el tipo, y una captura reescrita se ve nueva.
3567
+ */
3568
+ servidor.registrarRuta("GET", RUTA_IMAGEN_DEL_PROYECTO, async (peticion, respuesta) => {
3569
+ const responder = (codigo, texto) => {
3570
+ respuesta.writeHead(codigo, { "Content-Type": "text/plain; charset=utf-8" });
3571
+ respuesta.end(texto);
3572
+ };
3573
+ const ruta = new URLSearchParams((peticion.url ?? "").split("?")[1] ?? "").get("ruta");
3574
+ if (ruta === null || ruta === "") {
3575
+ responder(400, "falta la ruta");
3576
+ return;
3577
+ }
3578
+ if (mimeDeImagen(ruta) === undefined) {
3579
+ responder(403, "eso no es una imagen");
3580
+ return;
3581
+ }
3582
+ const abierto = vestibulo.proyectoAbierto();
3583
+ if (abierto === undefined || opciones.leerFichero === undefined) {
3584
+ responder(404, "no hay imagen");
3585
+ return;
3586
+ }
3587
+ let leido;
3588
+ try {
3589
+ leido = await opciones.leerFichero(abierto.raiz, ruta);
3590
+ }
3591
+ catch (error) {
3592
+ // Como en el resto: al cliente y a `informar` solo el código, nunca el mensaje de Node.
3593
+ informar(`no se pudo servir la imagen «${ruta}» (${codigoDe(error)})`);
3594
+ responder(500, "no se pudo leer");
3595
+ return;
3596
+ }
3597
+ if (leido.error !== undefined) {
3598
+ responder(leido.error === "no existe" ? 404 : 403, leido.error);
3599
+ return;
3600
+ }
3601
+ if (leido.base64 === undefined || leido.mime === undefined) {
3602
+ responder(413, "la imagen es demasiado grande");
3603
+ return;
3604
+ }
3605
+ const datos = Buffer.from(leido.base64, "base64");
3606
+ respuesta.writeHead(200, {
3607
+ "Content-Type": leido.mime,
3608
+ "Content-Length": datos.length,
3609
+ "Content-Security-Policy": "sandbox",
3610
+ "X-Content-Type-Options": "nosniff",
3611
+ "Cache-Control": "no-store",
3612
+ });
3613
+ respuesta.end(datos);
3614
+ });
3615
+ /**
3616
+ * `GET RUTA_CALLBACK_MCP` — a donde vuelve el navegador tras autorizar un conector.
3617
+ *
3618
+ * **Ruta PÚBLICA**, registrada con `registrarRutaPublica`: la cookie de sesión es
3619
+ * `SameSite=Strict`, así que la redirección que manda el proveedor OAuth llega SIN ella —
3620
+ * `Host` y `Origin` siguen delante, y la autenticación real es el `state` de un solo uso
3621
+ * que `completar` consume (`core/conectores.ts#interpretarCallback`).
3622
+ *
3623
+ * Solo se registra si esta ejecución monta conectores (`servicioConectores` viene de la
3624
+ * opción `conectores`): sin servicio no hay callback que atender.
3625
+ *
3626
+ * La página es MÍNIMA y NUESTRA — nunca repite la query cruda, que la escribe quien
3627
+ * redirige — y va con `Content-Security-Policy: default-src 'none'; style-src
3628
+ * 'unsafe-inline'` (sin red, sin script) y `Cache-Control: no-store` (no es un documento
3629
+ * que se pueda volver a servir de caché: el `state` ya se consumió).
3630
+ */
3631
+ if (servicioConectores !== undefined) {
3632
+ servidor.registrarRutaPublica("GET", RUTA_CALLBACK_MCP, async (peticion, respuesta) => {
3633
+ const query = new URLSearchParams((peticion.url ?? "").split("?")[1] ?? "");
3634
+ // `completar` no debería lanzar —sus propios fallos vuelven como `{ok:false}`—, pero
3635
+ // esto es una ruta PÚBLICA sin cookie por delante: un reventón aquí no puede dejar al
3636
+ // navegador con una petición colgada ni con un 500 sin nada que decir. Y tampoco puede
3637
+ // desaparecer en silencio: `contar` es el mismo hueco por el que se avisa de cualquier
3638
+ // otro fallo suelto de este cable (`probar`/`autorizar`, más abajo).
3639
+ const { mensaje } = await servicioConectores.completar(query).catch((error) => {
3640
+ contar(error);
3641
+ return { ok: false, mensaje: "no se pudo completar la autorización: vuelve a pulsar Conectar" };
3642
+ });
3643
+ const cuerpo = `<!doctype html><html lang="es"><head><meta charset="utf-8">` +
3644
+ `<title>xonecode</title></head><body><p>${escaparHtml(mensaje)}</p>` +
3645
+ `<p>Ya puedes cerrar esta pestaña.</p></body></html>`;
3646
+ respuesta.writeHead(200, {
3647
+ "Content-Type": "text/html; charset=utf-8",
3648
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
3649
+ "Cache-Control": "no-store",
3650
+ });
3651
+ respuesta.end(cuerpo);
3652
+ });
3653
+ }
3359
3654
  /**
3360
3655
  * `POST /adjunto?tarea=<id>&nombre=<fichero>` — los BYTES de un adjunto de tarea.
3361
3656
  *
@@ -3564,6 +3859,31 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3564
3859
  respuesta.end();
3565
3860
  return;
3566
3861
  }
3862
+ if (typeof mensaje === "object" &&
3863
+ mensaje !== null &&
3864
+ mensaje.clase === "entorno" &&
3865
+ mensaje.accion === "renombrar") {
3866
+ // Como `olvidar`: la negativa se decide AQUÍ y antes del 204, para que el motivo pueda
3867
+ // ir en la respuesta — `informar` no llega al navegador desde el vestíbulo. Y el tipo
3868
+ // se mira antes de la regla porque la regla es de cadenas: un `nombre` que no sea texto
3869
+ // la haría reventar dentro del manejador, que es la única forma de que esta guarda no
3870
+ // conteste nada.
3871
+ const motivo = typeof mensaje.nombre === "string"
3872
+ ? motivoDeNombreDeEntornoInaceptable(mensaje.nombre)
3873
+ : "el nombre tiene que ser texto";
3874
+ if (motivo !== undefined) {
3875
+ respuesta.writeHead(409, { "content-type": "application/json" });
3876
+ respuesta.end(JSON.stringify({ motivo }));
3877
+ return;
3878
+ }
3879
+ void vestibulo
3880
+ .renombrarEntorno(mensaje.entorno, mensaje.nombre)
3881
+ .catch(contar)
3882
+ .finally(() => void anunciarAlta().catch(contar));
3883
+ respuesta.writeHead(204);
3884
+ respuesta.end();
3885
+ return;
3886
+ }
3567
3887
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "revision") {
3568
3888
  void atenderRevision(mensaje.ruta).catch(contar);
3569
3889
  respuesta.writeHead(204);
@@ -3576,6 +3896,27 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3576
3896
  respuesta.end();
3577
3897
  return;
3578
3898
  }
3899
+ if (typeof mensaje === "object" &&
3900
+ mensaje !== null &&
3901
+ mensaje.clase === "modeloDelCambio" &&
3902
+ typeof mensaje.ruta === "string") {
3903
+ void atenderModeloDelCambio(mensaje.ruta).catch(contar);
3904
+ respuesta.writeHead(204);
3905
+ respuesta.end();
3906
+ return;
3907
+ }
3908
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "planes") {
3909
+ void atenderPlanes().catch(contar);
3910
+ respuesta.writeHead(204);
3911
+ respuesta.end();
3912
+ return;
3913
+ }
3914
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "colecciones") {
3915
+ void atenderColecciones().catch(contar);
3916
+ respuesta.writeHead(204);
3917
+ respuesta.end();
3918
+ return;
3919
+ }
3579
3920
  if (typeof mensaje === "object" &&
3580
3921
  mensaje !== null &&
3581
3922
  mensaje.clase === "sync" &&
@@ -3722,6 +4063,84 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3722
4063
  respuesta.end();
3723
4064
  return;
3724
4065
  }
4066
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "conector") {
4067
+ // Sin `as`: `mensaje.clase === "conector"` ya narrows a la forma de `MensajeDelCliente`
4068
+ // —la misma unión discriminada que `workspace` usa un renglón más arriba—, así que
4069
+ // `mensaje.accion` se lee directo. El `typeof` que sigue no es para el tipo (ese ya lo
4070
+ // sabe TypeScript): es la guarda de verdad contra un JSON malformado, que puede mentir
4071
+ // sobre lo que el tipo promete.
4072
+ if (servicioConectores === undefined ||
4073
+ typeof mensaje.accion !== "string" ||
4074
+ !esAccionDeConector(mensaje.accion)) {
4075
+ respuesta.writeHead(400);
4076
+ respuesta.end();
4077
+ return;
4078
+ }
4079
+ /**
4080
+ * `crear` va ANTES y se lleva su propia guarda: es la única acción que NO trae `id` —lo
4081
+ * DERIVA el servidor del nombre, que es lo que impide que el cliente elija el nombre de un
4082
+ * fichero— y la única cuyo cuerpo hay que comprobar por FORMA antes de mirarlo de verdad.
4083
+ */
4084
+ if (mensaje.accion === "crear") {
4085
+ const def = definicionDelCable(mensaje.definicion);
4086
+ // Un cuerpo sin la forma no es una definición inaceptable —esa se RECHAZA con su motivo,
4087
+ // y el motivo viaja en `error`—: es un mensaje malformado, y por eso es 400 y no 204.
4088
+ if (def === undefined) {
4089
+ respuesta.writeHead(400);
4090
+ respuesta.end();
4091
+ return;
4092
+ }
4093
+ // El servicio valida lo que tiene que valer (nombre, URL, slug libre) y su motivo sale en
4094
+ // `error`, que ya viaja.
4095
+ const id = servicioConectores.crear(def);
4096
+ // El encadenado es del SERVIDOR y va DESPUÉS de la escritura: pedir una clave —o abrir el
4097
+ // navegador— por un conector que no llegó a estar añadido no llevaría a ninguna parte. Y
4098
+ // sin `await`: esta respuesta sale en el acto y el resultado llega por `alCambiar`, igual
4099
+ // que en `probar` — con `await`, este POST se quedaría colgado hasta que alguien teclee.
4100
+ if (id !== undefined && def.autenticacion !== "ninguna")
4101
+ void autorizarConector(id).catch(contar);
4102
+ respuesta.writeHead(204);
4103
+ respuesta.end();
4104
+ return;
4105
+ }
4106
+ // Las otras CINCO sí traen id, y ahí el `typeof` es la guarda contra el JSON que miente.
4107
+ if (typeof mensaje.id !== "string") {
4108
+ respuesta.writeHead(400);
4109
+ respuesta.end();
4110
+ return;
4111
+ }
4112
+ const { id, accion } = mensaje;
4113
+ /**
4114
+ * Las cinco contestan 204 EN EL ACTO. `anadir`/`quitar`/`desconectar` son síncronas —su
4115
+ * `cambio()` ya reemite `conectores` antes de que esta función devuelva—; `probar` y
4116
+ * `autorizar` corren en segundo plano (red, o esperar al navegador que abre el sistema)
4117
+ * con `.catch(contar)`, la misma disciplina que `conexion` y `dispositivos`. El
4118
+ * resultado de las cinco llega por `emitir(mensajeDeConectores())` vía `alCambiar`,
4119
+ * nunca en esta respuesta.
4120
+ */
4121
+ switch (accion) {
4122
+ case "anadir":
4123
+ servicioConectores.anadir(id);
4124
+ break;
4125
+ case "quitar":
4126
+ servicioConectores.quitar(id);
4127
+ break;
4128
+ case "desconectar":
4129
+ servicioConectores.desconectar(id);
4130
+ break;
4131
+ case "probar":
4132
+ void servicioConectores.probar(id).catch(contar);
4133
+ break;
4134
+ case "autorizar":
4135
+ // Por dónde se autoriza lo decide el SERVIDOR —OAuth abre el navegador, `api-key` pide
4136
+ // la clave—, así que este `case` no sabe ni de carriles ni de puertos.
4137
+ void autorizarConector(id).catch(contar);
4138
+ break;
4139
+ }
4140
+ respuesta.writeHead(204);
4141
+ respuesta.end();
4142
+ return;
4143
+ }
3725
4144
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "modelosDeMotor" && typeof mensaje.motor === "string") {
3726
4145
  void atenderModelosDeMotor(mensaje.motor).catch(contar);
3727
4146
  respuesta.writeHead(204);
@@ -4352,6 +4771,21 @@ export function ajusteDeWorkspaceCableado(opciones) {
4352
4771
  },
4353
4772
  };
4354
4773
  }
4774
+ /**
4775
+ * La opción `conectores` de `montarRutas`, cableada — extraída por el MISMO motivo que
4776
+ * `ajusteDeWorkspaceCableado`: un literal inline dentro de `arrancarConsolaWeb` no quedaría
4777
+ * probado, y esa composición concreta —que la fábrica de verdad (`servicioDeConectoresCableado`)
4778
+ * llega montada, y con la CASA correcta— es exactamente el patrón de fallo que este repo ya
4779
+ * tiene documentado nueve veces.
4780
+ *
4781
+ * Es una función de una línea, pero la línea es la que puede caerse: sin este sitio propio,
4782
+ * `arrancarConsolaWeb` seguiría compilando y pasando sus tests aunque alguien borrara la
4783
+ * opción del objeto que le pasa a `montarRutas` — el mismo campo OPCIONAL que hace que
4784
+ * `ConsolaDeProyecto.consumo` fuera la novena vez.
4785
+ */
4786
+ export function ajusteDeConectoresCableado(opciones) {
4787
+ return { conectores: (alCambiar) => servicioDeConectoresCableado({ casa: opciones.casa, alCambiar }) };
4788
+ }
4355
4789
  export function construirCorredorDeTareasCableado(opciones) {
4356
4790
  const disco = opciones.tareasFabrica === undefined ? undefined : opciones.tareasFabrica(opciones.informar);
4357
4791
  /**
@@ -4666,6 +5100,9 @@ export async function arrancarConsolaWeb(opciones) {
4666
5100
  // (`ajusteDeWorkspaceCableado`) por el patrón de fallo de siempre — y aquí el escritor
4667
5101
  // ya venía con la marca puesta: existía con su test y sin un solo llamador.
4668
5102
  ...ajusteDeWorkspaceCableado({ casa: homedir() }),
5103
+ // Los conectores MCP, con la MISMA disciplina: la composición extraída y probada
5104
+ // (`ajusteDeConectoresCableado`), nunca un literal aquí dentro.
5105
+ ...ajusteDeConectoresCableado({ casa: homedir() }),
4669
5106
  // El selector nativo, solo si este sistema tiene uno. En el que no, la opción no se
4670
5107
  // monta y el botón no llega a existir.
4671
5108
  ...(haySelectorDeCarpeta()
@@ -4708,6 +5145,12 @@ export async function arrancarConsolaWeb(opciones) {
4708
5145
  // El proyecto tal como lo ve el agente, para la pestaña Ficheros: mismo filtro, misma
4709
5146
  // barrera de rutas (`agent/grafo/arbolDeProyecto.ts`).
4710
5147
  arbolDelProyecto: async (raiz) => arbolDeProyecto(raiz),
5148
+ // El modelo XOne con el MISMO cargador y el mismo conjunto de ficheros que la tool, para
5149
+ // que la pestaña y el agente no contesten distinto sobre el mismo proyecto.
5150
+ coleccionesDelProyecto: async (raiz) => fotoDeColecciones(await modeloEnDisco(raiz)(ficherosDelProyecto(raiz))),
5151
+ modeloDelCambio,
5152
+ planesDelProyecto,
5153
+ visorOpenui: () => leerVisorOpenui(raizDelCliente),
4711
5154
  leerFichero: leerFicheroDeProyecto,
4712
5155
  leerArtefacto: leerArtefactoDeSesion,
4713
5156
  leerArtefactoCrudo,