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
|
@@ -1,71 +1,307 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Aviso de sintaxis tras escribir un archivo.
|
|
4
|
+
*
|
|
5
|
+
* La version anterior contaba llaves con una maquina de estados de veinte
|
|
6
|
+
* lineas, y fallaba en las dos direcciones a la vez:
|
|
7
|
+
*
|
|
8
|
+
* - **Falsos positivos.** Trataba el apostrofo como comilla de cadena en
|
|
9
|
+
* cualquier lenguaje y dentro de los comentarios. Un comentario en
|
|
10
|
+
* castellano ("no está", "el año") abria una cadena que no se cerraba nunca,
|
|
11
|
+
* y a partir de ahi todo el archivo se leia mal. En Rust pasaba lo mismo con
|
|
12
|
+
* los tiempos de vida (`&'a str`). Un aviso que salta cuando no pasa nada
|
|
13
|
+
* ensena al modelo a ignorarlo.
|
|
14
|
+
* - **Falsos negativos.** Solo avisaba si quedaban menos de cinco simbolos sin
|
|
15
|
+
* cerrar (`stack.length < 5`), asi que un archivo MUY roto —justo el caso
|
|
16
|
+
* grave— no daba aviso ninguno. Tampoco entendia `/* *\/`, ni las plantillas
|
|
17
|
+
* con `${}`, ni las cadenas triples de Python, ni las expresiones regulares.
|
|
18
|
+
*
|
|
19
|
+
* La regla de esta version es: **ante la duda, callar**. Un aviso equivocado
|
|
20
|
+
* cuesta mas que ningun aviso, porque manda al agente a arreglar algo que no
|
|
21
|
+
* esta roto. Cuando el escaner encuentra algo que no sabe clasificar con
|
|
22
|
+
* seguridad, marca el analisis como inseguro y no se dice nada.
|
|
23
|
+
*/
|
|
24
|
+
/** Lenguajes que el escaner sabe leer con confianza. */
|
|
25
|
+
const LLAVES = new Set([".ts", ".js", ".tsx", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".go", ".rs", ".java", ".c", ".h", ".cpp", ".cs"]);
|
|
26
|
+
const PYTHON = new Set([".py", ".pyi"]);
|
|
27
|
+
const CIERRA = { "}": "{", "]": "[", ")": "(" };
|
|
28
|
+
/**
|
|
29
|
+
* Un "/" abre una expresion regular o divide, y solo se puede saber mirando
|
|
30
|
+
* atras. Si el ultimo caracter con significado es un operador, un abre-algo o
|
|
31
|
+
* el final de una palabra clave, lo que viene es una regex.
|
|
32
|
+
*/
|
|
33
|
+
function esInicioDeRegex(anterior, ultimaPalabra) {
|
|
34
|
+
if (!anterior)
|
|
35
|
+
return true;
|
|
36
|
+
if ("(,=:[!&|?{};+-*%^~<>".includes(anterior))
|
|
37
|
+
return true;
|
|
38
|
+
return ["return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "case", "do", "else", "yield", "await"].includes(ultimaPalabra);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Recorre el archivo contando parentesis, corchetes y llaves fuera de cadenas y
|
|
42
|
+
* comentarios.
|
|
43
|
+
*/
|
|
44
|
+
function analizar(contenido, ext) {
|
|
45
|
+
const esPython = PYTHON.has(ext);
|
|
46
|
+
const esRust = ext === ".rs";
|
|
47
|
+
const esGo = ext === ".go";
|
|
48
|
+
const esJs = LLAVES.has(ext) && !esRust && !esGo && ![".java", ".c", ".h", ".cpp", ".cs"].includes(ext);
|
|
49
|
+
const pila = [];
|
|
50
|
+
let linea = 1;
|
|
51
|
+
let anterior = "";
|
|
52
|
+
let ultimaPalabra = "";
|
|
53
|
+
let palabraEnCurso = "";
|
|
54
|
+
for (let i = 0; i < contenido.length; i++) {
|
|
55
|
+
const c = contenido[i];
|
|
56
|
+
const sig = contenido[i + 1];
|
|
57
|
+
if (c === "\n") {
|
|
58
|
+
linea++;
|
|
59
|
+
anterior = "";
|
|
60
|
+
if (palabraEnCurso)
|
|
61
|
+
ultimaPalabra = palabraEnCurso;
|
|
62
|
+
palabraEnCurso = "";
|
|
63
|
+
continue;
|
|
9
64
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
65
|
+
// ---- comentarios
|
|
66
|
+
if (!esPython && c === "/" && sig === "/") {
|
|
67
|
+
while (i < contenido.length && contenido[i] !== "\n")
|
|
68
|
+
i++;
|
|
69
|
+
i--;
|
|
70
|
+
continue;
|
|
15
71
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
if (!esPython && c === "/" && sig === "*") {
|
|
73
|
+
const fin = contenido.indexOf("*/", i + 2);
|
|
74
|
+
if (fin === -1)
|
|
75
|
+
return { desbalance: null, inseguro: true }; // comentario sin cerrar: no se puede juzgar
|
|
76
|
+
for (let k = i; k < fin; k++)
|
|
77
|
+
if (contenido[k] === "\n")
|
|
78
|
+
linea++;
|
|
79
|
+
i = fin + 1;
|
|
80
|
+
anterior = "";
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (esPython && c === "#") {
|
|
84
|
+
while (i < contenido.length && contenido[i] !== "\n")
|
|
85
|
+
i++;
|
|
86
|
+
i--;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// ---- cadenas triples de Python
|
|
90
|
+
if (esPython && (c === '"' || c === "'") && contenido[i + 1] === c && contenido[i + 2] === c) {
|
|
91
|
+
const marca = c.repeat(3);
|
|
92
|
+
const fin = contenido.indexOf(marca, i + 3);
|
|
93
|
+
if (fin === -1)
|
|
94
|
+
return { desbalance: null, inseguro: true };
|
|
95
|
+
for (let k = i; k < fin; k++)
|
|
96
|
+
if (contenido[k] === "\n")
|
|
97
|
+
linea++;
|
|
98
|
+
i = fin + 2;
|
|
99
|
+
anterior = "";
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// ---- cadenas crudas de Go y Rust
|
|
103
|
+
if (esGo && c === "`") {
|
|
104
|
+
const fin = contenido.indexOf("`", i + 1);
|
|
105
|
+
if (fin === -1)
|
|
106
|
+
return { desbalance: null, inseguro: true };
|
|
107
|
+
for (let k = i; k < fin; k++)
|
|
108
|
+
if (contenido[k] === "\n")
|
|
109
|
+
linea++;
|
|
110
|
+
i = fin;
|
|
111
|
+
anterior = "`";
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (esRust && c === "r" && (sig === '"' || sig === "#")) {
|
|
115
|
+
// r"..." y r#"..."#. El numero de almohadillas define el cierre.
|
|
116
|
+
let j = i + 1;
|
|
117
|
+
let almohadillas = 0;
|
|
118
|
+
while (contenido[j] === "#") {
|
|
119
|
+
almohadillas++;
|
|
120
|
+
j++;
|
|
121
|
+
}
|
|
122
|
+
if (contenido[j] === '"') {
|
|
123
|
+
const cierre = '"' + "#".repeat(almohadillas);
|
|
124
|
+
const fin = contenido.indexOf(cierre, j + 1);
|
|
125
|
+
if (fin === -1)
|
|
126
|
+
return { desbalance: null, inseguro: true };
|
|
127
|
+
for (let k = i; k < fin; k++)
|
|
128
|
+
if (contenido[k] === "\n")
|
|
129
|
+
linea++;
|
|
130
|
+
i = fin + cierre.length - 1;
|
|
131
|
+
anterior = '"';
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ---- tiempos de vida de Rust: 'a NO es una cadena
|
|
136
|
+
//
|
|
137
|
+
// Este es el falso positivo que rompia el validador entero en cuanto se
|
|
138
|
+
// tocaba un archivo .rs con genericos.
|
|
139
|
+
if (esRust && c === "'") {
|
|
140
|
+
// Un literal de caracter es 'x' o '\n': comilla, uno o dos, comilla.
|
|
141
|
+
const esCaracter = contenido[i + 1] === "\\"
|
|
142
|
+
? contenido.indexOf("'", i + 2) !== -1 && contenido.indexOf("'", i + 2) - i <= 4
|
|
143
|
+
: contenido[i + 2] === "'";
|
|
144
|
+
if (!esCaracter) {
|
|
145
|
+
anterior = "'";
|
|
146
|
+
continue; // es un tiempo de vida: se ignora la comilla y punto
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ---- plantillas con interpolacion
|
|
150
|
+
if (esJs && c === "`") {
|
|
151
|
+
let j = i + 1;
|
|
152
|
+
let profundidad = 0;
|
|
153
|
+
while (j < contenido.length) {
|
|
154
|
+
const t = contenido[j];
|
|
155
|
+
if (t === "\\") {
|
|
156
|
+
j += 2;
|
|
32
157
|
continue;
|
|
33
158
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
159
|
+
if (t === "\n")
|
|
160
|
+
linea++;
|
|
161
|
+
if (profundidad === 0 && t === "`")
|
|
162
|
+
break;
|
|
163
|
+
if (t === "$" && contenido[j + 1] === "{") {
|
|
164
|
+
profundidad++;
|
|
165
|
+
j += 2;
|
|
37
166
|
continue;
|
|
38
167
|
}
|
|
168
|
+
if (profundidad > 0 && t === "}")
|
|
169
|
+
profundidad--;
|
|
170
|
+
j++;
|
|
39
171
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
172
|
+
if (j >= contenido.length)
|
|
173
|
+
return { desbalance: null, inseguro: true };
|
|
174
|
+
i = j;
|
|
175
|
+
anterior = "`";
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// ---- cadenas normales
|
|
179
|
+
if (c === '"' || (c === "'" && (esPython || esJs))) {
|
|
180
|
+
let j = i + 1;
|
|
181
|
+
let cerrada = false;
|
|
182
|
+
while (j < contenido.length) {
|
|
183
|
+
if (contenido[j] === "\\") {
|
|
184
|
+
j += 2;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (contenido[j] === c) {
|
|
188
|
+
cerrada = true;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
// Una cadena de una linea no cruza el salto: si lo cruza, o es otro
|
|
192
|
+
// lenguaje o el archivo esta roto de otra forma. No se juzga.
|
|
193
|
+
if (contenido[j] === "\n")
|
|
194
|
+
break;
|
|
195
|
+
j++;
|
|
196
|
+
}
|
|
197
|
+
if (!cerrada)
|
|
198
|
+
return { desbalance: null, inseguro: true };
|
|
199
|
+
i = j;
|
|
200
|
+
anterior = c;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// ---- expresiones regulares de JS
|
|
204
|
+
if (esJs && c === "/" && esInicioDeRegex(anterior, ultimaPalabra)) {
|
|
205
|
+
let j = i + 1;
|
|
206
|
+
let enClase = false;
|
|
207
|
+
let cerrada = false;
|
|
208
|
+
while (j < contenido.length) {
|
|
209
|
+
const t = contenido[j];
|
|
210
|
+
if (t === "\\") {
|
|
211
|
+
j += 2;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (t === "\n")
|
|
215
|
+
break;
|
|
216
|
+
if (t === "[")
|
|
217
|
+
enClase = true;
|
|
218
|
+
else if (t === "]")
|
|
219
|
+
enClase = false;
|
|
220
|
+
else if (t === "/" && !enClase) {
|
|
221
|
+
cerrada = true;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
j++;
|
|
47
225
|
}
|
|
48
|
-
if (
|
|
226
|
+
if (cerrada) {
|
|
227
|
+
i = j;
|
|
228
|
+
anterior = "/";
|
|
49
229
|
continue;
|
|
50
|
-
if (["{", "[", "("].includes(char)) {
|
|
51
|
-
stack.push(char);
|
|
52
230
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
231
|
+
// No cerraba: era una division. Se sigue como si nada.
|
|
232
|
+
}
|
|
233
|
+
// ---- simbolos que si cuentan
|
|
234
|
+
if (c === "{" || c === "[" || c === "(") {
|
|
235
|
+
pila.push({ simbolo: c, linea });
|
|
236
|
+
}
|
|
237
|
+
else if (c === "}" || c === "]" || c === ")") {
|
|
238
|
+
const arriba = pila.pop();
|
|
239
|
+
if (!arriba || arriba.simbolo !== CIERRA[c]) {
|
|
240
|
+
return { desbalance: { linea, simbolo: c, sobra: true }, inseguro: false };
|
|
61
241
|
}
|
|
62
242
|
}
|
|
63
|
-
if (
|
|
243
|
+
if (/\S/.test(c))
|
|
244
|
+
anterior = c;
|
|
245
|
+
// La ultima palabra COMPLETA, no la que se esta escribiendo: hace falta
|
|
246
|
+
// para distinguir `return /re/` de `a / b`, y entre la palabra y la barra
|
|
247
|
+
// hay un espacio.
|
|
248
|
+
if (/[A-Za-z_$0-9]/.test(c)) {
|
|
249
|
+
palabraEnCurso += c;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
if (palabraEnCurso)
|
|
253
|
+
ultimaPalabra = palabraEnCurso;
|
|
254
|
+
palabraEnCurso = "";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (pila.length > 0) {
|
|
258
|
+
const primero = pila[0];
|
|
259
|
+
return { desbalance: { linea: primero.linea, simbolo: primero.simbolo, sobra: false }, inseguro: false };
|
|
260
|
+
}
|
|
261
|
+
return { desbalance: null, inseguro: false };
|
|
262
|
+
}
|
|
263
|
+
/** Tres lineas de contexto alrededor del problema, con numeros. */
|
|
264
|
+
function contexto(contenido, linea) {
|
|
265
|
+
const lineas = contenido.split("\n");
|
|
266
|
+
const desde = Math.max(0, linea - 1 - 3);
|
|
267
|
+
const hasta = Math.min(lineas.length, linea + 3);
|
|
268
|
+
return lineas
|
|
269
|
+
.slice(desde, hasta)
|
|
270
|
+
.map((l, i) => {
|
|
271
|
+
const n = desde + i + 1;
|
|
272
|
+
const marca = n === linea ? "█" : " ";
|
|
273
|
+
return `${marca}${String(n).padStart(5)} | ${l}`;
|
|
274
|
+
})
|
|
275
|
+
.join("\n");
|
|
276
|
+
}
|
|
277
|
+
export function validateFileSyntax(filePath, content) {
|
|
278
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
279
|
+
// 1. JSON: aqui no hay heuristica que valga, el parser es la verdad.
|
|
280
|
+
if (ext === ".json") {
|
|
281
|
+
try {
|
|
282
|
+
JSON.parse(content);
|
|
283
|
+
return { valid: true };
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
64
286
|
return {
|
|
65
287
|
valid: false,
|
|
66
|
-
warning: `⚠️
|
|
288
|
+
warning: `⚠️ Advertencia de sintaxis JSON: ${err?.message || "JSON inválido"}`,
|
|
67
289
|
};
|
|
68
290
|
}
|
|
69
291
|
}
|
|
70
|
-
|
|
292
|
+
if (!LLAVES.has(ext) && !PYTHON.has(ext))
|
|
293
|
+
return { valid: true };
|
|
294
|
+
const { desbalance, inseguro } = analizar(content, ext);
|
|
295
|
+
if (inseguro || !desbalance)
|
|
296
|
+
return { valid: true };
|
|
297
|
+
const nombre = path.basename(filePath);
|
|
298
|
+
const que = desbalance.sobra
|
|
299
|
+
? `sobra un "${desbalance.simbolo}" (o falta lo que deberia abrirlo)`
|
|
300
|
+
: `queda un "${desbalance.simbolo}" sin cerrar`;
|
|
301
|
+
return {
|
|
302
|
+
valid: false,
|
|
303
|
+
warning: `⚠️ Sintaxis: en "${nombre}", linea ${desbalance.linea}, ${que}.\n` +
|
|
304
|
+
`${contexto(content, desbalance.linea)}\n` +
|
|
305
|
+
`Revisa esa linea antes de seguir: si el archivo esta roto, todo lo que hagas encima lo estara.`,
|
|
306
|
+
};
|
|
71
307
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { PermissionManager } from "../config/permissions.js";
|
|
2
|
+
import { HookManager } from "../hooks/manager.js";
|
|
3
|
+
/**
|
|
4
|
+
* LA PUERTA DE LAS HERRAMIENTAS
|
|
5
|
+
*
|
|
6
|
+
* El bucle principal pasa cada llamada por tres controles antes de ejecutar
|
|
7
|
+
* nada: los hooks PreToolUse del proyecto, el bloqueo del modo plan y el dialogo
|
|
8
|
+
* de permiso. Los subagentes no pasaban por ninguno: llamaban a executeToolCall
|
|
9
|
+
* directamente. Bastaba con pedir "lanza un subagente general que borre X" para
|
|
10
|
+
* que las tres barreras desaparecieran de golpe, incluido el modo plan, que se
|
|
11
|
+
* anuncia al usuario como de solo lectura.
|
|
12
|
+
*
|
|
13
|
+
* El problema de fondo era de fontaneria: los permisos y los hooks son campos
|
|
14
|
+
* del AgentLoop, y el subagente corre tres niveles por debajo (loop -> runner ->
|
|
15
|
+
* subagent) por una firma que no los lleva. Este modulo es el sitio donde
|
|
16
|
+
* apoyarlos: el bucle registra sus dos gestores al construirse y cualquier
|
|
17
|
+
* ejecutor de herramientas, este donde este, consulta los mismos.
|
|
18
|
+
*
|
|
19
|
+
* Se registra la INSTANCIA, no una copia: el modo cambia en caliente con /plan y
|
|
20
|
+
* con Shift+Tab, y una copia se quedaria en el modo que hubiera al arrancar.
|
|
21
|
+
*
|
|
22
|
+
* El bucle conserva su propia version en linea de estos tres pasos porque ademas
|
|
23
|
+
* pinta el resultado, guarda patrones y recoge el texto que el usuario escribe
|
|
24
|
+
* al rechazar. Si se toca el orden de los controles en un sitio, hay que tocarlo
|
|
25
|
+
* en el otro.
|
|
26
|
+
*/
|
|
27
|
+
export interface ToolGate {
|
|
28
|
+
permissions: PermissionManager;
|
|
29
|
+
hooks: HookManager;
|
|
30
|
+
}
|
|
31
|
+
/** Lo llama el AgentLoop al construirse. */
|
|
32
|
+
export declare function registerToolGate(permissions: PermissionManager, hooks: HookManager): void;
|
|
33
|
+
/**
|
|
34
|
+
* Si nadie ha registrado nada todavia se fabrican unos por defecto en vez de
|
|
35
|
+
* dejar pasar todo. Un ejecutor que corre sin puerta es exactamente el fallo que
|
|
36
|
+
* este modulo viene a cerrar, asi que ante la duda se cierra, no se abre.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getToolGate(cwd?: string): ToolGate;
|
|
39
|
+
export interface DecisionDeHerramienta {
|
|
40
|
+
permitida: boolean;
|
|
41
|
+
/** Que contarle al modelo cuando no se autoriza. */
|
|
42
|
+
motivo?: string;
|
|
43
|
+
/** Los argumentos definitivos: un hook puede haberlos reescrito. */
|
|
44
|
+
args: Record<string, any>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Los mismos tres controles que aplica el bucle, en el mismo orden.
|
|
48
|
+
*
|
|
49
|
+
* `origen` es quien pide, para que el dialogo no aparezca de la nada mientras el
|
|
50
|
+
* usuario cree que solo hay un agente trabajando.
|
|
51
|
+
*/
|
|
52
|
+
export declare function autorizarHerramienta(toolName: string, args: Record<string, any>, cwd?: string, origen?: string): Promise<DecisionDeHerramienta>;
|
|
53
|
+
/**
|
|
54
|
+
* PostToolUse. Los linters y las politicas del proyecto tienen que ver TODAS las
|
|
55
|
+
* escrituras, tambien las que hace un subagente; si no, el guardia solo vigila la
|
|
56
|
+
* puerta principal.
|
|
57
|
+
*/
|
|
58
|
+
export declare function notificarPostHerramienta(toolName: string, args: Record<string, any>, result: string, cwd?: string): Promise<string | null>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { PermissionManager, commandSignature } from "../config/permissions.js";
|
|
4
|
+
import { HookManager } from "../hooks/manager.js";
|
|
5
|
+
import { askToolPermission } from "../ui/permissionPrompt.js";
|
|
6
|
+
import { releaseKeyboard } from "../ui/interrupt.js";
|
|
7
|
+
let activa = null;
|
|
8
|
+
/** Lo llama el AgentLoop al construirse. */
|
|
9
|
+
export function registerToolGate(permissions, hooks) {
|
|
10
|
+
activa = { permissions, hooks };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Si nadie ha registrado nada todavia se fabrican unos por defecto en vez de
|
|
14
|
+
* dejar pasar todo. Un ejecutor que corre sin puerta es exactamente el fallo que
|
|
15
|
+
* este modulo viene a cerrar, asi que ante la duda se cierra, no se abre.
|
|
16
|
+
*/
|
|
17
|
+
export function getToolGate(cwd = process.cwd()) {
|
|
18
|
+
if (!activa) {
|
|
19
|
+
activa = { permissions: new PermissionManager("default", cwd), hooks: new HookManager(cwd) };
|
|
20
|
+
}
|
|
21
|
+
return activa;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Los mismos tres controles que aplica el bucle, en el mismo orden.
|
|
25
|
+
*
|
|
26
|
+
* `origen` es quien pide, para que el dialogo no aparezca de la nada mientras el
|
|
27
|
+
* usuario cree que solo hay un agente trabajando.
|
|
28
|
+
*/
|
|
29
|
+
export async function autorizarHerramienta(toolName, args, cwd = process.cwd(), origen = "El agente") {
|
|
30
|
+
const { permissions, hooks } = getToolGate(cwd);
|
|
31
|
+
// 1. PreToolUse. Va antes del dialogo porque lo que el proyecto prohibe no
|
|
32
|
+
// deberia ni llegar a preguntarse.
|
|
33
|
+
const pre = await hooks.run("PreToolUse", { tool_name: toolName, tool_input: args }, toolName);
|
|
34
|
+
if (pre.permissionDecision === "deny") {
|
|
35
|
+
return {
|
|
36
|
+
permitida: false,
|
|
37
|
+
args,
|
|
38
|
+
motivo: `Un hook del proyecto bloqueo esta accion: ${pre.permissionDecisionReason || "sin motivo"}. ` +
|
|
39
|
+
"No la reintentes igual; busca otra via o dilo en tu informe.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const finales = pre.updatedInput ? { ...args, ...pre.updatedInput } : args;
|
|
43
|
+
if (pre.systemMessage) {
|
|
44
|
+
console.log(chalk.yellow(`\n⚑ ${pre.systemMessage}`));
|
|
45
|
+
}
|
|
46
|
+
// 2. Modo plan: bloqueo duro, no un dialogo. El modo promete solo lectura, asi
|
|
47
|
+
// que aqui no hay nada que preguntar.
|
|
48
|
+
const bloqueoPlan = permissions.isBlockedInPlanMode(toolName, finales);
|
|
49
|
+
if (bloqueoPlan) {
|
|
50
|
+
return {
|
|
51
|
+
permitida: false,
|
|
52
|
+
args: finales,
|
|
53
|
+
motivo: `${bloqueoPlan} Devuelve lo que hayas averiguado y deja la ejecucion para cuando el usuario salga del modo plan.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// 3. denyPatterns: bloqueo duro, igual que en el bucle padre. No se pregunta,
|
|
57
|
+
// no lo salva el --yes ni el modo bypass. Un deny que valiera solo para el
|
|
58
|
+
// agente principal seria un deny que se esquiva pidiendole el comando a un
|
|
59
|
+
// subagente.
|
|
60
|
+
const denegado = permissions.deniedBy(toolName, finales);
|
|
61
|
+
if (denegado) {
|
|
62
|
+
return {
|
|
63
|
+
permitida: false,
|
|
64
|
+
args: finales,
|
|
65
|
+
motivo: `Bloqueado por denyPatterns ("${denegado}") en la configuracion del usuario. ` +
|
|
66
|
+
"No hay forma de aprobarlo desde aqui: busca otra via o dilo en tu informe.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// 4. Permiso del usuario.
|
|
70
|
+
const saltaPermiso = pre.permissionDecision === "allow";
|
|
71
|
+
if (!saltaPermiso && permissions.requiresConfirmation(toolName, finales, cwd)) {
|
|
72
|
+
// El vigilante de Esc tiene el teclado en modo crudo mientras dura el turno.
|
|
73
|
+
// Si no se le suelta antes, se come las teclas del dialogo y el usuario ve
|
|
74
|
+
// una caja que no responde.
|
|
75
|
+
releaseKeyboard();
|
|
76
|
+
console.log(chalk.gray(`\n${origen} pide permiso para usar "${toolName}":`));
|
|
77
|
+
const res = await askToolPermission(toolName, finales, cwd);
|
|
78
|
+
if (!res.approved) {
|
|
79
|
+
return {
|
|
80
|
+
permitida: false,
|
|
81
|
+
args: finales,
|
|
82
|
+
motivo: `El usuario no autorizo esta accion: "${res.userFeedback || "Cancelado"}". ` +
|
|
83
|
+
"Ajustate a lo que dice y no la repitas igual.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (res.rememberPattern) {
|
|
87
|
+
// La firma (binario mas subcomando), NO el primer token: guardar "git" al
|
|
88
|
+
// aprobar un "git status" autoriza por prefijo todo lo que empiece igual.
|
|
89
|
+
const patron = finales.command
|
|
90
|
+
? `${toolName}:${commandSignature(String(finales.command))}`
|
|
91
|
+
: finales.path || toolName;
|
|
92
|
+
permissions.saveAllowPattern(String(patron));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { permitida: true, args: finales };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* PostToolUse. Los linters y las politicas del proyecto tienen que ver TODAS las
|
|
99
|
+
* escrituras, tambien las que hace un subagente; si no, el guardia solo vigila la
|
|
100
|
+
* puerta principal.
|
|
101
|
+
*/
|
|
102
|
+
export async function notificarPostHerramienta(toolName, args, result, cwd = process.cwd()) {
|
|
103
|
+
const { hooks } = getToolGate(cwd);
|
|
104
|
+
const post = await hooks.run("PostToolUse", { tool_name: toolName, tool_input: args, tool_response: result.slice(0, 4000) }, toolName);
|
|
105
|
+
// El systemMessage es para el usuario, no para el modelo, y por aqui se
|
|
106
|
+
// perdia: quien llama solo se lleva el contexto que hay que reinyectar.
|
|
107
|
+
if (post.systemMessage) {
|
|
108
|
+
console.log(chalk.yellow(`\n⚑ ${post.systemMessage}`));
|
|
109
|
+
}
|
|
110
|
+
return post.additionalContext || null;
|
|
111
|
+
}
|
package/dist/agent/tracker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { desglosarCoste } from "./usageMeter.js";
|
|
2
2
|
export class TokenTracker {
|
|
3
3
|
totalPromptTokens = 0;
|
|
4
4
|
totalCompletionTokens = 0;
|
|
@@ -14,12 +14,9 @@ export class TokenTracker {
|
|
|
14
14
|
this.cachedPromptTokens += cached;
|
|
15
15
|
}
|
|
16
16
|
calculateCost(promptTokens = this.totalPromptTokens, cachedTokens = this.cachedPromptTokens, completionTokens = this.totalCompletionTokens, modelName = "deepseek-v4-flash") {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const inputCost = (
|
|
20
|
-
(cachedTokens / 1_000_000) * pricing.inputCacheHit;
|
|
21
|
-
const outputCost = (completionTokens / 1_000_000) * pricing.output;
|
|
22
|
-
const totalCost = inputCost + outputCost;
|
|
17
|
+
// La tarifa se aplica en usageMeter.ts y solo alli: el pie del turno y lo
|
|
18
|
+
// que se apunta en la cuota tienen que salir de la misma cuenta.
|
|
19
|
+
const { inputCost, outputCost, totalCost } = desglosarCoste(promptTokens, cachedTokens, completionTokens, modelName);
|
|
23
20
|
const formattedCost = totalCost < 0.0001
|
|
24
21
|
? `$${totalCost.toFixed(5)} USD`
|
|
25
22
|
: `$${totalCost.toFixed(4)} USD`;
|
|
@@ -1,32 +1,65 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Estado de una ruta justo antes de que el turno la tocara.
|
|
3
|
+
*
|
|
4
|
+
* Antes esto era un solo campo `previousContent: Buffer | null` y ahi estaba el
|
|
5
|
+
* fallo: null significaba a la vez "no existia" y "no pude leerlo". Un
|
|
6
|
+
* readFileSync sobre un directorio lanza EISDIR, el catch guardaba null, y al
|
|
7
|
+
* deshacer se interpretaba como "lo creamos nosotros" -> unlinkSync. Es decir,
|
|
8
|
+
* /undo podia BORRAR un archivo del usuario que solo habia quedado bloqueado
|
|
9
|
+
* (EBUSY/EPERM en Windows), y anunciaba "Deshecho con exito" sin haber
|
|
10
|
+
* restaurado nada. Con tres estados explicitos no hay forma de confundirlos:
|
|
11
|
+
* solo se borra lo que consta como inexistente, y lo que no se pudo fotografiar
|
|
12
|
+
* se reporta como lo que es, un borrado sin vuelta atras.
|
|
13
|
+
*/
|
|
14
|
+
export type SnapshotKind = "absent" | "file" | "unrestorable";
|
|
15
|
+
export type FileSnapshot = {
|
|
2
16
|
filePath: string;
|
|
3
|
-
|
|
4
|
-
}
|
|
17
|
+
kind: "absent";
|
|
18
|
+
} | {
|
|
19
|
+
filePath: string;
|
|
20
|
+
kind: "file";
|
|
21
|
+
previousContent: Buffer;
|
|
22
|
+
} | {
|
|
23
|
+
filePath: string;
|
|
24
|
+
kind: "unrestorable";
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
5
27
|
export interface Checkpoint {
|
|
6
28
|
id: string;
|
|
7
29
|
timestamp: string;
|
|
8
30
|
description: string;
|
|
9
31
|
snapshots: FileSnapshot[];
|
|
10
32
|
}
|
|
33
|
+
export interface UndoResult {
|
|
34
|
+
success: boolean;
|
|
35
|
+
message: string;
|
|
36
|
+
restoredFiles: string[];
|
|
37
|
+
/** Rutas que el checkpoint no puede devolver a su estado anterior. */
|
|
38
|
+
failures: string[];
|
|
39
|
+
}
|
|
11
40
|
export declare class UndoManager {
|
|
12
41
|
private checkpoints;
|
|
13
42
|
private currentTurnSnapshots;
|
|
14
43
|
/**
|
|
15
|
-
*
|
|
44
|
+
* Fotografia una ruta antes de modificarla o borrarla.
|
|
45
|
+
*
|
|
46
|
+
* Devuelve el tipo de foto que se guardo para que quien llama pueda avisar en
|
|
47
|
+
* el momento: si sale "unrestorable", la operacion que venga despues es
|
|
48
|
+
* definitiva y no tiene sentido prometer un /undo que no va a funcionar.
|
|
16
49
|
*/
|
|
17
|
-
snapshotFile(filePath: string, cwd?: string):
|
|
50
|
+
snapshotFile(filePath: string, cwd?: string): SnapshotKind;
|
|
18
51
|
/**
|
|
19
52
|
* Finalizes the current turn and saves a rollback checkpoint
|
|
20
53
|
*/
|
|
21
54
|
commitTurn(description: string): void;
|
|
22
55
|
/**
|
|
23
|
-
*
|
|
56
|
+
* Deshace el ultimo turno de modificaciones.
|
|
57
|
+
*
|
|
58
|
+
* success solo es true si algo se restauro de verdad y nada fallo. Antes se
|
|
59
|
+
* devolvia true siempre, incluso con la lista de restaurados vacia, que es el
|
|
60
|
+
* peor mensaje posible: el usuario da por recuperado lo que ya no existe.
|
|
24
61
|
*/
|
|
25
|
-
undoLast():
|
|
26
|
-
success: boolean;
|
|
27
|
-
message: string;
|
|
28
|
-
restoredFiles: string[];
|
|
29
|
-
};
|
|
62
|
+
undoLast(): UndoResult;
|
|
30
63
|
getHistory(): {
|
|
31
64
|
id: string;
|
|
32
65
|
description: string;
|