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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { isBrowserAttached } from "../tools/browserSession.js";
|
|
4
5
|
/**
|
|
5
6
|
* Acciones de SOLO OBSERVAR de cada herramienta de control.
|
|
6
7
|
*
|
|
@@ -30,19 +31,131 @@ const OBSERVE_ONLY = {
|
|
|
30
31
|
};
|
|
31
32
|
/** Herramientas que conducen el ordenador o el navegador del usuario. */
|
|
32
33
|
const CONTROL_TOOLS = Object.keys(OBSERVE_ONLY);
|
|
34
|
+
/**
|
|
35
|
+
* Herramientas cuyas acciones "observadoras" pueden ARRANCAR el navegador.
|
|
36
|
+
*
|
|
37
|
+
* browserCdp llama a ensureBrowser() en todas las acciones menos "close", y
|
|
38
|
+
* ensureBrowser, si no encuentra un Chrome escuchando el puerto de depuracion,
|
|
39
|
+
* lo lanza con spawn(detached) y lo deja vivo cuando la CLI termina. Con
|
|
40
|
+
* useSystemProfile lo lanza sobre el perfil REAL del usuario —sus cookies y sus
|
|
41
|
+
* sesiones— con el 9222 abierto en 127.0.0.1, que cualquier proceso local puede
|
|
42
|
+
* conducir. Clasificar eso como "solo observar" no lo hacia inofensivo: lo hacia
|
|
43
|
+
* silencioso, y ademas lo colaba en modo plan.
|
|
44
|
+
*
|
|
45
|
+
* La regla sigue siendo "mirar es gratis", pero mirar solo es gratis cuando no
|
|
46
|
+
* hay nada que arrancar: con una sesion CDP ya viva, snapshot y tabs no hacen
|
|
47
|
+
* mas que leer.
|
|
48
|
+
*/
|
|
49
|
+
const BROWSER_TOOLS = new Set(["browser", "browser_live"]);
|
|
33
50
|
/** Herramientas que modifican el disco o el repositorio. */
|
|
34
51
|
const MODIFYING_TOOLS = ["edit_file", "write_file", "delete_file", "move_copy_file", "git_commit"];
|
|
52
|
+
/**
|
|
53
|
+
* Argumentos con ruta de cada herramienta que escribe en disco.
|
|
54
|
+
*
|
|
55
|
+
* Sirve para lo unico que la tabla no comprobaba en ningun sitio: si lo que se
|
|
56
|
+
* va a tocar cae dentro del proyecto o en cualquier otro punto del disco.
|
|
57
|
+
*/
|
|
58
|
+
const RUTAS_DE_ESCRITURA = {
|
|
59
|
+
write_file: ["path"],
|
|
60
|
+
edit_file: ["path"],
|
|
61
|
+
delete_file: ["path"],
|
|
62
|
+
move_copy_file: ["sourcePath", "destinationPath"],
|
|
63
|
+
create_image: ["outputPath"],
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Una ruta esta fuera del proyecto si, resuelta, no cuelga de la raiz.
|
|
67
|
+
*
|
|
68
|
+
* Vale para rutas absolutas (C:\Windows\..., ~/.ssh/...) y para las relativas
|
|
69
|
+
* con ".." que salen del arbol, que es la forma que no se ve venir leyendo la
|
|
70
|
+
* llamada por encima. path.relative en Windows ya compara sin distinguir
|
|
71
|
+
* mayusculas y devuelve la ruta entera cuando cambia de unidad.
|
|
72
|
+
*/
|
|
73
|
+
export function isOutsideProject(projectRoot, target) {
|
|
74
|
+
if (typeof target !== "string" || !target.trim())
|
|
75
|
+
return false;
|
|
76
|
+
const raiz = path.resolve(projectRoot);
|
|
77
|
+
const destino = path.resolve(raiz, target);
|
|
78
|
+
const rel = path.relative(raiz, destino);
|
|
79
|
+
if (rel === "")
|
|
80
|
+
return false;
|
|
81
|
+
return rel.startsWith("..") || path.isAbsolute(rel);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* create_image mete dos cosas muy distintas en el mismo argumento: un SVG, que
|
|
85
|
+
* solo se escribe en disco, o codigo Python, que se guarda en un .py y se lanza
|
|
86
|
+
* con `python`. Quien lo distingue es esta funcion, y la usan tanto la
|
|
87
|
+
* herramienta como la tabla de permisos: si cada una detectara el SVG a su
|
|
88
|
+
* manera, bastaria una diferencia entre las dos para colar codigo por la rama
|
|
89
|
+
* que no pregunta.
|
|
90
|
+
*/
|
|
91
|
+
export function isSvgSource(script) {
|
|
92
|
+
const s = typeof script === "string" ? script.trim() : "";
|
|
93
|
+
return s.startsWith("<svg") || s.startsWith("<?xml");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* create_image con Python es ejecucion de codigo arbitrario con nombre de
|
|
97
|
+
* herramienta de dibujo: el contenido que escriba el modelo acaba corriendo
|
|
98
|
+
* entero, no solo la parte que pinta. La especificacion la clasifica como
|
|
99
|
+
* `exec`, igual que run_command; la tabla la trataba como una escritura de
|
|
100
|
+
* archivo corriente porque clasificaba por nombre y este no salia en ninguna
|
|
101
|
+
* lista.
|
|
102
|
+
*/
|
|
103
|
+
function isCodeGeneration(toolName, args) {
|
|
104
|
+
return toolName === "create_image" && !isSvgSource(args?.generatorScript);
|
|
105
|
+
}
|
|
35
106
|
/** Solo lee estado, no toca nada. */
|
|
36
107
|
function isObserveOnly(toolName, args) {
|
|
37
108
|
const set = OBSERVE_ONLY[toolName];
|
|
38
109
|
if (!set)
|
|
39
110
|
return false;
|
|
40
|
-
|
|
111
|
+
if (!set.has(String(args?.action ?? "")))
|
|
112
|
+
return false;
|
|
113
|
+
// Sin sesion CDP viva, hasta el "attach" mas inocente puede acabar en un
|
|
114
|
+
// spawn(chrome.exe, --remote-debugging-port) que sobrevive a la sesion. Eso no
|
|
115
|
+
// es observar: es arrancar un proceso, y se pregunta.
|
|
116
|
+
if (BROWSER_TOOLS.has(toolName) && !isBrowserAttached())
|
|
117
|
+
return false;
|
|
118
|
+
return true;
|
|
41
119
|
}
|
|
42
120
|
/** Conduce de verdad el ordenador o el navegador: clics, teclado, navegacion, JS. */
|
|
43
121
|
function isActuation(toolName, args) {
|
|
44
122
|
return CONTROL_TOOLS.includes(toolName) && !isObserveOnly(toolName, args);
|
|
45
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Prefijo de git que admite opciones globales antes del subcomando.
|
|
126
|
+
*
|
|
127
|
+
* El regex anterior exigia el subcomando pegado a "git", asi que
|
|
128
|
+
* "git -C otra/ruta checkout -- ." no lo veia nadie: bastaba con decirle a git
|
|
129
|
+
* en que repositorio trabajar para saltarse el filtro entero.
|
|
130
|
+
*/
|
|
131
|
+
const GIT = String.raw `\bgit(?:\s+(?:-[cC]\s+\S+|--[\w-]+(?:=\S+)?|-[a-zA-Z]))*\s+`;
|
|
132
|
+
/**
|
|
133
|
+
* Git que destruye trabajo o pisa el remoto.
|
|
134
|
+
*
|
|
135
|
+
* Antes esto era un solo regex con las alternativas metidas en un grupo y un
|
|
136
|
+
* \b de cierre fuera: /\bgit\s+(...|restore\s+\.|checkout\s+\.|...)\b/. Como
|
|
137
|
+
* "restore \." y "checkout \." acaban en punto, que no es caracter de palabra,
|
|
138
|
+
* ese \b final NUNCA casaba y las dos ramas estaban muertas. Resultado: la regla
|
|
139
|
+
* quedaba del reves, "git checkout .gitignore" pedia dialogo (ahi el \b si
|
|
140
|
+
* encontraba frontera) y "git checkout .", que borra el arbol entero, se
|
|
141
|
+
* ejecutaba sin preguntar. Es el mismo fallo que el comentario de "format c:"
|
|
142
|
+
* dice haber arreglado, dejado intacto aqui.
|
|
143
|
+
*
|
|
144
|
+
* Ahora es una regla por forma destructiva, sin \b traicioneros.
|
|
145
|
+
*/
|
|
146
|
+
const GIT_DESTRUCTIVO = [
|
|
147
|
+
new RegExp(GIT + String.raw `reset\s+--hard`, "i"),
|
|
148
|
+
new RegExp(GIT + String.raw `clean\s+-[a-z]*[fxd]`, "i"),
|
|
149
|
+
new RegExp(GIT + String.raw `push\b[^\n]*(\s-f\b|--force)`, "i"),
|
|
150
|
+
new RegExp(GIT + String.raw `branch\s+-D\b`, "i"),
|
|
151
|
+
// "restore" existe justamente para descartar cambios del arbol de trabajo: no
|
|
152
|
+
// hay forma inocente de usarlo, ni siquiera --staged, que deshace el stage.
|
|
153
|
+
new RegExp(GIT + String.raw `restore\b`, "i"),
|
|
154
|
+
// "checkout" tambien cambia de rama, que no destruye nada. Lo que borra
|
|
155
|
+
// trabajo es el pathspec: un "." pelado, un "*", cualquier cosa detras de
|
|
156
|
+
// "--" (incluido el "git checkout HEAD -- ." de toda la vida) o un -f.
|
|
157
|
+
new RegExp(GIT + String.raw `checkout\b[^\n]*(\s--(\s|$)|\s\.(\s|$)|\s\*(\s|$)|\s-f\b|\s--force\b)`, "i"),
|
|
158
|
+
];
|
|
46
159
|
/**
|
|
47
160
|
* Detecta comandos destructivos o de alto riesgo que siempre deben pedir confirmación.
|
|
48
161
|
* Comandos seguros de desarrollo (python, npm, pytest, tsc, git status, etc.) no interrumpen al usuario.
|
|
@@ -54,13 +167,13 @@ export function isDangerousCommand(command) {
|
|
|
54
167
|
// 1. Borrado recursivo o forzado de archivos/directorios.
|
|
55
168
|
// Ojo con \b al final: "format c:" termina en ':', que no es caracter de
|
|
56
169
|
// palabra, asi que un \b de cierre nunca casa y el patron no servia de nada.
|
|
57
|
-
if (/\b(rm\s+[^\n]*-[^\n]*[rf]|rmdir\s+[^\n]*\/s|\brd\s+[^\n]*\/s|del\s+[^\n]*\/[fsq]|Remove-Item\s+[^\n]*-
|
|
170
|
+
if (/\b(rm\s+[^\n]*-[^\n]*[rf]|rmdir\s+[^\n]*\/s|\brd\s+[^\n]*\/s|del\s+[^\n]*\/[fsq]|Remove-Item\s+[^\n]*-r(?:e(?:c(?:u(?:r(?:s(?:e)?)?)?)?)?)?\b|mkfs)\b/i.test(cmd) ||
|
|
58
171
|
/\bformat\s+[a-z]:/i.test(cmd) ||
|
|
59
172
|
/\bdd\s+[^\n]*\bof=/i.test(cmd)) {
|
|
60
173
|
return true;
|
|
61
174
|
}
|
|
62
175
|
// 2. Operaciones destructivas de Git (pérdida irreversible de trabajo o sobreescritura remota)
|
|
63
|
-
if (
|
|
176
|
+
if (GIT_DESTRUCTIVO.some((re) => re.test(cmd))) {
|
|
64
177
|
return true;
|
|
65
178
|
}
|
|
66
179
|
// 3. Terminación de procesos y apagado del sistema
|
|
@@ -95,13 +208,91 @@ export function isDangerousCommand(command) {
|
|
|
95
208
|
}
|
|
96
209
|
return false;
|
|
97
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Cuanto restringe cada modo. Sirve para decidir si una capa de settings sube o
|
|
213
|
+
* baja el liston: el proyecto solo puede subirlo.
|
|
214
|
+
*/
|
|
215
|
+
const RESTRICCION = {
|
|
216
|
+
bypass: 0,
|
|
217
|
+
acceptEdits: 1,
|
|
218
|
+
default: 2,
|
|
219
|
+
plan: 3,
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Marcas de "aqui empieza otro comando".
|
|
223
|
+
*
|
|
224
|
+
* La lista anterior era /[;&|]/ y se dejaba fuera el salto de linea, que en
|
|
225
|
+
* PowerShell y en bash separa sentencias igual que un punto y coma. runCommand
|
|
226
|
+
* le pasa al shell la cadena entera de una pieza, asi que con un patron tan
|
|
227
|
+
* inocente como "npm" guardado (loop.ts guarda solo el primer token del comando
|
|
228
|
+
* aprobado: aceptar una vez `npm publish` guarda "npm"), esto se ejecutaba
|
|
229
|
+
* entero y sin un solo dialogo:
|
|
230
|
+
*
|
|
231
|
+
* npm test
|
|
232
|
+
* Remove-Item -Recurse -Force C:\Users\yo\proyecto
|
|
233
|
+
*
|
|
234
|
+
* Van tambien el retorno de carro y las dos formas de sustitucion de comandos,
|
|
235
|
+
* `...` y $(...), que lanzan un segundo proceso sin necesitar separador.
|
|
236
|
+
*/
|
|
237
|
+
const ENCADENADO = /[;&|\r\n`]|\$\(/;
|
|
238
|
+
/** Trocea la linea en sentencias sueltas. Un deny tiene que mirarlas todas. */
|
|
239
|
+
function sentencias(command) {
|
|
240
|
+
return command
|
|
241
|
+
.split(/[;\r\n]+|&&|\|\||[|&]/)
|
|
242
|
+
.map((s) => s.trim())
|
|
243
|
+
.filter(Boolean);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Piezas que por si solas no dicen que hace el comando: hay que mirar la
|
|
247
|
+
* siguiente. "npm run" puede ser cualquier cosa; "npm run build" ya no.
|
|
248
|
+
*/
|
|
249
|
+
const DESPACHADORES = new Set(["run", "run-script", "exec", "x", "dlx", "workspace"]);
|
|
250
|
+
/**
|
|
251
|
+
* Firma con la que se recuerda un comando aprobado.
|
|
252
|
+
*
|
|
253
|
+
* El bucle guardaba command.split(" ")[0], o sea solo el binario: aprobar un
|
|
254
|
+
* "git status" dejaba escrito el patron "git" y, como el allow autoriza por
|
|
255
|
+
* prefijo, a partir de ahi pasaban sin dialogo tambien "git push --force" y
|
|
256
|
+
* "git reset --hard", en todas las sesiones del proyecto.
|
|
257
|
+
*
|
|
258
|
+
* La firma es binario + subcomando ("git status", "npm run build"). Si el
|
|
259
|
+
* comando no trae subcomando sino opciones se guarda entero: ensancharlo hasta
|
|
260
|
+
* el binario es justo el fallo que se esta arreglando.
|
|
261
|
+
*/
|
|
262
|
+
export function commandSignature(command) {
|
|
263
|
+
if (!command || typeof command !== "string")
|
|
264
|
+
return "";
|
|
265
|
+
// Lo encadenado detras de ; && | no forma parte de lo que el usuario aprobo.
|
|
266
|
+
const primero = command.trim().split(/[;&|\r\n]/)[0].trim();
|
|
267
|
+
const partes = primero.split(/\s+/).filter(Boolean);
|
|
268
|
+
if (partes.length === 0)
|
|
269
|
+
return "";
|
|
270
|
+
const firma = [partes[0]];
|
|
271
|
+
for (let i = 1; i < partes.length && firma.length < 3; i++) {
|
|
272
|
+
const pieza = partes[i];
|
|
273
|
+
// Una opcion ya no describe que hace el comando; el subcomando, si.
|
|
274
|
+
if (pieza.startsWith("-"))
|
|
275
|
+
break;
|
|
276
|
+
firma.push(pieza);
|
|
277
|
+
if (!DESPACHADORES.has(pieza))
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
// Sin subcomando se guarda el comando entero. Reducirlo al binario es
|
|
281
|
+
// exactamente lo que dejaba "git" autorizando cualquier git.
|
|
282
|
+
if (firma.length === 1 && partes.length > 1)
|
|
283
|
+
return primero;
|
|
284
|
+
return firma.join(" ");
|
|
285
|
+
}
|
|
98
286
|
export class PermissionManager {
|
|
99
287
|
mode = "default";
|
|
100
288
|
allowPatterns = new Set();
|
|
101
289
|
denyPatterns = new Set();
|
|
102
290
|
settingsFilePath;
|
|
291
|
+
/** Raiz del proyecto: el limite dentro del cual escribir no interrumpe. */
|
|
292
|
+
projectRoot;
|
|
103
293
|
constructor(initialMode = "default", cwd = process.cwd()) {
|
|
104
294
|
this.mode = initialMode;
|
|
295
|
+
this.projectRoot = path.resolve(cwd);
|
|
105
296
|
this.settingsFilePath = path.join(cwd, ".chocolatito", "settings.json");
|
|
106
297
|
this.loadSettings();
|
|
107
298
|
}
|
|
@@ -111,7 +302,7 @@ export class PermissionManager {
|
|
|
111
302
|
if (fs.existsSync(globalPath)) {
|
|
112
303
|
try {
|
|
113
304
|
const parsed = JSON.parse(fs.readFileSync(globalPath, "utf-8"));
|
|
114
|
-
this.applySettings(parsed);
|
|
305
|
+
this.applySettings(parsed, "global");
|
|
115
306
|
}
|
|
116
307
|
catch { }
|
|
117
308
|
}
|
|
@@ -119,14 +310,33 @@ export class PermissionManager {
|
|
|
119
310
|
if (fs.existsSync(this.settingsFilePath)) {
|
|
120
311
|
try {
|
|
121
312
|
const parsed = JSON.parse(fs.readFileSync(this.settingsFilePath, "utf-8"));
|
|
122
|
-
this.applySettings(parsed);
|
|
313
|
+
this.applySettings(parsed, "proyecto");
|
|
123
314
|
}
|
|
124
315
|
catch { }
|
|
125
316
|
}
|
|
126
317
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Aplica una capa de settings.
|
|
320
|
+
*
|
|
321
|
+
* La capa del proyecto solo puede RESTRINGIR. Antes se aplicaba despues de la
|
|
322
|
+
* global y fijaba el modo sin mas, asi que un repositorio clonado con
|
|
323
|
+
* {"permissions":{"mode":"bypass"}} en su .chocolatito/settings.json apagaba
|
|
324
|
+
* los dialogos en la maquina de quien lo abriera, sin decir nada. Elevar
|
|
325
|
+
* permisos es cosa del usuario: su settings global o la linea de comandos.
|
|
326
|
+
*/
|
|
327
|
+
applySettings(settings, origen) {
|
|
328
|
+
const modo = settings.permissions?.mode;
|
|
329
|
+
if (modo) {
|
|
330
|
+
if (origen === "global" || RESTRICCION[modo] >= RESTRICCION[this.mode]) {
|
|
331
|
+
this.mode = modo;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Se avisa por pantalla: un intento de elevacion silencioso es
|
|
335
|
+
// exactamente lo que hay que ver, no lo que hay que tragarse.
|
|
336
|
+
console.warn(`Aviso de permisos: el settings.json de este proyecto pide el modo "${modo}" y se ignora. ` +
|
|
337
|
+
`Un repositorio no puede elevar los permisos de tu maquina; sigues en "${this.mode}". ` +
|
|
338
|
+
`Si lo quieres de verdad, ponlo en tu settings global o cambia de modo con shift+tab.`);
|
|
339
|
+
}
|
|
130
340
|
}
|
|
131
341
|
if (settings.permissions?.allowPatterns) {
|
|
132
342
|
settings.permissions.allowPatterns.forEach((p) => this.allowPatterns.add(p));
|
|
@@ -140,9 +350,21 @@ export class PermissionManager {
|
|
|
140
350
|
const dir = path.dirname(this.settingsFilePath);
|
|
141
351
|
if (!fs.existsSync(dir))
|
|
142
352
|
fs.mkdirSync(dir, { recursive: true });
|
|
353
|
+
// El modo no se vuelca desde memoria. El usuario puede estar en bypass
|
|
354
|
+
// temporal (shift+tab) y dejarlo escrito en un fichero que se commitea es
|
|
355
|
+
// justo el agujero de arriba, servido por nosotros mismos. Se respeta lo que
|
|
356
|
+
// ya hubiera en el fichero y punto.
|
|
357
|
+
let modoEnDisco;
|
|
358
|
+
try {
|
|
359
|
+
if (fs.existsSync(this.settingsFilePath)) {
|
|
360
|
+
const previo = JSON.parse(fs.readFileSync(this.settingsFilePath, "utf-8"));
|
|
361
|
+
modoEnDisco = previo.permissions?.mode;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch { }
|
|
143
365
|
const config = {
|
|
144
366
|
permissions: {
|
|
145
|
-
mode:
|
|
367
|
+
...(modoEnDisco ? { mode: modoEnDisco } : {}),
|
|
146
368
|
allowPatterns: Array.from(this.allowPatterns),
|
|
147
369
|
denyPatterns: Array.from(this.denyPatterns),
|
|
148
370
|
},
|
|
@@ -170,13 +392,42 @@ export class PermissionManager {
|
|
|
170
392
|
if (toolName === "run_command") {
|
|
171
393
|
return "El modo plan es de solo lectura: no se ejecutan comandos. Investiga con view_file, grep_search y list_dir.";
|
|
172
394
|
}
|
|
395
|
+
// Lanzar Chrome es arrancar un proceso, no mirar: en un modo que promete
|
|
396
|
+
// solo lectura no cabe, aunque la accion pedida se llame "snapshot".
|
|
397
|
+
if (BROWSER_TOOLS.has(toolName) && !isBrowserAttached()) {
|
|
398
|
+
return (`El modo plan es de solo lectura: no hay ningun Chrome enganchado, asi que "${toolName}" tendria que ` +
|
|
399
|
+
"lanzarlo con el puerto de depuracion abierto. Sobre un navegador ya enganchado si puedes mirar.");
|
|
400
|
+
}
|
|
173
401
|
if (isActuation(toolName, args)) {
|
|
174
402
|
return (`El modo plan es de solo lectura: "${toolName}" puede mirar (snapshot, screenshot, tabs) ` +
|
|
175
403
|
"pero no actuar sobre el ordenador ni el navegador del usuario.");
|
|
176
404
|
}
|
|
405
|
+
// Las dos ramas de create_image tienen efecto en disco: la de Python ejecuta
|
|
406
|
+
// el script y la de SVG escribe el archivo en cualquier ruta absoluta. Sin
|
|
407
|
+
// esto, el modo plan era de solo lectura para write_file y run_command pero
|
|
408
|
+
// no para la herramienta que hace las dos cosas a la vez.
|
|
409
|
+
if (toolName === "create_image") {
|
|
410
|
+
return isCodeGeneration(toolName, args)
|
|
411
|
+
? 'El modo plan es de solo lectura: "create_image" ejecuta el script generador con python. Descríbelo en el plan y ejecútalo al salir del modo plan.'
|
|
412
|
+
: 'El modo plan es de solo lectura: "create_image" escribe el archivo en disco.';
|
|
413
|
+
}
|
|
177
414
|
return null;
|
|
178
415
|
}
|
|
179
|
-
|
|
416
|
+
/**
|
|
417
|
+
* `cwd` es contra que directorio se mide "dentro del proyecto". Tiene que ser
|
|
418
|
+
* el MISMO que recibe executeToolCall, porque es contra el que las
|
|
419
|
+
* herramientas resuelven las rutas relativas: si aqui se midiera contra la
|
|
420
|
+
* raiz de la sesion y el bucle escribiera desde otro directorio (currentCwd
|
|
421
|
+
* cambia con cd), un "x.txt" saldria "dentro" en la comprobacion y caeria
|
|
422
|
+
* fuera al escribirse. Sin argumento se usa la raiz con la que se construyo.
|
|
423
|
+
*/
|
|
424
|
+
requiresConfirmation(toolName, args, cwd) {
|
|
425
|
+
// El deny va lo primero, antes incluso del bypass: si el usuario escribio
|
|
426
|
+
// algo en denyPatterns es que no lo quiere ver nunca, no "salvo que hoy
|
|
427
|
+
// tenga prisa". Quien llame a esto deberia usar deniedBy y cortar del todo;
|
|
428
|
+
// devolver true aqui es la red por si alguien solo mira este metodo.
|
|
429
|
+
if (this.deniedBy(toolName, args))
|
|
430
|
+
return true;
|
|
180
431
|
if (this.mode === "bypass")
|
|
181
432
|
return false;
|
|
182
433
|
if (this.mode === "plan")
|
|
@@ -192,6 +443,21 @@ export class PermissionManager {
|
|
|
192
443
|
if (toolName === "delete_file") {
|
|
193
444
|
return !this.isPatternAllowed(toolName, args);
|
|
194
445
|
}
|
|
446
|
+
// Mover o copiar encima de algo que ya existe borra el destino, exactamente
|
|
447
|
+
// igual que un delete_file, y hasta ahora caia en la rama de abajo y pasaba
|
|
448
|
+
// sin dialogo. La herramienta se niega a pisar nada sin overwrite, asi que
|
|
449
|
+
// este flag es la declaracion de intenciones de destruir el destino y se
|
|
450
|
+
// trata como el borrado que es. Se mira el argumento y no el disco: esta
|
|
451
|
+
// tabla decide con lo que pide el modelo, no con lo que haya en el sistema
|
|
452
|
+
// de ficheros en ese instante.
|
|
453
|
+
if (toolName === "move_copy_file" && args?.overwrite === true) {
|
|
454
|
+
return !this.isPatternAllowed(toolName, args);
|
|
455
|
+
}
|
|
456
|
+
// create_image que no sea SVG se pregunta como un comando: lo que corre es
|
|
457
|
+
// el texto que ha escrito el modelo, y ni el usuario ni /undo lo ven venir.
|
|
458
|
+
if (isCodeGeneration(toolName, args)) {
|
|
459
|
+
return !this.isPatternAllowed(toolName, args);
|
|
460
|
+
}
|
|
195
461
|
// Comandos de terminal: solo preguntar si son destructivos o peligrosos
|
|
196
462
|
if (toolName === "run_command") {
|
|
197
463
|
const command = typeof args?.command === "string" ? args.command : "";
|
|
@@ -200,12 +466,85 @@ export class PermissionManager {
|
|
|
200
466
|
}
|
|
201
467
|
return false;
|
|
202
468
|
}
|
|
469
|
+
// Escribir FUERA del proyecto no es una edicion, es tocar la maquina.
|
|
470
|
+
//
|
|
471
|
+
// El comentario de abajo lleva desde siempre diciendo "en el proyecto", pero
|
|
472
|
+
// nadie comprobaba la ruta: ni aqui ni en writeFile/editFile/moveCopyFile,
|
|
473
|
+
// que hacen path.resolve y siguen adelante (writeFile hasta crea los
|
|
474
|
+
// directorios que falten). Asi que write_file sobre C:\Windows\... o
|
|
475
|
+
// move_copy_file hacia ../../otro-proyecto salian sin un solo dialogo,
|
|
476
|
+
// mientras que el MISMO cambio hecho con run_command si lo pedia
|
|
477
|
+
// (isDangerousCommand, punto 4). La misma accion, bloqueada por un camino y
|
|
478
|
+
// abierta por el otro.
|
|
479
|
+
//
|
|
480
|
+
// Y /undo no es red aqui: vive solo en memoria de la sesion.
|
|
481
|
+
const fuera = this.rutasFueraDelProyecto(toolName, args, cwd);
|
|
482
|
+
if (fuera.length > 0) {
|
|
483
|
+
return !this.isPatternAllowed(toolName, args);
|
|
484
|
+
}
|
|
203
485
|
// Ediciones y escrituras en el proyecto: fluidas (el usuario puede usar /undo)
|
|
204
486
|
if (this.mode === "acceptEdits" || this.mode === "default") {
|
|
205
487
|
return false;
|
|
206
488
|
}
|
|
207
489
|
return false;
|
|
208
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Rutas de la llamada que caen fuera de la raiz del proyecto. Vacio si todo
|
|
493
|
+
* queda dentro (o si la herramienta no escribe en disco: leer fuera del
|
|
494
|
+
* proyecto es legitimo y no se pregunta).
|
|
495
|
+
*/
|
|
496
|
+
rutasFueraDelProyecto(toolName, args, cwd) {
|
|
497
|
+
const campos = RUTAS_DE_ESCRITURA[toolName];
|
|
498
|
+
if (!campos)
|
|
499
|
+
return [];
|
|
500
|
+
const raiz = cwd ? path.resolve(cwd) : this.projectRoot;
|
|
501
|
+
// create_image con SVG tambien escribe donde le digan; la rama de Python ya
|
|
502
|
+
// se pregunta antes por ser ejecucion de codigo.
|
|
503
|
+
return campos
|
|
504
|
+
.map((campo) => args?.[campo])
|
|
505
|
+
.filter((ruta) => isOutsideProject(raiz, ruta))
|
|
506
|
+
.map((ruta) => String(ruta));
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Patron denegado: devuelve cual, o null. Corta la llamada entera; no es un
|
|
510
|
+
* "pregunta antes".
|
|
511
|
+
*
|
|
512
|
+
* denyPatterns se leia del settings.json y se volvia a guardar tal cual, pero
|
|
513
|
+
* NADIE lo consultaba: la unica lista que se miraba era la de allow. Es decir,
|
|
514
|
+
* prohibir algo por configuracion no hacia absolutamente nada.
|
|
515
|
+
*
|
|
516
|
+
* La coincidencia es la misma que la del allow (firma exacta o prefijo de
|
|
517
|
+
* comando) con una diferencia deliberada: un allow no puede autorizar lo que
|
|
518
|
+
* venga encadenado detras, pero un deny si tiene que alcanzarlo. Si no,
|
|
519
|
+
* "echo hola && git push --force" se saltaria un deny de "git push" con solo
|
|
520
|
+
* poner algo inocente delante.
|
|
521
|
+
*/
|
|
522
|
+
deniedBy(toolName, args) {
|
|
523
|
+
if (this.denyPatterns.size === 0)
|
|
524
|
+
return null;
|
|
525
|
+
const command = typeof args?.command === "string" ? args.command.trim() : "";
|
|
526
|
+
const detail = String(args?.path ?? args?.outputPath ?? args?.action ?? "");
|
|
527
|
+
const trozos = command ? sentencias(command) : [];
|
|
528
|
+
for (const pattern of this.denyPatterns) {
|
|
529
|
+
const p = pattern.trim();
|
|
530
|
+
if (!p)
|
|
531
|
+
continue;
|
|
532
|
+
// Denegar una herramienta entera ("delete_file", "chrome") es una peticion
|
|
533
|
+
// razonable. Al reves, autorizarla entera, no lo es: por eso el allow no
|
|
534
|
+
// acepta esta forma.
|
|
535
|
+
if (p === toolName)
|
|
536
|
+
return pattern;
|
|
537
|
+
if (this.coincideExacta(p, toolName, command, detail))
|
|
538
|
+
return pattern;
|
|
539
|
+
if (this.coincidePrefijo(p, toolName, command))
|
|
540
|
+
return pattern;
|
|
541
|
+
for (const trozo of trozos) {
|
|
542
|
+
if (this.coincidePrefijo(p, toolName, trozo))
|
|
543
|
+
return pattern;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
209
548
|
/**
|
|
210
549
|
* Un patron autorizado vale para ESE comando, no para cualquiera que lo
|
|
211
550
|
* contenga. Con la comparacion por subcadena de antes, autorizar "git status"
|
|
@@ -214,26 +553,43 @@ export class PermissionManager {
|
|
|
214
553
|
*/
|
|
215
554
|
isPatternAllowed(toolName, args) {
|
|
216
555
|
const command = typeof args?.command === "string" ? args.command.trim() : "";
|
|
217
|
-
const detail = String(args?.path ?? args?.action ?? "");
|
|
556
|
+
const detail = String(args?.path ?? args?.outputPath ?? args?.action ?? "");
|
|
557
|
+
// Si la linea trae mas de una sentencia, ningun prefijo la autoriza: lo que
|
|
558
|
+
// va detras del separador no lo ha aprobado nadie. Solo vale entonces la
|
|
559
|
+
// firma exacta, que es el comando completo escrito a mano.
|
|
560
|
+
const encadenado = ENCADENADO.test(command);
|
|
561
|
+
// Segunda barrera, independiente de con que patron se guardo: lo
|
|
562
|
+
// irreversible se pregunta siempre. Los patrones demasiado anchos que ya
|
|
563
|
+
// estan escritos en el settings.json de mucha gente ("git", "rm") quedan
|
|
564
|
+
// asi desactivados sin tener que ir a buscarlos uno a uno.
|
|
565
|
+
if (command && isDangerousCommand(command))
|
|
566
|
+
return false;
|
|
218
567
|
for (const pattern of this.allowPatterns) {
|
|
219
568
|
const p = pattern.trim();
|
|
220
569
|
if (!p)
|
|
221
570
|
continue;
|
|
222
|
-
|
|
223
|
-
if (p === `${toolName}:${command || detail}`)
|
|
571
|
+
if (this.coincideExacta(p, toolName, command, detail))
|
|
224
572
|
return true;
|
|
225
|
-
|
|
226
|
-
// solo al principio de la linea y sin otro comando encadenado detras.
|
|
227
|
-
if (command && toolName === "run_command") {
|
|
228
|
-
const bare = p.startsWith("run_command:") ? p.slice("run_command:".length) : p;
|
|
229
|
-
if (bare && (command === bare || command.startsWith(`${bare} `)) && !/[;&|]/.test(command)) {
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// Rutas y acciones: coincidencia exacta.
|
|
234
|
-
if (detail && (p === detail || p === `${toolName}:${detail}`))
|
|
573
|
+
if (!encadenado && this.coincidePrefijo(p, toolName, command))
|
|
235
574
|
return true;
|
|
236
575
|
}
|
|
237
576
|
return false;
|
|
238
577
|
}
|
|
578
|
+
/** Firma exacta de la llamada ("run_command:npm test") o ruta/accion exacta. */
|
|
579
|
+
coincideExacta(p, toolName, command, detail) {
|
|
580
|
+
if (p === `${toolName}:${command || detail}`)
|
|
581
|
+
return true;
|
|
582
|
+
if (detail && (p === detail || p === `${toolName}:${detail}`))
|
|
583
|
+
return true;
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
/** Prefijo de comando ("npm", "git status"), solo al principio de la linea. */
|
|
587
|
+
coincidePrefijo(p, toolName, command) {
|
|
588
|
+
if (!command || toolName !== "run_command")
|
|
589
|
+
return false;
|
|
590
|
+
const bare = p.startsWith("run_command:") ? p.slice("run_command:".length) : p;
|
|
591
|
+
if (!bare)
|
|
592
|
+
return false;
|
|
593
|
+
return command === bare || command.startsWith(`${bare} `);
|
|
594
|
+
}
|
|
239
595
|
}
|
package/dist/config/quota.js
CHANGED
|
@@ -63,21 +63,44 @@ function load() {
|
|
|
63
63
|
return [];
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Guarda el libro de gasto de forma atomica.
|
|
68
|
+
*
|
|
69
|
+
* Antes se escribia directamente sobre usage.json. Si el proceso moria a mitad
|
|
70
|
+
* -Ctrl+C, un cierre de sesion de Windows, la bateria- el archivo quedaba
|
|
71
|
+
* truncado, load() no podia parsearlo y devolvia la lista vacia: el contador de
|
|
72
|
+
* cuota se ponia a cero y el usuario empezaba el mes de nuevo a costa nuestra.
|
|
73
|
+
*
|
|
74
|
+
* Escribir en un temporal y renombrar lo evita: el renombrado es atomico en el
|
|
75
|
+
* mismo sistema de archivos, asi que usage.json o es el de antes o es el nuevo,
|
|
76
|
+
* nunca la mitad de uno.
|
|
77
|
+
*/
|
|
66
78
|
function save(entries) {
|
|
67
79
|
try {
|
|
68
80
|
const dir = path.dirname(USAGE_FILE);
|
|
69
81
|
if (!fs.existsSync(dir))
|
|
70
82
|
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
-
|
|
83
|
+
const temporal = `${USAGE_FILE}.${process.pid}.tmp`;
|
|
84
|
+
fs.writeFileSync(temporal, JSON.stringify(entries), "utf-8");
|
|
85
|
+
fs.renameSync(temporal, USAGE_FILE);
|
|
72
86
|
}
|
|
73
87
|
catch {
|
|
74
88
|
// Si no se puede escribir, se sigue trabajando. Perder la cuenta es malo;
|
|
75
89
|
// dejar al usuario tirado por no poder escribir un archivo es peor.
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(`${USAGE_FILE}.${process.pid}.tmp`);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
76
94
|
}
|
|
77
95
|
}
|
|
78
96
|
export function recordUsage(costUsd, ahora = Date.now()) {
|
|
79
97
|
if (!Number.isFinite(costUsd) || costUsd <= 0)
|
|
80
98
|
return;
|
|
99
|
+
// Se relee justo antes de escribir, no al principio del turno: con dos
|
|
100
|
+
// sesiones abiertas, la que guardaba segunda pisaba el apunte de la primera y
|
|
101
|
+
// ese gasto desaparecia del libro. Releer aqui no elimina la carrera -harian
|
|
102
|
+
// falta cerrojos entre procesos- pero reduce la ventana a los microsegundos
|
|
103
|
+
// entre esta lectura y el renombrado.
|
|
81
104
|
const entries = load();
|
|
82
105
|
entries.push({ t: ahora, c: costUsd });
|
|
83
106
|
save(entries);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Cada cuanto se le pregunta al registro. Doce veces al dia no aporta nada. */
|
|
2
|
+
export declare const HORAS_ENTRE_COMPROBACIONES = 6;
|
|
3
|
+
export interface CacheDeActualizacion {
|
|
4
|
+
/** Milisegundos epoch de la ultima vez que se pregunto. */
|
|
5
|
+
comprobadoEn?: number;
|
|
6
|
+
/** Ultima version que dijo el registro. */
|
|
7
|
+
ultima?: string;
|
|
8
|
+
/** Version que ya se instalo sola, para no volver a anunciarla. */
|
|
9
|
+
avisadaDe?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Compara dos versiones de tipo 1.2.3.
|
|
13
|
+
*
|
|
14
|
+
* No se usa una libreria por una razon concreta: `semver` arrastra dependencias
|
|
15
|
+
* y esto se ejecuta en el arranque de un CLI. Lo que si se respeta es lo unico
|
|
16
|
+
* que importa aqui: una preversion (1.1.0-beta.1) NUNCA es mas nueva que la
|
|
17
|
+
* final del mismo numero, o el usuario acabaria bajando a una beta sin pedirlo.
|
|
18
|
+
*/
|
|
19
|
+
export declare function esMasNueva(candidata: string, actual: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* De donde sale el `chocolatito` que se esta ejecutando.
|
|
22
|
+
*
|
|
23
|
+
* Si al lado del `dist` hay un `.git` o un `src`, esto es el repositorio, no un
|
|
24
|
+
* paquete instalado: `npm link` o `node dist/index.js` a pelo. Instalar encima
|
|
25
|
+
* seria pisarle el codigo al que lo esta escribiendo.
|
|
26
|
+
*/
|
|
27
|
+
export declare function esInstalacionDeDesarrollo(raizDelPaquete: string): boolean;
|
|
28
|
+
export declare function leerCache(fichero?: string): CacheDeActualizacion;
|
|
29
|
+
export declare function guardarCache(datos: CacheDeActualizacion, fichero?: string): void;
|
|
30
|
+
/** Si toca preguntar al registro. Un reloj adelantado no deja de comprobar nunca. */
|
|
31
|
+
export declare function tocaComprobar(cache: CacheDeActualizacion, ahora: number): boolean;
|
|
32
|
+
/** Le pregunta al registro. Devuelve null si no contesta a tiempo o no se entiende. */
|
|
33
|
+
export declare function ultimaVersionPublicada(url?: string): Promise<string | null>;
|
|
34
|
+
export interface ResultadoDeInstalacion {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
/** Que decirle al usuario. Vacio significa que no hay nada que decir. */
|
|
37
|
+
mensaje: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Instala la version nueva en la carpeta global.
|
|
41
|
+
*
|
|
42
|
+
* Se hace con `npm install -g`, que es como se instalo, en vez de inventarse
|
|
43
|
+
* una descarga: asi funciona igual con nvm, con volta y con la instalacion de
|
|
44
|
+
* Windows, que ponen la carpeta global en sitios distintos.
|
|
45
|
+
*/
|
|
46
|
+
export declare function instalar(version: string, ejecutar?: typeof spawnNpm): Promise<ResultadoDeInstalacion>;
|
|
47
|
+
declare function spawnNpm(args: string[]): Promise<{
|
|
48
|
+
codigo: number;
|
|
49
|
+
salida: string;
|
|
50
|
+
}>;
|
|
51
|
+
export interface Ajustes {
|
|
52
|
+
versionActual: string;
|
|
53
|
+
raizDelPaquete: string;
|
|
54
|
+
ahora: number;
|
|
55
|
+
/** Fuerza la comprobacion aunque no toque, para el comando /actualizar. */
|
|
56
|
+
forzar?: boolean;
|
|
57
|
+
automatico?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* El paso completo: comprobar, y si procede instalar.
|
|
61
|
+
*
|
|
62
|
+
* Devuelve lo que hay que ensenarle al usuario, o cadena vacia si no hay nada.
|
|
63
|
+
* No imprime nada por su cuenta: quien llama decide cuando y como, que en un
|
|
64
|
+
* CLI con un prompt dibujado no es lo mismo escribir ahora que dentro de un
|
|
65
|
+
* segundo.
|
|
66
|
+
*/
|
|
67
|
+
export declare function comprobarActualizacion(ajustes: Ajustes): Promise<string>;
|
|
68
|
+
export {};
|