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.
- package/README.md +66 -0
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +218 -41
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +39 -0
- package/dist/agent/toolGate.js +6 -1
- package/dist/config/engine.d.ts +21 -1
- package/dist/config/engine.js +22 -1
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/index.js +22 -7
- package/dist/mcp/client.d.ts +28 -0
- package/dist/mcp/client.js +62 -8
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/tools/browserExtension.js +153 -7
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/definitions.js +9 -2
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/runner.js +1 -1
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +22 -1
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +53 -4
- package/dist/ui/keyboardGuard.d.ts +21 -0
- package/dist/ui/keyboardGuard.js +30 -1
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.d.ts +5 -0
- package/dist/ui/prompt.js +109 -16
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/dist/ui/spinner.d.ts +15 -8
- package/dist/ui/spinner.js +80 -42
- package/extension/background.js +191 -14
- package/extension/content.js +446 -48
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/ORIGEN.md +48 -0
- package/extension/iconos/logotipo.png +0 -0
- package/extension/manifest.json +27 -4
- package/extension/popup.html +16 -6
- package/package.json +3 -2
package/dist/config/engine.js
CHANGED
|
@@ -55,12 +55,33 @@ export function engineHeaders(route) {
|
|
|
55
55
|
cabeceras["X-Chocolatito-Instance"] = license.instanceId;
|
|
56
56
|
return cabeceras;
|
|
57
57
|
}
|
|
58
|
-
/**
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
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
|
-
|
|
278
|
+
imprimirSobreElPrompt(chalk.gray(`\n ⬆ ${aviso}\n`));
|
|
264
279
|
return aviso;
|
|
265
280
|
})
|
|
266
281
|
.catch(() => "");
|
package/dist/mcp/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/mcp/client.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/mcp/ide.js
ADDED
|
@@ -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
|
+
}
|
package/dist/mcp/manager.js
CHANGED
|
@@ -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. */
|