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.
Files changed (122) hide show
  1. package/README.md +155 -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 +642 -91
  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/salidaDelAgente.d.ts +43 -0
  19. package/dist/agent/salidaDelAgente.js +53 -0
  20. package/dist/agent/subagent.js +62 -15
  21. package/dist/agent/syntaxValidator.js +288 -52
  22. package/dist/agent/toolGate.d.ts +58 -0
  23. package/dist/agent/toolGate.js +111 -0
  24. package/dist/agent/tracker.js +4 -7
  25. package/dist/agent/undoManager.d.ts +44 -11
  26. package/dist/agent/undoManager.js +95 -24
  27. package/dist/agent/usageMeter.d.ts +56 -0
  28. package/dist/agent/usageMeter.js +51 -0
  29. package/dist/agent/verifier.d.ts +49 -0
  30. package/dist/agent/verifier.js +158 -0
  31. package/dist/config/constants.js +14 -0
  32. package/dist/config/engine.d.ts +19 -0
  33. package/dist/config/engine.js +35 -3
  34. package/dist/config/license.d.ts +5 -0
  35. package/dist/config/license.js +107 -9
  36. package/dist/config/limits.d.ts +28 -0
  37. package/dist/config/limits.js +51 -0
  38. package/dist/config/nodeVersion.d.ts +13 -0
  39. package/dist/config/nodeVersion.js +26 -0
  40. package/dist/config/permissions.d.ts +76 -1
  41. package/dist/config/permissions.js +379 -23
  42. package/dist/config/quota.js +24 -1
  43. package/dist/config/updater.d.ts +68 -0
  44. package/dist/config/updater.js +215 -0
  45. package/dist/hooks/manager.js +159 -14
  46. package/dist/index.js +240 -37
  47. package/dist/mcp/client.d.ts +50 -0
  48. package/dist/mcp/client.js +155 -19
  49. package/dist/mcp/ide.d.ts +76 -0
  50. package/dist/mcp/ide.js +160 -0
  51. package/dist/mcp/manager.js +9 -0
  52. package/dist/prompts/systemPrompt.js +12 -1
  53. package/dist/sessions/manager.d.ts +12 -0
  54. package/dist/sessions/manager.js +33 -5
  55. package/dist/sessions/resume.d.ts +36 -0
  56. package/dist/sessions/resume.js +43 -0
  57. package/dist/skills/manager.d.ts +1 -0
  58. package/dist/skills/manager.js +9 -0
  59. package/dist/tools/binary.d.ts +46 -0
  60. package/dist/tools/binary.js +100 -0
  61. package/dist/tools/browserCdp.js +13 -1
  62. package/dist/tools/browserExtension.d.ts +5 -0
  63. package/dist/tools/browserExtension.js +237 -22
  64. package/dist/tools/browserSession.d.ts +23 -0
  65. package/dist/tools/browserSession.js +28 -0
  66. package/dist/tools/computerUse.js +33 -1
  67. package/dist/tools/createImage.d.ts +11 -0
  68. package/dist/tools/createImage.js +13 -1
  69. package/dist/tools/definitions.js +17 -4
  70. package/dist/tools/editDiagnosis.d.ts +54 -0
  71. package/dist/tools/editDiagnosis.js +142 -0
  72. package/dist/tools/editFile.d.ts +1 -0
  73. package/dist/tools/editFile.js +118 -62
  74. package/dist/tools/findFiles.d.ts +21 -0
  75. package/dist/tools/findFiles.js +75 -6
  76. package/dist/tools/getSystemInfo.js +15 -7
  77. package/dist/tools/gitTools.d.ts +24 -0
  78. package/dist/tools/gitTools.js +87 -26
  79. package/dist/tools/grepSearch.js +111 -23
  80. package/dist/tools/moveCopyFile.d.ts +18 -1
  81. package/dist/tools/moveCopyFile.js +74 -4
  82. package/dist/tools/patchCascade.d.ts +57 -0
  83. package/dist/tools/patchCascade.js +338 -0
  84. package/dist/tools/runCommand.js +213 -25
  85. package/dist/tools/runner.js +58 -5
  86. package/dist/tools/toolDefsComputer.js +12 -2
  87. package/dist/tools/truncator.d.ts +27 -1
  88. package/dist/tools/truncator.js +117 -11
  89. package/dist/tools/viewFile.js +15 -11
  90. package/dist/tools/visionBridge.js +7 -0
  91. package/dist/tools/webSearch.d.ts +21 -0
  92. package/dist/tools/webSearch.js +125 -14
  93. package/dist/tools/writeFile.d.ts +16 -1
  94. package/dist/tools/writeFile.js +40 -2
  95. package/dist/ui/interrupt.d.ts +4 -1
  96. package/dist/ui/interrupt.js +59 -7
  97. package/dist/ui/keyboardGuard.d.ts +76 -0
  98. package/dist/ui/keyboardGuard.js +94 -0
  99. package/dist/ui/loopPrompt.d.ts +17 -0
  100. package/dist/ui/loopPrompt.js +59 -0
  101. package/dist/ui/pegados.d.ts +50 -0
  102. package/dist/ui/pegados.js +83 -0
  103. package/dist/ui/permissionPrompt.js +84 -2
  104. package/dist/ui/prompt.d.ts +40 -0
  105. package/dist/ui/prompt.js +436 -131
  106. package/dist/ui/reasoningStream.d.ts +8 -0
  107. package/dist/ui/reasoningStream.js +99 -7
  108. package/dist/ui/salida.d.ts +10 -0
  109. package/dist/ui/salida.js +50 -0
  110. package/extension/README.md +95 -74
  111. package/extension/background.js +772 -181
  112. package/extension/content.js +527 -46
  113. package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
  114. package/extension/iconos/128.png +0 -0
  115. package/extension/iconos/16.png +0 -0
  116. package/extension/iconos/32.png +0 -0
  117. package/extension/iconos/48.png +0 -0
  118. package/extension/iconos/logotipo.png +0 -0
  119. package/extension/manifest.json +28 -4
  120. package/extension/popup.html +16 -6
  121. package/extension/popup.js +28 -12
  122. package/package.json +3 -2
