chocolatito-code 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/agent/compaction.d.ts +65 -0
- package/dist/agent/compaction.js +134 -0
- package/dist/agent/context.d.ts +3 -0
- package/dist/agent/context.js +35 -0
- package/dist/agent/loop.d.ts +91 -2
- package/dist/agent/loop.js +642 -91
- package/dist/agent/loopDetector.d.ts +57 -0
- package/dist/agent/loopDetector.js +0 -0
- package/dist/agent/mentions.d.ts +13 -0
- package/dist/agent/mentions.js +49 -0
- package/dist/agent/repoMap.d.ts +117 -0
- package/dist/agent/repoMap.js +560 -0
- package/dist/agent/result.d.ts +64 -0
- package/dist/agent/result.js +48 -0
- package/dist/agent/retry.d.ts +35 -0
- package/dist/agent/retry.js +123 -4
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +53 -0
- package/dist/agent/subagent.js +62 -15
- package/dist/agent/syntaxValidator.js +288 -52
- package/dist/agent/toolGate.d.ts +58 -0
- package/dist/agent/toolGate.js +111 -0
- package/dist/agent/tracker.js +4 -7
- package/dist/agent/undoManager.d.ts +44 -11
- package/dist/agent/undoManager.js +95 -24
- package/dist/agent/usageMeter.d.ts +56 -0
- package/dist/agent/usageMeter.js +51 -0
- package/dist/agent/verifier.d.ts +49 -0
- package/dist/agent/verifier.js +158 -0
- package/dist/config/constants.js +14 -0
- package/dist/config/engine.d.ts +19 -0
- package/dist/config/engine.js +35 -3
- package/dist/config/license.d.ts +5 -0
- package/dist/config/license.js +107 -9
- package/dist/config/limits.d.ts +28 -0
- package/dist/config/limits.js +51 -0
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/config/permissions.d.ts +76 -1
- package/dist/config/permissions.js +379 -23
- package/dist/config/quota.js +24 -1
- package/dist/config/updater.d.ts +68 -0
- package/dist/config/updater.js +215 -0
- package/dist/hooks/manager.js +159 -14
- package/dist/index.js +240 -37
- package/dist/mcp/client.d.ts +50 -0
- package/dist/mcp/client.js +155 -19
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/prompts/systemPrompt.js +12 -1
- package/dist/sessions/manager.d.ts +12 -0
- package/dist/sessions/manager.js +33 -5
- package/dist/sessions/resume.d.ts +36 -0
- package/dist/sessions/resume.js +43 -0
- package/dist/skills/manager.d.ts +1 -0
- package/dist/skills/manager.js +9 -0
- package/dist/tools/binary.d.ts +46 -0
- package/dist/tools/binary.js +100 -0
- package/dist/tools/browserCdp.js +13 -1
- package/dist/tools/browserExtension.d.ts +5 -0
- package/dist/tools/browserExtension.js +237 -22
- package/dist/tools/browserSession.d.ts +23 -0
- package/dist/tools/browserSession.js +28 -0
- package/dist/tools/computerUse.js +33 -1
- package/dist/tools/createImage.d.ts +11 -0
- package/dist/tools/createImage.js +13 -1
- package/dist/tools/definitions.js +17 -4
- package/dist/tools/editDiagnosis.d.ts +54 -0
- package/dist/tools/editDiagnosis.js +142 -0
- package/dist/tools/editFile.d.ts +1 -0
- package/dist/tools/editFile.js +118 -62
- package/dist/tools/findFiles.d.ts +21 -0
- package/dist/tools/findFiles.js +75 -6
- package/dist/tools/getSystemInfo.js +15 -7
- package/dist/tools/gitTools.d.ts +24 -0
- package/dist/tools/gitTools.js +87 -26
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/moveCopyFile.d.ts +18 -1
- package/dist/tools/moveCopyFile.js +74 -4
- package/dist/tools/patchCascade.d.ts +57 -0
- package/dist/tools/patchCascade.js +338 -0
- package/dist/tools/runCommand.js +213 -25
- package/dist/tools/runner.js +58 -5
- package/dist/tools/toolDefsComputer.js +12 -2
- package/dist/tools/truncator.d.ts +27 -1
- package/dist/tools/truncator.js +117 -11
- package/dist/tools/viewFile.js +15 -11
- package/dist/tools/visionBridge.js +7 -0
- package/dist/tools/webSearch.d.ts +21 -0
- package/dist/tools/webSearch.js +125 -14
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +40 -2
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +59 -7
- package/dist/ui/keyboardGuard.d.ts +76 -0
- package/dist/ui/keyboardGuard.js +94 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +40 -0
- package/dist/ui/prompt.js +436 -131
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/extension/README.md +95 -74
- package/extension/background.js +772 -181
- package/extension/content.js +527 -46
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/logotipo.png +0 -0
- package/extension/manifest.json +28 -4
- package/extension/popup.html +16 -6
- package/extension/popup.js +28 -12
- package/package.json +3 -2
|
@@ -4,23 +4,61 @@ export class UndoManager {
|
|
|
4
4
|
checkpoints = [];
|
|
5
5
|
currentTurnSnapshots = new Map();
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Fotografia una ruta antes de modificarla o borrarla.
|
|
8
|
+
*
|
|
9
|
+
* Devuelve el tipo de foto que se guardo para que quien llama pueda avisar en
|
|
10
|
+
* el momento: si sale "unrestorable", la operacion que venga despues es
|
|
11
|
+
* definitiva y no tiene sentido prometer un /undo que no va a funcionar.
|
|
8
12
|
*/
|
|
9
13
|
snapshotFile(filePath, cwd = process.cwd()) {
|
|
10
14
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
const yaVisto = this.currentTurnSnapshots.get(resolved);
|
|
16
|
+
if (yaVisto)
|
|
17
|
+
return yaVisto.kind; // Ya capturado en este turno
|
|
18
|
+
const guardar = (state) => {
|
|
19
|
+
this.currentTurnSnapshots.set(resolved, state);
|
|
20
|
+
return state.kind;
|
|
21
|
+
};
|
|
22
|
+
// lstat y no existsSync + stat: un enlace roto existe como enlace pero no se
|
|
23
|
+
// puede leer, y nos interesa distinguir "no hay nada" de "hay algo raro".
|
|
24
|
+
let info;
|
|
25
|
+
try {
|
|
26
|
+
info = fs.lstatSync(resolved);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err?.code === "ENOENT")
|
|
30
|
+
return guardar({ kind: "absent" });
|
|
31
|
+
return guardar({
|
|
32
|
+
kind: "unrestorable",
|
|
33
|
+
reason: `no se pudo inspeccionar la ruta (${err?.code || err?.message})`,
|
|
34
|
+
});
|
|
21
35
|
}
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
// Un directorio no cabe en un Buffer. Antes acababa como null y /undo lo
|
|
37
|
+
// trataba como archivo nuevo; ahora consta como lo que es: sin copia.
|
|
38
|
+
if (info.isDirectory()) {
|
|
39
|
+
return guardar({
|
|
40
|
+
kind: "unrestorable",
|
|
41
|
+
reason: "es un directorio y el historial solo guarda archivos sueltos",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Enlaces, sockets, FIFOs: readFileSync seguiria el enlace y al restaurar
|
|
45
|
+
// dejariamos un archivo normal donde habia otra cosa.
|
|
46
|
+
if (!info.isFile()) {
|
|
47
|
+
return guardar({
|
|
48
|
+
kind: "unrestorable",
|
|
49
|
+
reason: "no es un archivo regular",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return guardar({ kind: "file", previousContent: fs.readFileSync(resolved) });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// EBUSY/EPERM (archivo abierto por otro proceso en Windows), EACCES...
|
|
57
|
+
// El archivo existe, asi que jamas debe acabar en la rama de borrado.
|
|
58
|
+
return guardar({
|
|
59
|
+
kind: "unrestorable",
|
|
60
|
+
reason: `existe pero no se pudo leer (${err?.code || err?.message})`,
|
|
61
|
+
});
|
|
24
62
|
}
|
|
25
63
|
}
|
|
26
64
|
/**
|
|
@@ -30,8 +68,8 @@ export class UndoManager {
|
|
|
30
68
|
if (this.currentTurnSnapshots.size === 0)
|
|
31
69
|
return;
|
|
32
70
|
const snapshots = [];
|
|
33
|
-
this.currentTurnSnapshots.forEach((
|
|
34
|
-
snapshots.push({ filePath,
|
|
71
|
+
this.currentTurnSnapshots.forEach((state, filePath) => {
|
|
72
|
+
snapshots.push({ filePath, ...state });
|
|
35
73
|
});
|
|
36
74
|
this.checkpoints.push({
|
|
37
75
|
id: `chk_${Date.now()}`,
|
|
@@ -46,7 +84,11 @@ export class UndoManager {
|
|
|
46
84
|
}
|
|
47
85
|
}
|
|
48
86
|
/**
|
|
49
|
-
*
|
|
87
|
+
* Deshace el ultimo turno de modificaciones.
|
|
88
|
+
*
|
|
89
|
+
* success solo es true si algo se restauro de verdad y nada fallo. Antes se
|
|
90
|
+
* devolvia true siempre, incluso con la lista de restaurados vacia, que es el
|
|
91
|
+
* peor mensaje posible: el usuario da por recuperado lo que ya no existe.
|
|
50
92
|
*/
|
|
51
93
|
undoLast() {
|
|
52
94
|
if (this.checkpoints.length === 0) {
|
|
@@ -54,33 +96,62 @@ export class UndoManager {
|
|
|
54
96
|
success: false,
|
|
55
97
|
message: "No hay acciones previas para deshacer en esta sesión.",
|
|
56
98
|
restoredFiles: [],
|
|
99
|
+
failures: [],
|
|
57
100
|
};
|
|
58
101
|
}
|
|
59
102
|
const checkpoint = this.checkpoints.pop();
|
|
60
103
|
const restoredFiles = [];
|
|
104
|
+
const failures = [];
|
|
61
105
|
for (const snap of checkpoint.snapshots) {
|
|
106
|
+
const nombre = path.basename(snap.filePath);
|
|
62
107
|
try {
|
|
63
|
-
if (snap.
|
|
64
|
-
|
|
108
|
+
if (snap.kind === "unrestorable") {
|
|
109
|
+
failures.push(`(Sin copia) ${nombre}: ${snap.reason}`);
|
|
110
|
+
}
|
|
111
|
+
else if (snap.kind === "absent") {
|
|
112
|
+
// No existia antes del turno: deshacer es eliminar lo que se creo.
|
|
65
113
|
if (fs.existsSync(snap.filePath)) {
|
|
66
114
|
fs.unlinkSync(snap.filePath);
|
|
67
|
-
restoredFiles.push(`(Eliminado archivo creado) ${
|
|
115
|
+
restoredFiles.push(`(Eliminado archivo creado) ${nombre}`);
|
|
68
116
|
}
|
|
117
|
+
// Si ya no esta, no hay nada que deshacer y no cuenta como restaurado.
|
|
69
118
|
}
|
|
70
119
|
else {
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
120
|
+
// Si el turno se llevo por delante la carpeta que lo contenia,
|
|
121
|
+
// writeFileSync fallaria con ENOENT en vez de devolver el archivo.
|
|
122
|
+
fs.mkdirSync(path.dirname(snap.filePath), { recursive: true });
|
|
123
|
+
fs.writeFileSync(snap.filePath, snap.previousContent);
|
|
124
|
+
restoredFiles.push(`(Restaurado) ${nombre}`);
|
|
74
125
|
}
|
|
75
126
|
}
|
|
76
127
|
catch (err) {
|
|
77
|
-
|
|
128
|
+
failures.push(`(Error restaurando) ${nombre}: ${err?.message}`);
|
|
78
129
|
}
|
|
79
130
|
}
|
|
131
|
+
const cabecera = `"${checkpoint.description}" (${checkpoint.timestamp})`;
|
|
132
|
+
if (failures.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
message: restoredFiles.length > 0
|
|
136
|
+
? `⚠ Deshecho a medias: ${cabecera} — ${failures.length} ruta(s) sin restaurar`
|
|
137
|
+
: `⚠ No se pudo deshacer: ${cabecera} — ${failures.length} ruta(s) sin copia`,
|
|
138
|
+
restoredFiles,
|
|
139
|
+
failures,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (restoredFiles.length === 0) {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
message: `Nada que deshacer en ${cabecera}: las rutas ya estaban en su estado anterior.`,
|
|
146
|
+
restoredFiles,
|
|
147
|
+
failures,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
80
150
|
return {
|
|
81
151
|
success: true,
|
|
82
|
-
message: `✔ Deshecho con éxito:
|
|
152
|
+
message: `✔ Deshecho con éxito: ${cabecera}`,
|
|
83
153
|
restoredFiles,
|
|
154
|
+
failures,
|
|
84
155
|
};
|
|
85
156
|
}
|
|
86
157
|
getHistory() {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { EngineRoute } from "../config/engine.js";
|
|
2
|
+
/**
|
|
3
|
+
* EL CONTADOR DE LO QUE SE GASTA, VENGA DE LA RUTA QUE VENGA.
|
|
4
|
+
*
|
|
5
|
+
* El motor se llama desde cuatro sitios —el bucle, los subagentes, el modelo de
|
|
6
|
+
* vision y la compactacion— y solo el primero apuntaba nada. Los otros tres
|
|
7
|
+
* gastan dinero de verdad y sumaban 0,00 $: la insignia del pie decia "12% de la
|
|
8
|
+
* cuota semanal" mientras el proxy llevaba cobradas varias semanas de
|
|
9
|
+
* presupuesto, el corte local nunca saltaba y el usuario se enteraba cuando le
|
|
10
|
+
* llegaba un 402 de la nada.
|
|
11
|
+
*
|
|
12
|
+
* La peor de las tres era la compactacion automatica: se dispara con el
|
|
13
|
+
* historial por encima de COMPACT_THRESHOLD_TOKENS (750.000 tokens) y manda ese
|
|
14
|
+
* historial entero como prompt. Una sola compactacion en Espectro 2.0 cuesta
|
|
15
|
+
* unos 0,99 $, el 85% del presupuesto semanal por defecto, y no la veia nadie.
|
|
16
|
+
*
|
|
17
|
+
* Esto sigue siendo el CONTADOR, no la barrera —la barrera vive en el proxy, ver
|
|
18
|
+
* la cabecera de quota.ts—, pero un contador que solo mira una de las cuatro
|
|
19
|
+
* rutas no sirve ni para avisar.
|
|
20
|
+
*/
|
|
21
|
+
export interface UsoDelMotor {
|
|
22
|
+
prompt_tokens?: number;
|
|
23
|
+
completion_tokens?: number;
|
|
24
|
+
prompt_tokens_details?: {
|
|
25
|
+
cached_tokens?: number;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
export interface DesgloseDeCoste {
|
|
29
|
+
inputCost: number;
|
|
30
|
+
outputCost: number;
|
|
31
|
+
totalCost: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* La tarifa aplicada a unos tokens. Unico sitio donde se multiplica por el
|
|
35
|
+
* precio: TokenTracker tambien pasa por aqui, para que el pie del turno y la
|
|
36
|
+
* cuota no puedan discrepar.
|
|
37
|
+
*/
|
|
38
|
+
export declare function desglosarCoste(promptTokens: number, cachedTokens: number, completionTokens: number, modelName: string): DesgloseDeCoste;
|
|
39
|
+
/** Lo que cuesta el `usage` que devuelve una respuesta del motor. */
|
|
40
|
+
export declare function costeDeUso(usage: UsoDelMotor | null | undefined, modelName: string): number;
|
|
41
|
+
/**
|
|
42
|
+
* Apunta en la cuota lo que ha costado una llamada al motor.
|
|
43
|
+
*
|
|
44
|
+
* Se llama desde donde se hace la llamada, no desde quien la orquesta: asi una
|
|
45
|
+
* ruta nueva no puede olvidarse de contar sin que se note, que es exactamente lo
|
|
46
|
+
* que paso con las tres de arriba.
|
|
47
|
+
*/
|
|
48
|
+
export declare function apuntarUso(usage: UsoDelMotor | null | undefined, modelName: string, ruta: EngineRoute): number;
|
|
49
|
+
/**
|
|
50
|
+
* Se lleva el gasto auxiliar acumulado y deja el contador a cero. Lo usa el pie
|
|
51
|
+
* del turno para decir cuanto se fue por rutas que el usuario no ve.
|
|
52
|
+
*/
|
|
53
|
+
export declare function tomarGastoAuxiliar(): {
|
|
54
|
+
total: number;
|
|
55
|
+
rutas: string;
|
|
56
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MODEL_PRICING, PRICING } from "../config/constants.js";
|
|
2
|
+
import { recordUsage } from "../config/quota.js";
|
|
3
|
+
/**
|
|
4
|
+
* La tarifa aplicada a unos tokens. Unico sitio donde se multiplica por el
|
|
5
|
+
* precio: TokenTracker tambien pasa por aqui, para que el pie del turno y la
|
|
6
|
+
* cuota no puedan discrepar.
|
|
7
|
+
*/
|
|
8
|
+
export function desglosarCoste(promptTokens, cachedTokens, completionTokens, modelName) {
|
|
9
|
+
const pricing = MODEL_PRICING[modelName] || PRICING;
|
|
10
|
+
const sinCache = Math.max(0, promptTokens - cachedTokens);
|
|
11
|
+
const inputCost = (sinCache / 1_000_000) * pricing.inputCacheMiss + (cachedTokens / 1_000_000) * pricing.inputCacheHit;
|
|
12
|
+
const outputCost = (completionTokens / 1_000_000) * pricing.output;
|
|
13
|
+
return { inputCost, outputCost, totalCost: inputCost + outputCost };
|
|
14
|
+
}
|
|
15
|
+
/** Lo que cuesta el `usage` que devuelve una respuesta del motor. */
|
|
16
|
+
export function costeDeUso(usage, modelName) {
|
|
17
|
+
if (!usage)
|
|
18
|
+
return 0;
|
|
19
|
+
return desglosarCoste(usage.prompt_tokens || 0, usage.prompt_tokens_details?.cached_tokens || 0, usage.completion_tokens || 0, modelName).totalCost;
|
|
20
|
+
}
|
|
21
|
+
/** Gasto de este turno que NO viene del bucle principal, por ruta. */
|
|
22
|
+
const pendiente = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* Apunta en la cuota lo que ha costado una llamada al motor.
|
|
25
|
+
*
|
|
26
|
+
* Se llama desde donde se hace la llamada, no desde quien la orquesta: asi una
|
|
27
|
+
* ruta nueva no puede olvidarse de contar sin que se note, que es exactamente lo
|
|
28
|
+
* que paso con las tres de arriba.
|
|
29
|
+
*/
|
|
30
|
+
export function apuntarUso(usage, modelName, ruta) {
|
|
31
|
+
const coste = costeDeUso(usage, modelName);
|
|
32
|
+
if (!Number.isFinite(coste) || coste <= 0)
|
|
33
|
+
return 0;
|
|
34
|
+
recordUsage(coste);
|
|
35
|
+
pendiente.set(ruta, (pendiente.get(ruta) || 0) + coste);
|
|
36
|
+
return coste;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Se lleva el gasto auxiliar acumulado y deja el contador a cero. Lo usa el pie
|
|
40
|
+
* del turno para decir cuanto se fue por rutas que el usuario no ve.
|
|
41
|
+
*/
|
|
42
|
+
export function tomarGastoAuxiliar() {
|
|
43
|
+
let total = 0;
|
|
44
|
+
const nombres = [];
|
|
45
|
+
for (const [ruta, coste] of pendiente) {
|
|
46
|
+
total += coste;
|
|
47
|
+
nombres.push(ruta);
|
|
48
|
+
}
|
|
49
|
+
pendiente.clear();
|
|
50
|
+
return { total, rutas: nombres.join(", ") };
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verificacion automatica despues de editar: se corre el linter o el
|
|
3
|
+
* comprobador de tipos del proyecto y, si falla, el error vuelve al modelo en
|
|
4
|
+
* el mismo turno.
|
|
5
|
+
*
|
|
6
|
+
* El agujero que tapa: hoy el agente edita, dice "listo" y cierra el turno. Si
|
|
7
|
+
* dejo el archivo con un error de tipos, nadie se entera hasta que el usuario
|
|
8
|
+
* compila —normalmente varios turnos despues, encima de codigo que ya se
|
|
9
|
+
* escribio sobre el error—. La regla 5 del prompt de sistema le pide verificar,
|
|
10
|
+
* pero pedirselo por escrito no es lo mismo que comprobarlo.
|
|
11
|
+
*
|
|
12
|
+
* Tres decisiones que hacen que esto no moleste:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Va por invitacion, no por defecto.** Ejecutar un comando del proyecto
|
|
15
|
+
* al final de cada turno cuesta segundos y es ejecutar codigo ajeno. El
|
|
16
|
+
* comando se DETECTA solo y se le ensena al usuario; correrlo lo decide el.
|
|
17
|
+
* 2. **Nunca corre los tests.** Un `npm test` puede levantar servidores, tocar
|
|
18
|
+
* bases de datos o tardar minutos. Se limita a lo que solo mira el codigo.
|
|
19
|
+
* 3. **Tope de vueltas.** Si a la tercera sigue fallando, se le dice al
|
|
20
|
+
* usuario en vez de dejar al agente peleandose solo con el compilador.
|
|
21
|
+
*/
|
|
22
|
+
/** Vueltas de correccion antes de rendirse y contarselo al usuario. */
|
|
23
|
+
export declare const MAX_REFLEXIONES = 3;
|
|
24
|
+
export interface ComandoDeVerificacion {
|
|
25
|
+
comando: string;
|
|
26
|
+
/** De donde salio: sirve para explicarselo al usuario sin que tenga que adivinar. */
|
|
27
|
+
origen: "configurado" | "detectado";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Que comando verificaria este proyecto. Devuelve null si no hay ninguno
|
|
31
|
+
* razonable: inventarse uno y ejecutarlo seria peor que no verificar.
|
|
32
|
+
*/
|
|
33
|
+
export declare function detectarComando(cwd: string): ComandoDeVerificacion | null;
|
|
34
|
+
/** Si el usuario ha pedido que se verifique solo. */
|
|
35
|
+
export declare function verificacionAutomaticaActiva(cwd: string): boolean;
|
|
36
|
+
export interface ResultadoVerificacion {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
salida: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function verificar(comando: string, cwd: string): Promise<ResultadoVerificacion>;
|
|
41
|
+
/**
|
|
42
|
+
* El mensaje que vuelve al modelo.
|
|
43
|
+
*
|
|
44
|
+
* Empieza por la instruccion y no por la salida a proposito: lo primero que
|
|
45
|
+
* lee tiene que decirle que se espera de el, no seiscientas lineas de tsc.
|
|
46
|
+
*/
|
|
47
|
+
export declare function mensajeDeVerificacion(comando: string, salida: string, vuelta: number): string;
|
|
48
|
+
/** Aviso al usuario cuando se agotan las vueltas. */
|
|
49
|
+
export declare function mensajeDeRendicion(comando: string): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
/**
|
|
6
|
+
* Verificacion automatica despues de editar: se corre el linter o el
|
|
7
|
+
* comprobador de tipos del proyecto y, si falla, el error vuelve al modelo en
|
|
8
|
+
* el mismo turno.
|
|
9
|
+
*
|
|
10
|
+
* El agujero que tapa: hoy el agente edita, dice "listo" y cierra el turno. Si
|
|
11
|
+
* dejo el archivo con un error de tipos, nadie se entera hasta que el usuario
|
|
12
|
+
* compila —normalmente varios turnos despues, encima de codigo que ya se
|
|
13
|
+
* escribio sobre el error—. La regla 5 del prompt de sistema le pide verificar,
|
|
14
|
+
* pero pedirselo por escrito no es lo mismo que comprobarlo.
|
|
15
|
+
*
|
|
16
|
+
* Tres decisiones que hacen que esto no moleste:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Va por invitacion, no por defecto.** Ejecutar un comando del proyecto
|
|
19
|
+
* al final de cada turno cuesta segundos y es ejecutar codigo ajeno. El
|
|
20
|
+
* comando se DETECTA solo y se le ensena al usuario; correrlo lo decide el.
|
|
21
|
+
* 2. **Nunca corre los tests.** Un `npm test` puede levantar servidores, tocar
|
|
22
|
+
* bases de datos o tardar minutos. Se limita a lo que solo mira el codigo.
|
|
23
|
+
* 3. **Tope de vueltas.** Si a la tercera sigue fallando, se le dice al
|
|
24
|
+
* usuario en vez de dejar al agente peleandose solo con el compilador.
|
|
25
|
+
*/
|
|
26
|
+
/** Vueltas de correccion antes de rendirse y contarselo al usuario. */
|
|
27
|
+
export const MAX_REFLEXIONES = 3;
|
|
28
|
+
/** Un comando de verificacion que tarda mas que esto no se puede correr por turno. */
|
|
29
|
+
const TIMEOUT_MS = 120_000;
|
|
30
|
+
/**
|
|
31
|
+
* Scripts que valen para verificar, en orden de preferencia.
|
|
32
|
+
*
|
|
33
|
+
* `test` no esta y no es un olvido: ver la decision 2 de arriba.
|
|
34
|
+
*/
|
|
35
|
+
const SCRIPTS_CANDIDATOS = ["typecheck", "type-check", "tsc", "lint", "check", "build"];
|
|
36
|
+
function leerConfig(cwd) {
|
|
37
|
+
const config = {};
|
|
38
|
+
const rutas = [
|
|
39
|
+
path.join(os.homedir(), ".chocolatito", "settings.json"),
|
|
40
|
+
path.join(cwd, ".chocolatito", "settings.json"),
|
|
41
|
+
];
|
|
42
|
+
for (const ruta of rutas) {
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.existsSync(ruta))
|
|
45
|
+
continue;
|
|
46
|
+
const parsed = JSON.parse(fs.readFileSync(ruta, "utf-8"));
|
|
47
|
+
if (typeof parsed?.verify?.command === "string")
|
|
48
|
+
config.verifyCmd = parsed.verify.command;
|
|
49
|
+
if (typeof parsed?.verify?.auto === "boolean")
|
|
50
|
+
config.autoVerify = parsed.verify.auto;
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
if (process.env.CHOCOLATITO_AUTO_VERIFY === "1")
|
|
55
|
+
config.autoVerify = true;
|
|
56
|
+
if (process.env.CHOCOLATITO_VERIFY_CMD)
|
|
57
|
+
config.verifyCmd = process.env.CHOCOLATITO_VERIFY_CMD;
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Que comando verificaria este proyecto. Devuelve null si no hay ninguno
|
|
62
|
+
* razonable: inventarse uno y ejecutarlo seria peor que no verificar.
|
|
63
|
+
*/
|
|
64
|
+
export function detectarComando(cwd) {
|
|
65
|
+
const config = leerConfig(cwd);
|
|
66
|
+
if (config.verifyCmd)
|
|
67
|
+
return { comando: config.verifyCmd, origen: "configurado" };
|
|
68
|
+
try {
|
|
69
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
70
|
+
if (fs.existsSync(pkgPath)) {
|
|
71
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
72
|
+
const scripts = pkg.scripts || {};
|
|
73
|
+
for (const nombre of SCRIPTS_CANDIDATOS) {
|
|
74
|
+
if (typeof scripts[nombre] === "string") {
|
|
75
|
+
return { comando: `npm run ${nombre}`, origen: "detectado" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Sin script, pero con TypeScript: el comprobador universal y barato.
|
|
79
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
80
|
+
if (deps.typescript && fs.existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
81
|
+
return { comando: "npx tsc --noEmit", origen: "detectado" };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
87
|
+
return { comando: "cargo check", origen: "detectado" };
|
|
88
|
+
}
|
|
89
|
+
if (fs.existsSync(path.join(cwd, "go.mod"))) {
|
|
90
|
+
return { comando: "go build ./...", origen: "detectado" };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** Si el usuario ha pedido que se verifique solo. */
|
|
95
|
+
export function verificacionAutomaticaActiva(cwd) {
|
|
96
|
+
return leerConfig(cwd).autoVerify === true;
|
|
97
|
+
}
|
|
98
|
+
/** Cuanta salida del verificador se le devuelve al modelo. */
|
|
99
|
+
const MAX_SALIDA = 6_000;
|
|
100
|
+
/**
|
|
101
|
+
* "El comando no existe" no es "tu codigo esta roto".
|
|
102
|
+
*
|
|
103
|
+
* Con reject:false, un comando inexistente vuelve como una ejecucion normal con
|
|
104
|
+
* codigo distinto de cero, indistinguible de un build que falla. Sin esta
|
|
105
|
+
* comprobacion, configurar mal el comando —o no tener `cargo` instalado— hace
|
|
106
|
+
* que el agente se ponga a "arreglar" un codigo que no ha roto, tres veces
|
|
107
|
+
* seguidas, con lo que eso cuesta.
|
|
108
|
+
*/
|
|
109
|
+
function elVerificadorNoSePudoEjecutar(codigo, salida) {
|
|
110
|
+
if (codigo === 127)
|
|
111
|
+
return true; // convenio de sh
|
|
112
|
+
return /command not found|is not recognized as the name of a cmdlet|CommandNotFoundException|no such file or directory|ENOENT/i.test(salida);
|
|
113
|
+
}
|
|
114
|
+
export async function verificar(comando, cwd) {
|
|
115
|
+
const shell = process.platform === "win32" ? "powershell.exe" : "/bin/sh";
|
|
116
|
+
const args = process.platform === "win32" ? ["-NoProfile", "-Command", comando] : ["-c", comando];
|
|
117
|
+
try {
|
|
118
|
+
const res = await execa(shell, args, {
|
|
119
|
+
cwd,
|
|
120
|
+
timeout: TIMEOUT_MS,
|
|
121
|
+
reject: false,
|
|
122
|
+
all: true,
|
|
123
|
+
});
|
|
124
|
+
const salida = String(res.all ?? `${res.stdout}\n${res.stderr}`).trim();
|
|
125
|
+
if (res.timedOut) {
|
|
126
|
+
// Un build colgado tampoco es culpa de la ultima edicion, y ademas el
|
|
127
|
+
// agente no puede hacer nada con "se agoto el tiempo".
|
|
128
|
+
return { ok: true, salida: `(la verificacion "${comando}" no termino a tiempo; no se comprobo nada)` };
|
|
129
|
+
}
|
|
130
|
+
if (res.exitCode !== 0 && elVerificadorNoSePudoEjecutar(res.exitCode ?? undefined, salida)) {
|
|
131
|
+
return { ok: true, salida: `(no se pudo ejecutar "${comando}": revisa la configuracion de verificacion)` };
|
|
132
|
+
}
|
|
133
|
+
return { ok: res.exitCode === 0, salida: salida.slice(0, MAX_SALIDA) };
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
// Que el propio verificador reviente no es un fallo del codigo del agente.
|
|
137
|
+
// Decirlo como si lo fuera le manda a "arreglar" algo que no ha roto.
|
|
138
|
+
return { ok: true, salida: `(no se pudo ejecutar la verificacion: ${err?.message || String(err)})` };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* El mensaje que vuelve al modelo.
|
|
143
|
+
*
|
|
144
|
+
* Empieza por la instruccion y no por la salida a proposito: lo primero que
|
|
145
|
+
* lee tiene que decirle que se espera de el, no seiscientas lineas de tsc.
|
|
146
|
+
*/
|
|
147
|
+
export function mensajeDeVerificacion(comando, salida, vuelta) {
|
|
148
|
+
return (`# Corrige estos errores si puedes.\n\n` +
|
|
149
|
+
`Se ejecuto la verificacion del proyecto (\`${comando}\`) sobre lo que acabas de editar y ha ` +
|
|
150
|
+
`fallado. Arregla la causa, no el sintoma, y no des el trabajo por terminado hasta que pase ` +
|
|
151
|
+
`(intento ${vuelta} de ${MAX_REFLEXIONES}).\n\n` +
|
|
152
|
+
`\`\`\`\n${salida}\n\`\`\``);
|
|
153
|
+
}
|
|
154
|
+
/** Aviso al usuario cuando se agotan las vueltas. */
|
|
155
|
+
export function mensajeDeRendicion(comando) {
|
|
156
|
+
return (`La verificacion (\`${comando}\`) sigue fallando despues de ${MAX_REFLEXIONES} intentos. ` +
|
|
157
|
+
`Lo dejo aqui en vez de seguir gastando turnos: mira la ultima salida.`);
|
|
158
|
+
}
|
package/dist/config/constants.js
CHANGED
|
@@ -37,6 +37,20 @@ export function getEngineBaseUrl() {
|
|
|
37
37
|
const cfg = rc();
|
|
38
38
|
if (typeof cfg.engineUrl === "string" && cfg.engineUrl.trim())
|
|
39
39
|
return cfg.engineUrl.trim();
|
|
40
|
+
// La variable de entorno manda sobre el fichero, igual que en isManagedMode()
|
|
41
|
+
// y en engineCredential().
|
|
42
|
+
//
|
|
43
|
+
// Aqui solo se miraba `cfg.managed`, y las otras dos si respetaban la
|
|
44
|
+
// variable: poner CHOCOLATITO_MANAGED=0 apagaba la cuota y cambiaba la
|
|
45
|
+
// credencial a la clave propia del usuario... pero seguia mandandola al proxy
|
|
46
|
+
// de suscripcion, que la valida como licencia de Lemon Squeezy. Resultado: un
|
|
47
|
+
// 401 sin explicacion, y la unica via documentada para trabajar con clave
|
|
48
|
+
// propia no funcionaba.
|
|
49
|
+
const forzado = process.env.CHOCOLATITO_MANAGED;
|
|
50
|
+
if (forzado === "1")
|
|
51
|
+
return MANAGED_ENGINE_URL;
|
|
52
|
+
if (forzado === "0")
|
|
53
|
+
return DIRECT_ENGINE_URL;
|
|
40
54
|
return cfg.managed === true ? MANAGED_ENGINE_URL : DIRECT_ENGINE_URL;
|
|
41
55
|
}
|
|
42
56
|
/**
|
package/dist/config/engine.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import type { MotivoParada } from "../agent/result.js";
|
|
2
3
|
/**
|
|
3
4
|
* QUIEN HABLA CON EL MOTOR Y COMO SE IDENTIFICA
|
|
4
5
|
*
|
|
@@ -39,6 +40,15 @@ export declare function engineCredential(fallbackApiKey: string): string;
|
|
|
39
40
|
export declare function engineHeaders(route: EngineRoute): Record<string, string>;
|
|
40
41
|
/** Cliente del motor ya configurado. Es el unico sitio donde se construye. */
|
|
41
42
|
export declare function createEngineClient(apiKey: string, route: EngineRoute): OpenAI;
|
|
43
|
+
/**
|
|
44
|
+
* De que tipo es el fallo, sin mirar el texto ya traducido.
|
|
45
|
+
*
|
|
46
|
+
* Quien llama necesita decidir cosas —con que codigo salir, si tiene sentido
|
|
47
|
+
* reintentar— y hacerlo buscando palabras dentro del mensaje para el usuario
|
|
48
|
+
* es fragil: cambia una frase y se rompe la decision.
|
|
49
|
+
*/
|
|
50
|
+
export type FalloDelMotor = "cuota" | "licencia";
|
|
51
|
+
export declare function clasificarErrorDelMotor(err: any): FalloDelMotor | null;
|
|
42
52
|
/**
|
|
43
53
|
* Traduce un error del motor a algo que el usuario entienda.
|
|
44
54
|
*
|
|
@@ -46,4 +56,13 @@ export declare function createEngineClient(apiKey: string, route: EngineRoute):
|
|
|
46
56
|
* "Error en el motor tras varios reintentos: 402 ...", que ademas miente: un 402
|
|
47
57
|
* no se reintenta nunca.
|
|
48
58
|
*/
|
|
59
|
+
/**
|
|
60
|
+
* Con que motivo se para cuando el error viene del motor.
|
|
61
|
+
*
|
|
62
|
+
* Existe porque el bucle hacia `clasificar(err) === "cuota" ? "cuota" :
|
|
63
|
+
* "licencia"`, y en cuanto hubo un tercer caso explicable —los huecos
|
|
64
|
+
* ocupados— ese ternario empezo a mentir: paraba con motivo "licencia" y
|
|
65
|
+
* codigo de salida 4 por algo que no tenia nada que ver con la licencia.
|
|
66
|
+
*/
|
|
67
|
+
export declare function motivoDeParadaDelError(err: any): MotivoParada;
|
|
49
68
|
export declare function explicarErrorDelMotor(err: any): string | null;
|
package/dist/config/engine.js
CHANGED
|
@@ -5,6 +5,7 @@ import OpenAI from "openai";
|
|
|
5
5
|
import { getEngineBaseUrl } from "./constants.js";
|
|
6
6
|
import { getStoredLicense } from "./license.js";
|
|
7
7
|
import { isManagedMode } from "./quota.js";
|
|
8
|
+
import { esDemasiadasEnCurso } from "../agent/retry.js";
|
|
8
9
|
let versionCache = null;
|
|
9
10
|
/** Version del paquete instalado, para versionar el contrato con el proxy. */
|
|
10
11
|
export function clientVersion() {
|
|
@@ -62,6 +63,14 @@ export function createEngineClient(apiKey, route) {
|
|
|
62
63
|
defaultHeaders: engineHeaders(route),
|
|
63
64
|
});
|
|
64
65
|
}
|
|
66
|
+
export function clasificarErrorDelMotor(err) {
|
|
67
|
+
const status = err?.status ?? err?.response?.status;
|
|
68
|
+
if (status === 402)
|
|
69
|
+
return "cuota";
|
|
70
|
+
if (status === 401 || status === 403)
|
|
71
|
+
return "licencia";
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
65
74
|
/**
|
|
66
75
|
* Traduce un error del motor a algo que el usuario entienda.
|
|
67
76
|
*
|
|
@@ -69,13 +78,36 @@ export function createEngineClient(apiKey, route) {
|
|
|
69
78
|
* "Error en el motor tras varios reintentos: 402 ...", que ademas miente: un 402
|
|
70
79
|
* no se reintenta nunca.
|
|
71
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Con que motivo se para cuando el error viene del motor.
|
|
83
|
+
*
|
|
84
|
+
* Existe porque el bucle hacia `clasificar(err) === "cuota" ? "cuota" :
|
|
85
|
+
* "licencia"`, y en cuanto hubo un tercer caso explicable —los huecos
|
|
86
|
+
* ocupados— ese ternario empezo a mentir: paraba con motivo "licencia" y
|
|
87
|
+
* codigo de salida 4 por algo que no tenia nada que ver con la licencia.
|
|
88
|
+
*/
|
|
89
|
+
export function motivoDeParadaDelError(err) {
|
|
90
|
+
if (esDemasiadasEnCurso(err))
|
|
91
|
+
return "ocupado";
|
|
92
|
+
return clasificarErrorDelMotor(err) === "cuota" ? "cuota" : "licencia";
|
|
93
|
+
}
|
|
72
94
|
export function explicarErrorDelMotor(err) {
|
|
73
|
-
const
|
|
95
|
+
const tipo = clasificarErrorDelMotor(err);
|
|
74
96
|
const mensaje = String(err?.error?.message || err?.message || "");
|
|
75
|
-
|
|
97
|
+
// Los huecos de peticiones simultaneas de esta licencia estan ocupados. Es lo
|
|
98
|
+
// unico que el usuario puede resolver esperando, asi que se le dice cuanto y
|
|
99
|
+
// por que, en vez de dejarle "Error en el motor tras varios reintentos".
|
|
100
|
+
if (esDemasiadasEnCurso(err)) {
|
|
101
|
+
const segundos = Number(err?.error?.retry_after_s);
|
|
102
|
+
const cuando = Number.isFinite(segundos) && segundos > 0 ? ` Reintenta en ${segundos}s.` : "";
|
|
103
|
+
return ((mensaje || "Tienes varias peticiones en curso con esta licencia.") +
|
|
104
|
+
cuando +
|
|
105
|
+
" No es un problema de tu conexion: si interrumpiste una tarea, su hueco se libera solo.");
|
|
106
|
+
}
|
|
107
|
+
if (tipo === "cuota") {
|
|
76
108
|
return mensaje || "Has agotado la cuota de uso incluida en tu suscripción.";
|
|
77
109
|
}
|
|
78
|
-
if (
|
|
110
|
+
if (tipo === "licencia") {
|
|
79
111
|
if (isManagedMode()) {
|
|
80
112
|
return ("Tu licencia no es válida para el motor. Comprueba tu suscripción con /licencia, " +
|
|
81
113
|
"o vuelve a activarla si la renovaste hace poco.");
|
package/dist/config/license.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export type LicenseOutcome = {
|
|
|
20
20
|
| {
|
|
21
21
|
kind: "unreachable";
|
|
22
22
|
reason: string;
|
|
23
|
+
}
|
|
24
|
+
/** Lemon Squeezy dijo que si, pero la licencia no se pudo guardar en disco. */
|
|
25
|
+
| {
|
|
26
|
+
kind: "unpersisted";
|
|
27
|
+
reason: string;
|
|
23
28
|
};
|
|
24
29
|
export declare function activate(licenseKey: string): Promise<LicenseOutcome>;
|
|
25
30
|
export declare function validate(license: StoredLicense): Promise<LicenseOutcome>;
|