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.
Files changed (140) hide show
  1. package/.mcp.json.ejemplo +19 -0
  2. package/COMPUTER-USE.md +484 -0
  3. package/LICENSE +21 -0
  4. package/README.md +186 -0
  5. package/dist/agent/context.d.ts +9 -0
  6. package/dist/agent/context.js +123 -0
  7. package/dist/agent/fileTracker.d.ts +16 -0
  8. package/dist/agent/fileTracker.js +53 -0
  9. package/dist/agent/loop.d.ts +25 -0
  10. package/dist/agent/loop.js +524 -0
  11. package/dist/agent/mentions.d.ts +11 -0
  12. package/dist/agent/mentions.js +43 -0
  13. package/dist/agent/retry.d.ts +17 -0
  14. package/dist/agent/retry.js +57 -0
  15. package/dist/agent/subagent.d.ts +2 -0
  16. package/dist/agent/subagent.js +116 -0
  17. package/dist/agent/syntaxValidator.d.ts +5 -0
  18. package/dist/agent/syntaxValidator.js +71 -0
  19. package/dist/agent/tracker.d.ts +19 -0
  20. package/dist/agent/tracker.js +38 -0
  21. package/dist/agent/undoManager.d.ts +37 -0
  22. package/dist/agent/undoManager.js +95 -0
  23. package/dist/config/constants.d.ts +77 -0
  24. package/dist/config/constants.js +146 -0
  25. package/dist/config/engine.d.ts +49 -0
  26. package/dist/config/engine.js +86 -0
  27. package/dist/config/env.d.ts +13 -0
  28. package/dist/config/env.js +103 -0
  29. package/dist/config/license.d.ts +39 -0
  30. package/dist/config/license.js +306 -0
  31. package/dist/config/permissions.d.ts +38 -0
  32. package/dist/config/permissions.js +239 -0
  33. package/dist/config/quota.d.ts +58 -0
  34. package/dist/config/quota.js +156 -0
  35. package/dist/hooks/manager.d.ts +60 -0
  36. package/dist/hooks/manager.js +193 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +470 -0
  39. package/dist/mcp/client.d.ts +55 -0
  40. package/dist/mcp/client.js +282 -0
  41. package/dist/mcp/manager.d.ts +26 -0
  42. package/dist/mcp/manager.js +167 -0
  43. package/dist/memory/manager.d.ts +9 -0
  44. package/dist/memory/manager.js +59 -0
  45. package/dist/prompts/systemPrompt.d.ts +5 -0
  46. package/dist/prompts/systemPrompt.js +177 -0
  47. package/dist/sessions/manager.d.ts +21 -0
  48. package/dist/sessions/manager.js +54 -0
  49. package/dist/skills/manager.d.ts +14 -0
  50. package/dist/skills/manager.js +86 -0
  51. package/dist/tools/browserCdp.d.ts +26 -0
  52. package/dist/tools/browserCdp.js +610 -0
  53. package/dist/tools/browserExtension.d.ts +58 -0
  54. package/dist/tools/browserExtension.js +479 -0
  55. package/dist/tools/browserLive.d.ts +12 -0
  56. package/dist/tools/browserLive.js +265 -0
  57. package/dist/tools/changeDir.d.ts +5 -0
  58. package/dist/tools/changeDir.js +63 -0
  59. package/dist/tools/computerUse.d.ts +58 -0
  60. package/dist/tools/computerUse.js +567 -0
  61. package/dist/tools/createImage.d.ts +1 -0
  62. package/dist/tools/createImage.js +34 -0
  63. package/dist/tools/definitions.d.ts +8 -0
  64. package/dist/tools/definitions.js +401 -0
  65. package/dist/tools/deleteFile.d.ts +1 -0
  66. package/dist/tools/deleteFile.js +17 -0
  67. package/dist/tools/editFile.d.ts +5 -0
  68. package/dist/tools/editFile.js +88 -0
  69. package/dist/tools/findFiles.d.ts +1 -0
  70. package/dist/tools/findFiles.js +33 -0
  71. package/dist/tools/getSystemInfo.d.ts +1 -0
  72. package/dist/tools/getSystemInfo.js +23 -0
  73. package/dist/tools/gitTools.d.ts +3 -0
  74. package/dist/tools/gitTools.js +47 -0
  75. package/dist/tools/grepSearch.d.ts +1 -0
  76. package/dist/tools/grepSearch.js +37 -0
  77. package/dist/tools/listDir.d.ts +1 -0
  78. package/dist/tools/listDir.js +33 -0
  79. package/dist/tools/moveCopyFile.d.ts +1 -0
  80. package/dist/tools/moveCopyFile.js +21 -0
  81. package/dist/tools/runCommand.d.ts +1 -0
  82. package/dist/tools/runCommand.js +61 -0
  83. package/dist/tools/runner.d.ts +5 -0
  84. package/dist/tools/runner.js +177 -0
  85. package/dist/tools/safety.d.ts +17 -0
  86. package/dist/tools/safety.js +33 -0
  87. package/dist/tools/todoTool.d.ts +8 -0
  88. package/dist/tools/todoTool.js +27 -0
  89. package/dist/tools/toolDefsComputer.d.ts +13 -0
  90. package/dist/tools/toolDefsComputer.js +205 -0
  91. package/dist/tools/truncator.d.ts +11 -0
  92. package/dist/tools/truncator.js +24 -0
  93. package/dist/tools/viewFile.d.ts +1 -0
  94. package/dist/tools/viewFile.js +45 -0
  95. package/dist/tools/visionBridge.d.ts +37 -0
  96. package/dist/tools/visionBridge.js +107 -0
  97. package/dist/tools/webFetch.d.ts +1 -0
  98. package/dist/tools/webFetch.js +25 -0
  99. package/dist/tools/webSearch.d.ts +1 -0
  100. package/dist/tools/webSearch.js +35 -0
  101. package/dist/tools/win/hostClient.d.ts +31 -0
  102. package/dist/tools/win/hostClient.js +158 -0
  103. package/dist/tools/win/hostScript.d.ts +1 -0
  104. package/dist/tools/win/hostScript.js +925 -0
  105. package/dist/tools/writeFile.d.ts +1 -0
  106. package/dist/tools/writeFile.js +13 -0
  107. package/dist/ui/banner.d.ts +10 -0
  108. package/dist/ui/banner.js +165 -0
  109. package/dist/ui/diffViewer.d.ts +1 -0
  110. package/dist/ui/diffViewer.js +60 -0
  111. package/dist/ui/interrupt.d.ts +17 -0
  112. package/dist/ui/interrupt.js +156 -0
  113. package/dist/ui/markdownStream.d.ts +23 -0
  114. package/dist/ui/markdownStream.js +0 -0
  115. package/dist/ui/modes.d.ts +28 -0
  116. package/dist/ui/modes.js +57 -0
  117. package/dist/ui/permissionPrompt.d.ts +35 -0
  118. package/dist/ui/permissionPrompt.js +271 -0
  119. package/dist/ui/prompt.d.ts +7 -0
  120. package/dist/ui/prompt.js +466 -0
  121. package/dist/ui/reasoningStream.d.ts +6 -0
  122. package/dist/ui/reasoningStream.js +38 -0
  123. package/dist/ui/renderer.d.ts +10 -0
  124. package/dist/ui/renderer.js +142 -0
  125. package/dist/ui/spinner.d.ts +24 -0
  126. package/dist/ui/spinner.js +111 -0
  127. package/dist/ui/theme.d.ts +36 -0
  128. package/dist/ui/theme.js +37 -0
  129. package/dist/ui/typeAhead.d.ts +31 -0
  130. package/dist/ui/typeAhead.js +87 -0
  131. package/extension/README.md +74 -0
  132. package/extension/background.js +575 -0
  133. package/extension/content.js +446 -0
  134. package/extension/manifest.json +36 -0
  135. package/extension/popup.html +22 -0
  136. package/extension/popup.js +12 -0
  137. package/package.json +66 -0
  138. package/skills/complex-refactor/SKILL.md +13 -0
  139. package/skills/deep-debugger/SKILL.md +14 -0
  140. 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
+ }