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.
Files changed (122) hide show
  1. package/README.md +155 -0
  2. package/dist/agent/compaction.d.ts +65 -0
  3. package/dist/agent/compaction.js +134 -0
  4. package/dist/agent/context.d.ts +3 -0
  5. package/dist/agent/context.js +35 -0
  6. package/dist/agent/loop.d.ts +91 -2
  7. package/dist/agent/loop.js +642 -91
  8. package/dist/agent/loopDetector.d.ts +57 -0
  9. package/dist/agent/loopDetector.js +0 -0
  10. package/dist/agent/mentions.d.ts +13 -0
  11. package/dist/agent/mentions.js +49 -0
  12. package/dist/agent/repoMap.d.ts +117 -0
  13. package/dist/agent/repoMap.js +560 -0
  14. package/dist/agent/result.d.ts +64 -0
  15. package/dist/agent/result.js +48 -0
  16. package/dist/agent/retry.d.ts +35 -0
  17. package/dist/agent/retry.js +123 -4
  18. package/dist/agent/salidaDelAgente.d.ts +43 -0
  19. package/dist/agent/salidaDelAgente.js +53 -0
  20. package/dist/agent/subagent.js +62 -15
  21. package/dist/agent/syntaxValidator.js +288 -52
  22. package/dist/agent/toolGate.d.ts +58 -0
  23. package/dist/agent/toolGate.js +111 -0
  24. package/dist/agent/tracker.js +4 -7
  25. package/dist/agent/undoManager.d.ts +44 -11
  26. package/dist/agent/undoManager.js +95 -24
  27. package/dist/agent/usageMeter.d.ts +56 -0
  28. package/dist/agent/usageMeter.js +51 -0
  29. package/dist/agent/verifier.d.ts +49 -0
  30. package/dist/agent/verifier.js +158 -0
  31. package/dist/config/constants.js +14 -0
  32. package/dist/config/engine.d.ts +19 -0
  33. package/dist/config/engine.js +35 -3
  34. package/dist/config/license.d.ts +5 -0
  35. package/dist/config/license.js +107 -9
  36. package/dist/config/limits.d.ts +28 -0
  37. package/dist/config/limits.js +51 -0
  38. package/dist/config/nodeVersion.d.ts +13 -0
  39. package/dist/config/nodeVersion.js +26 -0
  40. package/dist/config/permissions.d.ts +76 -1
  41. package/dist/config/permissions.js +379 -23
  42. package/dist/config/quota.js +24 -1
  43. package/dist/config/updater.d.ts +68 -0
  44. package/dist/config/updater.js +215 -0
  45. package/dist/hooks/manager.js +159 -14
  46. package/dist/index.js +240 -37
  47. package/dist/mcp/client.d.ts +50 -0
  48. package/dist/mcp/client.js +155 -19
  49. package/dist/mcp/ide.d.ts +76 -0
  50. package/dist/mcp/ide.js +160 -0
  51. package/dist/mcp/manager.js +9 -0
  52. package/dist/prompts/systemPrompt.js +12 -1
  53. package/dist/sessions/manager.d.ts +12 -0
  54. package/dist/sessions/manager.js +33 -5
  55. package/dist/sessions/resume.d.ts +36 -0
  56. package/dist/sessions/resume.js +43 -0
  57. package/dist/skills/manager.d.ts +1 -0
  58. package/dist/skills/manager.js +9 -0
  59. package/dist/tools/binary.d.ts +46 -0
  60. package/dist/tools/binary.js +100 -0
  61. package/dist/tools/browserCdp.js +13 -1
  62. package/dist/tools/browserExtension.d.ts +5 -0
  63. package/dist/tools/browserExtension.js +237 -22
  64. package/dist/tools/browserSession.d.ts +23 -0
  65. package/dist/tools/browserSession.js +28 -0
  66. package/dist/tools/computerUse.js +33 -1
  67. package/dist/tools/createImage.d.ts +11 -0
  68. package/dist/tools/createImage.js +13 -1
  69. package/dist/tools/definitions.js +17 -4
  70. package/dist/tools/editDiagnosis.d.ts +54 -0
  71. package/dist/tools/editDiagnosis.js +142 -0
  72. package/dist/tools/editFile.d.ts +1 -0
  73. package/dist/tools/editFile.js +118 -62
  74. package/dist/tools/findFiles.d.ts +21 -0
  75. package/dist/tools/findFiles.js +75 -6
  76. package/dist/tools/getSystemInfo.js +15 -7
  77. package/dist/tools/gitTools.d.ts +24 -0
  78. package/dist/tools/gitTools.js +87 -26
  79. package/dist/tools/grepSearch.js +111 -23
  80. package/dist/tools/moveCopyFile.d.ts +18 -1
  81. package/dist/tools/moveCopyFile.js +74 -4
  82. package/dist/tools/patchCascade.d.ts +57 -0
  83. package/dist/tools/patchCascade.js +338 -0
  84. package/dist/tools/runCommand.js +213 -25
  85. package/dist/tools/runner.js +58 -5
  86. package/dist/tools/toolDefsComputer.js +12 -2
  87. package/dist/tools/truncator.d.ts +27 -1
  88. package/dist/tools/truncator.js +117 -11
  89. package/dist/tools/viewFile.js +15 -11
  90. package/dist/tools/visionBridge.js +7 -0
  91. package/dist/tools/webSearch.d.ts +21 -0
  92. package/dist/tools/webSearch.js +125 -14
  93. package/dist/tools/writeFile.d.ts +16 -1
  94. package/dist/tools/writeFile.js +40 -2
  95. package/dist/ui/interrupt.d.ts +4 -1
  96. package/dist/ui/interrupt.js +59 -7
  97. package/dist/ui/keyboardGuard.d.ts +76 -0
  98. package/dist/ui/keyboardGuard.js +94 -0
  99. package/dist/ui/loopPrompt.d.ts +17 -0
  100. package/dist/ui/loopPrompt.js +59 -0
  101. package/dist/ui/pegados.d.ts +50 -0
  102. package/dist/ui/pegados.js +83 -0
  103. package/dist/ui/permissionPrompt.js +84 -2
  104. package/dist/ui/prompt.d.ts +40 -0
  105. package/dist/ui/prompt.js +436 -131
  106. package/dist/ui/reasoningStream.d.ts +8 -0
  107. package/dist/ui/reasoningStream.js +99 -7
  108. package/dist/ui/salida.d.ts +10 -0
  109. package/dist/ui/salida.js +50 -0
  110. package/extension/README.md +95 -74
  111. package/extension/background.js +772 -181
  112. package/extension/content.js +527 -46
  113. package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
  114. package/extension/iconos/128.png +0 -0
  115. package/extension/iconos/16.png +0 -0
  116. package/extension/iconos/32.png +0 -0
  117. package/extension/iconos/48.png +0 -0
  118. package/extension/iconos/logotipo.png +0 -0
  119. package/extension/manifest.json +28 -4
  120. package/extension/popup.html +16 -6
  121. package/extension/popup.js +28 -12
  122. package/package.json +3 -2
