chocolatito-code 1.1.0 → 1.3.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 (55) hide show
  1. package/README.md +66 -0
  2. package/dist/agent/huecos.d.ts +58 -0
  3. package/dist/agent/huecos.js +81 -0
  4. package/dist/agent/loop.js +218 -41
  5. package/dist/agent/salidaDelAgente.d.ts +43 -0
  6. package/dist/agent/salidaDelAgente.js +39 -0
  7. package/dist/agent/toolGate.js +6 -1
  8. package/dist/config/engine.d.ts +21 -1
  9. package/dist/config/engine.js +22 -1
  10. package/dist/config/nodeVersion.d.ts +13 -0
  11. package/dist/config/nodeVersion.js +26 -0
  12. package/dist/index.js +22 -7
  13. package/dist/mcp/client.d.ts +28 -0
  14. package/dist/mcp/client.js +62 -8
  15. package/dist/mcp/ide.d.ts +76 -0
  16. package/dist/mcp/ide.js +160 -0
  17. package/dist/mcp/manager.js +9 -0
  18. package/dist/tools/browserExtension.js +153 -7
  19. package/dist/tools/computerUse.d.ts +1 -1
  20. package/dist/tools/computerUse.js +183 -2
  21. package/dist/tools/definitions.js +9 -2
  22. package/dist/tools/grepSearch.js +111 -23
  23. package/dist/tools/runner.js +1 -1
  24. package/dist/tools/toolDefsComputer.js +6 -2
  25. package/dist/tools/visionBridge.js +60 -3
  26. package/dist/tools/win/hostScript.js +154 -0
  27. package/dist/tools/writeFile.d.ts +16 -1
  28. package/dist/tools/writeFile.js +22 -1
  29. package/dist/ui/interrupt.d.ts +10 -0
  30. package/dist/ui/interrupt.js +53 -4
  31. package/dist/ui/keyboardGuard.d.ts +21 -0
  32. package/dist/ui/keyboardGuard.js +30 -1
  33. package/dist/ui/pegados.d.ts +50 -0
  34. package/dist/ui/pegados.js +83 -0
  35. package/dist/ui/pieFijo.d.ts +56 -0
  36. package/dist/ui/pieFijo.js +309 -0
  37. package/dist/ui/prompt.d.ts +5 -0
  38. package/dist/ui/prompt.js +109 -16
  39. package/dist/ui/reasoningStream.js +22 -12
  40. package/dist/ui/salida.d.ts +10 -0
  41. package/dist/ui/salida.js +50 -0
  42. package/dist/ui/spinner.d.ts +15 -8
  43. package/dist/ui/spinner.js +80 -42
  44. package/extension/background.js +191 -14
  45. package/extension/content.js +446 -48
  46. package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
  47. package/extension/iconos/128.png +0 -0
  48. package/extension/iconos/16.png +0 -0
  49. package/extension/iconos/32.png +0 -0
  50. package/extension/iconos/48.png +0 -0
  51. package/extension/iconos/ORIGEN.md +48 -0
  52. package/extension/iconos/logotipo.png +0 -0
  53. package/extension/manifest.json +27 -4
  54. package/extension/popup.html +16 -6
  55. package/package.json +3 -2
@@ -55,12 +55,33 @@ export function engineHeaders(route) {
55
55
  cabeceras["X-Chocolatito-Instance"] = license.instanceId;
56
56
  return cabeceras;
57
57
  }
58
- /** Cliente del motor ya configurado. Es el unico sitio donde se construye. */
58
+ /**
59
+ * Cliente del motor ya configurado. Es el unico sitio donde se construye.
60
+ *
61
+ * POR QUE `maxRetries: 0`
62
+ *
63
+ * El SDK reintenta por su cuenta dos veces mas (su valor por defecto), y encima
64
+ * de eso el agente pasa cada llamada por `withRetry`. Dos capas apiladas, y solo
65
+ * una de las dos sabe algo del proxy.
66
+ *
67
+ * Medido contra un servidor de mentira: una sola llamada que recibe un 429 de
68
+ * "tienes 4 peticiones en curso" sale por la red TRES veces antes de que el
69
+ * error llegue siquiera a nuestro codigo. Y nuestro codigo lo tiene bien
70
+ * clasificado -`isRetryable` devuelve false para ese 429 justamente porque cada
71
+ * intento ocupa un hueco-, pero para entonces ya se han gastado los otros dos.
72
+ *
73
+ * Asi es como un solo corte de conexion se comia los cuatro huecos de la
74
+ * licencia y dejaba al usuario tres minutos sin servicio. La politica de
75
+ * reintentos vive en `withRetry` y en un solo sitio: ahi se sabe que un
76
+ * desbordamiento de contexto no puede empezar a caber, y que insistir con los
77
+ * huecos ocupados es la forma mas rapida de quedarse sin ninguno.
78
+ */
59
79
  export function createEngineClient(apiKey, route) {
60
80
  return new OpenAI({
61
81
  apiKey: engineCredential(apiKey),
62
82
  baseURL: getEngineBaseUrl(),
63
83
  defaultHeaders: engineHeaders(route),
84
+ maxRetries: 0,
64
85
  });
65
86
  }
