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,215 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
/**
|
|
6
|
+
* Actualizaciones solas, sin que el usuario tenga que enterarse.
|
|
7
|
+
*
|
|
8
|
+
* POR QUE HACE FALTA
|
|
9
|
+
*
|
|
10
|
+
* Se arreglo un fallo del teclado tres veces y el dueno del producto seguia
|
|
11
|
+
* viendolo, porque su `chocolatito` apuntaba al paquete publicado dos dias
|
|
12
|
+
* antes. Si al que tiene el repositorio delante le pasa eso, al que paga 16,99 $
|
|
13
|
+
* al mes le va a pasar siempre: nadie corre `npm update -g` por gusto. Un
|
|
14
|
+
* arreglo que no llega es un arreglo que no existe.
|
|
15
|
+
*
|
|
16
|
+
* COMO FUNCIONA
|
|
17
|
+
*
|
|
18
|
+
* Al arrancar se le pregunta al registro de npm cual es la ultima version. La
|
|
19
|
+
* respuesta se guarda seis horas, asi que el coste real es una peticion cada
|
|
20
|
+
* medio dia. Si hay una nueva se instala de fondo y se avisa; el cambio entra
|
|
21
|
+
* en el siguiente arranque, no a mitad de sesion (cambiarle el codigo por
|
|
22
|
+
* debajo a un proceso vivo es como se rompen las cosas de verdad).
|
|
23
|
+
*
|
|
24
|
+
* LAS TRES COSAS QUE NUNCA HACE
|
|
25
|
+
*
|
|
26
|
+
* 1. No toca una instalacion de desarrollo. Si `chocolatito` sale de un
|
|
27
|
+
* `npm link` sobre el repositorio, instalar encima le borraria el trabajo al
|
|
28
|
+
* que lo esta escribiendo. Se detecta y se calla.
|
|
29
|
+
* 2. No bloquea el arranque. Si el registro no contesta, no contesta: la sesion
|
|
30
|
+
* ya va por su cuenta y el aviso aparece cuando llegue, o no aparece.
|
|
31
|
+
* 3. No insiste. Si no hay permiso para escribir en la carpeta global, se dice
|
|
32
|
+
* una vez el comando que hay que correr y se deja en paz.
|
|
33
|
+
*
|
|
34
|
+
* Para apagarlo del todo: CHOCOLATITO_SIN_ACTUALIZAR=1.
|
|
35
|
+
*/
|
|
36
|
+
const PAQUETE = "chocolatito-code";
|
|
37
|
+
const REGISTRO = `https://registry.npmjs.org/${PAQUETE}/latest`;
|
|
38
|
+
/** Cada cuanto se le pregunta al registro. Doce veces al dia no aporta nada. */
|
|
39
|
+
export const HORAS_ENTRE_COMPROBACIONES = 6;
|
|
40
|
+
/** Lo que se espera al registro. Pasado esto, la sesion vale mas que el aviso. */
|
|
41
|
+
const TIMEOUT_MS = 4000;
|
|
42
|
+
const FICHERO_CACHE = path.join(os.homedir(), ".chocolatito", "actualizacion.json");
|
|
43
|
+
/**
|
|
44
|
+
* Compara dos versiones de tipo 1.2.3.
|
|
45
|
+
*
|
|
46
|
+
* No se usa una libreria por una razon concreta: `semver` arrastra dependencias
|
|
47
|
+
* y esto se ejecuta en el arranque de un CLI. Lo que si se respeta es lo unico
|
|
48
|
+
* que importa aqui: una preversion (1.1.0-beta.1) NUNCA es mas nueva que la
|
|
49
|
+
* final del mismo numero, o el usuario acabaria bajando a una beta sin pedirlo.
|
|
50
|
+
*/
|
|
51
|
+
export function esMasNueva(candidata, actual) {
|
|
52
|
+
const trocear = (v) => {
|
|
53
|
+
const [nucleo, pre] = String(v || "").trim().split("-", 2);
|
|
54
|
+
const partes = nucleo.split(".").map((n) => Number.parseInt(n, 10));
|
|
55
|
+
return {
|
|
56
|
+
nums: [partes[0] || 0, partes[1] || 0, partes[2] || 0],
|
|
57
|
+
pre: pre ?? null,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const a = trocear(candidata);
|
|
61
|
+
const b = trocear(actual);
|
|
62
|
+
for (let i = 0; i < 3; i++) {
|
|
63
|
+
if (a.nums[i] > b.nums[i])
|
|
64
|
+
return true;
|
|
65
|
+
if (a.nums[i] < b.nums[i])
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
// Mismo numero: solo gana la candidata si la actual es preversion y ella no.
|
|
69
|
+
return a.pre === null && b.pre !== null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* De donde sale el `chocolatito` que se esta ejecutando.
|
|
73
|
+
*
|
|
74
|
+
* Si al lado del `dist` hay un `.git` o un `src`, esto es el repositorio, no un
|
|
75
|
+
* paquete instalado: `npm link` o `node dist/index.js` a pelo. Instalar encima
|
|
76
|
+
* seria pisarle el codigo al que lo esta escribiendo.
|
|
77
|
+
*/
|
|
78
|
+
export function esInstalacionDeDesarrollo(raizDelPaquete) {
|
|
79
|
+
try {
|
|
80
|
+
return (fs.existsSync(path.join(raizDelPaquete, ".git")) ||
|
|
81
|
+
fs.existsSync(path.join(raizDelPaquete, "src")));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function leerCache(fichero = FICHERO_CACHE) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(fs.readFileSync(fichero, "utf-8"));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function guardarCache(datos, fichero = FICHERO_CACHE) {
|
|
96
|
+
try {
|
|
97
|
+
fs.mkdirSync(path.dirname(fichero), { recursive: true });
|
|
98
|
+
fs.writeFileSync(fichero, JSON.stringify(datos, null, 2), "utf-8");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Sin cache se pregunta en cada arranque: molesto, no grave. No es motivo
|
|
102
|
+
// para romperle la sesion a nadie.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Si toca preguntar al registro. Un reloj adelantado no deja de comprobar nunca. */
|
|
106
|
+
export function tocaComprobar(cache, ahora) {
|
|
107
|
+
const ultima = cache.comprobadoEn;
|
|
108
|
+
if (typeof ultima !== "number" || !Number.isFinite(ultima))
|
|
109
|
+
return true;
|
|
110
|
+
const transcurrido = ahora - ultima;
|
|
111
|
+
// El valor absoluto cubre el reloj hacia atras: una marca en el futuro
|
|
112
|
+
// significa que algo esta mal, y lo seguro es volver a preguntar.
|
|
113
|
+
return Math.abs(transcurrido) >= HORAS_ENTRE_COMPROBACIONES * 60 * 60 * 1000;
|
|
114
|
+
}
|
|
115
|
+
/** Le pregunta al registro. Devuelve null si no contesta a tiempo o no se entiende. */
|
|
116
|
+
export async function ultimaVersionPublicada(url = REGISTRO) {
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch(url, {
|
|
119
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
120
|
+
headers: { accept: "application/json" },
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok)
|
|
123
|
+
return null;
|
|
124
|
+
const datos = (await res.json());
|
|
125
|
+
const v = String(datos?.version ?? "");
|
|
126
|
+
return /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Sin red, con proxy raro, o el registro caido. Nada de esto es problema
|
|
130
|
+
// del usuario y nada de esto merece un mensaje de error.
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Instala la version nueva en la carpeta global.
|
|
136
|
+
*
|
|
137
|
+
* Se hace con `npm install -g`, que es como se instalo, en vez de inventarse
|
|
138
|
+
* una descarga: asi funciona igual con nvm, con volta y con la instalacion de
|
|
139
|
+
* Windows, que ponen la carpeta global en sitios distintos.
|
|
140
|
+
*/
|
|
141
|
+
export function instalar(version, ejecutar = spawnNpm) {
|
|
142
|
+
return ejecutar(["install", "-g", `${PAQUETE}@${version}`]).then((salida) => {
|
|
143
|
+
if (salida.codigo === 0) {
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
mensaje: `Actualizado a ${version}. Entra al reiniciar chocolatito.`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// El caso que de verdad pasa: la carpeta global es de administrador o de
|
|
150
|
+
// otro usuario. Decir "fallo la actualizacion" no ayuda; decir el comando si.
|
|
151
|
+
const texto = `${salida.salida}`.toLowerCase();
|
|
152
|
+
const sinPermiso = texto.includes("eacces") || texto.includes("eperm") || texto.includes("acceso denegado");
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
mensaje: sinPermiso
|
|
156
|
+
? `Hay una version nueva (${version}) pero no hay permiso para instalarla. Ejecuta: npm i -g ${PAQUETE}@latest`
|
|
157
|
+
: `No se pudo instalar la ${version} sola. Ejecuta: npm i -g ${PAQUETE}@latest`,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function spawnNpm(args) {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
// shell:true porque en Windows `npm` es un .cmd y sin shell no se encuentra.
|
|
164
|
+
const p = spawn("npm", args, { shell: true, windowsHide: true });
|
|
165
|
+
let salida = "";
|
|
166
|
+
p.stdout?.on("data", (d) => (salida += d));
|
|
167
|
+
p.stderr?.on("data", (d) => (salida += d));
|
|
168
|
+
p.on("error", (e) => resolve({ codigo: 1, salida: String(e?.message || e) }));
|
|
169
|
+
p.on("close", (codigo) => resolve({ codigo: codigo ?? 1, salida }));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* El paso completo: comprobar, y si procede instalar.
|
|
174
|
+
*
|
|
175
|
+
* Devuelve lo que hay que ensenarle al usuario, o cadena vacia si no hay nada.
|
|
176
|
+
* No imprime nada por su cuenta: quien llama decide cuando y como, que en un
|
|
177
|
+
* CLI con un prompt dibujado no es lo mismo escribir ahora que dentro de un
|
|
178
|
+
* segundo.
|
|
179
|
+
*/
|
|
180
|
+
export async function comprobarActualizacion(ajustes) {
|
|
181
|
+
if (!ajustes.forzar && process.env.CHOCOLATITO_SIN_ACTUALIZAR === "1")
|
|
182
|
+
return "";
|
|
183
|
+
const desarrollo = esInstalacionDeDesarrollo(ajustes.raizDelPaquete);
|
|
184
|
+
if (desarrollo && !ajustes.forzar)
|
|
185
|
+
return "";
|
|
186
|
+
const cache = leerCache();
|
|
187
|
+
if (!ajustes.forzar && !tocaComprobar(cache, ajustes.ahora)) {
|
|
188
|
+
// Ya se pregunto hace poco. Si en su dia se vio una version nueva y sigue
|
|
189
|
+
// sin instalarse (sin permisos, por ejemplo), se recuerda sin volver a la red.
|
|
190
|
+
const guardada = cache.ultima;
|
|
191
|
+
if (guardada && esMasNueva(guardada, ajustes.versionActual) && cache.avisadaDe !== guardada) {
|
|
192
|
+
return `Hay una version nueva (${guardada}). Ejecuta: npm i -g ${PAQUETE}@latest`;
|
|
193
|
+
}
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
const ultima = await ultimaVersionPublicada();
|
|
197
|
+
guardarCache({ ...cache, comprobadoEn: ajustes.ahora, ultima: ultima ?? cache.ultima });
|
|
198
|
+
if (!ultima)
|
|
199
|
+
return ajustes.forzar ? "No se pudo consultar el registro de npm. ¿Hay conexion?" : "";
|
|
200
|
+
if (!esMasNueva(ultima, ajustes.versionActual)) {
|
|
201
|
+
return ajustes.forzar ? `Ya vas por la ultima version (${ajustes.versionActual}).` : "";
|
|
202
|
+
}
|
|
203
|
+
if (desarrollo) {
|
|
204
|
+
// Solo con /actualizar se llega aqui, y aun asi no se instala: el aviso es
|
|
205
|
+
// util (te dice que lo publicado va por delante de lo que tienes), pisar el
|
|
206
|
+
// repositorio no.
|
|
207
|
+
return `Publicada la ${ultima} y tu vas por la ${ajustes.versionActual}, pero esto es una instalacion de desarrollo: no se toca. Publica desde el repositorio.`;
|
|
208
|
+
}
|
|
209
|
+
if (ajustes.automatico === false) {
|
|
210
|
+
return `Hay una version nueva (${ultima}). Ejecuta: npm i -g ${PAQUETE}@latest`;
|
|
211
|
+
}
|
|
212
|
+
const res = await instalar(ultima);
|
|
213
|
+
guardarCache({ ...leerCache(), comprobadoEn: ajustes.ahora, ultima, avisadaDe: res.ok ? ultima : undefined });
|
|
214
|
+
return res.mensaje;
|
|
215
|
+
}
|
package/dist/hooks/manager.js
CHANGED
|
@@ -1,8 +1,96 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import chalk from "chalk";
|
|
4
5
|
import { execa } from "execa";
|
|
5
6
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
/**
|
|
8
|
+
* EL SHELL DE LOS HOOKS, RESUELTO A UNA RUTA QUE EXISTE
|
|
9
|
+
*
|
|
10
|
+
* Antes esto era una sola linea: `shell: usaPowerShell ? "powershell.exe" :
|
|
11
|
+
* "/bin/bash"`. Ese "/bin/bash" literal no existe en Windows (Git Bash se
|
|
12
|
+
* instala en "C:\Program Files\Git\bin\bash.exe"; /bin/bash es una ruta interna
|
|
13
|
+
* de MSYS que el spawn de Windows no resuelve) y tampoco en una imagen Alpine,
|
|
14
|
+
* NixOS o slim, donde el unico shell es /bin/sh.
|
|
15
|
+
*
|
|
16
|
+
* Y no se notaba: con `reject: false` execa no lanza excepcion cuando el shell
|
|
17
|
+
* no existe, devuelve exitCode undefined con stdout y stderr vacios. O sea que
|
|
18
|
+
* un hook PreToolUse de deny quedaba descartado por el `if (!salida) continue` y
|
|
19
|
+
* la accion peligrosa se ejecutaba: el fallo silencioso en la direccion
|
|
20
|
+
* insegura, que es la peor de las dos.
|
|
21
|
+
*/
|
|
22
|
+
const shellsResueltos = new Map();
|
|
23
|
+
function primeraExistente(rutas) {
|
|
24
|
+
return rutas.find((r) => !!r && fs.existsSync(r));
|
|
25
|
+
}
|
|
26
|
+
/** Busca un ejecutable por el PATH sin lanzar procesos (nada de `which`). */
|
|
27
|
+
function buscarEnPath(nombres, filtroDir) {
|
|
28
|
+
const dirs = (process.env.PATH || process.env.Path || "")
|
|
29
|
+
.split(path.delimiter)
|
|
30
|
+
.map((d) => d.trim().replace(/^"|"$/g, ""))
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.filter((d) => !filtroDir || filtroDir(d));
|
|
33
|
+
for (const nombre of nombres) {
|
|
34
|
+
for (const dir of dirs) {
|
|
35
|
+
const candidato = path.join(dir, nombre);
|
|
36
|
+
if (fs.existsSync(candidato))
|
|
37
|
+
return candidato;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function buscarBash() {
|
|
43
|
+
if (process.platform !== "win32") {
|
|
44
|
+
// /bin/sh va al final a proposito: en Alpine, NixOS o una imagen slim es el
|
|
45
|
+
// unico que hay, y un hook de una linea funciona igual. Preferir un shell
|
|
46
|
+
// que si existe es mejor que no ejecutar la politica del usuario.
|
|
47
|
+
return (primeraExistente(["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/bin/sh", "/usr/bin/sh"]) ?? null);
|
|
48
|
+
}
|
|
49
|
+
const bases = [
|
|
50
|
+
process.env["ProgramFiles"],
|
|
51
|
+
process.env["ProgramW6432"],
|
|
52
|
+
process.env["ProgramFiles(x86)"],
|
|
53
|
+
process.env["LOCALAPPDATA"] ? path.join(process.env["LOCALAPPDATA"], "Programs") : undefined,
|
|
54
|
+
].filter((b) => !!b);
|
|
55
|
+
const git = primeraExistente(bases.map((b) => path.join(b, "Git", "bin", "bash.exe")));
|
|
56
|
+
if (git)
|
|
57
|
+
return git;
|
|
58
|
+
// Ultimo recurso, el PATH, saltandose System32\bash.exe: ese es el lanzador de
|
|
59
|
+
// WSL, que ve otro sistema de archivos y otras rutas. Un hook que crea estar
|
|
60
|
+
// mirando el proyecto miraria otra cosa.
|
|
61
|
+
return buscarEnPath(["bash.exe"], (dir) => !/system32\\?$/i.test(dir)) ?? null;
|
|
62
|
+
}
|
|
63
|
+
function buscarPowerShell() {
|
|
64
|
+
if (process.platform === "win32") {
|
|
65
|
+
const sysroot = process.env["SystemRoot"] || "C:\\Windows";
|
|
66
|
+
return (primeraExistente([path.join(sysroot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")]) ??
|
|
67
|
+
buscarEnPath(["pwsh.exe", "powershell.exe"]) ??
|
|
68
|
+
null);
|
|
69
|
+
}
|
|
70
|
+
// Fuera de Windows solo tiene sentido si el usuario pidio "powershell" a mano.
|
|
71
|
+
return buscarEnPath(["pwsh"]) ?? null;
|
|
72
|
+
}
|
|
73
|
+
function resolverShell(tipo) {
|
|
74
|
+
if (shellsResueltos.has(tipo))
|
|
75
|
+
return shellsResueltos.get(tipo);
|
|
76
|
+
const ruta = tipo === "powershell" ? buscarPowerShell() : buscarBash();
|
|
77
|
+
shellsResueltos.set(tipo, ruta);
|
|
78
|
+
return ruta;
|
|
79
|
+
}
|
|
80
|
+
function recortar(texto, max = 400) {
|
|
81
|
+
const limpio = texto.trim().replace(/\s+$/g, "");
|
|
82
|
+
return limpio.length > max ? `${limpio.slice(0, max)}…` : limpio;
|
|
83
|
+
}
|
|
84
|
+
/** Por que no salio bien, en una linea, para poder contarselo al usuario. */
|
|
85
|
+
function motivoDelFallo(res) {
|
|
86
|
+
if (res?.timedOut)
|
|
87
|
+
return "agoto el tiempo limite";
|
|
88
|
+
if (res?.exitCode === undefined || res?.exitCode === null) {
|
|
89
|
+
const detalle = recortar(String(res?.shortMessage || res?.message || ""), 160).split("\n")[0];
|
|
90
|
+
return detalle ? `no llego a ejecutarse (${detalle})` : "no llego a ejecutarse";
|
|
91
|
+
}
|
|
92
|
+
return `codigo de salida ${res.exitCode}`;
|
|
93
|
+
}
|
|
6
94
|
export class HookManager {
|
|
7
95
|
config = {};
|
|
8
96
|
disabled = false;
|
|
@@ -113,17 +201,63 @@ export class HookManager {
|
|
|
113
201
|
});
|
|
114
202
|
const decision = {};
|
|
115
203
|
const contextos = [];
|
|
204
|
+
/**
|
|
205
|
+
* Un hook que no arranca, revienta o se cuelga NO bloquea (solo el codigo 2
|
|
206
|
+
* bloquea, igual que en Claude Code), pero TIENE que verse. Antes se caia
|
|
207
|
+
* por el `if (!salida) continue` de mas abajo y la politica quedaba muerta
|
|
208
|
+
* sin una sola linea de aviso: el usuario seguia creyendo que su guardia
|
|
209
|
+
* estaba puesto. Se imprime aqui, y no en el llamador, porque a run() la
|
|
210
|
+
* llaman el bucle, la puerta de herramientas y los subagentes, y solo uno de
|
|
211
|
+
* ellos pinta systemMessage. Por eso el aviso NO se mete ademas en
|
|
212
|
+
* systemMessage: el bucle lo imprimiria otra vez y el usuario veria el mismo
|
|
213
|
+
* fallo dos veces.
|
|
214
|
+
*/
|
|
215
|
+
const avisar = (texto) => {
|
|
216
|
+
contextos.push(`[${texto}]`);
|
|
217
|
+
console.error(chalk.yellow(`\n⚠ ${texto}`));
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* En PostToolUse la herramienta YA se ejecuto: "bloquear" no puede abortar
|
|
221
|
+
* nada, solo devolverle el motivo al modelo para que lo corrija. Y era justo
|
|
222
|
+
* eso lo que no pasaba: el bloqueo se empaquetaba en permissionDecision /
|
|
223
|
+
* permissionDecisionReason y se volvia de inmediato, mientras que el unico
|
|
224
|
+
* consumidor de PostToolUse lee additionalContext. Resultado: el atajo que
|
|
225
|
+
* documenta el propio proyecto —salir con codigo 2, o imprimir
|
|
226
|
+
* decision:"block"— era un agujero negro. El hook se ejecutaba, salia en
|
|
227
|
+
* /hooks como configurado, y ni el usuario ni el modelo veian una palabra.
|
|
228
|
+
*
|
|
229
|
+
* Devuelve true cuando hay que cortar la cadena, que solo tiene sentido
|
|
230
|
+
* antes de ejecutar. En PostToolUse se sigue: los demas hooks —linters,
|
|
231
|
+
* tests— tambien tienen algo que decir sobre lo que ya ha pasado.
|
|
232
|
+
*/
|
|
233
|
+
const esPost = event === "PostToolUse";
|
|
234
|
+
const anotarBloqueo = (motivo) => {
|
|
235
|
+
decision.permissionDecision = "deny";
|
|
236
|
+
decision.permissionDecisionReason = motivo;
|
|
237
|
+
if (!esPost)
|
|
238
|
+
return true;
|
|
239
|
+
contextos.push(`BLOQUEO del hook PostToolUse: ${motivo}`);
|
|
240
|
+
return false;
|
|
241
|
+
};
|
|
116
242
|
for (const hook of hooks) {
|
|
117
243
|
try {
|
|
118
|
-
const
|
|
244
|
+
const tipo = hook.shell === "powershell" || (hook.shell !== "bash" && process.platform === "win32")
|
|
245
|
+
? "powershell"
|
|
246
|
+
: "bash";
|
|
247
|
+
const shell = resolverShell(tipo);
|
|
248
|
+
if (!shell) {
|
|
249
|
+
avisar(`El hook ${event} "${hook.command}" NO se ejecuto: no hay ningun ${tipo} en este sistema. ` +
|
|
250
|
+
"La regla que impone no se esta aplicando.");
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
119
253
|
// PowerShell NO propaga el codigo de salida del programa que ejecuta:
|
|
120
254
|
// powershell.exe sale con 0 aunque el hook haya salido con 2. Sin este
|
|
121
255
|
// "exit $LASTEXITCODE", el bloqueo por codigo de salida no funcionaba en
|
|
122
256
|
// Windows y el hook parecia estar aprobando siempre.
|
|
123
|
-
const comando =
|
|
257
|
+
const comando = tipo === "powershell" ? `${hook.command}; exit $LASTEXITCODE` : hook.command;
|
|
124
258
|
const res = await execa(comando, {
|
|
125
259
|
cwd: this.cwd,
|
|
126
|
-
shell
|
|
260
|
+
shell,
|
|
127
261
|
input,
|
|
128
262
|
timeout: hook.timeout ? hook.timeout * 1000 : DEFAULT_TIMEOUT_MS,
|
|
129
263
|
reject: false,
|
|
@@ -132,10 +266,19 @@ export class HookManager {
|
|
|
132
266
|
// Codigo 2 = bloqueo. No se puede sobrescribir con JSON: es el atajo
|
|
133
267
|
// para un hook de una linea que solo quiere decir "aqui no".
|
|
134
268
|
if (res.exitCode === 2) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
269
|
+
const motivo = (res.stderr || res.stdout || "").trim() || `El hook "${hook.command}" bloqueo la accion.`;
|
|
270
|
+
if (anotarBloqueo(motivo))
|
|
271
|
+
return decision;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
// Cualquier otro final anormal (script inexistente, error de sintaxis,
|
|
275
|
+
// timeout, shell que no arranco) se cuenta. No bloquea, pero se dice:
|
|
276
|
+
// ese silencio era el fallo, no el hecho de dejar pasar.
|
|
277
|
+
if (res.exitCode !== 0) {
|
|
278
|
+
const detalle = recortar(String(res.stderr || res.stdout || ""));
|
|
279
|
+
avisar(`El hook ${event} "${hook.command}" fallo (${motivoDelFallo(res)}) y su regla no se aplico.` +
|
|
280
|
+
(detalle ? `\n ${detalle.split("\n").join("\n ")}` : ""));
|
|
281
|
+
continue;
|
|
139
282
|
}
|
|
140
283
|
const salida = (res.stdout || "").trim();
|
|
141
284
|
if (!salida)
|
|
@@ -151,14 +294,16 @@ export class HookManager {
|
|
|
151
294
|
}
|
|
152
295
|
const esp = parsed.hookSpecificOutput || {};
|
|
153
296
|
if (parsed.decision === "block") {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
297
|
+
if (anotarBloqueo(parsed.reason || "Bloqueado por un hook."))
|
|
298
|
+
return decision;
|
|
299
|
+
if (parsed.systemMessage)
|
|
300
|
+
decision.systemMessage = parsed.systemMessage;
|
|
301
|
+
continue;
|
|
157
302
|
}
|
|
158
303
|
if (esp.permissionDecision === "deny") {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
304
|
+
if (anotarBloqueo(esp.permissionDecisionReason || "Bloqueado por un hook."))
|
|
305
|
+
return decision;
|
|
306
|
+
continue;
|
|
162
307
|
}
|
|
163
308
|
// "allow" no gana sobre un "deny" posterior: por eso no se corta aqui.
|
|
164
309
|
if (esp.permissionDecision && !decision.permissionDecision) {
|
|
@@ -178,7 +323,7 @@ export class HookManager {
|
|
|
178
323
|
}
|
|
179
324
|
}
|
|
180
325
|
catch (err) {
|
|
181
|
-
|
|
326
|
+
avisar(`El hook ${event} "${hook.command}" fallo y su regla no se aplico: ${err?.message || err}`);
|
|
182
327
|
}
|
|
183
328
|
}
|
|
184
329
|
if (contextos.length > 0)
|