@@ -1,71 +1,307 @@
1
1
  import path from "node:path";
2
- export function validateFileSyntax(filePath, content) {
3
- const ext = path.extname(filePath).toLowerCase();
4
- // 1. JSON validation
5
- if (ext === ".json") {
6
- try {
7
- JSON.parse(content);
8
- return { valid: true };
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
- catch (err) {
11
- return {
12
- valid: false,
13
- warning: `⚠️ Advertencia de sintaxis JSON: ${err?.message || "JSON inválido"}`,
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
- // 2. Matching brackets & braces check for JS/TS/Python
18
- if ([".ts", ".js", ".tsx", ".jsx", ".py", ".rs", ".go"].includes(ext)) {
19
- const stack = [];
20
- const pairs = { "}": "{", "]": "[", ")": "(" };
21
- // Ignore comments & strings roughly to avoid false positives
22
- let inString = null;
23
- let inComment = false;
24
- for (let i = 0; i < content.length; i++) {
25
- const char = content[i];
26
- const nextChar = content[i + 1];
27
- // Comments
28
- if (!inString) {
29
- if (char === "/" && nextChar === "/") {
30
- while (i < content.length && content[i] !== "\n")
31
- i++;
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 (char === "#" && ext === ".py") {
35
- while (i < content.length && content[i] !== "\n")
36
- i++;
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
- // Strings
41
- if ((char === '"' || char === "'" || char === "`") && content[i - 1] !== "\\") {
42
- if (!inString)
43
- inString = char;
44
- else if (inString === char)
45
- inString = null;
46
- continue;
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 (inString)
226
+ if (cerrada) {
227
+ i = j;
228
+ anterior = "/";
49
229
  continue;
50
- if (["{", "[", "("].includes(char)) {
51
- stack.push(char);
52
230
  }
53
- else if (["}", "]", ")"].includes(char)) {
54
- const expected = pairs[char];
55
- if (stack.length === 0 || stack.pop() !== expected) {
56
- return {
57
- valid: false,
58
- warning: `⚠️ Posible desbalance de llaves/paréntesis en "${path.basename(filePath)}" cerca del cierre '${char}'.`,
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 (stack.length > 0 && stack.length < 5) {
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: `⚠️ Posible llave o paréntesis sin cerrar en "${path.basename(filePath)}" ('${stack.join("', '")}').`,
288
+ warning: `⚠️ Advertencia de sintaxis JSON: ${err?.message || "JSON inválido"}`,
67
289
  };
68
290
  }
69
291
  }
70
- return { valid: true };
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
+ }
@@ -1,4 +1,4 @@
1
- import { MODEL_PRICING, PRICING } from "../config/constants.js";
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
- const pricing = MODEL_PRICING[modelName] || PRICING;
18
- const uncachedPrompt = Math.max(0, promptTokens - cachedTokens);
19
- const inputCost = (uncachedPrompt / 1_000_000) * pricing.inputCacheMiss +
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
- export interface FileSnapshot {
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
- previousContent: string | null;
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
- * Captures the state of a file before modifying or deleting it
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): void;
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
- * Undoes the last turn of modifications
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;