66
87
  export function clasificarErrorDelMotor(err) {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Un Node viejo se dice claro, no se descubre a base de errores raros.
3
+ *
4
+ * `engines` en el package.json solo hace que npm suelte un aviso que nadie lee:
5
+ * la instalacion termina bien y el fallo aparece despues, al arrancar, con un
6
+ * "fetch is not defined" o un error de sintaxis dentro de una dependencia. El
7
+ * que acaba de pagar da por hecho que el producto esta roto, y no le falta
8
+ * razon desde donde el lo ve.
9
+ *
10
+ * Se comprueba antes que nada: antes de la licencia, antes de leer la
11
+ * configuracion y antes de importar nada que pueda reventar por esto mismo.
12
+ */
13
+ export declare function comprobarNode(version?: string): string | null;
@@ -0,0 +1,26 @@
1
+ /** La version de Node mas antigua con la que esto funciona. */
2
+ const NODE_MINIMO = 20;
3
+ /**
4
+ * Un Node viejo se dice claro, no se descubre a base de errores raros.
5
+ *
6
+ * `engines` en el package.json solo hace que npm suelte un aviso que nadie lee:
7
+ * la instalacion termina bien y el fallo aparece despues, al arrancar, con un
8
+ * "fetch is not defined" o un error de sintaxis dentro de una dependencia. El
9
+ * que acaba de pagar da por hecho que el producto esta roto, y no le falta
10
+ * razon desde donde el lo ve.
11
+ *
12
+ * Se comprueba antes que nada: antes de la licencia, antes de leer la
13
+ * configuracion y antes de importar nada que pueda reventar por esto mismo.
14
+ */
15
+ export function comprobarNode(version = process.version) {
16
+ const mayor = Number.parseInt(String(version).replace(/^v/, "").split(".")[0], 10);
17
+ // Si no se entiende la version, no es motivo para no dejar trabajar a nadie.
18
+ if (!Number.isFinite(mayor))
19
+ return null;
20
+ if (mayor >= NODE_MINIMO)
21
+ return null;
22
+ return (`Chocolatito Code necesita Node ${NODE_MINIMO} o superior, y estas con ${version}.\n\n` +
23
+ ` Descargalo en https://nodejs.org (la version LTS vale).\n` +
24
+ ` Despues, vuelve a instalarlo: npm i -g chocolatito-code\n\n` +
25
+ `No es un problema de tu licencia ni de tu conexion.`);
26
+ }
package/dist/index.js CHANGED
@@ -29,6 +29,8 @@ import { releaseKeyboard } from "./ui/interrupt.js";
29
29
  import { takeQueued, resetTypeAhead } from "./ui/typeAhead.js";
30
30
  import { mcpManager } from "./mcp/manager.js";
31
31
  import { comprobarActualizacion } from "./config/updater.js";
32
+ import { comprobarNode } from "./config/nodeVersion.js";
33
+ import { imprimirSobreElPrompt } from "./ui/prompt.js";
32
34
  /**
33
35
  * Version leida del package.json instalado, no escrita a mano: una constante
34
36
  * duplicada aqui se queda vieja en cuanto se publica una version nueva.
@@ -178,6 +180,15 @@ async function dispararStop(hooks, datos) {
178
180
  return null;
179
181
  }
180
182
  async function main() {
183
+ // Lo primero de todo: un Node viejo se dice claro, no se descubre a base de
184
+ // errores raros dentro de una dependencia. Ver config/nodeVersion.ts.
185
+ const nodeViejo = comprobarNode();
186
+ if (nodeViejo) {
187
+ console.error(chalk.red(`
188
+ ${nodeViejo}
189
+ `));
190
+ process.exit(EXIT_USO);
191
+ }
181
192
  const rawArgs = process.argv.slice(2);
182
193
  if (handleFlags(rawArgs))
183
194
  return;
@@ -237,9 +248,13 @@ async function main() {
237
248
  // Si tardan, el primer turno los espera; el usuario ya puede escribir.
238
249
  const mcpReady = mcpManager.loadAll().then((resumen) => {
239
250
  if (resumen) {
240
- console.log(chalk.hex("#D97757")("\n🔌 Servidores MCP:"));
241
- console.log(resumen);
242
- console.log(chalk.gray(` ${mcpManager.toolCount} herramientas MCP disponibles.\n`));
251
+ // Con `console.log` esto se escribia ENCIMA del marco del prompt y lo
252
+ // destrozaba: los servidores tardan, y para cuando contestan el prompt ya
253
+ // esta dibujado. Se vio asi en una sesion real del usuario:
254
+ // ✔ ide (http): 4 herramientasb modo · esc cancelar
255
+ imprimirSobreElPrompt(chalk.hex("#D97757")("\n🔌 Servidores MCP:") +
256
+ `\n${resumen}\n` +
257
+ chalk.gray(` ${mcpManager.toolCount} herramientas MCP disponibles.\n`));
243
258
  }
244
259
  return resumen;
245
260
  });
@@ -256,11 +271,11 @@ async function main() {
256
271
  })
257
272
  .then((aviso) => {
258
273
  avisoDeActualizacion = aviso;
259
- // Se escribe solo si hay algo que decir y el usuario no esta a mitad de
260
- // frase: el prompt se redibuja entero en la siguiente tecla, asi que una
261
- // linea de mas aqui no rompe el marco.
274
+ // Llega tarde a proposito, y para entonces el prompt ya suele estar
275
+ // dibujado: por eso NO se usa console.log, que lo pisaria. Ver
276
+ // imprimirSobreElPrompt en ui/prompt.ts.
262
277
  if (aviso)
263
- console.log(chalk.gray(`\n ⬆ ${aviso}\n`));
278
+ imprimirSobreElPrompt(chalk.gray(`\n ⬆ ${aviso}\n`));
264
279
  return aviso;
265
280
  })
266
281
  .catch(() => "");
@@ -68,7 +68,35 @@ export declare class McpClient {
68
68
  private spawnServer;
69
69
  private onMessage;
70
70
  private request;
71
+ /**
72
+ * Identificador de sesion que asigna el servidor HTTP.
73
+ *
74
+ * El transporte "streamable HTTP" del protocolo permite que el servidor
75
+ * devuelva una cabecera `Mcp-Session-Id` al responder al `initialize`, y a
76
+ * partir de ahi EXIGE que el cliente la mande en todas las peticiones
77
+ * siguientes. Sin esto, un servidor con sesion acepta el saludo y luego
78
+ * rechaza absolutamente todo lo demas: la conexion parece buena, la lista de
79
+ * herramientas llega vacia o con un 400, y desde fuera solo se ve un servidor
80
+ * que "no funciona" sin decir por que.
81
+ */
82
+ private sessionId;
83
+ /** Las mismas cabeceras para peticiones y notificaciones, sesion incluida. */
84
+ private cabecerasHttp;
71
85
  private notify;
86
+ /**
87
+ * Una notificacion por HTTP.
88
+ *
89
+ * Antes esto no existia y `notify()` se iba de vacio en cuanto habia una URL,
90
+ * asi que `notifications/initialized` solo viajaba por stdio. El protocolo la
91
+ * exige despues del `initialize`, y hay servidores que no atienden nada mas
92
+ * hasta recibirla: quedaban colgados en el saludo.
93
+ *
94
+ * No lleva id y no espera resultado -el servidor contesta 202 con el cuerpo
95
+ * vacio-, asi que no se parsea nada. Un fallo aqui tampoco se propaga: si el
96
+ * servidor no la queria, se vera en la peticion siguiente, que si tiene a
97
+ * quien quejarse.
98
+ */
99
+ private httpNotify;
72
100
  private httpRequest;
73
101
  private initialize;
74
102
  private listTools;
@@ -272,13 +272,62 @@ export class McpClient {
272
272
  }
273
273
  });
