chocolatito-code 1.0.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/.mcp.json.ejemplo +19 -0
- package/COMPUTER-USE.md +484 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/agent/context.d.ts +9 -0
- package/dist/agent/context.js +123 -0
- package/dist/agent/fileTracker.d.ts +16 -0
- package/dist/agent/fileTracker.js +53 -0
- package/dist/agent/loop.d.ts +25 -0
- package/dist/agent/loop.js +524 -0
- package/dist/agent/mentions.d.ts +11 -0
- package/dist/agent/mentions.js +43 -0
- package/dist/agent/retry.d.ts +17 -0
- package/dist/agent/retry.js +57 -0
- package/dist/agent/subagent.d.ts +2 -0
- package/dist/agent/subagent.js +116 -0
- package/dist/agent/syntaxValidator.d.ts +5 -0
- package/dist/agent/syntaxValidator.js +71 -0
- package/dist/agent/tracker.d.ts +19 -0
- package/dist/agent/tracker.js +38 -0
- package/dist/agent/undoManager.d.ts +37 -0
- package/dist/agent/undoManager.js +95 -0
- package/dist/config/constants.d.ts +77 -0
- package/dist/config/constants.js +146 -0
- package/dist/config/engine.d.ts +49 -0
- package/dist/config/engine.js +86 -0
- package/dist/config/env.d.ts +13 -0
- package/dist/config/env.js +103 -0
- package/dist/config/license.d.ts +39 -0
- package/dist/config/license.js +306 -0
- package/dist/config/permissions.d.ts +38 -0
- package/dist/config/permissions.js +239 -0
- package/dist/config/quota.d.ts +58 -0
- package/dist/config/quota.js +156 -0
- package/dist/hooks/manager.d.ts +60 -0
- package/dist/hooks/manager.js +193 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +470 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +282 -0
- package/dist/mcp/manager.d.ts +26 -0
- package/dist/mcp/manager.js +167 -0
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +59 -0
- package/dist/prompts/systemPrompt.d.ts +5 -0
- package/dist/prompts/systemPrompt.js +177 -0
- package/dist/sessions/manager.d.ts +21 -0
- package/dist/sessions/manager.js +54 -0
- package/dist/skills/manager.d.ts +14 -0
- package/dist/skills/manager.js +86 -0
- package/dist/tools/browserCdp.d.ts +26 -0
- package/dist/tools/browserCdp.js +610 -0
- package/dist/tools/browserExtension.d.ts +58 -0
- package/dist/tools/browserExtension.js +479 -0
- package/dist/tools/browserLive.d.ts +12 -0
- package/dist/tools/browserLive.js +265 -0
- package/dist/tools/changeDir.d.ts +5 -0
- package/dist/tools/changeDir.js +63 -0
- package/dist/tools/computerUse.d.ts +58 -0
- package/dist/tools/computerUse.js +567 -0
- package/dist/tools/createImage.d.ts +1 -0
- package/dist/tools/createImage.js +34 -0
- package/dist/tools/definitions.d.ts +8 -0
- package/dist/tools/definitions.js +401 -0
- package/dist/tools/deleteFile.d.ts +1 -0
- package/dist/tools/deleteFile.js +17 -0
- package/dist/tools/editFile.d.ts +5 -0
- package/dist/tools/editFile.js +88 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findFiles.js +33 -0
- package/dist/tools/getSystemInfo.d.ts +1 -0
- package/dist/tools/getSystemInfo.js +23 -0
- package/dist/tools/gitTools.d.ts +3 -0
- package/dist/tools/gitTools.js +47 -0
- package/dist/tools/grepSearch.d.ts +1 -0
- package/dist/tools/grepSearch.js +37 -0
- package/dist/tools/listDir.d.ts +1 -0
- package/dist/tools/listDir.js +33 -0
- package/dist/tools/moveCopyFile.d.ts +1 -0
- package/dist/tools/moveCopyFile.js +21 -0
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runCommand.js +61 -0
- package/dist/tools/runner.d.ts +5 -0
- package/dist/tools/runner.js +177 -0
- package/dist/tools/safety.d.ts +17 -0
- package/dist/tools/safety.js +33 -0
- package/dist/tools/todoTool.d.ts +8 -0
- package/dist/tools/todoTool.js +27 -0
- package/dist/tools/toolDefsComputer.d.ts +13 -0
- package/dist/tools/toolDefsComputer.js +205 -0
- package/dist/tools/truncator.d.ts +11 -0
- package/dist/tools/truncator.js +24 -0
- package/dist/tools/viewFile.d.ts +1 -0
- package/dist/tools/viewFile.js +45 -0
- package/dist/tools/visionBridge.d.ts +37 -0
- package/dist/tools/visionBridge.js +107 -0
- package/dist/tools/webFetch.d.ts +1 -0
- package/dist/tools/webFetch.js +25 -0
- package/dist/tools/webSearch.d.ts +1 -0
- package/dist/tools/webSearch.js +35 -0
- package/dist/tools/win/hostClient.d.ts +31 -0
- package/dist/tools/win/hostClient.js +158 -0
- package/dist/tools/win/hostScript.d.ts +1 -0
- package/dist/tools/win/hostScript.js +925 -0
- package/dist/tools/writeFile.d.ts +1 -0
- package/dist/tools/writeFile.js +13 -0
- package/dist/ui/banner.d.ts +10 -0
- package/dist/ui/banner.js +165 -0
- package/dist/ui/diffViewer.d.ts +1 -0
- package/dist/ui/diffViewer.js +60 -0
- package/dist/ui/interrupt.d.ts +17 -0
- package/dist/ui/interrupt.js +156 -0
- package/dist/ui/markdownStream.d.ts +23 -0
- package/dist/ui/markdownStream.js +0 -0
- package/dist/ui/modes.d.ts +28 -0
- package/dist/ui/modes.js +57 -0
- package/dist/ui/permissionPrompt.d.ts +35 -0
- package/dist/ui/permissionPrompt.js +271 -0
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.js +466 -0
- package/dist/ui/reasoningStream.d.ts +6 -0
- package/dist/ui/reasoningStream.js +38 -0
- package/dist/ui/renderer.d.ts +10 -0
- package/dist/ui/renderer.js +142 -0
- package/dist/ui/spinner.d.ts +24 -0
- package/dist/ui/spinner.js +111 -0
- package/dist/ui/theme.d.ts +36 -0
- package/dist/ui/theme.js +37 -0
- package/dist/ui/typeAhead.d.ts +31 -0
- package/dist/ui/typeAhead.js +87 -0
- package/extension/README.md +74 -0
- package/extension/background.js +575 -0
- package/extension/content.js +446 -0
- package/extension/manifest.json +36 -0
- package/extension/popup.html +22 -0
- package/extension/popup.js +12 -0
- package/package.json +66 -0
- package/skills/complex-refactor/SKILL.md +13 -0
- package/skills/deep-debugger/SKILL.md +14 -0
- package/skills/software-architect/SKILL.md +20 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
/**
|
|
5
|
+
* Acciones de SOLO OBSERVAR de cada herramienta de control.
|
|
6
|
+
*
|
|
7
|
+
* La tabla anterior estaba del reves: preguntaba por 'ui_snapshot' (que solo lee
|
|
8
|
+
* la pantalla) y dejaba pasar sin preguntar la herramienta 'chrome' entera, que
|
|
9
|
+
* conduce el navegador REAL del usuario con sus sesiones ya iniciadas. En la
|
|
10
|
+
* practica eso empujaba al usuario a poner el modo bypass para dejar de
|
|
11
|
+
* contestar dialogos inutiles, y con el bypass puesto ya no quedaba ninguna
|
|
12
|
+
* barrera justo donde hacia falta.
|
|
13
|
+
*
|
|
14
|
+
* La regla ahora es una sola: mirar es gratis, actuar se pregunta.
|
|
15
|
+
*/
|
|
16
|
+
const OBSERVE_ONLY = {
|
|
17
|
+
computer_use: new Set([
|
|
18
|
+
"screenshot",
|
|
19
|
+
"ui_snapshot",
|
|
20
|
+
"list_windows",
|
|
21
|
+
"find_element",
|
|
22
|
+
"get_active_window",
|
|
23
|
+
"cursor_position",
|
|
24
|
+
"wait",
|
|
25
|
+
"sleep",
|
|
26
|
+
]),
|
|
27
|
+
chrome: new Set(["status", "tabs", "snapshot", "get_text", "screenshot", "wait_for", "notice"]),
|
|
28
|
+
browser: new Set(["attach", "tabs", "snapshot", "get_text", "screenshot", "wait_for", "console"]),
|
|
29
|
+
browser_live: new Set(["attach", "tabs", "snapshot", "get_text", "screenshot", "wait_for", "console"]),
|
|
30
|
+
};
|
|
31
|
+
/** Herramientas que conducen el ordenador o el navegador del usuario. */
|
|
32
|
+
const CONTROL_TOOLS = Object.keys(OBSERVE_ONLY);
|
|
33
|
+
/** Herramientas que modifican el disco o el repositorio. */
|
|
34
|
+
const MODIFYING_TOOLS = ["edit_file", "write_file", "delete_file", "move_copy_file", "git_commit"];
|
|
35
|
+
/** Solo lee estado, no toca nada. */
|
|
36
|
+
function isObserveOnly(toolName, args) {
|
|
37
|
+
const set = OBSERVE_ONLY[toolName];
|
|
38
|
+
if (!set)
|
|
39
|
+
return false;
|
|
40
|
+
return set.has(String(args?.action ?? ""));
|
|
41
|
+
}
|
|
42
|
+
/** Conduce de verdad el ordenador o el navegador: clics, teclado, navegacion, JS. */
|
|
43
|
+
function isActuation(toolName, args) {
|
|
44
|
+
return CONTROL_TOOLS.includes(toolName) && !isObserveOnly(toolName, args);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Detecta comandos destructivos o de alto riesgo que siempre deben pedir confirmación.
|
|
48
|
+
* Comandos seguros de desarrollo (python, npm, pytest, tsc, git status, etc.) no interrumpen al usuario.
|
|
49
|
+
*/
|
|
50
|
+
export function isDangerousCommand(command) {
|
|
51
|
+
if (!command || typeof command !== "string")
|
|
52
|
+
return false;
|
|
53
|
+
const cmd = command.trim();
|
|
54
|
+
// 1. Borrado recursivo o forzado de archivos/directorios.
|
|
55
|
+
// Ojo con \b al final: "format c:" termina en ':', que no es caracter de
|
|
56
|
+
// 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]*-Recurse|mkfs)\b/i.test(cmd) ||
|
|
58
|
+
/\bformat\s+[a-z]:/i.test(cmd) ||
|
|
59
|
+
/\bdd\s+[^\n]*\bof=/i.test(cmd)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
// 2. Operaciones destructivas de Git (pérdida irreversible de trabajo o sobreescritura remota)
|
|
63
|
+
if (/\bgit\s+(reset\s+--hard|clean\s+-[fxd]+|push\s+[^\n]*(-f\b|--force)|restore\s+\.|checkout\s+\.|branch\s+-D\b)\b/i.test(cmd)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
// 3. Terminación de procesos y apagado del sistema
|
|
67
|
+
if (/\b(taskkill|kill\s+-9|pkill\s+-9|shutdown|reboot|stop-computer|restart-computer)\b/i.test(cmd) ||
|
|
68
|
+
/\bStop-Process\b[^\n]*-Force/i.test(cmd)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// 4. Modificación de directorios críticos del sistema operativo
|
|
72
|
+
if (/(c:\\windows|c:\\program files|\/etc\/|\/usr\/bin\/|\/bin\/)/i.test(cmd) &&
|
|
73
|
+
/\b(copy|move|del|rm|write|out-file|>)\b/i.test(cmd)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// 5. Descargar y ejecutar de un tirón. Lo que corre luego no lo ha visto
|
|
77
|
+
// nadie: ni el modelo ni el usuario. Es el único caso de esta lista donde
|
|
78
|
+
// el comando parece inofensivo y el contenido es el peligro.
|
|
79
|
+
if (/\b(curl|wget|iwr|Invoke-WebRequest)\b[^\n]*\|\s*(ba|z|k)?sh\b/i.test(cmd) ||
|
|
80
|
+
/\b(curl|iwr|Invoke-WebRequest|Invoke-RestMethod)\b[^\n]*\|\s*(iex|Invoke-Expression)\b/i.test(cmd) ||
|
|
81
|
+
/\b(iex|Invoke-Expression)\s*\(/i.test(cmd)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
// 6. Acciones hacia fuera: salen de la máquina y no se pueden deshacer.
|
|
85
|
+
// Publicar una versión equivocada se arregla con otra versión, nunca
|
|
86
|
+
// borrando la anterior.
|
|
87
|
+
if (/\b(npm|yarn|pnpm)\s+publish\b/i.test(cmd) ||
|
|
88
|
+
/\bdocker\s+push\b/i.test(cmd) ||
|
|
89
|
+
/\bgh\s+(release\s+create|repo\s+delete)\b/i.test(cmd)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// 7. Permisos abiertos de par en par sobre un árbol entero.
|
|
93
|
+
if (/\bchmod\s+-R\s+777\b/i.test(cmd)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
export class PermissionManager {
|
|
99
|
+
mode = "default";
|
|
100
|
+
allowPatterns = new Set();
|
|
101
|
+
denyPatterns = new Set();
|
|
102
|
+
settingsFilePath;
|
|
103
|
+
constructor(initialMode = "default", cwd = process.cwd()) {
|
|
104
|
+
this.mode = initialMode;
|
|
105
|
+
this.settingsFilePath = path.join(cwd, ".chocolatito", "settings.json");
|
|
106
|
+
this.loadSettings();
|
|
107
|
+
}
|
|
108
|
+
loadSettings() {
|
|
109
|
+
// 1. Global settings ~/.chocolatito/settings.json
|
|
110
|
+
const globalPath = path.join(os.homedir(), ".chocolatito", "settings.json");
|
|
111
|
+
if (fs.existsSync(globalPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(fs.readFileSync(globalPath, "utf-8"));
|
|
114
|
+
this.applySettings(parsed);
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
}
|
|
118
|
+
// 2. Local project settings .chocolatito/settings.json
|
|
119
|
+
if (fs.existsSync(this.settingsFilePath)) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(fs.readFileSync(this.settingsFilePath, "utf-8"));
|
|
122
|
+
this.applySettings(parsed);
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
applySettings(settings) {
|
|
128
|
+
if (settings.permissions?.mode) {
|
|
129
|
+
this.mode = settings.permissions.mode;
|
|
130
|
+
}
|
|
131
|
+
if (settings.permissions?.allowPatterns) {
|
|
132
|
+
settings.permissions.allowPatterns.forEach((p) => this.allowPatterns.add(p));
|
|
133
|
+
}
|
|
134
|
+
if (settings.permissions?.denyPatterns) {
|
|
135
|
+
settings.permissions.denyPatterns.forEach((p) => this.denyPatterns.add(p));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
saveAllowPattern(pattern) {
|
|
139
|
+
this.allowPatterns.add(pattern);
|
|
140
|
+
const dir = path.dirname(this.settingsFilePath);
|
|
141
|
+
if (!fs.existsSync(dir))
|
|
142
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
const config = {
|
|
144
|
+
permissions: {
|
|
145
|
+
mode: this.mode,
|
|
146
|
+
allowPatterns: Array.from(this.allowPatterns),
|
|
147
|
+
denyPatterns: Array.from(this.denyPatterns),
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
fs.writeFileSync(this.settingsFilePath, JSON.stringify(config, null, 2), "utf-8");
|
|
151
|
+
}
|
|
152
|
+
setMode(mode) {
|
|
153
|
+
this.mode = mode;
|
|
154
|
+
}
|
|
155
|
+
getMode() {
|
|
156
|
+
return this.mode;
|
|
157
|
+
}
|
|
158
|
+
isReadOnly() {
|
|
159
|
+
return this.mode === "plan";
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Modo plan = SOLO LECTURA de verdad.
|
|
163
|
+
*/
|
|
164
|
+
isBlockedInPlanMode(toolName, args) {
|
|
165
|
+
if (this.mode !== "plan")
|
|
166
|
+
return null;
|
|
167
|
+
if (MODIFYING_TOOLS.includes(toolName)) {
|
|
168
|
+
return `El modo plan es de solo lectura: "${toolName}" no puede modificar archivos ni el repositorio.`;
|
|
169
|
+
}
|
|
170
|
+
if (toolName === "run_command") {
|
|
171
|
+
return "El modo plan es de solo lectura: no se ejecutan comandos. Investiga con view_file, grep_search y list_dir.";
|
|
172
|
+
}
|
|
173
|
+
if (isActuation(toolName, args)) {
|
|
174
|
+
return (`El modo plan es de solo lectura: "${toolName}" puede mirar (snapshot, screenshot, tabs) ` +
|
|
175
|
+
"pero no actuar sobre el ordenador ni el navegador del usuario.");
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
requiresConfirmation(toolName, args) {
|
|
180
|
+
if (this.mode === "bypass")
|
|
181
|
+
return false;
|
|
182
|
+
if (this.mode === "plan")
|
|
183
|
+
return false;
|
|
184
|
+
// Mirar es gratis en cualquier modo
|
|
185
|
+
if (isObserveOnly(toolName, args))
|
|
186
|
+
return false;
|
|
187
|
+
// Conducir el ordenador o el navegador real del usuario siempre se pregunta
|
|
188
|
+
if (isActuation(toolName, args)) {
|
|
189
|
+
return !this.isPatternAllowed(toolName, args);
|
|
190
|
+
}
|
|
191
|
+
// Borrado directo de archivos: siempre se pregunta
|
|
192
|
+
if (toolName === "delete_file") {
|
|
193
|
+
return !this.isPatternAllowed(toolName, args);
|
|
194
|
+
}
|
|
195
|
+
// Comandos de terminal: solo preguntar si son destructivos o peligrosos
|
|
196
|
+
if (toolName === "run_command") {
|
|
197
|
+
const command = typeof args?.command === "string" ? args.command : "";
|
|
198
|
+
if (isDangerousCommand(command)) {
|
|
199
|
+
return !this.isPatternAllowed(toolName, args);
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
// Ediciones y escrituras en el proyecto: fluidas (el usuario puede usar /undo)
|
|
204
|
+
if (this.mode === "acceptEdits" || this.mode === "default") {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Un patron autorizado vale para ESE comando, no para cualquiera que lo
|
|
211
|
+
* contenga. Con la comparacion por subcadena de antes, autorizar "git status"
|
|
212
|
+
* dejaba pasar tambien "rm -rf . && git status": bastaba con que el patron
|
|
213
|
+
* apareciera en algun sitio de la linea.
|
|
214
|
+
*/
|
|
215
|
+
isPatternAllowed(toolName, args) {
|
|
216
|
+
const command = typeof args?.command === "string" ? args.command.trim() : "";
|
|
217
|
+
const detail = String(args?.path ?? args?.action ?? "");
|
|
218
|
+
for (const pattern of this.allowPatterns) {
|
|
219
|
+
const p = pattern.trim();
|
|
220
|
+
if (!p)
|
|
221
|
+
continue;
|
|
222
|
+
// Firma exacta de la llamada: "run_command:npm test".
|
|
223
|
+
if (p === `${toolName}:${command || detail}`)
|
|
224
|
+
return true;
|
|
225
|
+
// Un prefijo de comando ("npm", "git status") autoriza sus variantes, pero
|
|
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}`))
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CUOTA DE USO
|
|
3
|
+
*
|
|
4
|
+
* Dos ventanas deslizantes a la vez, igual que hacen las herramientas del ramo:
|
|
5
|
+
*
|
|
6
|
+
* - Una de 5 horas, que impide que una sola sesion intensa se coma la semana.
|
|
7
|
+
* - Una de 7 dias, que es el limite de verdad.
|
|
8
|
+
*
|
|
9
|
+
* Deslizantes, no de calendario: la de 5 horas mira las ultimas 5 horas desde
|
|
10
|
+
* AHORA, no un bloque fijo de reloj. Con bloques fijos, quien empieza a las
|
|
11
|
+
* 4:59 se queda sin nada en un minuto, y eso se percibe como una averia.
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANTE — ESTO ES EL CONTADOR, NO LA BARRERA.
|
|
14
|
+
*
|
|
15
|
+
* Corre en la maquina del cliente, en JavaScript legible. Quien quiera saltarselo
|
|
16
|
+
* solo tiene que abrir dist/ y borrar la comprobacion. Sirve para que el usuario
|
|
17
|
+
* honesto sepa cuanto lleva gastado y no se lleve sorpresas, y para que la
|
|
18
|
+
* cuenta este hecha y probada. La barrera de verdad tiene que vivir en el
|
|
19
|
+
* servidor que guarda la clave maestra: es el unico sitio donde el cliente no
|
|
20
|
+
* puede tocar. Mientras el usuario ponga su propia clave de API, aqui no se
|
|
21
|
+
* limita nada, porque el dinero que gasta es suyo.
|
|
22
|
+
*/
|
|
23
|
+
export interface QuotaConfig {
|
|
24
|
+
/** Presupuesto mensual de API por usuario, en USD. */
|
|
25
|
+
monthlyBudgetUsd: number;
|
|
26
|
+
/** Tamano de la ventana corta, en horas. */
|
|
27
|
+
windowHours: number;
|
|
28
|
+
/** Tope de la ventana corta, en USD. */
|
|
29
|
+
windowBudgetUsd: number;
|
|
30
|
+
/** Tope de los ultimos 7 dias, en USD. */
|
|
31
|
+
weeklyBudgetUsd: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function getQuotaConfig(): QuotaConfig;
|
|
34
|
+
/**
|
|
35
|
+
* Modo gestionado: la API la paga Chocolatito, no el usuario.
|
|
36
|
+
*
|
|
37
|
+
* Solo entonces tiene sentido limitar. Si el usuario trae su propia clave, el
|
|
38
|
+
* gasto es suyo y ponerle un tope seria impertinente.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isManagedMode(): boolean;
|
|
41
|
+
export declare function recordUsage(costUsd: number, ahora?: number): void;
|
|
42
|
+
export interface QuotaStatus {
|
|
43
|
+
allowed: boolean;
|
|
44
|
+
/** Motivo legible cuando no se permite. */
|
|
45
|
+
reason?: string;
|
|
46
|
+
/** Cuando vuelve a haber margen. */
|
|
47
|
+
resetAt?: Date;
|
|
48
|
+
spentWindowUsd: number;
|
|
49
|
+
spentWeekUsd: number;
|
|
50
|
+
config: QuotaConfig;
|
|
51
|
+
}
|
|
52
|
+
export declare function checkQuota(ahora?: number): QuotaStatus;
|
|
53
|
+
/** Cuanto falta para la hora indicada, en lenguaje llano. */
|
|
54
|
+
export declare function faltaPara(fecha: Date, ahora?: number): string;
|
|
55
|
+
/** Mensaje completo para cuando se agota la cuota. */
|
|
56
|
+
export declare function mensajeDeCorte(status: QuotaStatus): string;
|
|
57
|
+
/** Resumen corto para el pie de cada turno. Vacio si no hay nada que decir. */
|
|
58
|
+
export declare function quotaBadge(status: QuotaStatus): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const DEFAULT_MONTHLY_USD = 5;
|
|
5
|
+
const WINDOW_HOURS = 5;
|
|
6
|
+
/** La ventana corta es un freno de golpe, no un reparto a partes iguales:
|
|
7
|
+
* se deja gastar hasta un 40% de la semana en una sola sesion intensa. */
|
|
8
|
+
const WINDOW_SHARE_OF_WEEK = 0.4;
|
|
9
|
+
function num(name, fallback) {
|
|
10
|
+
const raw = process.env[name];
|
|
11
|
+
if (!raw)
|
|
12
|
+
return fallback;
|
|
13
|
+
const v = Number(raw);
|
|
14
|
+
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
15
|
+
}
|
|
16
|
+
export function getQuotaConfig() {
|
|
17
|
+
const monthly = num("CHOCOLATITO_MONTHLY_BUDGET_USD", DEFAULT_MONTHLY_USD);
|
|
18
|
+
const weekly = num("CHOCOLATITO_WEEKLY_BUDGET_USD", (monthly * 7) / 30);
|
|
19
|
+
return {
|
|
20
|
+
monthlyBudgetUsd: monthly,
|
|
21
|
+
windowHours: num("CHOCOLATITO_WINDOW_HOURS", WINDOW_HOURS),
|
|
22
|
+
windowBudgetUsd: num("CHOCOLATITO_WINDOW_BUDGET_USD", weekly * WINDOW_SHARE_OF_WEEK),
|
|
23
|
+
weeklyBudgetUsd: weekly,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Modo gestionado: la API la paga Chocolatito, no el usuario.
|
|
28
|
+
*
|
|
29
|
+
* Solo entonces tiene sentido limitar. Si el usuario trae su propia clave, el
|
|
30
|
+
* gasto es suyo y ponerle un tope seria impertinente.
|
|
31
|
+
*/
|
|
32
|
+
export function isManagedMode() {
|
|
33
|
+
if (process.env.CHOCOLATITO_MANAGED === "1")
|
|
34
|
+
return true;
|
|
35
|
+
if (process.env.CHOCOLATITO_MANAGED === "0")
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
const rc = path.join(os.homedir(), ".chocolatitorc");
|
|
39
|
+
if (!fs.existsSync(rc))
|
|
40
|
+
return false;
|
|
41
|
+
const cfg = JSON.parse(fs.readFileSync(rc, "utf-8"));
|
|
42
|
+
return cfg.managed === true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const USAGE_FILE = path.join(os.homedir(), ".chocolatito", "usage.json");
|
|
49
|
+
const RETAIN_MS = 8 * 24 * 60 * 60 * 1000;
|
|
50
|
+
function load() {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(USAGE_FILE))
|
|
53
|
+
return [];
|
|
54
|
+
const raw = JSON.parse(fs.readFileSync(USAGE_FILE, "utf-8"));
|
|
55
|
+
if (!Array.isArray(raw))
|
|
56
|
+
return [];
|
|
57
|
+
const corte = Date.now() - RETAIN_MS;
|
|
58
|
+
// Se descarta lo viejo al leer: el archivo no crece sin fin.
|
|
59
|
+
return raw.filter((e) => typeof e?.t === "number" && typeof e?.c === "number" && e.t > corte);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Un archivo corrupto no puede dejar al usuario sin herramienta.
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function save(entries) {
|
|
67
|
+
try {
|
|
68
|
+
const dir = path.dirname(USAGE_FILE);
|
|
69
|
+
if (!fs.existsSync(dir))
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
fs.writeFileSync(USAGE_FILE, JSON.stringify(entries), "utf-8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Si no se puede escribir, se sigue trabajando. Perder la cuenta es malo;
|
|
75
|
+
// dejar al usuario tirado por no poder escribir un archivo es peor.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function recordUsage(costUsd, ahora = Date.now()) {
|
|
79
|
+
if (!Number.isFinite(costUsd) || costUsd <= 0)
|
|
80
|
+
return;
|
|
81
|
+
const entries = load();
|
|
82
|
+
entries.push({ t: ahora, c: costUsd });
|
|
83
|
+
save(entries);
|
|
84
|
+
}
|
|
85
|
+
function gastadoDesde(entries, desde) {
|
|
86
|
+
return entries.reduce((suma, e) => (e.t >= desde ? suma + e.c : suma), 0);
|
|
87
|
+
}
|
|
88
|
+
/** Cuando caduca el gasto mas antiguo que aun cuenta en la ventana. */
|
|
89
|
+
function seLiberaEn(entries, desde, ventanaMs) {
|
|
90
|
+
const dentro = entries.filter((e) => e.t >= desde).sort((a, b) => a.t - b.t);
|
|
91
|
+
if (dentro.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
return new Date(dentro[0].t + ventanaMs);
|
|
94
|
+
}
|
|
95
|
+
export function checkQuota(ahora = Date.now()) {
|
|
96
|
+
const config = getQuotaConfig();
|
|
97
|
+
const entries = load();
|
|
98
|
+
const ventanaMs = config.windowHours * 60 * 60 * 1000;
|
|
99
|
+
const semanaMs = 7 * 24 * 60 * 60 * 1000;
|
|
100
|
+
const spentWindowUsd = gastadoDesde(entries, ahora - ventanaMs);
|
|
101
|
+
const spentWeekUsd = gastadoDesde(entries, ahora - semanaMs);
|
|
102
|
+
if (!isManagedMode()) {
|
|
103
|
+
// Clave propia del usuario: se lleva la cuenta para informar, no se frena.
|
|
104
|
+
return { allowed: true, spentWindowUsd, spentWeekUsd, config };
|
|
105
|
+
}
|
|
106
|
+
if (spentWeekUsd >= config.weeklyBudgetUsd) {
|
|
107
|
+
return {
|
|
108
|
+
allowed: false,
|
|
109
|
+
reason: "Has agotado tu cuota semanal de uso.",
|
|
110
|
+
resetAt: seLiberaEn(entries, ahora - semanaMs, semanaMs) || undefined,
|
|
111
|
+
spentWindowUsd,
|
|
112
|
+
spentWeekUsd,
|
|
113
|
+
config,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (spentWindowUsd >= config.windowBudgetUsd) {
|
|
117
|
+
return {
|
|
118
|
+
allowed: false,
|
|
119
|
+
reason: `Has agotado tu cuota de las ultimas ${config.windowHours} horas.`,
|
|
120
|
+
resetAt: seLiberaEn(entries, ahora - ventanaMs, ventanaMs) || undefined,
|
|
121
|
+
spentWindowUsd,
|
|
122
|
+
spentWeekUsd,
|
|
123
|
+
config,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { allowed: true, spentWindowUsd, spentWeekUsd, config };
|
|
127
|
+
}
|
|
128
|
+
/** Cuanto falta para la hora indicada, en lenguaje llano. */
|
|
129
|
+
export function faltaPara(fecha, ahora = Date.now()) {
|
|
130
|
+
const ms = fecha.getTime() - ahora;
|
|
131
|
+
if (ms <= 0)
|
|
132
|
+
return "ya";
|
|
133
|
+
const minutos = Math.ceil(ms / 60000);
|
|
134
|
+
if (minutos < 60)
|
|
135
|
+
return `${minutos} min`;
|
|
136
|
+
const horas = Math.floor(minutos / 60);
|
|
137
|
+
const resto = minutos % 60;
|
|
138
|
+
if (horas < 24)
|
|
139
|
+
return resto > 0 ? `${horas} h ${resto} min` : `${horas} h`;
|
|
140
|
+
const dias = Math.floor(horas / 24);
|
|
141
|
+
return `${dias} d ${horas % 24} h`;
|
|
142
|
+
}
|
|
143
|
+
/** Mensaje completo para cuando se agota la cuota. */
|
|
144
|
+
export function mensajeDeCorte(status) {
|
|
145
|
+
const cuando = status.resetAt
|
|
146
|
+
? ` Vuelves a tener margen en ${faltaPara(status.resetAt)} (${status.resetAt.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" })}).`
|
|
147
|
+
: "";
|
|
148
|
+
return `${status.reason}${cuando}`;
|
|
149
|
+
}
|
|
150
|
+
/** Resumen corto para el pie de cada turno. Vacio si no hay nada que decir. */
|
|
151
|
+
export function quotaBadge(status) {
|
|
152
|
+
if (!isManagedMode())
|
|
153
|
+
return "";
|
|
154
|
+
const pct = Math.min(100, Math.round((status.spentWeekUsd / status.config.weeklyBudgetUsd) * 100));
|
|
155
|
+
return `${pct}% de la cuota semanal`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks: puntos de enganche para que el usuario imponga sus propias reglas.
|
|
3
|
+
*
|
|
4
|
+
* La version anterior solo miraba si existia un script llamado como el evento y
|
|
5
|
+
* se quedaba con lo que imprimiera. No podia BLOQUEAR nada, que es justo lo que
|
|
6
|
+
* hace util un hook: impedir un `rm -rf`, exigir que se formatee tras editar, o
|
|
7
|
+
* inyectar contexto antes de que el modelo responda.
|
|
8
|
+
*
|
|
9
|
+
* Se sigue el modelo de Claude Code: configuracion en settings.json con
|
|
10
|
+
* matchers, el evento entra por stdin como JSON, y el hook contesta por stdout
|
|
11
|
+
* con un JSON que puede permitir, denegar o pedir confirmacion.
|
|
12
|
+
*/
|
|
13
|
+
export type HookEvent = "SessionStart" | "SessionEnd" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "Stop";
|
|
14
|
+
export interface HookDefinition {
|
|
15
|
+
type?: "command";
|
|
16
|
+
command: string;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
shell?: "bash" | "powershell";
|
|
19
|
+
}
|
|
20
|
+
export interface HookMatcher {
|
|
21
|
+
/** Nombre de herramienta, lista separada por "|", o expresion regular. */
|
|
22
|
+
matcher?: string;
|
|
23
|
+
hooks: HookDefinition[];
|
|
24
|
+
}
|
|
25
|
+
export type HooksConfig = Partial<Record<HookEvent, HookMatcher[]>>;
|
|
26
|
+
export interface HookDecision {
|
|
27
|
+
/** "deny" corta la accion; "allow" se la salta el permiso; "ask" pregunta. */
|
|
28
|
+
permissionDecision?: "allow" | "deny" | "ask";
|
|
29
|
+
permissionDecisionReason?: string;
|
|
30
|
+
/** Texto que se anade al contexto del modelo. */
|
|
31
|
+
additionalContext?: string;
|
|
32
|
+
/** Reemplaza los argumentos de la herramienta antes de ejecutarla. */
|
|
33
|
+
updatedInput?: Record<string, any>;
|
|
34
|
+
/** Mensaje informativo para el usuario. */
|
|
35
|
+
systemMessage?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare class HookManager {
|
|
38
|
+
private config;
|
|
39
|
+
private disabled;
|
|
40
|
+
private cwd;
|
|
41
|
+
constructor(cwd?: string);
|
|
42
|
+
/** settings.json del usuario y del proyecto; el proyecto tiene prioridad. */
|
|
43
|
+
private load;
|
|
44
|
+
get hasHooks(): boolean;
|
|
45
|
+
describe(): string;
|
|
46
|
+
/**
|
|
47
|
+
* El matcher sigue las reglas de Claude Code: vacio o "*" vale para todo, un
|
|
48
|
+
* texto simple (o lista con "|") compara exacto, y cualquier otra cosa se
|
|
49
|
+
* trata como expresion regular.
|
|
50
|
+
*/
|
|
51
|
+
private matches;
|
|
52
|
+
private hooksFor;
|
|
53
|
+
/**
|
|
54
|
+
* Ejecuta los hooks del evento y devuelve la decision combinada.
|
|
55
|
+
* Gana el veto: si CUALQUIER hook deniega, se deniega.
|
|
56
|
+
*/
|
|
57
|
+
run(event: HookEvent, payload: Record<string, any>, matchValue?: string): Promise<HookDecision>;
|
|
58
|
+
/** Compatibilidad con la API anterior. */
|
|
59
|
+
triggerHook(event: HookEvent, context: Record<string, any>): Promise<string | null>;
|
|
60
|
+
}
|