chocolatito-code 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/agent/compaction.d.ts +65 -0
- package/dist/agent/compaction.js +134 -0
- package/dist/agent/context.d.ts +3 -0
- package/dist/agent/context.js +35 -0
- package/dist/agent/loop.d.ts +91 -2
- package/dist/agent/loop.js +642 -91
- package/dist/agent/loopDetector.d.ts +57 -0
- package/dist/agent/loopDetector.js +0 -0
- package/dist/agent/mentions.d.ts +13 -0
- package/dist/agent/mentions.js +49 -0
- package/dist/agent/repoMap.d.ts +117 -0
- package/dist/agent/repoMap.js +560 -0
- package/dist/agent/result.d.ts +64 -0
- package/dist/agent/result.js +48 -0
- package/dist/agent/retry.d.ts +35 -0
- package/dist/agent/retry.js +123 -4
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +53 -0
- package/dist/agent/subagent.js +62 -15
- package/dist/agent/syntaxValidator.js +288 -52
- package/dist/agent/toolGate.d.ts +58 -0
- package/dist/agent/toolGate.js +111 -0
- package/dist/agent/tracker.js +4 -7
- package/dist/agent/undoManager.d.ts +44 -11
- package/dist/agent/undoManager.js +95 -24
- package/dist/agent/usageMeter.d.ts +56 -0
- package/dist/agent/usageMeter.js +51 -0
- package/dist/agent/verifier.d.ts +49 -0
- package/dist/agent/verifier.js +158 -0
- package/dist/config/constants.js +14 -0
- package/dist/config/engine.d.ts +19 -0
- package/dist/config/engine.js +35 -3
- package/dist/config/license.d.ts +5 -0
- package/dist/config/license.js +107 -9
- package/dist/config/limits.d.ts +28 -0
- package/dist/config/limits.js +51 -0
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/config/permissions.d.ts +76 -1
- package/dist/config/permissions.js +379 -23
- package/dist/config/quota.js +24 -1
- package/dist/config/updater.d.ts +68 -0
- package/dist/config/updater.js +215 -0
- package/dist/hooks/manager.js +159 -14
- package/dist/index.js +240 -37
- package/dist/mcp/client.d.ts +50 -0
- package/dist/mcp/client.js +155 -19
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/prompts/systemPrompt.js +12 -1
- package/dist/sessions/manager.d.ts +12 -0
- package/dist/sessions/manager.js +33 -5
- package/dist/sessions/resume.d.ts +36 -0
- package/dist/sessions/resume.js +43 -0
- package/dist/skills/manager.d.ts +1 -0
- package/dist/skills/manager.js +9 -0
- package/dist/tools/binary.d.ts +46 -0
- package/dist/tools/binary.js +100 -0
- package/dist/tools/browserCdp.js +13 -1
- package/dist/tools/browserExtension.d.ts +5 -0
- package/dist/tools/browserExtension.js +237 -22
- package/dist/tools/browserSession.d.ts +23 -0
- package/dist/tools/browserSession.js +28 -0
- package/dist/tools/computerUse.js +33 -1
- package/dist/tools/createImage.d.ts +11 -0
- package/dist/tools/createImage.js +13 -1
- package/dist/tools/definitions.js +17 -4
- package/dist/tools/editDiagnosis.d.ts +54 -0
- package/dist/tools/editDiagnosis.js +142 -0
- package/dist/tools/editFile.d.ts +1 -0
- package/dist/tools/editFile.js +118 -62
- package/dist/tools/findFiles.d.ts +21 -0
- package/dist/tools/findFiles.js +75 -6
- package/dist/tools/getSystemInfo.js +15 -7
- package/dist/tools/gitTools.d.ts +24 -0
- package/dist/tools/gitTools.js +87 -26
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/moveCopyFile.d.ts +18 -1
- package/dist/tools/moveCopyFile.js +74 -4
- package/dist/tools/patchCascade.d.ts +57 -0
- package/dist/tools/patchCascade.js +338 -0
- package/dist/tools/runCommand.js +213 -25
- package/dist/tools/runner.js +58 -5
- package/dist/tools/toolDefsComputer.js +12 -2
- package/dist/tools/truncator.d.ts +27 -1
- package/dist/tools/truncator.js +117 -11
- package/dist/tools/viewFile.js +15 -11
- package/dist/tools/visionBridge.js +7 -0
- package/dist/tools/webSearch.d.ts +21 -0
- package/dist/tools/webSearch.js +125 -14
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +40 -2
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +59 -7
- package/dist/ui/keyboardGuard.d.ts +76 -0
- package/dist/ui/keyboardGuard.js +94 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +40 -0
- package/dist/ui/prompt.js +436 -131
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/extension/README.md +95 -74
- package/extension/background.js +772 -181
- package/extension/content.js +527 -46
- 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/logotipo.png +0 -0
- package/extension/manifest.json +28 -4
- package/extension/popup.html +16 -6
- package/extension/popup.js +28 -12
- package/package.json +3 -2
package/dist/mcp/client.js
CHANGED
|
@@ -37,6 +37,96 @@ function expandDeep(value) {
|
|
|
37
37
|
}
|
|
38
38
|
return value;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Trocea un cuerpo SSE en la carga util de cada evento.
|
|
42
|
+
*
|
|
43
|
+
* Se sigue la especificacion: los comentarios (`:` inicial) se ignoran, las
|
|
44
|
+
* lineas `data:` de un mismo evento se concatenan con salto de linea y el
|
|
45
|
+
* evento termina en la linea en blanco. El espacio opcional tras los dos puntos
|
|
46
|
+
* no forma parte del dato.
|
|
47
|
+
*/
|
|
48
|
+
function eventosSse(text) {
|
|
49
|
+
const eventos = [];
|
|
50
|
+
let buffer = [];
|
|
51
|
+
const cerrar = () => {
|
|
52
|
+
if (buffer.length)
|
|
53
|
+
eventos.push(buffer.join("\n"));
|
|
54
|
+
buffer = [];
|
|
55
|
+
};
|
|
56
|
+
for (const line of text.split(/\r?\n/)) {
|
|
57
|
+
if (line === "") {
|
|
58
|
+
cerrar();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (line.startsWith(":"))
|
|
62
|
+
continue;
|
|
63
|
+
if (line.startsWith("data:"))
|
|
64
|
+
buffer.push(line.slice(5).replace(/^ /, ""));
|
|
65
|
+
}
|
|
66
|
+
cerrar();
|
|
67
|
+
return eventos;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extrae el mensaje JSON-RPC de una respuesta HTTP, sea JSON directo o SSE.
|
|
71
|
+
*
|
|
72
|
+
* Antes esto se decidia con `text.includes("data:")` sobre el cuerpo entero, y
|
|
73
|
+
* eso rompia por dos sitios a la vez:
|
|
74
|
+
*
|
|
75
|
+
* 1. Cualquier respuesta JSON normal que mencionara `data:` en su contenido (un
|
|
76
|
+
* data URI, una cabecera, un trozo de CSS, la descripcion de una herramienta
|
|
77
|
+
* de captura o de fetch web) entraba por la rama SSE. Ninguna linea empezaba
|
|
78
|
+
* por `data:`, el `.pop()` daba undefined y el `|| "{}"` convertia la
|
|
79
|
+
* respuesta en un objeto vacio. Como `httpRequest` es la unica via de todo el
|
|
80
|
+
* protocolo HTTP, eso no solo devolvia «(sin contenido)» en `tools/call`:
|
|
81
|
+
* tambien vaciaba `tools/list`, y el servidor quedaba «conectado» con cero
|
|
82
|
+
* herramientas sin un solo mensaje de error.
|
|
83
|
+
* 2. En SSE de verdad, `.pop()` se quedaba con el ULTIMO evento en vez de con el
|
|
84
|
+
* que responde a nuestro `id`. Un ping o una notificacion posterior tapaban
|
|
85
|
+
* el resultado.
|
|
86
|
+
*
|
|
87
|
+
* Ahora manda el Content-Type, la forma del cuerpo desempata cuando el servidor
|
|
88
|
+
* no lo declara bien, y entre los eventos se elige el que lleva el `id` pedido.
|
|
89
|
+
*/
|
|
90
|
+
export function parseHttpBody(text, contentType, id) {
|
|
91
|
+
const cuerpo = text.trim();
|
|
92
|
+
if (!cuerpo)
|
|
93
|
+
throw new Error("el servidor MCP devolvio un cuerpo vacio");
|
|
94
|
+
const declaraSse = /text\/event-stream/i.test(contentType || "");
|
|
95
|
+
if (!declaraSse) {
|
|
96
|
+
// JSON directo. Si el servidor miente en el Content-Type pero manda SSE,
|
|
97
|
+
// el parse falla y se reintenta abajo como flujo de eventos.
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(cuerpo);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (!/^data:/m.test(cuerpo))
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const mensajes = [];
|
|
107
|
+
for (const evento of eventosSse(cuerpo)) {
|
|
108
|
+
try {
|
|
109
|
+
mensajes.push(JSON.parse(evento));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Keepalives y eventos que no son JSON-RPC: se ignoran.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!mensajes.length)
|
|
116
|
+
throw new Error("el flujo SSE del servidor MCP no traia ningun mensaje JSON-RPC");
|
|
117
|
+
if (id !== undefined) {
|
|
118
|
+
const propio = mensajes.find((m) => m && m.id !== undefined && String(m.id) === String(id));
|
|
119
|
+
if (propio)
|
|
120
|
+
return propio;
|
|
121
|
+
}
|
|
122
|
+
// Sin coincidencia de id nos quedamos con la ultima respuesta de verdad, no
|
|
123
|
+
// con una notificacion del servidor (que no lleva ni result ni error).
|
|
124
|
+
const respuestas = mensajes.filter((m) => m && ("result" in m || "error" in m));
|
|
125
|
+
const elegido = respuestas.length ? respuestas[respuestas.length - 1] : null;
|
|
126
|
+
if (!elegido)
|
|
127
|
+
throw new Error("el flujo SSE del servidor MCP no traia respuesta a la peticion");
|
|
128
|
+
return elegido;
|
|
129
|
+
}
|
|
40
130
|
export class McpClient {
|
|
41
131
|
name;
|
|
42
132
|
tools = [];
|
|
@@ -182,41 +272,84 @@ export class McpClient {
|
|
|
182
272
|
}
|
|
183
273
|
});
|
|
184
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
|
+
}
|
|
185
297
|
notify(method, params) {
|
|
186
|
-
if (this.config.url
|
|
187
|
-
return;
|
|
298
|
+
if (this.config.url)
|
|
299
|
+
return this.httpNotify(method, params);
|
|
300
|
+
if (!this.proc)
|
|
301
|
+
return Promise.resolve();
|
|
188
302
|
try {
|
|
189
303
|
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
|
|
190
304
|
}
|
|
191
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 { }
|
|
192
331
|
}
|
|
193
332
|
async httpRequest(method, params, timeoutMs) {
|
|
194
333
|
const controller = new AbortController();
|
|
195
334
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
335
|
+
const id = this.nextId++;
|
|
196
336
|
try {
|
|
197
337
|
const res = await fetch(this.config.url, {
|
|
198
338
|
method: "POST",
|
|
199
|
-
headers:
|
|
200
|
-
|
|
201
|
-
Accept: "application/json, text/event-stream",
|
|
202
|
-
...(this.config.headers || {}),
|
|
203
|
-
},
|
|
204
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: this.nextId++, method, params }),
|
|
339
|
+
headers: this.cabecerasHttp(),
|
|
340
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
|
|
205
341
|
signal: controller.signal,
|
|
206
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;
|
|
207
349
|
if (!res.ok)
|
|
208
350
|
throw new Error(`HTTP ${res.status}`);
|
|
209
351
|
const text = await res.text();
|
|
210
|
-
|
|
211
|
-
const body = text.includes("data:")
|
|
212
|
-
? text
|
|
213
|
-
.split("\n")
|
|
214
|
-
.filter((l) => l.startsWith("data:"))
|
|
215
|
-
.map((l) => l.slice(5).trim())
|
|
216
|
-
.filter(Boolean)
|
|
217
|
-
.pop() || "{}"
|
|
218
|
-
: text;
|
|
219
|
-
const parsed = JSON.parse(body);
|
|
352
|
+
const parsed = parseHttpBody(text, res.headers.get("content-type"), id);
|
|
220
353
|
if (parsed.error)
|
|
221
354
|
throw new Error(parsed.error.message || JSON.stringify(parsed.error));
|
|
222
355
|
return parsed.result;
|
|
@@ -232,7 +365,10 @@ export class McpClient {
|
|
|
232
365
|
capabilities: { tools: {} },
|
|
233
366
|
clientInfo: { name: "chocolatito-code", version: "1.0.0" },
|
|
234
367
|
}, timeoutMs);
|
|
235
|
-
|
|
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", {});
|
|
236
372
|
}
|
|
237
373
|
async listTools(timeoutMs) {
|
|
238
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. */
|
|
@@ -20,6 +20,17 @@ export function buildSystemPrompt(context, model, skillManager, memoryManager, i
|
|
|
20
20
|
const rulesSection = context.rulesContent
|
|
21
21
|
? `\n\n--- REGLAS DEL PROYECTO (CHOCOLATITO.md) ---\n${context.rulesContent}\n------------------------------------------`
|
|
22
22
|
: "";
|
|
23
|
+
// El mapa del repositorio va aqui, antes de las reglas de conducta, porque es
|
|
24
|
+
// lo que le dice al agente que existe en el proyecto. Sin el, para saber que
|
|
25
|
+
// hay una funcion tiene que buscarla, y para buscarla tiene que sospechar que
|
|
26
|
+
// existe: de ahi los turnos gastados en grep_search antes de tocar nada.
|
|
27
|
+
const mapaSection = context.repoMap
|
|
28
|
+
? `--- BLOQUE 3b: MAPA DEL REPOSITORIO ---\n` +
|
|
29
|
+
`Indice de los simbolos mas relevantes del proyecto, ordenado por cuanto los usa el resto\n` +
|
|
30
|
+
`del codigo. Son NOMBRES Y LINEAS, no contenido: sirve para saber que existe y donde mirar,\n` +
|
|
31
|
+
`no para editar a ciegas. Antes de tocar cualquiera de estos archivos, leelo con 'view_file'.\n` +
|
|
32
|
+
`Puede estar desactualizado respecto al disco; manda siempre lo que leas.\n\n${context.repoMap}\n\n`
|
|
33
|
+
: "";
|
|
23
34
|
const planModeNotice = isPlanMode
|
|
24
35
|
? `\n\n⚠️ MODO PLAN ACTIVO (SOLO LECTURA): No intentes escribir ni modificar archivos. Investiga el código con herramientas de lectura y genera un plan arquitectónico estructurado para que el usuario lo apruebe.`
|
|
25
36
|
: "";
|
|
@@ -44,7 +55,7 @@ ${memoryIndex}
|
|
|
44
55
|
Tienes acceso a skills especializados. Usa la herramienta 'use_skill' para cargar las instrucciones completas de un skill cuando lo necesites:
|
|
45
56
|
${skillsListing}
|
|
46
57
|
|
|
47
|
-
--- BLOQUE 4: SUBAGENTES DISPONIBLES ---
|
|
58
|
+
${mapaSection}--- BLOQUE 4: SUBAGENTES DISPONIBLES ---
|
|
48
59
|
Puedes delegar tareas aisladas con 'spawn_agent':
|
|
49
60
|
- 'explore': Investigación y búsqueda de código de solo lectura sin saturar el contexto principal.
|
|
50
61
|
- 'plan': Generación de planes arquitectónicos.
|
|
@@ -13,6 +13,18 @@ export declare class SessionManager {
|
|
|
13
13
|
saveSession(session: SessionData): void;
|
|
14
14
|
loadLatestSession(): SessionData | null;
|
|
15
15
|
loadSession(id: string): SessionData | null;
|
|
16
|
+
/**
|
|
17
|
+
* Sesiones guardadas, de la mas reciente a la mas antigua.
|
|
18
|
+
*
|
|
19
|
+
* El try/catch envolvia el map ENTERO, asi que un solo archivo corrupto
|
|
20
|
+
* -una sesion que se estaba escribiendo cuando se cerro el portatil, o un
|
|
21
|
+
* JSON truncado- hacia saltar la excepcion y devolver la lista vacia. El
|
|
22
|
+
* usuario con treinta sesiones guardadas abria /resume y no veia ninguna, y
|
|
23
|
+
* no habia forma de saber por que: sus sesiones seguian en el disco.
|
|
24
|
+
*
|
|
25
|
+
* Ahora cada archivo se lee por su cuenta. Uno malo se salta; los demas se
|
|
26
|
+
* listan.
|
|
27
|
+
*/
|
|
16
28
|
listSessions(): Array<{
|
|
17
29
|
id: string;
|
|
18
30
|
updatedAt: string;
|
package/dist/sessions/manager.js
CHANGED
|
@@ -37,18 +37,46 @@ export class SessionManager {
|
|
|
37
37
|
}
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Sesiones guardadas, de la mas reciente a la mas antigua.
|
|
42
|
+
*
|
|
43
|
+
* El try/catch envolvia el map ENTERO, asi que un solo archivo corrupto
|
|
44
|
+
* -una sesion que se estaba escribiendo cuando se cerro el portatil, o un
|
|
45
|
+
* JSON truncado- hacia saltar la excepcion y devolver la lista vacia. El
|
|
46
|
+
* usuario con treinta sesiones guardadas abria /resume y no veia ninguna, y
|
|
47
|
+
* no habia forma de saber por que: sus sesiones seguian en el disco.
|
|
48
|
+
*
|
|
49
|
+
* Ahora cada archivo se lee por su cuenta. Uno malo se salta; los demas se
|
|
50
|
+
* listan.
|
|
51
|
+
*/
|
|
40
52
|
listSessions() {
|
|
41
53
|
if (!fs.existsSync(this.sessionsDir))
|
|
42
54
|
return [];
|
|
55
|
+
let files;
|
|
43
56
|
try {
|
|
44
|
-
|
|
45
|
-
return files.map((f) => {
|
|
46
|
-
const data = JSON.parse(fs.readFileSync(path.join(this.sessionsDir, f), "utf-8"));
|
|
47
|
-
return { id: data.id, updatedAt: data.updatedAt, cwd: data.cwd };
|
|
48
|
-
});
|
|
57
|
+
files = fs.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".json") && f !== "latest.json");
|
|
49
58
|
}
|
|
50
59
|
catch {
|
|
51
60
|
return [];
|
|
52
61
|
}
|
|
62
|
+
const sesiones = [];
|
|
63
|
+
for (const f of files) {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(fs.readFileSync(path.join(this.sessionsDir, f), "utf-8"));
|
|
66
|
+
// Sin id no se puede reanudar, asi que listarla solo seria ofrecer algo
|
|
67
|
+
// que no funciona.
|
|
68
|
+
if (!data || typeof data.id !== "string")
|
|
69
|
+
continue;
|
|
70
|
+
sesiones.push({
|
|
71
|
+
id: data.id,
|
|
72
|
+
updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "",
|
|
73
|
+
cwd: typeof data.cwd === "string" ? data.cwd : "",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Archivo ilegible: se salta este, no la lista.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return sesiones.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
53
81
|
}
|
|
54
82
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { SessionData } from "./manager.js";
|
|
3
|
+
/** Lo que hay que aplicar al agente para reanudar una sesion guardada. */
|
|
4
|
+
export interface RestauracionSesion {
|
|
5
|
+
/** Directorio en el que debe quedar el agente. */
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** Motor que estaba activo al guardar, o el actual si aquel ya no existe. */
|
|
8
|
+
modelId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Historial sin ningun mensaje de sistema. El prompt se reconstruye fuera,
|
|
11
|
+
* con el entorno de HOY, y se antepone a esta lista.
|
|
12
|
+
*/
|
|
13
|
+
conversacion: OpenAI.Chat.ChatCompletionMessageParam[];
|
|
14
|
+
/** El directorio guardado ya no existe: se sigue donde se estaba. */
|
|
15
|
+
directorioPerdido: boolean;
|
|
16
|
+
/** El motor guardado ya no esta en el catalogo: se sigue con el actual. */
|
|
17
|
+
motorPerdido: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Separa lo que se restaura de una sesion (la conversacion) de lo que se
|
|
21
|
+
* reconstruye (el entorno).
|
|
22
|
+
*
|
|
23
|
+
* El JSON guarda el historial ENTERO y su primer mensaje es el prompt de
|
|
24
|
+
* sistema que se armo al abrir aquella sesion: fecha, directorio, reglas de
|
|
25
|
+
* CHOCOLATITO.md, indice de memoria y listado de skills de aquel dia. Volcar el
|
|
26
|
+
* array tal cual devolvia al modelo a un entorno que ya no existe —anunciaba el
|
|
27
|
+
* cwd viejo y la fecha vieja— mientras las herramientas seguian resolviendo
|
|
28
|
+
* rutas contra el cwd real: pedia un archivo creyendo estar en un proyecto y
|
|
29
|
+
* editaba el del otro. Por eso aqui se tira todo mensaje de sistema y se
|
|
30
|
+
* devuelven ademas el directorio y el motor de la sesion, que hasta ahora se
|
|
31
|
+
* escribian en el JSON y no los leia nadie.
|
|
32
|
+
*/
|
|
33
|
+
export declare function prepararRestauracion(sesion: SessionData, actual: {
|
|
34
|
+
cwd: string;
|
|
35
|
+
modelId: string;
|
|
36
|
+
}, motores?: Record<string, unknown>): RestauracionSesion;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { ESPECTRO_MODELS } from "../config/constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Separa lo que se restaura de una sesion (la conversacion) de lo que se
|
|
5
|
+
* reconstruye (el entorno).
|
|
6
|
+
*
|
|
7
|
+
* El JSON guarda el historial ENTERO y su primer mensaje es el prompt de
|
|
8
|
+
* sistema que se armo al abrir aquella sesion: fecha, directorio, reglas de
|
|
9
|
+
* CHOCOLATITO.md, indice de memoria y listado de skills de aquel dia. Volcar el
|
|
10
|
+
* array tal cual devolvia al modelo a un entorno que ya no existe —anunciaba el
|
|
11
|
+
* cwd viejo y la fecha vieja— mientras las herramientas seguian resolviendo
|
|
12
|
+
* rutas contra el cwd real: pedia un archivo creyendo estar en un proyecto y
|
|
13
|
+
* editaba el del otro. Por eso aqui se tira todo mensaje de sistema y se
|
|
14
|
+
* devuelven ademas el directorio y el motor de la sesion, que hasta ahora se
|
|
15
|
+
* escribian en el JSON y no los leia nadie.
|
|
16
|
+
*/
|
|
17
|
+
export function prepararRestauracion(sesion, actual, motores = ESPECTRO_MODELS) {
|
|
18
|
+
const mensajes = Array.isArray(sesion.messages) ? sesion.messages : [];
|
|
19
|
+
// El directorio guardado manda mientras siga existiendo; si lo borraron o
|
|
20
|
+
// esta en un disco que hoy no esta montado, quedarse en el actual es lo unico
|
|
21
|
+
// que no deja al agente apuntando a la nada.
|
|
22
|
+
const directorioPerdido = !sesion.cwd || !existeDirectorio(sesion.cwd);
|
|
23
|
+
const cwd = directorioPerdido ? actual.cwd : sesion.cwd;
|
|
24
|
+
// Igual con el motor: un id de una version vieja del catalogo no puede
|
|
25
|
+
// dejar `currentModel` en undefined.
|
|
26
|
+
const motorPerdido = !sesion.modelId || !motores[sesion.modelId];
|
|
27
|
+
const modelId = motorPerdido ? actual.modelId : sesion.modelId;
|
|
28
|
+
return {
|
|
29
|
+
cwd,
|
|
30
|
+
modelId,
|
|
31
|
+
conversacion: mensajes.filter((m) => m.role !== "system"),
|
|
32
|
+
directorioPerdido,
|
|
33
|
+
motorPerdido,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function existeDirectorio(ruta) {
|
|
37
|
+
try {
|
|
38
|
+
return fs.statSync(ruta).isDirectory();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/skills/manager.d.ts
CHANGED