chocolatito-code 1.0.0 → 1.1.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 +89 -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 +536 -78
- 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/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/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 +222 -34
- package/dist/mcp/client.d.ts +22 -0
- package/dist/mcp/client.js +93 -11
- 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 +202 -18
- 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 +9 -3
- 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/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 +57 -4
- 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.js +18 -1
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +56 -5
- package/dist/ui/keyboardGuard.d.ts +55 -0
- package/dist/ui/keyboardGuard.js +65 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +35 -0
- package/dist/ui/prompt.js +345 -125
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/extension/README.md +95 -74
- package/extension/background.js +598 -178
- package/extension/content.js +92 -9
- package/extension/manifest.json +2 -1
- package/extension/popup.js +28 -12
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESTADO DE LA SESION DE NAVEGADOR
|
|
3
|
+
*
|
|
4
|
+
* Un solo booleano, en un modulo propio, por una razon concreta: la tabla de
|
|
5
|
+
* permisos necesita saber si hay un Chrome ya enganchado y no puede importar
|
|
6
|
+
* browserCdp para averiguarlo (arrastraria puppeteer-core entero dentro de
|
|
7
|
+
* config/, y ademas browserCdp acabaria importando permisos: ciclo).
|
|
8
|
+
*
|
|
9
|
+
* Por que hace falta el dato: browserCdp clasificaba `attach`, `tabs`,
|
|
10
|
+
* `snapshot`, `get_text`, `screenshot`, `wait_for` y `console` como "solo
|
|
11
|
+
* observar", pero TODAS pasan por ensureBrowser(), que si no encuentra un Chrome
|
|
12
|
+
* escuchando el puerto de depuracion LANZA uno con spawn(detached). O sea que la
|
|
13
|
+
* accion mas inocente de la lista arrancaba un proceso que sobrevive a la CLI, y
|
|
14
|
+
* con useSystemProfile lo arrancaba sobre el perfil real del usuario con el
|
|
15
|
+
* 9222 abierto en 127.0.0.1, sin un solo dialogo y tambien en modo plan.
|
|
16
|
+
*
|
|
17
|
+
* Mirar solo es gratis cuando NO hay nada que arrancar. Ese es el matiz que
|
|
18
|
+
* guarda este flag.
|
|
19
|
+
*/
|
|
20
|
+
/** Lo llama browserCdp cada vez que se conecta o se suelta un navegador. */
|
|
21
|
+
export declare function setBrowserAttached(valor: boolean): void;
|
|
22
|
+
/** true solo si hay una sesion CDP viva: entonces observar no arranca nada. */
|
|
23
|
+
export declare function isBrowserAttached(): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESTADO DE LA SESION DE NAVEGADOR
|
|
3
|
+
*
|
|
4
|
+
* Un solo booleano, en un modulo propio, por una razon concreta: la tabla de
|
|
5
|
+
* permisos necesita saber si hay un Chrome ya enganchado y no puede importar
|
|
6
|
+
* browserCdp para averiguarlo (arrastraria puppeteer-core entero dentro de
|
|
7
|
+
* config/, y ademas browserCdp acabaria importando permisos: ciclo).
|
|
8
|
+
*
|
|
9
|
+
* Por que hace falta el dato: browserCdp clasificaba `attach`, `tabs`,
|
|
10
|
+
* `snapshot`, `get_text`, `screenshot`, `wait_for` y `console` como "solo
|
|
11
|
+
* observar", pero TODAS pasan por ensureBrowser(), que si no encuentra un Chrome
|
|
12
|
+
* escuchando el puerto de depuracion LANZA uno con spawn(detached). O sea que la
|
|
13
|
+
* accion mas inocente de la lista arrancaba un proceso que sobrevive a la CLI, y
|
|
14
|
+
* con useSystemProfile lo arrancaba sobre el perfil real del usuario con el
|
|
15
|
+
* 9222 abierto en 127.0.0.1, sin un solo dialogo y tambien en modo plan.
|
|
16
|
+
*
|
|
17
|
+
* Mirar solo es gratis cuando NO hay nada que arrancar. Ese es el matiz que
|
|
18
|
+
* guarda este flag.
|
|
19
|
+
*/
|
|
20
|
+
let enganchado = false;
|
|
21
|
+
/** Lo llama browserCdp cada vez que se conecta o se suelta un navegador. */
|
|
22
|
+
export function setBrowserAttached(valor) {
|
|
23
|
+
enganchado = valor;
|
|
24
|
+
}
|
|
25
|
+
/** true solo si hay una sesion CDP viva: entonces observar no arranca nada. */
|
|
26
|
+
export function isBrowserAttached() {
|
|
27
|
+
return enganchado;
|
|
28
|
+
}
|
|
@@ -412,10 +412,42 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
412
412
|
const combo = normalizeKeys(params.keys || params.key || params.text || "");
|
|
413
413
|
if (!combo)
|
|
414
414
|
return fail(action, 'indica la tecla en "keys" (ej. "Return", "ctrl+t", "alt+Tab").');
|
|
415
|
+
// Un atajo sale por SendInput, que entra SIEMPRE por la ventana con el
|
|
416
|
+
// foco. No hay atajo "de fondo": PostMessage no deja las modificadoras
|
|
417
|
+
// en el estado de teclado del destino, asi que ctrl+s llegaria como una
|
|
418
|
+
// "s" suelta. Hasta aqui `window` se aceptaba y se tiraba a la basura:
|
|
419
|
+
// el modelo pedia ctrl+w "en el Bloc de notas" y la combinacion caia en
|
|
420
|
+
// el Word que el usuario tenia delante; con alt+F4 le cerraba la
|
|
421
|
+
// aplicacion. Ahora, si se pide una ventana, se enfoca antes de teclear
|
|
422
|
+
// y si no se puede no se manda nada.
|
|
423
|
+
let destino = "la ventana enfocada";
|
|
424
|
+
if (params.window) {
|
|
425
|
+
const elegida = await resolveWindow(params);
|
|
426
|
+
if (!elegida.ok)
|
|
427
|
+
return fail(action, elegida.error);
|
|
428
|
+
// Mismo corte que focus_window: enfocar y teclear a ciegas entre
|
|
429
|
+
// ventanas empatadas es como se cierra la que no era.
|
|
430
|
+
if (elegida.matches > 1) {
|
|
431
|
+
return fail(action, `"${params.window}" coincide con ${elegida.matches} ventanas (${elegida.candidates}). ` +
|
|
432
|
+
`Un atajo no se deshace: usa list_windows y pasa un titulo mas concreto.`);
|
|
433
|
+
}
|
|
434
|
+
const enfocada = await winHost.send("focus_window", {
|
|
435
|
+
...pinned(elegida),
|
|
436
|
+
...(elegida.hwnd ? {} : windowArgs(params)),
|
|
437
|
+
});
|
|
438
|
+
if (!enfocada.ok) {
|
|
439
|
+
return fail(action, `no se pudo poner delante "${elegida.title || params.window}" para mandarle "${combo}" ` +
|
|
440
|
+
`(ahora manda "${enfocada.foreground || "?"}"). No se envia nada: iria a la ventana equivocada.`);
|
|
441
|
+
}
|
|
442
|
+
// El foco no cambia en el mismo instante en que la API dice que si:
|
|
443
|
+
// sin esta pausa la primera pulsacion se la queda la ventana anterior.
|
|
444
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
445
|
+
destino = `"${enfocada.title || elegida.title || params.window}"`;
|
|
446
|
+
}
|
|
415
447
|
const res = await winHost.send("key", { keys: combo, repeat: params.repeat ?? 1 });
|
|
416
448
|
if (!res.ok)
|
|
417
449
|
return fail(action, res.error || "fallo desconocido");
|
|
418
|
-
return `Tecla "${combo}"${res.repeat > 1 ? ` x${res.repeat}` : ""} enviada a
|
|
450
|
+
return `Tecla "${combo}"${res.repeat > 1 ? ` x${res.repeat}` : ""} enviada a ${destino}.`;
|
|
419
451
|
}
|
|
420
452
|
case "open_app": {
|
|
421
453
|
const target = params.appName || params.text;
|
|
@@ -1 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genera una imagen a partir de un SVG o de un script de Python.
|
|
3
|
+
*
|
|
4
|
+
* La segunda rama no dibuja: guarda el texto que ha escrito el modelo en un .py
|
|
5
|
+
* y lo ejecuta entero. Un `shutil.rmtree(...)` metido ahi borra el arbol igual
|
|
6
|
+
* que por run_command, solo que con nombre de herramienta de dibujo. Por eso
|
|
7
|
+
* `permissions.ts` la clasifica como `exec` (pregunta salvo allowlist) y la
|
|
8
|
+
* bloquea en modo plan, y por eso el SVG se detecta con `isSvgSource`, la misma
|
|
9
|
+
* funcion que mira la tabla de permisos: dos detecciones distintas serian dos
|
|
10
|
+
* criterios distintos sobre que se ejecuta y que no.
|
|
11
|
+
*/
|
|
1
12
|
export declare function createImage(outputPath: string, generatorScript: string, cwd?: string): Promise<string>;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execa } from "execa";
|
|
4
|
+
import { isSvgSource } from "../config/permissions.js";
|
|
5
|
+
/**
|
|
6
|
+
* Genera una imagen a partir de un SVG o de un script de Python.
|
|
7
|
+
*
|
|
8
|
+
* La segunda rama no dibuja: guarda el texto que ha escrito el modelo en un .py
|
|
9
|
+
* y lo ejecuta entero. Un `shutil.rmtree(...)` metido ahi borra el arbol igual
|
|
10
|
+
* que por run_command, solo que con nombre de herramienta de dibujo. Por eso
|
|
11
|
+
* `permissions.ts` la clasifica como `exec` (pregunta salvo allowlist) y la
|
|
12
|
+
* bloquea en modo plan, y por eso el SVG se detecta con `isSvgSource`, la misma
|
|
13
|
+
* funcion que mira la tabla de permisos: dos detecciones distintas serian dos
|
|
14
|
+
* criterios distintos sobre que se ejecuta y que no.
|
|
15
|
+
*/
|
|
4
16
|
export async function createImage(outputPath, generatorScript, cwd = process.cwd()) {
|
|
5
17
|
const resolved = path.isAbsolute(outputPath) ? outputPath : path.resolve(cwd, outputPath);
|
|
6
18
|
const outDir = path.dirname(resolved);
|
|
@@ -8,7 +20,7 @@ export async function createImage(outputPath, generatorScript, cwd = process.cwd
|
|
|
8
20
|
fs.mkdirSync(outDir, { recursive: true });
|
|
9
21
|
}
|
|
10
22
|
// If generatorScript is SVG, save directly
|
|
11
|
-
if (
|
|
23
|
+
if (isSvgSource(generatorScript)) {
|
|
12
24
|
fs.writeFileSync(resolved, generatorScript, "utf-8");
|
|
13
25
|
return `✔ Imagen SVG generada y guardada en "${outputPath}" (${fs.statSync(resolved).size} bytes).`;
|
|
14
26
|
}
|
|
@@ -205,7 +205,7 @@ export const TOOLS = [
|
|
|
205
205
|
type: "function",
|
|
206
206
|
function: {
|
|
207
207
|
name: "write_file",
|
|
208
|
-
description: "Crea un archivo nuevo o sobreescribe uno existente con el contenido provisto.",
|
|
208
|
+
description: "Crea un archivo nuevo o sobreescribe uno existente con el contenido provisto. Si el archivo ya existe hay que haberlo leido antes con view_file: sobreescribir borra todo lo que hubiera dentro.",
|
|
209
209
|
parameters: {
|
|
210
210
|
type: "object",
|
|
211
211
|
properties: {
|
|
@@ -226,7 +226,7 @@ export const TOOLS = [
|
|
|
226
226
|
type: "function",
|
|
227
227
|
function: {
|
|
228
228
|
name: "delete_file",
|
|
229
|
-
description: "Elimina un archivo o directorio
|
|
229
|
+
description: "Elimina un archivo o directorio. El borrado es permanente: /undo solo puede devolver archivos sueltos, nunca un directorio ni un archivo bloqueado por otro proceso.",
|
|
230
230
|
parameters: {
|
|
231
231
|
type: "object",
|
|
232
232
|
properties: {
|
|
@@ -243,7 +243,8 @@ export const TOOLS = [
|
|
|
243
243
|
type: "function",
|
|
244
244
|
function: {
|
|
245
245
|
name: "move_copy_file",
|
|
246
|
-
description: "Mueve, renombra o copia archivos y directorios."
|
|
246
|
+
description: "Mueve, renombra o copia archivos y directorios. Si el destino ya existe la llamada se corta sin tocar el disco: " +
|
|
247
|
+
"reemplazarlo borra lo que hubiera ahi y exige overwrite=true, que el usuario tiene que confirmar.",
|
|
247
248
|
parameters: {
|
|
248
249
|
type: "object",
|
|
249
250
|
properties: {
|
|
@@ -260,6 +261,11 @@ export const TOOLS = [
|
|
|
260
261
|
enum: ["move", "copy"],
|
|
261
262
|
description: "Operación a realizar.",
|
|
262
263
|
},
|
|
264
|
+
overwrite: {
|
|
265
|
+
type: "boolean",
|
|
266
|
+
description: "Solo si de verdad quieres reemplazar un destino que ya existe: borra su contenido anterior. " +
|
|
267
|
+
"Por defecto false, y sin él la operación se rechaza en vez de pisar nada.",
|
|
268
|
+
},
|
|
263
269
|
},
|
|
264
270
|
required: ["sourcePath", "destinationPath"],
|
|
265
271
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Que se le contesta al modelo cuando un edit_file no encuentra su fragmento.
|
|
3
|
+
*
|
|
4
|
+
* El mensaje anterior era "No se encontro el fragmento de texto. Asegurate de
|
|
5
|
+
* copiar las lineas exactas": cierto, inutil y, sobre todo, IDENTICO cada vez.
|
|
6
|
+
* Un mensaje que no aporta informacion nueva invita a reintentar lo mismo, y
|
|
7
|
+
* cada reintento cuesta una iteracion y dinero de la cuota.
|
|
8
|
+
*
|
|
9
|
+
* Aider resuelve esto devolviendo el tramo real mas parecido ("did you mean
|
|
10
|
+
* these actual lines?"), y Crush pintando los espacios invisibles. Las dos
|
|
11
|
+
* cosas atacan el mismo problema: el modelo no puede corregir una diferencia
|
|
12
|
+
* que no ve.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Coeficiente de Dice sobre bigramas de caracteres.
|
|
16
|
+
*
|
|
17
|
+
* Se usa en vez de una distancia de edicion porque es lineal y aqui hay que
|
|
18
|
+
* puntuar una ventana por cada linea del archivo. Da lo mismo que el ratio de
|
|
19
|
+
* SequenceMatcher para lo unico que se necesita: ordenar candidatos.
|
|
20
|
+
*/
|
|
21
|
+
export declare function similitud(a: string, b: string): number;
|
|
22
|
+
export interface TramoParecido {
|
|
23
|
+
/** Numero de la primera linea del tramo, 1-based. */
|
|
24
|
+
primeraLinea: number;
|
|
25
|
+
lineas: string[];
|
|
26
|
+
parecido: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Ventana del archivo mas parecida al fragmento buscado.
|
|
30
|
+
*
|
|
31
|
+
* Se comparan ventanas del MISMO numero de lineas que el fragmento, deslizando
|
|
32
|
+
* una a una. Si el mejor candidato no llega al umbral, no se devuelve nada:
|
|
33
|
+
* ensenar un tramo al azar es peor que no ensenar ninguno, porque el modelo se
|
|
34
|
+
* lo cree y edita ahi.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tramoMasParecido(contenido: string, fragmento: string, umbral?: number): TramoParecido | null;
|
|
37
|
+
/**
|
|
38
|
+
* Pinta la sangria y los espacios finales, que son invisibles y son justo lo
|
|
39
|
+
* que falla. El interior de la linea se deja tal cual: marcarlo entero hace el
|
|
40
|
+
* texto ilegible y no anade informacion.
|
|
41
|
+
*/
|
|
42
|
+
export declare function marcarEspacios(texto: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Mensaje de error de edit_file cuando no hay coincidencia.
|
|
45
|
+
*
|
|
46
|
+
* Tres piezas, en orden de utilidad:
|
|
47
|
+
* 1. "esto ya esta aplicado" corta en seco el bucle de reaplicar un cambio que
|
|
48
|
+
* ya se hizo, que es el atasco mas caro y el mas frecuente;
|
|
49
|
+
* 2. el tramo real mas parecido, con numeros de linea y los espacios pintados;
|
|
50
|
+
* 3. que reenvie SOLO este bloque.
|
|
51
|
+
*/
|
|
52
|
+
export declare function diagnosticarFallo(rutaMostrada: string, contenido: string, viejo: string, nuevo: string): string;
|
|
53
|
+
/** Mensaje cuando el fragmento aparece varias veces. */
|
|
54
|
+
export declare function diagnosticarAmbiguedad(rutaMostrada: string, ocurrencias: number, estrategia: string): string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Que se le contesta al modelo cuando un edit_file no encuentra su fragmento.
|
|
3
|
+
*
|
|
4
|
+
* El mensaje anterior era "No se encontro el fragmento de texto. Asegurate de
|
|
5
|
+
* copiar las lineas exactas": cierto, inutil y, sobre todo, IDENTICO cada vez.
|
|
6
|
+
* Un mensaje que no aporta informacion nueva invita a reintentar lo mismo, y
|
|
7
|
+
* cada reintento cuesta una iteracion y dinero de la cuota.
|
|
8
|
+
*
|
|
9
|
+
* Aider resuelve esto devolviendo el tramo real mas parecido ("did you mean
|
|
10
|
+
* these actual lines?"), y Crush pintando los espacios invisibles. Las dos
|
|
11
|
+
* cosas atacan el mismo problema: el modelo no puede corregir una diferencia
|
|
12
|
+
* que no ve.
|
|
13
|
+
*/
|
|
14
|
+
/** Numero de lineas de contexto que se ensenan alrededor del tramo candidato. */
|
|
15
|
+
const CONTEXTO = 3;
|
|
16
|
+
/** Similitud minima para molestarse en ensenar un tramo. */
|
|
17
|
+
const UMBRAL_PARECIDO = 0.6;
|
|
18
|
+
/**
|
|
19
|
+
* Coeficiente de Dice sobre bigramas de caracteres.
|
|
20
|
+
*
|
|
21
|
+
* Se usa en vez de una distancia de edicion porque es lineal y aqui hay que
|
|
22
|
+
* puntuar una ventana por cada linea del archivo. Da lo mismo que el ratio de
|
|
23
|
+
* SequenceMatcher para lo unico que se necesita: ordenar candidatos.
|
|
24
|
+
*/
|
|
25
|
+
export function similitud(a, b) {
|
|
26
|
+
if (a === b)
|
|
27
|
+
return 1;
|
|
28
|
+
if (a.length < 2 || b.length < 2)
|
|
29
|
+
return 0;
|
|
30
|
+
const bigramas = new Map();
|
|
31
|
+
for (let i = 0; i < a.length - 1; i++) {
|
|
32
|
+
const par = a.slice(i, i + 2);
|
|
33
|
+
bigramas.set(par, (bigramas.get(par) || 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
let comunes = 0;
|
|
36
|
+
for (let i = 0; i < b.length - 1; i++) {
|
|
37
|
+
const par = b.slice(i, i + 2);
|
|
38
|
+
const quedan = bigramas.get(par) || 0;
|
|
39
|
+
if (quedan > 0) {
|
|
40
|
+
bigramas.set(par, quedan - 1);
|
|
41
|
+
comunes++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return (2 * comunes) / (a.length - 1 + b.length - 1);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ventana del archivo mas parecida al fragmento buscado.
|
|
48
|
+
*
|
|
49
|
+
* Se comparan ventanas del MISMO numero de lineas que el fragmento, deslizando
|
|
50
|
+
* una a una. Si el mejor candidato no llega al umbral, no se devuelve nada:
|
|
51
|
+
* ensenar un tramo al azar es peor que no ensenar ninguno, porque el modelo se
|
|
52
|
+
* lo cree y edita ahi.
|
|
53
|
+
*/
|
|
54
|
+
export function tramoMasParecido(contenido, fragmento, umbral = UMBRAL_PARECIDO) {
|
|
55
|
+
const lineasArchivo = contenido.split("\n");
|
|
56
|
+
const lineasFragmento = fragmento.split("\n");
|
|
57
|
+
const ventana = lineasFragmento.length;
|
|
58
|
+
if (ventana === 0 || lineasArchivo.length < ventana)
|
|
59
|
+
return null;
|
|
60
|
+
const buscado = lineasFragmento.map((l) => l.trim()).join("\n");
|
|
61
|
+
if (!buscado.trim())
|
|
62
|
+
return null;
|
|
63
|
+
let mejor = null;
|
|
64
|
+
for (let i = 0; i + ventana <= lineasArchivo.length; i++) {
|
|
65
|
+
const trozo = lineasArchivo.slice(i, i + ventana);
|
|
66
|
+
const parecido = similitud(buscado, trozo.map((l) => l.trim()).join("\n"));
|
|
67
|
+
if (!mejor || parecido > mejor.parecido) {
|
|
68
|
+
mejor = { primeraLinea: i + 1, lineas: trozo, parecido };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!mejor || mejor.parecido < umbral)
|
|
72
|
+
return null;
|
|
73
|
+
// Se amplia con contexto para que el modelo pueda reconstruir un fragmento
|
|
74
|
+
// unico, no solo el tramo justo.
|
|
75
|
+
const desde = Math.max(0, mejor.primeraLinea - 1 - CONTEXTO);
|
|
76
|
+
const hasta = Math.min(lineasArchivo.length, mejor.primeraLinea - 1 + ventana + CONTEXTO);
|
|
77
|
+
return {
|
|
78
|
+
primeraLinea: desde + 1,
|
|
79
|
+
lineas: lineasArchivo.slice(desde, hasta),
|
|
80
|
+
parecido: mejor.parecido,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Pinta la sangria y los espacios finales, que son invisibles y son justo lo
|
|
85
|
+
* que falla. El interior de la linea se deja tal cual: marcarlo entero hace el
|
|
86
|
+
* texto ilegible y no anade informacion.
|
|
87
|
+
*/
|
|
88
|
+
export function marcarEspacios(texto) {
|
|
89
|
+
return texto
|
|
90
|
+
.split("\n")
|
|
91
|
+
.map((linea) => {
|
|
92
|
+
const inicio = linea.match(/^[ \t]*/)?.[0] ?? "";
|
|
93
|
+
const resto = linea.slice(inicio.length);
|
|
94
|
+
const finalEspacios = resto.match(/[ \t]*$/)?.[0] ?? "";
|
|
95
|
+
const cuerpo = finalEspacios ? resto.slice(0, resto.length - finalEspacios.length) : resto;
|
|
96
|
+
const pintar = (s) => s.replace(/\t/g, "→").replace(/ /g, "·");
|
|
97
|
+
return pintar(inicio) + cuerpo + pintar(finalEspacios);
|
|
98
|
+
})
|
|
99
|
+
.join("\n");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Mensaje de error de edit_file cuando no hay coincidencia.
|
|
103
|
+
*
|
|
104
|
+
* Tres piezas, en orden de utilidad:
|
|
105
|
+
* 1. "esto ya esta aplicado" corta en seco el bucle de reaplicar un cambio que
|
|
106
|
+
* ya se hizo, que es el atasco mas caro y el mas frecuente;
|
|
107
|
+
* 2. el tramo real mas parecido, con numeros de linea y los espacios pintados;
|
|
108
|
+
* 3. que reenvie SOLO este bloque.
|
|
109
|
+
*/
|
|
110
|
+
export function diagnosticarFallo(rutaMostrada, contenido, viejo, nuevo) {
|
|
111
|
+
const partes = [
|
|
112
|
+
`Error: no se encontro el fragmento a reemplazar en "${rutaMostrada}".`,
|
|
113
|
+
];
|
|
114
|
+
if (nuevo.trim() && contenido.includes(nuevo)) {
|
|
115
|
+
partes.push(`El texto NUEVO ya esta en el archivo: este cambio parece hecho. ` +
|
|
116
|
+
`Comprueba con view_file antes de volver a intentarlo, y si ya esta, sigue con lo siguiente.`);
|
|
117
|
+
return partes.join("\n\n");
|
|
118
|
+
}
|
|
119
|
+
const tramo = tramoMasParecido(contenido, viejo);
|
|
120
|
+
if (tramo) {
|
|
121
|
+
const numeradas = tramo.lineas
|
|
122
|
+
.map((l, i) => `${String(tramo.primeraLinea + i).padStart(5)} | ${marcarEspacios(l)}`)
|
|
123
|
+
.join("\n");
|
|
124
|
+
partes.push(`Lo mas parecido que hay en el archivo (${Math.round(tramo.parecido * 100)}% de coincidencia) ` +
|
|
125
|
+
`esta en la linea ${tramo.primeraLinea}. ¿Querias decir esto?\n\n${numeradas}`);
|
|
126
|
+
partes.push(`Los espacios se ven como "·" y los tabuladores como "→": la diferencia suele estar ahi. ` +
|
|
127
|
+
`Copia las lineas tal cual aparecen arriba.`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
partes.push(`No hay ningun tramo parecido: puede que el archivo no sea el que crees, o que haya ` +
|
|
131
|
+
`cambiado. Leelo con view_file antes de volver a editar.`);
|
|
132
|
+
}
|
|
133
|
+
partes.push(`Reenvia SOLO este bloque corregido. Las demas ediciones que hayan funcionado ya estan ` +
|
|
134
|
+
`aplicadas: no las repitas.`);
|
|
135
|
+
return partes.join("\n\n");
|
|
136
|
+
}
|
|
137
|
+
/** Mensaje cuando el fragmento aparece varias veces. */
|
|
138
|
+
export function diagnosticarAmbiguedad(rutaMostrada, ocurrencias, estrategia) {
|
|
139
|
+
return (`Error: el fragmento aparece ${ocurrencias} veces en "${rutaMostrada}" (coincidencia por ` +
|
|
140
|
+
`${estrategia}). No voy a adivinar cual. Anade lineas de alrededor —la firma de la funcion, ` +
|
|
141
|
+
`el comentario de encima— hasta que el fragmento sea unico.`);
|
|
142
|
+
}
|
package/dist/tools/editFile.d.ts
CHANGED
package/dist/tools/editFile.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { formatNumberedDiff } from "../ui/diffViewer.js";
|
|
4
|
+
import { binaryRefusalReason, quickBinaryReason } from "./binary.js";
|
|
5
|
+
import { localizarFragmento, esRechazo } from "./patchCascade.js";
|
|
6
|
+
import { diagnosticarFallo, diagnosticarAmbiguedad } from "./editDiagnosis.js";
|
|
7
|
+
/**
|
|
8
|
+
* El mensaje empieza por "Error" a proposito: es lo que mira runner.ts para no
|
|
9
|
+
* dar la edicion por buena.
|
|
10
|
+
*/
|
|
11
|
+
function mensajeBinario(filePath, motivo) {
|
|
12
|
+
return (`Error: "${filePath}" ${motivo}. edit_file lee y reescribe el archivo entero como texto UTF-8, ` +
|
|
13
|
+
`asi que editarlo lo dejaria irrecuperable. Usa run_command con la herramienta que corresponda.`);
|
|
14
|
+
}
|
|
4
15
|
export async function editFile(filePath, oldText, newText, cwd = process.cwd()) {
|
|
5
16
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
6
17
|
if (!fs.existsSync(resolved)) {
|
|
@@ -9,80 +20,125 @@ export async function editFile(filePath, oldText, newText, cwd = process.cwd())
|
|
|
9
20
|
message: `Error: El archivo "${filePath}" no existe.`,
|
|
10
21
|
};
|
|
11
22
|
}
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let count = 0;
|
|
24
|
-
let from = 0;
|
|
25
|
-
while (true) {
|
|
26
|
-
const at = normalizedCurrent.indexOf(normalizedOld, from);
|
|
27
|
-
if (at === -1)
|
|
28
|
-
break;
|
|
29
|
-
count++;
|
|
30
|
-
if (count > 1)
|
|
31
|
-
break;
|
|
32
|
-
from = at + normalizedOld.length;
|
|
33
|
-
}
|
|
34
|
-
if (count > 1) {
|
|
35
|
-
return {
|
|
36
|
-
success: false,
|
|
37
|
-
message: `Error: el fragmento aparece mas de una vez en "${filePath}". No voy a adivinar cual. ` +
|
|
38
|
-
`Incluye lineas de alrededor para que el fragmento sea unico.`,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
23
|
+
// Antes se leia directamente con "utf-8" y se reescribia el archivo entero
|
|
24
|
+
// desde ese string: cada byte invalido volvia al disco como EF BF BD, asi que
|
|
25
|
+
// un edit_file con exito sobre un mp4 o un png lo dejaba irrecuperable y
|
|
26
|
+
// respondia "editado exitosamente". Nadie confirma esta escritura en ningun
|
|
27
|
+
// modo de permisos, asi que la unica defensa es negarse aqui.
|
|
28
|
+
//
|
|
29
|
+
// Primero la comprobacion barata (extension y cabecera), porque el propio
|
|
30
|
+
// readFileSync de un video de 500 MB ya es el problema.
|
|
31
|
+
const motivoRapido = quickBinaryReason(resolved);
|
|
32
|
+
if (motivoRapido) {
|
|
33
|
+
return { success: false, message: mensajeBinario(filePath, motivoRapido) };
|
|
41
34
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
35
|
+
const buffer = fs.readFileSync(resolved);
|
|
36
|
+
const motivoBinario = binaryRefusalReason(resolved, buffer);
|
|
37
|
+
if (motivoBinario) {
|
|
38
|
+
return { success: false, message: mensajeBinario(filePath, motivoBinario) };
|
|
39
|
+
}
|
|
40
|
+
const currentContent = buffer.toString("utf-8");
|
|
41
|
+
// El BOM se aparta aqui y se vuelve a poner al escribir. Si se quedara dentro,
|
|
42
|
+
// ensuciaria el primer caracter del archivo y el emparejamiento por lineas
|
|
43
|
+
// nunca casaria con la primera linea.
|
|
44
|
+
const tieneBom = currentContent.charCodeAt(0) === 0xfeff;
|
|
45
|
+
const bom = tieneBom ? currentContent[0] : "";
|
|
46
|
+
const original = bom ? currentContent.slice(1) : currentContent;
|
|
47
|
+
// Terminador dominante del archivo. Solo se usa para las lineas NUEVAS: lo que
|
|
48
|
+
// no se edita se copia byte a byte, incluidos los finales mezclados.
|
|
49
|
+
const crlfCount = (original.match(/\r\n/g) || []).length;
|
|
50
|
+
const lfCount = (original.match(/\n/g) || []).length - crlfCount;
|
|
51
|
+
const eol = crlfCount > lfCount ? "\r\n" : "\n";
|
|
52
|
+
// Normalizar a LF sirve para BUSCAR: el fragmento que manda el modelo casi
|
|
53
|
+
// nunca trae los \r. Pero antes se escribia esa version normalizada, asi que
|
|
54
|
+
// tocar tres lineas de un repo con CRLF reescribia el archivo entero y el diff
|
|
55
|
+
// salia completo. Ahora se guarda a que posicion del original corresponde cada
|
|
56
|
+
// caracter del normalizado, para recortar sobre el original y dejar intacto
|
|
57
|
+
// todo lo que no se edita.
|
|
58
|
+
//
|
|
59
|
+
// Un "\n" que venia de un "\r\n" apunta al "\r", no al "\n": asi un fragmento
|
|
60
|
+
// que termina justo antes del salto no deja huerfano el retorno de carro.
|
|
61
|
+
let normalizedCurrent;
|
|
62
|
+
let mapaOriginal = null;
|
|
63
|
+
if (crlfCount > 0) {
|
|
64
|
+
const trozos = [];
|
|
65
|
+
mapaOriginal = [];
|
|
66
|
+
for (let i = 0; i < original.length; i++) {
|
|
67
|
+
if (original.charCodeAt(i) === 13 && original.charCodeAt(i + 1) === 10) {
|
|
68
|
+
trozos.push("\n");
|
|
69
|
+
mapaOriginal.push(i);
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
64
72
|
}
|
|
73
|
+
trozos.push(original[i]);
|
|
74
|
+
mapaOriginal.push(i);
|
|
65
75
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
normalizedCurrent = trozos.join("");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
normalizedCurrent = original;
|
|
80
|
+
}
|
|
81
|
+
/** Posicion en el archivo original del caracter normalizado numero idx. */
|
|
82
|
+
const aPosicionOriginal = (idx) => {
|
|
83
|
+
if (!mapaOriginal)
|
|
84
|
+
return Math.min(idx, original.length);
|
|
85
|
+
return idx < mapaOriginal.length ? mapaOriginal[idx] : original.length;
|
|
86
|
+
};
|
|
87
|
+
const normalizedOld = oldText.replace(/\r\n/g, "\n");
|
|
88
|
+
const normalizedNew = newText.replace(/\r\n/g, "\n");
|
|
89
|
+
// Un oldText vacio no identifica ningun sitio: "reemplaza la nada" es una
|
|
90
|
+
// orden sin respuesta. Antes se resolvia con indexOf("") === 0, que inserta al
|
|
91
|
+
// principio del archivo y responde "editado exitosamente" en un sitio que
|
|
92
|
+
// nadie ha pedido.
|
|
93
|
+
if (normalizedOld.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
message: `Error: oldText esta vacio, asi que no señala ningun sitio del archivo. ` +
|
|
97
|
+
`Si quieres anadir texto, incluye en oldText la linea de alrededor donde va (y repitela ` +
|
|
98
|
+
`en newText); si quieres crear o reescribir el archivo entero, usa write_file.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// La busqueda ya no es un indexOf con un parche detras, sino una cascada de
|
|
102
|
+
// estrategias que va de la mas estricta a la mas flexible (ver patchCascade).
|
|
103
|
+
// Cada una lleva su propio cerrojo de unicidad, asi que tolerar mas formas de
|
|
104
|
+
// equivocarse no relaja la garantia de "o el sitio es inequivoco, o no se
|
|
105
|
+
// escribe".
|
|
106
|
+
const coincidencia = localizarFragmento(normalizedCurrent, normalizedOld, normalizedNew);
|
|
107
|
+
if (esRechazo(coincidencia)) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
message: diagnosticarAmbiguedad(filePath, coincidencia.ocurrencias, coincidencia.estrategia),
|
|
111
|
+
};
|
|
71
112
|
}
|
|
72
|
-
if (
|
|
113
|
+
if (!coincidencia) {
|
|
114
|
+
// Antes se devolvia siempre el mismo texto generico, que no le decia al
|
|
115
|
+
// modelo nada que no supiera y le invitaba a reintentar identico.
|
|
73
116
|
return {
|
|
74
117
|
success: false,
|
|
75
|
-
message:
|
|
118
|
+
message: diagnosticarFallo(filePath, normalizedCurrent, normalizedOld, normalizedNew),
|
|
76
119
|
};
|
|
77
120
|
}
|
|
121
|
+
const { inicio, fin, textoNuevo: reemplazo, estrategia } = coincidencia;
|
|
122
|
+
const finalOldText = normalizedCurrent.slice(inicio, fin);
|
|
78
123
|
// Calculate real starting line number in the original file
|
|
79
|
-
const startLine = normalizedCurrent.slice(0,
|
|
80
|
-
|
|
124
|
+
const startLine = normalizedCurrent.slice(0, inicio).split("\n").length;
|
|
125
|
+
// Se recorta sobre el ORIGINAL, no sobre el normalizado: lo de fuera del
|
|
126
|
+
// fragmento vuelve al disco tal cual estaba. Solo el texto nuevo se escribe con
|
|
127
|
+
// el terminador dominante.
|
|
128
|
+
const inicioOriginal = aPosicionOriginal(inicio);
|
|
129
|
+
const finOriginal = aPosicionOriginal(fin);
|
|
130
|
+
const textoNuevo = eol === "\n" ? reemplazo : reemplazo.replace(/\n/g, "\r\n");
|
|
131
|
+
const updatedContent = bom + original.slice(0, inicioOriginal) + textoNuevo + original.slice(finOriginal);
|
|
81
132
|
fs.writeFileSync(resolved, updatedContent, "utf-8");
|
|
82
|
-
const diffText = formatNumberedDiff(path.relative(cwd, resolved) || filePath, finalOldText,
|
|
133
|
+
const diffText = formatNumberedDiff(path.relative(cwd, resolved) || filePath, finalOldText, reemplazo, startLine);
|
|
134
|
+
// Que estrategia acerto no es cosmetico: si el modelo empieza a depender del
|
|
135
|
+
// ancla de bloque es que esta copiando mal los fragmentos, y eso se puede
|
|
136
|
+
// medir en vez de intuirlo.
|
|
137
|
+
const nota = estrategia === "exacta" ? "" : ` [coincidencia por ${estrategia}]`;
|
|
83
138
|
return {
|
|
84
139
|
success: true,
|
|
85
|
-
message: `✔ Archivo "${filePath}" editado exitosamente (Línea ${startLine}+)
|
|
140
|
+
message: `✔ Archivo "${filePath}" editado exitosamente (Línea ${startLine}+).${nota}`,
|
|
86
141
|
diffText,
|
|
142
|
+
estrategia,
|
|
87
143
|
};
|
|
88
144
|
}
|
|
@@ -1 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traduce lo que escribe el modelo al glob que fast-glob necesita.
|
|
3
|
+
*
|
|
4
|
+
* Dos trampas, y las dos daban cero resultados sin decir nada:
|
|
5
|
+
*
|
|
6
|
+
* 1. En glob, `*.ts` significa ".ts en el nivel raiz", no "en todo el arbol".
|
|
7
|
+
* La descripcion de la herramienta pone `'*.tsx'` como ejemplo, asi que el
|
|
8
|
+
* modelo lo escribe tal cual, recibe cero y concluye que el proyecto no
|
|
9
|
+
* tiene TypeScript. El codigo viejo hacia justo lo contrario de lo que hace
|
|
10
|
+
* falta: solo volvia recursivos los patrones SIN comodin.
|
|
11
|
+
*
|
|
12
|
+
* 2. fast-glob trata `\` como caracter de escape, no como separador. Un
|
|
13
|
+
* patron con rutas de Windows (`src\**\*.ts`) se convertia en `src**.ts` y
|
|
14
|
+
* no casaba con nada.
|
|
15
|
+
*
|
|
16
|
+
* Devuelve una lista porque para un patron con ruta se prueban dos: la ruta tal
|
|
17
|
+
* cual (relativa a la carpeta de busqueda) y la misma anclada a cualquier nivel,
|
|
18
|
+
* que es lo que suele querer decir quien escribe `src/*.ts` sin saber desde
|
|
19
|
+
* donde se esta buscando. fast-glob deduplica.
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizarPatron(pattern: string): string[];
|
|
1
22
|
export declare function findFiles(pattern: string, searchDirectory?: string, cwd?: string): Promise<string>;
|