274
274
  }
275
+ /**
276
+ * Identificador de sesion que asigna el servidor HTTP.
277
+ *
278
+ * El transporte "streamable HTTP" del protocolo permite que el servidor
279
+ * devuelva una cabecera `Mcp-Session-Id` al responder al `initialize`, y a
280
+ * partir de ahi EXIGE que el cliente la mande en todas las peticiones
281
+ * siguientes. Sin esto, un servidor con sesion acepta el saludo y luego
282
+ * rechaza absolutamente todo lo demas: la conexion parece buena, la lista de
283
+ * herramientas llega vacia o con un 400, y desde fuera solo se ve un servidor
284
+ * que "no funciona" sin decir por que.
285
+ */
286
+ sessionId = null;
287
+ /** Las mismas cabeceras para peticiones y notificaciones, sesion incluida. */
288
+ cabecerasHttp() {
289
+ return {
290
+ "Content-Type": "application/json",
291
+ Accept: "application/json, text/event-stream",
292
+ ...(this.sessionId ? { "Mcp-Session-Id": this.sessionId } : {}),
293
+ // Las del usuario van al final: si configura una a mano, manda la suya.
294
+ ...(this.config.headers || {}),
295
+ };
296
+ }
275
297
  notify(method, params) {
276
- if (this.config.url || !this.proc)
277
- return;
298
+ if (this.config.url)
299
+ return this.httpNotify(method, params);
300
+ if (!this.proc)
301
+ return Promise.resolve();
278
302
  try {
279
303
  this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
280
304
  }
281
305
  catch { }
306
+ return Promise.resolve();
307
+ }
308
+ /**
309
+ * Una notificacion por HTTP.
310
+ *
311
+ * Antes esto no existia y `notify()` se iba de vacio en cuanto habia una URL,
312
+ * asi que `notifications/initialized` solo viajaba por stdio. El protocolo la
313
+ * exige despues del `initialize`, y hay servidores que no atienden nada mas
314
+ * hasta recibirla: quedaban colgados en el saludo.
315
+ *
316
+ * No lleva id y no espera resultado -el servidor contesta 202 con el cuerpo
317
+ * vacio-, asi que no se parsea nada. Un fallo aqui tampoco se propaga: si el
318
+ * servidor no la queria, se vera en la peticion siguiente, que si tiene a
319
+ * quien quejarse.
320
+ */
321
+ async httpNotify(method, params) {
322
+ try {
323
+ await fetch(this.config.url, {
324
+ method: "POST",
325
+ headers: this.cabecerasHttp(),
326
+ body: JSON.stringify({ jsonrpc: "2.0", method, params }),
327
+ signal: AbortSignal.timeout(5000),
328
+ });
329
+ }
330
+ catch { }
282
331
  }