@@ -0,0 +1,560 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ /** Palabras que no son referencias a nada aunque lo parezcan. */
5
+ const RESERVADAS = new Set([
6
+ "if", "else", "for", "while", "return", "const", "let", "var", "function", "class", "new",
7
+ "this", "typeof", "instanceof", "import", "export", "from", "default", "async", "await",
8
+ "try", "catch", "finally", "throw", "switch", "case", "break", "continue", "do", "in", "of",
9
+ "true", "false", "null", "undefined", "void", "delete", "yield", "static", "public", "private",
10
+ "protected", "readonly", "extends", "implements", "interface", "type", "enum", "namespace",
11
+ "def", "self", "None", "True", "False", "elif", "pass", "lambda", "with", "as", "not", "and",
12
+ "or", "is", "raise", "except", "global", "nonlocal", "assert", "del", "print", "func", "package",
13
+ "struct", "trait", "impl", "fn", "let", "mut", "pub", "use", "mod", "match", "string", "int",
14
+ "bool", "float", "number", "boolean", "any", "unknown", "never", "object", "Promise", "Array",
15
+ ]);
16
+ /**
17
+ * Patrones de definicion por lenguaje.
18
+ *
19
+ * Se prefiere perder alguna definicion a inventarsela: un simbolo fantasma en
20
+ * el mapa manda al agente a buscar algo que no existe, que es peor que no
21
+ * mencionarlo.
22
+ */
23
+ const DEFINICIONES = {
24
+ js: [
25
+ /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/,
26
+ /^\s*(?:export\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/,
27
+ /^\s*(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/,
28
+ /^\s*(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\s*[=<]/,
29
+ /^\s*(?:export\s+)?(?:declare\s+)?enum\s+([A-Za-z_$][\w$]*)/,
30
+ /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/,
31
+ /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s+)?function/,
32
+ // Metodo de clase: dos espacios o mas de sangria y parentesis detras.
33
+ /^\s{2,}(?:(?:public|private|protected|static|async|readonly|get|set)\s+)*([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*[:{]/,
34
+ ],
35
+ py: [
36
+ /^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)/,
37
+ /^\s*class\s+([A-Za-z_][\w]*)/,
38
+ /^([A-Z][A-Z0-9_]*)\s*=/, // constantes de modulo
39
+ ],
40
+ go: [
41
+ /^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*)/,
42
+ /^\s*type\s+([A-Za-z_][\w]*)/,
43
+ ],
44
+ rs: [
45
+ /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)/,
46
+ /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum|trait|type)\s+([A-Za-z_][\w]*)/,
47
+ ],
48
+ };
49
+ const EXTENSIONES = {
50
+ ".ts": "js", ".tsx": "js", ".js": "js", ".jsx": "js", ".mjs": "js", ".cjs": "js",
51
+ ".mts": "js", ".cts": "js",
52
+ ".py": "py", ".pyi": "py",
53
+ ".go": "go",
54
+ ".rs": "rs",
55
+ };
56
+ export function lenguajeDe(ruta) {
57
+ return EXTENSIONES[path.extname(ruta).toLowerCase()] ?? null;
58
+ }
59
+ export function extraerSimbolos(ruta, contenido) {
60
+ const lenguaje = lenguajeDe(ruta);
61
+ if (!lenguaje)
62
+ return { definiciones: [], referencias: new Map() };
63
+ const definiciones = [];
64
+ const definidosAqui = new Set();
65
+ const lineas = contenido.split("\n");
66
+ for (let i = 0; i < lineas.length; i++) {
67
+ const linea = lineas[i];
68
+ if (linea.length > 500)
69
+ continue; // linea minificada: no dice nada
70
+ for (const patron of DEFINICIONES[lenguaje]) {
71
+ const m = patron.exec(linea);
72
+ if (m && m[1] && !RESERVADAS.has(m[1]) && m[1] !== "constructor") {
73
+ if (!definidosAqui.has(m[1])) {
74
+ definidosAqui.add(m[1]);
75
+ definiciones.push({ archivo: ruta, linea: i + 1, nombre: m[1], clase: "def" });
76
+ }
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ const referencias = new Map();
82
+ for (const m of contenido.matchAll(/[A-Za-z_$][\w$]*/g)) {
83
+ const nombre = m[0];
84
+ if (nombre.length < 3)
85
+ continue;
86
+ if (RESERVADAS.has(nombre))
87
+ continue;
88
+ if (definidosAqui.has(nombre))
89
+ continue; // usarse a si mismo no es un voto
90
+ // `algo.push(...)` no es una referencia al simbolo `push` de nadie: es un
91
+ // acceso a propiedad. Tree-sitter lo distingue gratis; con expresiones
92
+ // regulares hace falta mirar el caracter de antes.
93
+ //
94
+ // Sin esto el mapa entero se descuadra, y se comprobo ejecutandolo:
95
+ // `push`, `flush`, `get` o `stop` aparecen en TODOS los archivos del
96
+ // proyecto, asi que el modulo que resulte definir un metodo con ese nombre
97
+ // recibe un voto de cada archivo del repositorio. `src/ui/reasoningStream.ts`
98
+ // salia el primero del ranking, por delante del bucle del agente, por
99
+ // definir `push` y `flush`.
100
+ const antes = contenido.slice(Math.max(0, m.index - 40), m.index).replace(/\s+$/, "");
101
+ if (antes.endsWith(".") && !antes.endsWith(".."))
102
+ continue;
103
+ referencias.set(nombre, (referencias.get(nombre) ?? 0) + 1);
104
+ }
105
+ return { definiciones, referencias };
106
+ }
107
+ /**
108
+ * Un archivo de pruebas es parte del repositorio, pero no es por donde se
109
+ * empieza a entender un proyecto: nombra a todo el mundo (por eso puntua alto
110
+ * en el grafo) y no lo llama nadie. Se deja en el mapa, pero al final.
111
+ */
112
+ export function esArchivoDePruebas(ruta) {
113
+ const normalizado = ruta.replace(/\\/g, "/").toLowerCase();
114
+ return (/(^|\/)(tests?|__tests__|spec)\//.test(normalizado) ||
115
+ /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalizado) ||
116
+ /(^|\/)test_[^/]*\.py$/.test(normalizado));
117
+ }
118
+ /**
119
+ * Que archivos e identificadores esta nombrando el usuario.
120
+ *
121
+ * Las tres reglas son de Aider y las tres importan:
122
+ * - la ruta completa cuenta siempre, normalizando "\" a "/" (en Windows es la
123
+ * diferencia entre funcionar y no funcionar);
124
+ * - el nombre suelto ("loop.ts") solo cuenta si lleva un separador o un punto
125
+ * y si es UNICO en el proyecto, para no confundir "run" o "make";
126
+ * - un identificador de 5 letras o mas que coincide con el nombre de un
127
+ * archivo lo trae al mapa: escribir "arregla el fileTracker" deberia sacar
128
+ * src/agent/fileTracker.ts sin que nadie escriba la ruta.
129
+ */
130
+ export function extraerMenciones(texto, archivos) {
131
+ const archivosMencionados = new Set();
132
+ const identificadores = new Set();
133
+ const palabras = new Set(texto
134
+ .split(/\s+/)
135
+ .map((p) => p.replace(/[,.!;:?]+$/, "").replace(/^["'`*_]+|["'`*_]+$/g, ""))
136
+ .map((p) => p.replace(/\\/g, "/"))
137
+ .filter(Boolean));
138
+ for (const m of texto.matchAll(/[A-Za-z_$][\w$]*/g)) {
139
+ if (!RESERVADAS.has(m[0]))
140
+ identificadores.add(m[0]);
141
+ }
142
+ // Cuantas veces aparece cada nombre base: si se repite, no se puede resolver.
143
+ const porNombre = new Map();
144
+ for (const a of archivos) {
145
+ const base = path.basename(a);
146
+ const lista = porNombre.get(base);
147
+ if (lista)
148
+ lista.push(a);
149
+ else
150
+ porNombre.set(base, [a]);
151
+ }
152
+ for (const a of archivos) {
153
+ const normalizado = a.replace(/\\/g, "/");
154
+ if (palabras.has(normalizado)) {
155
+ archivosMencionados.add(a);
156
+ continue;
157
+ }
158
+ const base = path.basename(a);
159
+ if (palabras.has(base) &&
160
+ /[/\\._-]/.test(base) &&
161
+ (porNombre.get(base)?.length ?? 0) === 1) {
162
+ archivosMencionados.add(a);
163
+ }
164
+ }
165
+ // Puente identificador -> archivo, por el nombre sin extension.
166
+ for (const ident of identificadores) {
167
+ if (ident.length < 5)
168
+ continue;
169
+ for (const a of archivos) {
170
+ const raiz = path.basename(a, path.extname(a));
171
+ if (raiz.length >= 5 && raiz.toLowerCase() === ident.toLowerCase()) {
172
+ archivosMencionados.add(a);
173
+ }
174
+ }
175
+ }
176
+ return { archivos: archivosMencionados, identificadores };
177
+ }
178
+ // ------------------------------------------------------------------ PageRank
179
+ /**
180
+ * PageRank por iteracion de potencia. No hace falta una libreria de grafos:
181
+ * son veinte lineas y evita una dependencia nativa mas.
182
+ */
183
+ export function pageRank(nodos, aristas, personalizacion, amortiguacion = 0.85, vueltas = 30) {
184
+ const n = nodos.length;
185
+ if (n === 0)
186
+ return new Map();
187
+ let rango = new Map(nodos.map((x) => [x, 1 / n]));
188
+ // El vector de personalizacion es a donde salta el navegante aleatorio: los
189
+ // archivos que el usuario ha nombrado, en vez de uno cualquiera.
190
+ const totalPers = [...personalizacion.values()].reduce((a, b) => a + b, 0);
191
+ const salto = new Map(nodos.map((x) => [x, totalPers > 0 ? (personalizacion.get(x) ?? 0) / totalPers : 1 / n]));
192
+ const salientes = new Map();
193
+ for (const [origen, lista] of aristas) {
194
+ salientes.set(origen, lista.reduce((a, e) => a + e.peso, 0));
195
+ }
196
+ for (let v = 0; v < vueltas; v++) {
197
+ const siguiente = new Map(nodos.map((x) => [x, 0]));
198
+ let colgante = 0;
199
+ for (const nodo of nodos) {
200
+ const r = rango.get(nodo) ?? 0;
201
+ const lista = aristas.get(nodo);
202
+ const total = salientes.get(nodo) ?? 0;
203
+ if (!lista || total === 0) {
204
+ colgante += r; // sin salidas: su rango se reparte por el salto
205
+ continue;
206
+ }
207
+ for (const { destino, peso } of lista) {
208
+ siguiente.set(destino, (siguiente.get(destino) ?? 0) + (r * peso) / total);
209
+ }
210
+ }
211
+ for (const nodo of nodos) {
212
+ const base = (salto.get(nodo) ?? 0) * (1 - amortiguacion + amortiguacion * colgante);
213
+ siguiente.set(nodo, base + amortiguacion * (siguiente.get(nodo) ?? 0));
214
+ }
215
+ rango = siguiente;
216
+ }
217
+ return rango;
218
+ }
219
+ /** Archivos que van siempre delante, aunque nadie los referencie. */
220
+ const IMPORTANTES = [
221
+ "README.md", "package.json", "tsconfig.json", "Cargo.toml", "go.mod", "pyproject.toml",
222
+ "requirements.txt", "Dockerfile", "CHOCOLATITO.md", "CLAUDE.md",
223
+ ];
224
+ /**
225
+ * Lista de archivos del proyecto. Con git es instantaneo y respeta .gitignore
226
+ * gratis; sin git se recorre a mano evitando lo de siempre.
227
+ */
228
+ /**
229
+ * La lista de archivos se pide en cada turno (para las menciones) y en cada
230
+ * mapa. Recorrer el repositorio entero para eso es tiempo regalado, pero
231
+ * guardarla para siempre haria invisible cualquier archivo creado a mitad de
232
+ * sesion. Treinta segundos es el punto medio: ningun turno la recalcula dos
233
+ * veces, y un archivo nuevo aparece en el siguiente.
234
+ */
235
+ const cacheDeListados = new Map();
236
+ const VIDA_DEL_LISTADO_MS = 30_000;
237
+ export function listarArchivosCacheado(cwd, maxArchivos = 3_000) {
238
+ const clave = path.resolve(cwd);
239
+ const guardado = cacheDeListados.get(clave);
240
+ if (guardado && guardado.caduca > Date.now())
241
+ return guardado.lista;
242
+ const lista = listarArchivos(clave, maxArchivos);
243
+ cacheDeListados.set(clave, { lista, caduca: Date.now() + VIDA_DEL_LISTADO_MS });
244
+ return lista;
245
+ }
246
+ /**
247
+ * Indice simbolo -> archivos que lo definen, cacheado como el listado.
248
+ *
249
+ * Sirve para lo que la gente hace de verdad: escribir "arregla
250
+ * calcularImpuesto" en vez de la ruta. El puente por nombre de archivo de
251
+ * extraerMenciones no llega ahi, porque el simbolo casi nunca se llama igual
252
+ * que su archivo.
253
+ */
254
+ const cacheDeIndices = new Map();
255
+ export function indiceDeDefiniciones(cwd) {
256
+ const clave = path.resolve(cwd);
257
+ const guardado = cacheDeIndices.get(clave);
258
+ if (guardado && guardado.caduca > Date.now())
259
+ return guardado.indice;
260
+ const indice = new Map();
261
+ for (const rel of listarArchivosCacheado(clave)) {
262
+ if (!lenguajeDe(rel))
263
+ continue;
264
+ let contenido;
265
+ try {
266
+ const abs = path.join(clave, rel);
267
+ if (fs.statSync(abs).size > 400_000)
268
+ continue;
269
+ contenido = fs.readFileSync(abs, "utf-8");
270
+ }
271
+ catch {
272
+ continue;
273
+ }
274
+ for (const d of extraerSimbolos(rel, contenido).definiciones) {
275
+ const lista = indice.get(d.nombre);
276
+ if (lista)
277
+ lista.push(rel);
278
+ else
279
+ indice.set(d.nombre, [rel]);
280
+ }
281
+ }
282
+ cacheDeIndices.set(clave, { indice, caduca: Date.now() + VIDA_DEL_LISTADO_MS });
283
+ return indice;
284
+ }
285
+ export function listarArchivos(cwd, maxArchivos = 3_000) {
286
+ try {
287
+ const salida = execFileSync("git", ["ls-files"], {
288
+ cwd,
289
+ encoding: "utf-8",
290
+ maxBuffer: 20 * 1024 * 1024,
291
+ stdio: ["ignore", "pipe", "ignore"],
292
+ });
293
+ const lista = salida.split("\n").map((l) => l.trim()).filter(Boolean);
294
+ if (lista.length > 0)
295
+ return lista.slice(0, maxArchivos);
296
+ }
297
+ catch { }
298
+ const IGNORAR = new Set(["node_modules", ".git", "dist", "build", "out", "target", "__pycache__", ".venv", "venv", "coverage"]);
299
+ const encontrados = [];
300
+ const recorrer = (dir, prefijo) => {
301
+ if (encontrados.length >= maxArchivos)
302
+ return;
303
+ let entradas;
304
+ try {
305
+ entradas = fs.readdirSync(dir, { withFileTypes: true });
306
+ }
307
+ catch {
308
+ return;
309
+ }
310
+ for (const e of entradas) {
311
+ if (encontrados.length >= maxArchivos)
312
+ return;
313
+ if (e.name.startsWith(".") && e.name !== ".github")
314
+ continue;
315
+ const rel = prefijo ? `${prefijo}/${e.name}` : e.name;
316
+ if (e.isDirectory()) {
317
+ if (IGNORAR.has(e.name))
318
+ continue;
319
+ recorrer(path.join(dir, e.name), rel);
320
+ }
321
+ else {
322
+ encontrados.push(rel);
323
+ }
324
+ }
325
+ };
326
+ recorrer(cwd, "");
327
+ return encontrados;
328
+ }
329
+ /**
330
+ * Construye el mapa. Devuelve el texto listo para inyectar, o "" si no hay
331
+ * nada que ensenar (proyecto sin codigo reconocible, o presupuesto cero).
332
+ */
333
+ export function construirMapa(opciones) {
334
+ const { cwd, peticion = "", abiertos = [], presupuesto = 4_000 } = opciones;
335
+ if (presupuesto <= 0)
336
+ return "";
337
+ const todos = listarArchivos(cwd, opciones.maxArchivos ?? 3_000);
338
+ const menciones = extraerMenciones(peticion, todos);
339
+ // Los tests salen del grafo, no solo del ranking final.
340
+ //
341
+ // Se comprobo ejecutandolo: dejandolos dentro, `tests/input.test.mjs` era el
342
+ // archivo con mas rango de todo el proyecto. Un test define ayudantes con
343
+ // nombres corrientes (`read`, `tecla`, `dist`) y nombra a media base de
344
+ // codigo, asi que acumula votos por los dos lados sin ser una dependencia de
345
+ // nada. Si el usuario los nombra, vuelven a entrar.
346
+ const pidenTests = /\b(test|tests|prueba|pruebas|spec)\b/i.test(peticion) ||
347
+ [...menciones.archivos].some(esArchivoDePruebas);
348
+ const conCodigo = todos.filter((a) => lenguajeDe(a) !== null && (pidenTests || !esArchivoDePruebas(a)));
349
+ if (conCodigo.length === 0)
350
+ return "";
351
+ // Un archivo por encima de 400 KB no es codigo que nadie vaya a leer.
352
+ const defines = new Map(); // ident -> archivos que lo definen
353
+ const referencias = new Map(); // ident -> archivo -> veces
354
+ const definicionesPorArchivo = new Map();
355
+ for (const rel of conCodigo) {
356
+ const abs = path.join(cwd, rel);
357
+ let contenido;
358
+ try {
359
+ if (fs.statSync(abs).size > 400_000)
360
+ continue;
361
+ contenido = fs.readFileSync(abs, "utf-8");
362
+ }
363
+ catch {
364
+ continue;
365
+ }
366
+ const simbolos = extraerSimbolos(rel, contenido);
367
+ if (simbolos.definiciones.length > 0)
368
+ definicionesPorArchivo.set(rel, simbolos.definiciones);
369
+ for (const d of simbolos.definiciones) {
370
+ let quienes = defines.get(d.nombre);
371
+ if (!quienes)
372
+ defines.set(d.nombre, (quienes = new Set()));
373
+ quienes.add(rel);
374
+ }
375
+ for (const [nombre, veces] of simbolos.referencias) {
376
+ let porArchivo = referencias.get(nombre);
377
+ if (!porArchivo)
378
+ referencias.set(nombre, (porArchivo = new Map()));
379
+ porArchivo.set(rel, (porArchivo.get(rel) ?? 0) + veces);
380
+ }
381
+ }
382
+ // ---- grafo
383
+ const aristas = new Map();
384
+ /** destino -> identificador -> peso total que le llega. */
385
+ const votos = new Map();
386
+ const anadir = (origen, destino, peso, ident) => {
387
+ let lista = aristas.get(origen);
388
+ if (!lista)
389
+ aristas.set(origen, (lista = []));
390
+ lista.push({ destino, peso });
391
+ let porDef = votos.get(destino);
392
+ if (!porDef)
393
+ votos.set(destino, (porDef = new Map()));
394
+ porDef.set(ident, (porDef.get(ident) ?? 0) + peso);
395
+ };
396
+ const abiertosSet = new Set(abiertos.map((a) => a.replace(/\\/g, "/")));
397
+ for (const [ident, definidores] of defines) {
398
+ const usos = referencias.get(ident);
399
+ if (!usos)
400
+ continue;
401
+ for (const [usuario, veces] of usos) {
402
+ for (const definidor of definidores) {
403
+ if (definidor === usuario)
404
+ continue;
405
+ let mul = 1;
406
+ if (menciones.identificadores.has(ident))
407
+ mul *= 10; // lo nombro el usuario
408
+ if (/[_-]/.test(ident) || (/[a-z][A-Z]/.test(ident) && ident.length >= 8))
409
+ mul *= 10;
410
+ if (ident.startsWith("_"))
411
+ mul *= 0.1;
412
+ if (definidores.size > 5)
413
+ mul *= 0.1; // nombre demasiado comun
414
+ if (abiertosSet.has(usuario.replace(/\\/g, "/")))
415
+ mul *= 50;
416
+ // La raiz cuadrada evita que un identificador citado 200 veces aplaste
417
+ // a todos los demas.
418
+ anadir(usuario, definidor, mul * Math.sqrt(veces), ident);
419
+ }
420
+ }
421
+ }
422
+ const nodos = [...new Set([...definicionesPorArchivo.keys(), ...aristas.keys()])];
423
+ if (nodos.length === 0)
424
+ return "";
425
+ const personalizacion = new Map();
426
+ const enElGrafo = new Set(nodos);
427
+ for (const a of menciones.archivos)
428
+ if (enElGrafo.has(a))
429
+ personalizacion.set(a, 100 / nodos.length);
430
+ for (const a of abiertosSet)
431
+ if (enElGrafo.has(a))
432
+ personalizacion.set(a, 100 / nodos.length);
433
+ // Si el usuario nombra un simbolo, sube el archivo que lo DEFINE.
434
+ //
435
+ // El multiplicador x10 de las aristas solo funciona cuando alguien referencia
436
+ // ese simbolo: para una funcion nueva, o una que solo se usa en un sitio, no
437
+ // hay aristas que multiplicar y la mencion no servia de nada. Es justo el
438
+ // caso mas comun de una peticion ("cambia generarInforme").
439
+ for (const ident of menciones.identificadores) {
440
+ const definidores = defines.get(ident);
441
+ if (!definidores || definidores.size > 3)
442
+ continue; // un nombre comun no senala nada
443
+ for (const archivo of definidores) {
444
+ if (enElGrafo.has(archivo))
445
+ personalizacion.set(archivo, 100 / nodos.length);
446
+ }
447
+ }
448
+ // Sin pistas del usuario, PageRank contesta a "de quien depende todo el
449
+ // mundo", que son los cimientos compartidos. Es una respuesta util, pero deja
450
+ // fuera el punto de entrada y el orquestador, que no los llama casi nadie y
451
+ // son por donde se empieza a leer un proyecto. Se les da un empujon del
452
+ // tamano de una mencion.
453
+ if (personalizacion.size === 0) {
454
+ for (const nodo of nodos) {
455
+ const base = path.basename(nodo);
456
+ const enRaizDeFuentes = /^(src\/)?[^/]+$/.test(nodo.replace(/\\/g, "/"));
457
+ if (enRaizDeFuentes && /^(index|main|cli|app)\.[cm]?[jt]sx?$/.test(base)) {
458
+ personalizacion.set(nodo, 100 / nodos.length);
459
+ }
460
+ }
461
+ }
462
+ const rango = pageRank(nodos, aristas, personalizacion);
463
+ // ---- repartir el rango del archivo entre sus definiciones
464
+ const definiciones = [];
465
+ for (const [archivo, defs] of definicionesPorArchivo) {
466
+ const rangoArchivo = rango.get(archivo) ?? 0;
467
+ const recibidos = votos.get(archivo);
468
+ const totalVotos = recibidos ? [...recibidos.values()].reduce((a, b) => a + b, 0) : 0;
469
+ // Los tests nombran a todo el mundo y no los llama nadie: sin esto copan
470
+ // la cabeza del mapa y tapan el codigo que se quiere entender.
471
+ const castigoPruebas = esArchivoDePruebas(archivo) ? 0.05 : 1;
472
+ for (const d of defs) {
473
+ const voto = recibidos?.get(d.nombre) ?? 0;
474
+ // Una definicion que no usa nadie no vale cero: vale poco. Si valiera
475
+ // cero, un archivo nuevo no apareceria nunca en el mapa.
476
+ const parte = totalVotos > 0 ? voto / totalVotos : 0;
477
+ // Un nombre que se define en medio proyecto (push, flush, run, handler)
478
+ // no identifica nada: aparece arriba solo porque lo referencia todo el
479
+ // mundo, y es justo el que menos ayuda a orientarse.
480
+ const cuantosLoDefinen = defines.get(d.nombre)?.size ?? 1;
481
+ const castigoGenerico = cuantosLoDefinen > 3 ? 0.1 : cuantosLoDefinen > 1 ? 0.5 : 1;
482
+ // El usuario lo ha escrito con todas las letras. El x10 de las aristas no
483
+ // llega aqui: solo multiplica votos, y una funcion que no usa nadie
484
+ // todavia no tiene ninguno. Sin esto, pedir "cambia generarInforme" no
485
+ // subia generarInforme, que es lo unico que se pidio.
486
+ const empujonMencion = menciones.identificadores.has(d.nombre) ? 10 : 1;
487
+ definiciones.push({
488
+ archivo,
489
+ linea: d.linea,
490
+ nombre: d.nombre,
491
+ rango: rangoArchivo * (0.1 + parte) * castigoGenerico * castigoPruebas * empujonMencion,
492
+ });
493
+ }
494
+ }
495
+ definiciones.sort((a, b) => b.rango - a.rango);
496
+ if (definiciones.length === 0)
497
+ return "";
498
+ // ---- presupuesto por biseccion
499
+ const importantes = IMPORTANTES.filter((f) => todos.includes(f));
500
+ const render = (cuantas) => renderizar(definiciones.slice(0, cuantas), importantes);
501
+ const tokens = (texto) => Math.ceil(texto.length / 4);
502
+ let bajo = 1;
503
+ let alto = definiciones.length;
504
+ let medio = Math.min(Math.floor(presupuesto / 25), definiciones.length) || 1;
505
+ let mejor = "";
506
+ while (bajo <= alto) {
507
+ const texto = render(medio);
508
+ const t = tokens(texto);
509
+ const error = Math.abs(t - presupuesto) / presupuesto;
510
+ if (t <= presupuesto && texto.length > mejor.length)
511
+ mejor = texto;
512
+ if (error < 0.15 && t <= presupuesto)
513
+ break;
514
+ if (t < presupuesto)
515
+ bajo = medio + 1;
516
+ else
517
+ alto = medio - 1;
518
+ medio = Math.floor((bajo + alto) / 2);
519
+ if (medio < 1)
520
+ break;
521
+ }
522
+ return mejor || render(1);
523
+ }
524
+ function renderizar(definiciones, importantes) {
525
+ const porArchivo = new Map();
526
+ for (const d of definiciones) {
527
+ let lista = porArchivo.get(d.archivo);
528
+ if (!lista)
529
+ porArchivo.set(d.archivo, (lista = []));
530
+ lista.push(d);
531
+ }
532
+ const lineas = [];
533
+ if (importantes.length > 0) {
534
+ lineas.push(`Archivos clave: ${importantes.join(", ")}`);
535
+ lineas.push("");
536
+ }
537
+ for (const [archivo, defs] of porArchivo) {
538
+ lineas.push(`${archivo}:`);
539
+ const ordenadas = [...defs].sort((a, b) => a.linea - b.linea);
540
+ for (const d of ordenadas) {
541
+ lineas.push(` ${d.nombre} (L${d.linea})`);
542
+ }
543
+ lineas.push("");
544
+ }
545
+ return lineas.join("\n").trimEnd();
546
+ }
547
+ /**
548
+ * El bloque tal y como se le entrega al modelo.
549
+ *
550
+ * El encabezado no es decorativo: sin el, el modelo trata el mapa como codigo
551
+ * que acaba de leer y se pone a editar simbolos que solo ha visto de nombre.
552
+ */
553
+ export function bloqueDeMapa(mapa) {
554
+ if (!mapa)
555
+ return "";
556
+ return (`[MAPA DEL REPOSITORIO]\n` +
557
+ `Indice de los simbolos mas relevantes del proyecto, ordenado por cuanto los usa el resto ` +
558
+ `del codigo. Son NOMBRES Y LINEAS, no el contenido: sirve para saber que existe y donde ` +
559
+ `mirar. Antes de editar cualquiera de estos archivos, leelo con view_file.\n\n${mapa}`);
560
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * COMO TERMINA UNA TAREA Y CON QUE CODIGO SE SALE
3
+ *
4
+ * El bucle sabia perfectamente por que paraba —cuota agotada, licencia caida,
5
+ * Esc, limite de iteraciones— pero no se lo contaba a nadie: runTask devolvia
6
+ * void y lo imprimia por pantalla. El modo no interactivo hacia process.exit(0)
7
+ * detras, asi que en CI un 401 o una cuota agotada se reportaban como exito y
8
+ * el pipeline seguia adelante creyendo que el agente habia hecho el trabajo.
9
+ *
10
+ * Aqui vive el vocabulario compartido: por que paro y que numero le corresponde.
11
+ * Va en su propio archivo para que index.ts pueda traducir motivo -> codigo sin
12
+ * arrastrar el bucle entero, y para que la tabla del README tenga una sola
13
+ * fuente de verdad.
14
+ */
15
+ export type MotivoParada =
16
+ /** El modelo cerro su turno con una respuesta. Lo unico que es exito. */
17
+ "completado"
18
+ /** No habia margen de cuota (contador local) o el motor devolvio 402. */
19
+ | "cuota"
20
+ /** 401/403: licencia no valida para el motor, o clave de API mala. */
21
+ | "licencia"
22
+ /**
23
+ * 429 con los huecos de peticiones simultaneas ocupados.
24
+ *
25
+ * No es "error-motor": el motor esta bien y la licencia tambien. Es la unica
26
+ * parada que se arregla esperando, asi que merece codigo propio: un script
27
+ * puede dormir y reintentar en vez de dar la tarea por fallida.
28
+ */
29
+ | "ocupado"
30
+ /** El usuario pulso Esc a mitad del turno. */
31
+ | "interrumpido"
32
+ /** El motor fallo de verdad y los reintentos no lo salvaron. */
33
+ | "error-motor"
34
+ /** Se acabaron las iteraciones sin que el modelo cerrara. */
35
+ | "limite-iteraciones";
36
+ export interface ResultadoTarea {
37
+ /** Solo true si el turno acabo por su propio pie. */
38
+ exito: boolean;
39
+ motivo: MotivoParada;
40
+ /** Texto ya legible del corte, si lo hay. Es lo que se le enseña al usuario. */
41
+ detalle?: string;
42
+ /** Iteraciones consumidas del bucle de herramientas. */
43
+ turnos: number;
44
+ tokensEntrada: number;
45
+ tokensSalida: number;
46
+ }
47
+ /**
48
+ * CODIGOS DE SALIDA
49
+ *
50
+ * 0/1/2 son la convencion de toda la vida (exito, fallo, uso incorrecto). Cuota
51
+ * y licencia llevan codigo propio porque son las dos unicas paradas que NO se
52
+ * arreglan reintentando: un pipeline que las distinga puede avisar a quien paga
53
+ * en vez de volver a lanzar el trabajo. El 130 es 128+SIGINT, lo que ya espera
54
+ * cualquier script cuando se interrumpe algo.
55
+ */
56
+ export declare const EXIT_OK = 0;
57
+ export declare const EXIT_FALLO = 1;
58
+ export declare const EXIT_USO = 2;
59
+ export declare const EXIT_CUOTA = 3;
60
+ export declare const EXIT_LICENCIA = 4;
61
+ /** Huecos simultaneos ocupados: lo unico que se arregla esperando y reintentando. */
62
+ export declare const EXIT_OCUPADO = 5;
63
+ export declare const EXIT_INTERRUMPIDO = 130;
64
+ export declare function codigoDeSalida(resultado: ResultadoTarea): number;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * COMO TERMINA UNA TAREA Y CON QUE CODIGO SE SALE
3
+ *
4
+ * El bucle sabia perfectamente por que paraba —cuota agotada, licencia caida,
5
+ * Esc, limite de iteraciones— pero no se lo contaba a nadie: runTask devolvia
6
+ * void y lo imprimia por pantalla. El modo no interactivo hacia process.exit(0)
7
+ * detras, asi que en CI un 401 o una cuota agotada se reportaban como exito y
8
+ * el pipeline seguia adelante creyendo que el agente habia hecho el trabajo.
9
+ *
10
+ * Aqui vive el vocabulario compartido: por que paro y que numero le corresponde.
11
+ * Va en su propio archivo para que index.ts pueda traducir motivo -> codigo sin
12
+ * arrastrar el bucle entero, y para que la tabla del README tenga una sola
13
+ * fuente de verdad.
14
+ */
15
+ /**
16
+ * CODIGOS DE SALIDA
17
+ *
18
+ * 0/1/2 son la convencion de toda la vida (exito, fallo, uso incorrecto). Cuota
19
+ * y licencia llevan codigo propio porque son las dos unicas paradas que NO se
20
+ * arreglan reintentando: un pipeline que las distinga puede avisar a quien paga
21
+ * en vez de volver a lanzar el trabajo. El 130 es 128+SIGINT, lo que ya espera
22
+ * cualquier script cuando se interrumpe algo.
23
+ */
24
+ export const EXIT_OK = 0;
25
+ export const EXIT_FALLO = 1;
26
+ export const EXIT_USO = 2;
27
+ export const EXIT_CUOTA = 3;
28
+ export const EXIT_LICENCIA = 4;
29
+ /** Huecos simultaneos ocupados: lo unico que se arregla esperando y reintentando. */
30
+ export const EXIT_OCUPADO = 5;
31
+ export const EXIT_INTERRUMPIDO = 130;
32
+ export function codigoDeSalida(resultado) {
33
+ switch (resultado.motivo) {
34
+ case "completado":
35
+ return EXIT_OK;
36
+ case "cuota":
37
+ return EXIT_CUOTA;
38
+ case "licencia":
39
+ return EXIT_LICENCIA;
40
+ case "ocupado":
41
+ return EXIT_OCUPADO;
42
+ case "interrumpido":
43
+ return EXIT_INTERRUMPIDO;
44
+ case "error-motor":
45
+ case "limite-iteraciones":
46
+ return EXIT_FALLO;
47
+ }
48
+ }