chocolatito-code 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +89 -0
  2. package/dist/agent/compaction.d.ts +65 -0
  3. package/dist/agent/compaction.js +134 -0
  4. package/dist/agent/context.d.ts +3 -0
  5. package/dist/agent/context.js +35 -0
  6. package/dist/agent/loop.d.ts +91 -2
  7. package/dist/agent/loop.js +536 -78
  8. package/dist/agent/loopDetector.d.ts +57 -0
  9. package/dist/agent/loopDetector.js +0 -0
  10. package/dist/agent/mentions.d.ts +13 -0
  11. package/dist/agent/mentions.js +49 -0
  12. package/dist/agent/repoMap.d.ts +117 -0
  13. package/dist/agent/repoMap.js +560 -0
  14. package/dist/agent/result.d.ts +64 -0
  15. package/dist/agent/result.js +48 -0
  16. package/dist/agent/retry.d.ts +35 -0
  17. package/dist/agent/retry.js +123 -4
  18. package/dist/agent/subagent.js +62 -15
  19. package/dist/agent/syntaxValidator.js +288 -52
  20. package/dist/agent/toolGate.d.ts +58 -0
  21. package/dist/agent/toolGate.js +111 -0
  22. package/dist/agent/tracker.js +4 -7
  23. package/dist/agent/undoManager.d.ts +44 -11
  24. package/dist/agent/undoManager.js +95 -24
  25. package/dist/agent/usageMeter.d.ts +56 -0
  26. package/dist/agent/usageMeter.js +51 -0
  27. package/dist/agent/verifier.d.ts +49 -0
  28. package/dist/agent/verifier.js +158 -0
  29. package/dist/config/constants.js +14 -0
  30. package/dist/config/engine.d.ts +19 -0
  31. package/dist/config/engine.js +35 -3
  32. package/dist/config/license.d.ts +5 -0
  33. package/dist/config/license.js +107 -9
  34. package/dist/config/limits.d.ts +28 -0
  35. package/dist/config/limits.js +51 -0
  36. package/dist/config/permissions.d.ts +76 -1
  37. package/dist/config/permissions.js +379 -23
  38. package/dist/config/quota.js +24 -1
  39. package/dist/config/updater.d.ts +68 -0
  40. package/dist/config/updater.js +215 -0
  41. package/dist/hooks/manager.js +159 -14
  42. package/dist/index.js +222 -34
  43. package/dist/mcp/client.d.ts +22 -0
  44. package/dist/mcp/client.js +93 -11
  45. package/dist/prompts/systemPrompt.js +12 -1
  46. package/dist/sessions/manager.d.ts +12 -0
  47. package/dist/sessions/manager.js +33 -5
  48. package/dist/sessions/resume.d.ts +36 -0
  49. package/dist/sessions/resume.js +43 -0
  50. package/dist/skills/manager.d.ts +1 -0
  51. package/dist/skills/manager.js +9 -0
  52. package/dist/tools/binary.d.ts +46 -0
  53. package/dist/tools/binary.js +100 -0
  54. package/dist/tools/browserCdp.js +13 -1
  55. package/dist/tools/browserExtension.d.ts +5 -0
  56. package/dist/tools/browserExtension.js +202 -18
  57. package/dist/tools/browserSession.d.ts +23 -0
  58. package/dist/tools/browserSession.js +28 -0
  59. package/dist/tools/computerUse.js +33 -1
  60. package/dist/tools/createImage.d.ts +11 -0
  61. package/dist/tools/createImage.js +13 -1
  62. package/dist/tools/definitions.js +9 -3
  63. package/dist/tools/editDiagnosis.d.ts +54 -0
  64. package/dist/tools/editDiagnosis.js +142 -0
  65. package/dist/tools/editFile.d.ts +1 -0
  66. package/dist/tools/editFile.js +118 -62
  67. package/dist/tools/findFiles.d.ts +21 -0
  68. package/dist/tools/findFiles.js +75 -6
  69. package/dist/tools/getSystemInfo.js +15 -7
  70. package/dist/tools/gitTools.d.ts +24 -0
  71. package/dist/tools/gitTools.js +87 -26
  72. package/dist/tools/moveCopyFile.d.ts +18 -1
  73. package/dist/tools/moveCopyFile.js +74 -4
  74. package/dist/tools/patchCascade.d.ts +57 -0
  75. package/dist/tools/patchCascade.js +338 -0
  76. package/dist/tools/runCommand.js +213 -25
  77. package/dist/tools/runner.js +57 -4
  78. package/dist/tools/toolDefsComputer.js +12 -2
  79. package/dist/tools/truncator.d.ts +27 -1
  80. package/dist/tools/truncator.js +117 -11
  81. package/dist/tools/viewFile.js +15 -11
  82. package/dist/tools/visionBridge.js +7 -0
  83. package/dist/tools/webSearch.d.ts +21 -0
  84. package/dist/tools/webSearch.js +125 -14
  85. package/dist/tools/writeFile.js +18 -1
  86. package/dist/ui/interrupt.d.ts +4 -1
  87. package/dist/ui/interrupt.js +56 -5
  88. package/dist/ui/keyboardGuard.d.ts +55 -0
  89. package/dist/ui/keyboardGuard.js +65 -0
  90. package/dist/ui/loopPrompt.d.ts +17 -0
  91. package/dist/ui/loopPrompt.js +59 -0
  92. package/dist/ui/permissionPrompt.js +84 -2
  93. package/dist/ui/prompt.d.ts +35 -0
  94. package/dist/ui/prompt.js +345 -125
  95. package/dist/ui/reasoningStream.d.ts +8 -0
  96. package/dist/ui/reasoningStream.js +99 -7
  97. package/extension/README.md +95 -74
  98. package/extension/background.js +598 -178
  99. package/extension/content.js +92 -9
  100. package/extension/manifest.json +2 -1
  101. package/extension/popup.js +28 -12
  102. package/package.json +1 -1
@@ -4,23 +4,61 @@ export class UndoManager {
4
4
  checkpoints = [];
5
5
  currentTurnSnapshots = new Map();
6
6
  /**
7
- * Captures the state of a file before modifying or deleting it
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
- if (this.currentTurnSnapshots.has(resolved))
12
- return; // Already captured for this turn
13
- if (fs.existsSync(resolved)) {
14
- try {
15
- const content = fs.readFileSync(resolved, "utf-8");
16
- this.currentTurnSnapshots.set(resolved, content);
17
- }
18
- catch {
19
- this.currentTurnSnapshots.set(resolved, null);
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
- else {
23
- this.currentTurnSnapshots.set(resolved, null); // New file to be created
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((content, filePath) => {
34
- snapshots.push({ filePath, previousContent: content });
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
- * Undoes the last turn of modifications
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.previousContent === null) {
64
- // File was newly created during the turn, delete it
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) ${path.basename(snap.filePath)}`);
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
- // File was modified or deleted, restore original content
72
- fs.writeFileSync(snap.filePath, snap.previousContent, "utf-8");
73
- restoredFiles.push(`(Restaurado) ${path.basename(snap.filePath)}`);
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
- restoredFiles.push(`(Error restaurando) ${path.basename(snap.filePath)}: ${err?.message}`);
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: "${checkpoint.description}" (${checkpoint.timestamp})`,
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
+ }
@@ -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
  /**
@@ -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;
@@ -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 status = err?.status ?? err?.response?.status;
95
+ const tipo = clasificarErrorDelMotor(err);
74
96
  const mensaje = String(err?.error?.message || err?.message || "");
75
- if (status === 402) {
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 (status === 401 || status === 403) {
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.");
@@ -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>;