283
332
  async httpRequest(method, params, timeoutMs) {
284
333
  const controller = new AbortController();
@@ -287,14 +336,16 @@ export class McpClient {
287
336
  try {
288
337
  const res = await fetch(this.config.url, {
289
338
  method: "POST",
290
- headers: {
291
- "Content-Type": "application/json",
292
- Accept: "application/json, text/event-stream",
293
- ...(this.config.headers || {}),
294
- },
339
+ headers: this.cabecerasHttp(),
295
340
  body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
296
341
  signal: controller.signal,
297
342
  });
343
+ // La sesion se apunta ANTES de mirar si la respuesta fue buena: hay
344
+ // servidores que la asignan y en la misma respuesta se quejan de algo
345
+ // menor, y tirarla aqui condenaria todas las peticiones siguientes.
346
+ const sesion = res.headers.get("mcp-session-id");
347
+ if (sesion)
348
+ this.sessionId = sesion;
298
349
  if (!res.ok)
299
350
  throw new Error(`HTTP ${res.status}`);
300
351
  const text = await res.text();
@@ -314,7 +365,10 @@ export class McpClient {
314
365
  capabilities: { tools: {} },
315
366
  clientInfo: { name: "chocolatito-code", version: "1.0.0" },
316
367
  }, timeoutMs);
317
- this.notify("notifications/initialized", {});
368
+ // Se ESPERA a que salga, no se lanza y se olvida. Por HTTP la notificacion
369
+ // es otra peticion, y si `tools/list` sale antes de que el servidor haya
370
+ // visto el "ya estoy listo", hay servidores que la rechazan.
371
+ await this.notify("notifications/initialized", {});
318
372
  }
319
373
  async listTools(timeoutMs) {
320
374
  const out = [];
@@ -0,0 +1,76 @@
1
+ import type { McpServerConfig } from "./client.js";
2
+ /**
3
+ * Encontrar el editor que hay delante, sin que el usuario configure nada.
4
+ *
5
+ * COMO SE HABLAN
6
+ *
7
+ * Al reves de lo que parece: la extension del editor es el SERVIDOR MCP y el
8
+ * agente es el cliente. La extension levanta un servidor en localhost y deja
9
+ * aqui una ficha con el puerto; el agente la lee al arrancar y se conecta solo.
10
+ * Asi el editor le presta al agente lo unico que el agente no puede tener por su
11
+ * cuenta: los diagnosticos del servidor de lenguaje, lo que hay seleccionado, y
12
+ * un sitio donde pintar los diffs de verdad.
13
+ *
14
+ * POR QUE UNA FICHA POR VENTANA Y NO UN PUERTO FIJO
15
+ *
16
+ * Porque casi nadie tiene un solo VSCode abierto. Con un puerto fijo, el segundo
17
+ * que arranca no puede escuchar, y el agente de un proyecto acabaria pintando
18
+ * sus diffs en la ventana de otro. Cada ventana deja su ficha con su carpeta, y
19
+ * aqui se elige la que de verdad corresponde al directorio de trabajo.
20
+ *
21
+ * SEGURIDAD
22
+ *
23
+ * El servidor escucha solo en 127.0.0.1 y la ficha lleva un token que hay que
24
+ * mandar en cada peticion. Sin eso, cualquier programa de la maquina podria
25
+ * pedirle al editor del usuario que abriera y modificara archivos. La ficha vive
26
+ * en el home, que es donde ya viven las credenciales.
27
+ */
28
+ /** Donde deja su ficha cada ventana del editor. */
29
+ export declare function carpetaDeFichas(): string;
30
+ export interface FichaDeEditor {
31
+ /** Puerto del servidor MCP de esa ventana. */
32
+ puerto: number;
33
+ /** Secreto que hay que presentar en cada peticion. */
34
+ token: string;
35
+ /** Carpeta abierta en esa ventana. Es lo que decide cual toca. */
36
+ carpeta: string;
37
+ /** Proceso de la ventana, para saber si sigue viva. */
38
+ pid: number;
39
+ /** "Visual Studio Code", "Cursor"... solo para poder decirlo por pantalla. */
40
+ editor?: string;
41
+ }
42
+ /** Si `hijo` esta dentro de `padre` (o es el mismo). */
43
+ export declare function estaDentro(hijo: string, padre: string): boolean;
44
+ /**
45
+ * Lee las fichas que hay ahora mismo, tirando las de ventanas ya cerradas.
46
+ *
47
+ * `limpiar` borra las huerfanas del disco. Se hace de paso, no en un proceso
48
+ * aparte: si no, se acumulan para siempre y cada arranque tiene que descartar
49
+ * mas basura.
50
+ */
51
+ export declare function leerFichas(dir?: string, limpiar?: boolean): FichaDeEditor[];
52
+ /**
53
+ * De todas las ventanas abiertas, la que corresponde a este directorio.
54
+ *
55
+ * Nunca devuelve "la primera que haya": si ninguna contiene al directorio de
56
+ * trabajo, se devuelve null y el agente sigue sin editor. Conectarse a una
57
+ * ventana ajena seria peor que no conectarse a ninguna, porque los diffs
58
+ * apareceran en el proyecto equivocado y el usuario los aceptaria sin mirar.
59
+ *
60
+ * Cuando varias contienen al directorio (un monorepo abierto entero y un
61
+ * paquete suyo abierto aparte), gana LA MAS CERCANA: la carpeta mas larga es la
62
+ * mas especifica.
63
+ */
64
+ export declare function elegirFicha(cwd: string, fichas: FichaDeEditor[]): FichaDeEditor | null;
65
+ /** La configuracion de servidor MCP que representa a ese editor. */
66
+ export declare function configDeFicha(ficha: FichaDeEditor): McpServerConfig;
67
+ /**
68
+ * Lo que hay que anadir a la configuracion MCP por tener un editor delante.
69
+ *
70
+ * Devuelve un objeto vacio si no hay ninguno, que es el caso normal de quien usa
71
+ * solo la terminal: nadie tiene que configurar nada para que esto no le moleste.
72
+ */
73
+ export declare function servidorDelEditor(cwd: string, dir?: string): {
74
+ config: Record<string, McpServerConfig>;
75
+ ficha: FichaDeEditor | null;
76
+ };
@@ -0,0 +1,160 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /**
5
+ * Encontrar el editor que hay delante, sin que el usuario configure nada.
6
+ *
7
+ * COMO SE HABLAN
8
+ *
9
+ * Al reves de lo que parece: la extension del editor es el SERVIDOR MCP y el
10
+ * agente es el cliente. La extension levanta un servidor en localhost y deja
11
+ * aqui una ficha con el puerto; el agente la lee al arrancar y se conecta solo.
12
+ * Asi el editor le presta al agente lo unico que el agente no puede tener por su
13
+ * cuenta: los diagnosticos del servidor de lenguaje, lo que hay seleccionado, y
14
+ * un sitio donde pintar los diffs de verdad.
15
+ *
16
+ * POR QUE UNA FICHA POR VENTANA Y NO UN PUERTO FIJO
17
+ *
18
+ * Porque casi nadie tiene un solo VSCode abierto. Con un puerto fijo, el segundo
19
+ * que arranca no puede escuchar, y el agente de un proyecto acabaria pintando
20
+ * sus diffs en la ventana de otro. Cada ventana deja su ficha con su carpeta, y
21
+ * aqui se elige la que de verdad corresponde al directorio de trabajo.
22
+ *
23
+ * SEGURIDAD
24
+ *
25
+ * El servidor escucha solo en 127.0.0.1 y la ficha lleva un token que hay que
26
+ * mandar en cada peticion. Sin eso, cualquier programa de la maquina podria
27
+ * pedirle al editor del usuario que abriera y modificara archivos. La ficha vive
28
+ * en el home, que es donde ya viven las credenciales.
29
+ */
30
+ /** Donde deja su ficha cada ventana del editor. */
31
+ export function carpetaDeFichas() {
32
+ return path.join(os.homedir(), ".chocolatito", "ide");
33
+ }
34
+ function normalizar(p) {
35
+ const limpio = path.resolve(p).replace(/[\\/]+$/, "");
36
+ // Windows no distingue mayusculas en las rutas, y comparar sin normalizar
37
+ // hace que "C:\Users" y "c:\users" parezcan proyectos distintos.
38
+ return process.platform === "win32" ? limpio.toLowerCase() : limpio;
39
+ }
40
+ /** Si `hijo` esta dentro de `padre` (o es el mismo). */
41
+ export function estaDentro(hijo, padre) {
42
+ const h = normalizar(hijo);
43
+ const p = normalizar(padre);
44
+ if (h === p)
45
+ return true;
46
+ // El separador importa: sin el, "/proyecto-viejo" pasaria por estar dentro de
47
+ // "/proyecto".
48
+ return h.startsWith(p + path.sep) || h.startsWith(p + "/");
49
+ }
50
+ /** Si el proceso de esa ventana sigue vivo. Una ficha huerfana no vale. */
51
+ function sigueViva(pid) {
52
+ if (!Number.isInteger(pid) || pid <= 0)
53
+ return false;
54
+ try {
55
+ // La senal 0 no manda nada: solo pregunta si existe y si se puede tocar.
56
+ process.kill(pid, 0);
57
+ return true;
58
+ }
59
+ catch (err) {
60
+ // EPERM significa que existe pero es de otro usuario: existe, que es lo que
61
+ // se preguntaba.
62
+ return err?.code === "EPERM";
63
+ }
64
+ }
65
+ function esFicha(x) {
66
+ return (x &&
67
+ Number.isInteger(x.puerto) &&
68
+ x.puerto > 0 &&
69
+ typeof x.token === "string" &&
70
+ x.token.length > 0 &&
71
+ typeof x.carpeta === "string" &&
72
+ x.carpeta.length > 0);
73
+ }
74
+ /**
75
+ * Lee las fichas que hay ahora mismo, tirando las de ventanas ya cerradas.
76
+ *
77
+ * `limpiar` borra las huerfanas del disco. Se hace de paso, no en un proceso
78
+ * aparte: si no, se acumulan para siempre y cada arranque tiene que descartar
79
+ * mas basura.
80
+ */
81
+ export function leerFichas(dir = carpetaDeFichas(), limpiar = true) {
82
+ let nombres;
83
+ try {
84
+ nombres = fs.readdirSync(dir).filter((n) => n.endsWith(".json"));
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ const vivas = [];
90
+ for (const nombre of nombres) {
91
+ const completo = path.join(dir, nombre);
92
+ let ficha;
93
+ try {
94
+ ficha = JSON.parse(fs.readFileSync(completo, "utf-8"));
95
+ }
96
+ catch {
97
+ // Una ficha rota no puede impedir encontrar las buenas.
98
+ continue;
99
+ }
100
+ if (!esFicha(ficha))
101
+ continue;
102
+ if (!sigueViva(ficha.pid)) {
103
+ if (limpiar) {
104
+ try {
105
+ fs.unlinkSync(completo);
106
+ }
107
+ catch { }
108
+ }
109
+ continue;
110
+ }
111
+ vivas.push(ficha);
112
+ }
113
+ return vivas;
114
+ }
115
+ /**
116
+ * De todas las ventanas abiertas, la que corresponde a este directorio.
117
+ *
118
+ * Nunca devuelve "la primera que haya": si ninguna contiene al directorio de
119
+ * trabajo, se devuelve null y el agente sigue sin editor. Conectarse a una
120
+ * ventana ajena seria peor que no conectarse a ninguna, porque los diffs
121
+ * apareceran en el proyecto equivocado y el usuario los aceptaria sin mirar.
122
+ *
123
+ * Cuando varias contienen al directorio (un monorepo abierto entero y un
124
+ * paquete suyo abierto aparte), gana LA MAS CERCANA: la carpeta mas larga es la
125
+ * mas especifica.
126
+ */
127
+ export function elegirFicha(cwd, fichas) {
128
+ const candidatas = fichas.filter((f) => estaDentro(cwd, f.carpeta));
129
+ if (candidatas.length === 0)
130
+ return null;
131
+ return candidatas.sort((a, b) => normalizar(b.carpeta).length - normalizar(a.carpeta).length)[0];
132
+ }
133
+ /** La configuracion de servidor MCP que representa a ese editor. */
134
+ export function configDeFicha(ficha) {
135
+ return {
136
+ type: "http",
137
+ // Solo el bucle local: nunca la interfaz de red, que expondria el editor
138
+ // del usuario a cualquiera que comparta su wifi.
139
+ url: `http://127.0.0.1:${ficha.puerto}/mcp`,
140
+ headers: { Authorization: `Bearer ${ficha.token}` },
141
+ // Corto a proposito: es un proceso local. Si no contesta en 5 s es que la
142
+ // ventana esta colgada, y esperar 20 retrasaria el arranque para nada.
143
+ timeout: 5000,
144
+ };
145
+ }
146
+ /**
147
+ * Lo que hay que anadir a la configuracion MCP por tener un editor delante.
148
+ *
149
+ * Devuelve un objeto vacio si no hay ninguno, que es el caso normal de quien usa
150
+ * solo la terminal: nadie tiene que configurar nada para que esto no le moleste.
151
+ */
152
+ export function servidorDelEditor(cwd, dir = carpetaDeFichas()) {
153
+ // Puerta de salida para quien no lo quiera, y para poder reproducir fallos.
154
+ if (process.env.CHOCOLATITO_SIN_EDITOR === "1")
155
+ return { config: {}, ficha: null };
156
+ const ficha = elegirFicha(cwd, leerFichas(dir));
157
+ if (!ficha)
158
+ return { config: {}, ficha: null };
159
+ return { config: { ide: configDeFicha(ficha) }, ficha };
160
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { McpClient } from "./client.js";
5
+ import { servidorDelEditor } from "./ide.js";
5
6
  /**
6
7
  * Gestor de servidores MCP.
7
8
  *
@@ -49,6 +50,14 @@ export class McpManager {
49
50
  // Un JSON roto no debe impedir arrancar el agente.
50
51
  }
51
52
  }
53
+ // El editor que haya delante se anade solo, sin que nadie lo configure. Va
54
+ // el ultimo a proposito: si el usuario define un servidor llamado "ide" a
55
+ // mano, gana el suyo. Ver ide.ts.
56
+ const { config: delEditor } = servidorDelEditor(this.cwd);
57
+ for (const [name, cfg] of Object.entries(delEditor)) {
58
+ if (!merged[name])
59
+ merged[name] = cfg;
60
+ }
52
61
  return merged;
53
62
  }
54
63
  /** Conecta todos los servidores configurados. Devuelve un resumen legible. */