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
package/dist/index.js CHANGED
@@ -10,36 +10,44 @@ import { getEngineBaseUrl, DEFAULT_ESPECTRO, ESPECTRO_MODELS, RULES_FILE_NAME }
10
10
  import { getProjectContext } from "./agent/context.js";
11
11
  import { buildSystemPrompt } from "./prompts/systemPrompt.js";
12
12
  import { AgentLoop } from "./agent/loop.js";
13
+ import { codigoDeSalida, EXIT_OK, EXIT_FALLO, EXIT_USO, EXIT_LICENCIA } from "./agent/result.js";
13
14
  import { showBanner, animateStartupBanner, showHelp, showEffortMenu } from "./ui/banner.js";
14
15
  import { askInteractivePrompt, SLASH_COMMANDS } from "./ui/prompt.js";
15
16
  import { changeDir } from "./tools/changeDir.js";
16
17
  import { gitDiff, gitCommit } from "./tools/gitTools.js";
17
18
  import { renderUserPrompt } from "./ui/renderer.js";
18
- import { parseFileMentions, buildPromptWithMentions } from "./agent/mentions.js";
19
+ import { parseFileMentions, buildPromptWithMentions, notaDeMencionesImplicitas } from "./agent/mentions.js";
19
20
  import { SkillManager } from "./skills/manager.js";
20
21
  import { MemoryManager } from "./memory/manager.js";
21
22
  import { HookManager } from "./hooks/manager.js";
22
23
  import { SessionManager } from "./sessions/manager.js";
24
+ import { prepararRestauracion } from "./sessions/resume.js";
23
25
  import { globalUndoManager } from "./agent/undoManager.js";
24
26
  import { setAutoApprove } from "./ui/permissionPrompt.js";
25
27
  import { onModeChange, getMode, modeAnnouncement } from "./ui/modes.js";
26
28
  import { releaseKeyboard } from "./ui/interrupt.js";
27
29
  import { takeQueued, resetTypeAhead } from "./ui/typeAhead.js";
28
30
  import { mcpManager } from "./mcp/manager.js";
31
+ import { comprobarActualizacion } from "./config/updater.js";
32
+ import { comprobarNode } from "./config/nodeVersion.js";
33
+ import { imprimirSobreElPrompt } from "./ui/prompt.js";
29
34
  /**
30
35
  * Version leida del package.json instalado, no escrita a mano: una constante
31
36
  * duplicada aqui se queda vieja en cuanto se publica una version nueva.
32
37
  */
33
38
  function readVersion() {
34
39
  try {
35
- const here = path.dirname(fileURLToPath(import.meta.url));
36
- const pkg = JSON.parse(fs.readFileSync(path.resolve(here, "..", "package.json"), "utf-8"));
40
+ const pkg = JSON.parse(fs.readFileSync(path.join(raizDelPaquete(), "package.json"), "utf-8"));
37
41
  return String(pkg.version || "desconocida");
38
42
  }
39
43
  catch {
40
44
  return "desconocida";
41
45
  }
42
46
  }
47
+ /** La carpeta que contiene el `dist`. El actualizador la necesita para saber si esto es el repositorio. */
48
+ function raizDelPaquete() {
49
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
50
+ }
43
51
  /**
44
52
  * Banderas que se resuelven sin arrancar el agente.
45
53
  *
@@ -76,6 +84,14 @@ function handleFlags(argv) {
76
84
  " API key: variable CHOCOLATITO_API_KEY, un .env del proyecto o ~/.chocolatitorc.",
77
85
  " Reglas del proyecto: CHOCOLATITO.md. Permisos y hooks: .chocolatito/settings.json.",
78
86
  "",
87
+ "CODIGOS DE SALIDA (modo orden unica)",
88
+ " 0 La tarea se completo.",
89
+ " 1 El agente no pudo terminar (fallo del motor o limite de iteraciones).",
90
+ " 2 Error de uso: falta la orden o no hay terminal para pedirla.",
91
+ " 3 Cuota agotada.",
92
+ " 4 Licencia o API key no valida.",
93
+ " 130 Interrumpido con Esc.",
94
+ "",
79
95
  ].join("\n"));
80
96
  return true;
81
97
  }
@@ -118,17 +134,90 @@ function instalarRedDeSeguridad() {
118
134
  restaurarTerminal();
119
135
  console.log(chalk.red(`\n✖ Error inesperado: ${err?.message || String(err)}\n`) +
120
136
  chalk.gray(`${(err?.stack || "").split("\n").slice(1, 4).join("\n")}\n`));
121
- process.exit(1);
137
+ process.exit(EXIT_FALLO);
122
138
  });
123
139
  }
140
+ /**
141
+ * UserPromptSubmit: se dispara con lo que el usuario acaba de escribir, antes de
142
+ * mandarlo al modelo. Sirve para vetar un prompt (un secreto pegado sin querer,
143
+ * una politica de la empresa) o para inyectarle contexto.
144
+ *
145
+ * Estaba declarado en HookEvent y /hooks lo listaba tan tranquilo, pero no lo
146
+ * disparaba nadie: se configuraba y no pasaba absolutamente nada. Un hook puesto
147
+ * para bloquear prompts con credenciales los dejaba pasar todos.
148
+ *
149
+ * Devuelve null si el prompt queda vetado.
150
+ */
151
+ async function pasarPorUserPromptSubmit(hooks, prompt) {
152
+ const d = await hooks.run("UserPromptSubmit", { prompt }, prompt);
153
+ if (d.systemMessage)
154
+ console.log(chalk.yellow(`\n⚑ ${d.systemMessage}`));
155
+ if (d.permissionDecision === "deny") {
156
+ console.log(chalk.red(`\n✖ Un hook rechazo este prompt: ${d.permissionDecisionReason || "sin motivo"}\n`));
157
+ return null;
158
+ }
159
+ if (d.additionalContext) {
160
+ return `${prompt}\n\n[Contexto anadido por un hook UserPromptSubmit]\n${d.additionalContext}`;
161
+ }
162
+ return prompt;
163
+ }
164
+ /**
165
+ * Stop: fin de turno, el sitio donde el usuario engancha su linter o sus tests.
166
+ * Mismo caso que el anterior: declarado, listado y jamas disparado.
167
+ *
168
+ * Un "deny" (codigo 2) significa "no pares todavia" y devuelve el motivo para
169
+ * reinyectarlo como siguiente orden; quien llama decide si lo hace, porque un
170
+ * hook que siempre diga que no dejaria al agente dando vueltas para siempre.
171
+ */
172
+ async function dispararStop(hooks, datos) {
173
+ const d = await hooks.run("Stop", datos);
174
+ const aviso = d.systemMessage || d.additionalContext;
175
+ if (aviso)
176
+ console.log(chalk.yellow(`\n⚑ ${aviso}`));
177
+ if (d.permissionDecision === "deny") {
178
+ return d.permissionDecisionReason || "Un hook Stop pidio seguir trabajando.";
179
+ }
180
+ return null;
181
+ }
124
182
  async function main() {
125
- if (handleFlags(process.argv.slice(2)))
183
+ // Lo primero de todo: un Node viejo se dice claro, no se descubre a base de
184
+ // errores raros dentro de una dependencia. Ver config/nodeVersion.ts.
185
+ const nodeViejo = comprobarNode();
186
+ if (nodeViejo) {
187
+ console.error(chalk.red(`
188
+ ${nodeViejo}
189
+ `));
190
+ process.exit(EXIT_USO);
191
+ }
192
+ const rawArgs = process.argv.slice(2);
193
+ if (handleFlags(rawArgs))
126
194
  return;
127
195
  instalarRedDeSeguridad();
196
+ // --yes / -y aprueba las acciones sin preguntar. Util en modo comando unico y CI.
197
+ const autoYes = rawArgs.includes("--yes") || rawArgs.includes("-y");
198
+ const args = rawArgs.filter((a) => a !== "--yes" && a !== "-y");
199
+ const singlePrompt = args.join(" ").trim();
200
+ // Los errores de uso se resuelven ANTES de la licencia y de la API key: no
201
+ // dependen de estar suscrito, y en CI hay que poder distinguir "lo has
202
+ // invocado mal" de "el agente no pudo".
203
+ if (args.length > 0 && !singlePrompt) {
204
+ console.error(chalk.red("\n✖ No hay ninguna orden que ejecutar.") +
205
+ chalk.gray('\n Uso: chocolatito "haz X". Con --help tienes el resto.\n'));
206
+ process.exit(EXIT_USO);
207
+ }
208
+ // Sin orden y sin terminal no hay a quien preguntar: el REPL moria en el
209
+ // primer prompt y salia con 0, que en CI se lee como "todo bien".
210
+ if (!singlePrompt && !process.stdin.isTTY) {
211
+ console.error(chalk.red("\n✖ La entrada no es una terminal y no se dio ninguna orden.") +
212
+ chalk.gray('\n En scripts y CI usa: chocolatito --yes "haz X".\n'));
213
+ process.exit(EXIT_USO);
214
+ }
215
+ if (autoYes)
216
+ setAutoApprove(true);
128
217
  // La licencia va ANTES de la API key: no tiene sentido pedirle a alguien que
129
218
  // configure DeepSeek si despues le vamos a decir que no ha comprado.
130
219
  if (!(await ensureLicense())) {
131
- process.exit(1);
220
+ process.exit(EXIT_LICENCIA);
132
221
  }
133
222
  // En suscripcion el motor lo paga Chocolatito: el cliente NO tiene ni necesita
134
223
  // clave de DeepSeek, y su credencial es la propia licencia. Preguntarsela era
@@ -159,13 +248,41 @@ async function main() {
159
248
  // Si tardan, el primer turno los espera; el usuario ya puede escribir.
160
249
  const mcpReady = mcpManager.loadAll().then((resumen) => {
161
250
  if (resumen) {
162
- console.log(chalk.hex("#D97757")("\n🔌 Servidores MCP:"));
163
- console.log(resumen);
164
- console.log(chalk.gray(` ${mcpManager.toolCount} herramientas MCP disponibles.\n`));
251
+ // Con `console.log` esto se escribia ENCIMA del marco del prompt y lo
252
+ // destrozaba: los servidores tardan, y para cuando contestan el prompt ya
253
+ // esta dibujado. Se vio asi en una sesion real del usuario:
254
+ // ✔ ide (http): 4 herramientasb modo · esc cancelar
255
+ imprimirSobreElPrompt(chalk.hex("#D97757")("\n🔌 Servidores MCP:") +
256
+ `\n${resumen}\n` +
257
+ chalk.gray(` ${mcpManager.toolCount} herramientas MCP disponibles.\n`));
165
258
  }
166
259
  return resumen;
167
260
  });
261
+ // Actualizacion sola, en segundo plano.
262
+ //
263
+ // No se espera: si el registro tarda, la sesion ya esta lista y el aviso
264
+ // aparece cuando llegue. Se guarda la promesa en vez de soltarla para que
265
+ // /actualizar pueda ensenar el resultado si el usuario pregunta antes.
266
+ let avisoDeActualizacion = "";
267
+ const actualizacionEnCurso = comprobarActualizacion({
268
+ versionActual: readVersion(),
269
+ raizDelPaquete: raizDelPaquete(),
270
+ ahora: Date.now(),
271
+ })
272
+ .then((aviso) => {
273
+ avisoDeActualizacion = aviso;
274
+ // Llega tarde a proposito, y para entonces el prompt ya suele estar
275
+ // dibujado: por eso NO se usa console.log, que lo pisaria. Ver
276
+ // imprimirSobreElPrompt en ui/prompt.ts.
277
+ if (aviso)
278
+ imprimirSobreElPrompt(chalk.gray(`\n ⬆ ${aviso}\n`));
279
+ return aviso;
280
+ })
281
+ .catch(() => "");
168
282
  // Add /resume to slash commands
283
+ if (!SLASH_COMMANDS.some((c) => c.name === "/actualizar")) {
284
+ SLASH_COMMANDS.push({ name: "/actualizar", description: "Buscar e instalar la última versión de Chocolatito Code" });
285
+ }
169
286
  if (!SLASH_COMMANDS.some((c) => c.name === "/licencia")) {
170
287
  SLASH_COMMANDS.push({ name: "/licencia", description: "Ver la licencia activa o liberar este equipo", args: "[liberar]" });
171
288
  }
@@ -173,21 +290,37 @@ async function main() {
173
290
  SLASH_COMMANDS.splice(1, 0, { name: "/resume", description: "Reanudar una sesión anterior guardada", args: "[id]" });
174
291
  }
175
292
  // Single-command mode: chocolatito "crea index.html"
176
- const rawArgs = process.argv.slice(2);
177
- // --yes / -y aprueba las acciones sin preguntar. Util en modo comando unico y CI.
178
- const autoYes = rawArgs.includes("--yes") || rawArgs.includes("-y");
179
- if (autoYes)
180
- setAutoApprove(true);
181
- const args = rawArgs.filter((a) => a !== "--yes" && a !== "-y");
182
- if (args.length > 0) {
183
- const singlePrompt = args.join(" ");
293
+ if (singlePrompt) {
184
294
  showBanner(context.cwd, currentModel, "idle");
185
295
  renderUserPrompt(singlePrompt);
186
- const { attachedFiles } = parseFileMentions(singlePrompt, context.cwd);
187
- const enrichedPrompt = buildPromptWithMentions(singlePrompt, attachedFiles);
296
+ // El modo no interactivo pasa por los mismos hooks que el REPL: un veto que
297
+ // solo valiera cuando hay alguien mirando no es una politica.
298
+ const revisado = await pasarPorUserPromptSubmit(hookManager, singlePrompt);
299
+ if (revisado === null) {
300
+ await hookManager.triggerHook("SessionEnd", { sessionId, cwd: agent.currentCwd });
301
+ process.exit(EXIT_FALLO);
302
+ }
303
+ const { attachedFiles } = parseFileMentions(revisado, context.cwd);
304
+ const enrichedPrompt = buildPromptWithMentions(revisado, attachedFiles) +
305
+ notaDeMencionesImplicitas(revisado, context.cwd, attachedFiles.map((f) => f.relativePath));
188
306
  await mcpReady;
189
- await agent.runTask(enrichedPrompt);
190
- process.exit(0);
307
+ // Aqui estaba el fallo: process.exit(0) incondicional. Una cuota agotada,
308
+ // una licencia caida o el limite de iteraciones salian con 0 y el pipeline
309
+ // seguia como si el trabajo estuviera hecho.
310
+ const resultado = await agent.runTask(enrichedPrompt);
311
+ // Aqui tampoco se disparaba nada: el modo de una sola orden salia por
312
+ // process.exit() sin pasar por Stop ni por SessionEnd, asi que el linter del
313
+ // final del turno y el cierre de sesion solo existian en la sesion
314
+ // interactiva. Su "deny" no reinyecta nada: no hay turno siguiente al que
315
+ // devolverle el trabajo, se avisa y se sale.
316
+ await dispararStop(hookManager, { session_id: sessionId, cwd: agent.currentCwd, stop_hook_active: false });
317
+ await hookManager.triggerHook("SessionEnd", { sessionId, cwd: agent.currentCwd });
318
+ if (!resultado.exito) {
319
+ console.error(chalk.red(`✖ La tarea no se completo (${resultado.motivo})`) +
320
+ (resultado.detalle ? chalk.gray(`: ${resultado.detalle}`) : "") +
321
+ "\n");
322
+ }
323
+ process.exit(codigoDeSalida(resultado));
191
324
  }
192
325
  // Interactive REPL Mode with lively startup walking animation
193
326
  await animateStartupBanner(context.cwd, currentModel);
@@ -198,21 +331,34 @@ async function main() {
198
331
  // Lo que el usuario escribio mientras el agente trabajaba entra como siguiente
199
332
  // orden, sin tener que volver a teclearlo.
200
333
  let pendiente = null;
334
+ // Lo que un hook Stop pide seguir haciendo va aparte de `pendiente`: hay que
335
+ // distinguir "sigue tu" de "el usuario escribio algo", porque el permiso para
336
+ // reinyectar se gasta una sola vez por orden del usuario. Sin ese limite, un
337
+ // Stop que siempre deniega deja al agente en bucle infinito quemando cuota.
338
+ let continuacionDeStop = null;
339
+ let stopYaForzado = false;
201
340
  while (true) {
202
341
  let userInput = "";
203
- if (pendiente) {
342
+ if (continuacionDeStop) {
343
+ userInput = continuacionDeStop;
344
+ continuacionDeStop = null;
345
+ renderUserPrompt(userInput);
346
+ }
347
+ else if (pendiente) {
204
348
  userInput = pendiente;
205
349
  pendiente = null;
350
+ stopYaForzado = false;
206
351
  renderUserPrompt(userInput);
207
352
  }
208
353
  else {
209
354
  try {
210
355
  userInput = await askInteractivePrompt(isPlanMode ? chalk.cyan("plan ❯") : "❯", agent.currentCwd);
356
+ stopYaForzado = false;
211
357
  }
212
358
  catch {
213
359
  console.log(chalk.gray("\n👋 Sesión finalizada."));
214
360
  await hookManager.triggerHook("SessionEnd", { sessionId, cwd: agent.currentCwd });
215
- process.exit(0);
361
+ process.exit(EXIT_OK);
216
362
  }
217
363
  }
218
364
  const trimmed = userInput.trim();
@@ -222,7 +368,7 @@ async function main() {
222
368
  if (trimmed === "/exit" || trimmed === "exit" || trimmed === "quit") {
223
369
  console.log(chalk.gray("\n👋 Sesión finalizada."));
224
370
  await hookManager.triggerHook("SessionEnd", { sessionId, cwd: agent.currentCwd });
225
- process.exit(0);
371
+ process.exit(EXIT_OK);
226
372
  }
227
373
  if (trimmed === "/licencia" || trimmed.startsWith("/licencia ")) {
228
374
  const sub = trimmed.slice("/licencia".length).trim();
@@ -237,14 +383,13 @@ async function main() {
237
383
  }
238
384
  if (trimmed === "/undo") {
239
385
  const undoRes = globalUndoManager.undoLast();
240
- if (undoRes.success) {
241
- console.log(chalk.green(`\n${undoRes.message}`));
242
- undoRes.restoredFiles.forEach((f) => console.log(` ${chalk.gray("•")} ${chalk.white(f)}`));
243
- console.log("");
244
- }
245
- else {
246
- console.log(chalk.yellow(`\n⚠ ${undoRes.message}\n`));
247
- }
386
+ // Lo que no se pudo deshacer se imprime siempre, tambien cuando algo si se
387
+ // restauro. Antes solo se veia la lista de exitos bajo un "Deshecho con
388
+ // exito" fijo, asi que un borrado irreversible pasaba por reparado.
389
+ console.log(undoRes.success ? chalk.green(`\n${undoRes.message}`) : chalk.yellow(`\n${undoRes.message}`));
390
+ undoRes.restoredFiles.forEach((f) => console.log(` ${chalk.gray("•")} ${chalk.white(f)}`));
391
+ undoRes.failures.forEach((f) => console.log(` ${chalk.red("✗")} ${chalk.yellow(f)}`));
392
+ console.log("");
248
393
  continue;
249
394
  }
250
395
  if (trimmed.startsWith("/export")) {
@@ -276,8 +421,26 @@ ${sessionData}
276
421
  const targetId = trimmed.slice(7).trim();
277
422
  const loaded = targetId ? sessionManager.loadSession(targetId) : sessionManager.loadLatestSession();
278
423
  if (loaded) {
279
- agent.messages = loaded.messages;
280
- console.log(chalk.green(`\n✔ Sesión "${loaded.id}" restaurada con éxito (${loaded.messages.length} mensajes cargados).\n`));
424
+ // Se restaura la conversacion, no el entorno. El array guardado trae en
425
+ // su primer mensaje el prompt de sistema de aquel dia; asignarlo entero
426
+ // dejaba al modelo anunciando otro directorio, otra fecha y otras reglas
427
+ // mientras las herramientas trabajaban contra el cwd real.
428
+ const restauracion = prepararRestauracion(loaded, { cwd: agent.currentCwd, modelId: currentModel.id });
429
+ agent.currentCwd = restauracion.cwd;
430
+ currentModel = ESPECTRO_MODELS[restauracion.modelId] || currentModel;
431
+ const refreshedContext = await getProjectContext(agent.currentCwd);
432
+ systemPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
433
+ agent.messages = [{ role: "system", content: systemPrompt }, ...restauracion.conversacion];
434
+ // Sincroniza el motor del bucle con el de la sesion: el prompt ya
435
+ // anuncia ese nivel de esfuerzo, y anunciar uno y correr otro es el
436
+ // mismo desfase que se acaba de arreglar.
437
+ agent.setEspectroModel(currentModel);
438
+ console.log(chalk.green(`\n✔ Sesión "${loaded.id}" restaurada con éxito (${restauracion.conversacion.length} mensajes cargados).`));
439
+ console.log(chalk.gray(` Directorio: ${agent.currentCwd} · Motor: ${currentModel.name}`));
440
+ if (restauracion.directorioPerdido && loaded.cwd) {
441
+ console.log(chalk.yellow(` ⚠ El directorio guardado (${loaded.cwd}) ya no existe; se sigue aquí.`));
442
+ }
443
+ console.log("");
281
444
  }
282
445
  else {
283
446
  const available = sessionManager.listSessions();
@@ -434,6 +597,25 @@ Directivas de ejecución:
434
597
  console.log(chalk.green("✔ Historial reiniciado.\n"));
435
598
  continue;
436
599
  }
600
+ if (trimmed === "/actualizar") {
601
+ // Si la comprobacion del arranque ya trajo algo que decir, se dice eso en
602
+ // vez de volver a la red: preguntar dos veces lo mismo no cambia la
603
+ // respuesta y hace esperar al usuario para nada.
604
+ await actualizacionEnCurso;
605
+ if (avisoDeActualizacion) {
606
+ console.log(chalk.gray(`\n ⬆ ${avisoDeActualizacion}\n`));
607
+ continue;
608
+ }
609
+ console.log(chalk.gray("\n Buscando una version mas nueva…"));
610
+ const aviso = await comprobarActualizacion({
611
+ versionActual: readVersion(),
612
+ raizDelPaquete: raizDelPaquete(),
613
+ ahora: Date.now(),
614
+ forzar: true,
615
+ });
616
+ console.log(chalk.gray(` ${aviso || "Sin novedades."}\n`));
617
+ continue;
618
+ }
437
619
  if (trimmed === "/init") {
438
620
  const rulesPath = path.join(agent.currentCwd, RULES_FILE_NAME);
439
621
  if (!fs.existsSync(rulesPath)) {
@@ -446,13 +628,34 @@ Directivas de ejecución:
446
628
  }
447
629
  continue;
448
630
  }
631
+ // UserPromptSubmit: el hook ve el prompt antes que el modelo y puede vetarlo
632
+ // o anadirle contexto. Va aqui, despues de los comandos de barra, porque
633
+ // "/clear" o "/hooks" no son prompts para el modelo.
634
+ const revisado = await pasarPorUserPromptSubmit(hookManager, trimmed);
635
+ if (revisado === null)
636
+ continue;
449
637
  // Parse and auto-attach @file mentions
450
- const { attachedFiles } = parseFileMentions(trimmed, agent.currentCwd);
451
- const enrichedPrompt = buildPromptWithMentions(trimmed, attachedFiles);
638
+ const { attachedFiles } = parseFileMentions(revisado, agent.currentCwd);
639
+ // Y los que nombra sin la @ ("arregla el fileTracker"): solo la ruta, para
640
+ // que no tenga que buscar a ciegas un archivo que el usuario acaba de decir.
641
+ const enrichedPrompt = buildPromptWithMentions(revisado, attachedFiles) +
642
+ notaDeMencionesImplicitas(revisado, agent.currentCwd, attachedFiles.map((f) => f.relativePath));
452
643
  // Execute task with agent loop
453
644
  await agent.runTask(enrichedPrompt);
454
645
  // Lo que se escribio mientras trabajaba pasa a ser la siguiente orden.
455
646
  pendiente = takeQueued();
647
+ // Stop: fin de turno. Si deniega, pide seguir trabajando; solo se le hace
648
+ // caso una vez por orden del usuario y nunca por delante de lo que el
649
+ // usuario haya escrito mientras tanto.
650
+ const motivoSeguir = await dispararStop(hookManager, {
651
+ session_id: sessionId,
652
+ cwd: agent.currentCwd,
653
+ stop_hook_active: stopYaForzado,
654
+ });
655
+ if (motivoSeguir && !stopYaForzado && !pendiente) {
656
+ stopYaForzado = true;
657
+ continuacionDeStop = `[Hook Stop] ${motivoSeguir}`;
658
+ }
456
659
  // Persist session state
457
660
  sessionManager.saveSession({
458
661
  id: sessionId,
@@ -466,5 +669,5 @@ Directivas de ejecución:
466
669
  }
467
670
  main().catch((err) => {
468
671
  console.error(chalk.red(`\nError: ${err?.message || String(err)}`));
469
- process.exit(1);
672
+ process.exit(EXIT_FALLO);
470
673
  });
@@ -17,6 +17,28 @@ export interface McpServerConfig {
17
17
  * guardar la configuracion en el repositorio sin meter secretos dentro.
18
18
  */
19
19
  export declare function expandVars(value: string): string;
20
+ /**
21
+ * Extrae el mensaje JSON-RPC de una respuesta HTTP, sea JSON directo o SSE.
22
+ *
23
+ * Antes esto se decidia con `text.includes("data:")` sobre el cuerpo entero, y
24
+ * eso rompia por dos sitios a la vez:
25
+ *
26
+ * 1. Cualquier respuesta JSON normal que mencionara `data:` en su contenido (un
27
+ * data URI, una cabecera, un trozo de CSS, la descripcion de una herramienta
28
+ * de captura o de fetch web) entraba por la rama SSE. Ninguna linea empezaba
29
+ * por `data:`, el `.pop()` daba undefined y el `|| "{}"` convertia la
30
+ * respuesta en un objeto vacio. Como `httpRequest` es la unica via de todo el
31
+ * protocolo HTTP, eso no solo devolvia «(sin contenido)» en `tools/call`:
32
+ * tambien vaciaba `tools/list`, y el servidor quedaba «conectado» con cero
33
+ * herramientas sin un solo mensaje de error.
34
+ * 2. En SSE de verdad, `.pop()` se quedaba con el ULTIMO evento en vez de con el
35
+ * que responde a nuestro `id`. Un ping o una notificacion posterior tapaban
36
+ * el resultado.
37
+ *
38
+ * Ahora manda el Content-Type, la forma del cuerpo desempata cuando el servidor
39
+ * no lo declara bien, y entre los eventos se elige el que lleva el `id` pedido.
40
+ */
41
+ export declare function parseHttpBody(text: string, contentType: string | null, id?: number | string): any;
20
42
  export declare class McpClient {
21
43
  readonly name: string;
22
44
  tools: McpTool[];
@@ -46,7 +68,35 @@ export declare class McpClient {
46
68
  private spawnServer;
47
69
  private onMessage;
48
70
  private request;
71
+ /**
72
+ * Identificador de sesion que asigna el servidor HTTP.
73
+ *
74
+ * El transporte "streamable HTTP" del protocolo permite que el servidor
75
+ * devuelva una cabecera `Mcp-Session-Id` al responder al `initialize`, y a
76
+ * partir de ahi EXIGE que el cliente la mande en todas las peticiones
77
+ * siguientes. Sin esto, un servidor con sesion acepta el saludo y luego
78
+ * rechaza absolutamente todo lo demas: la conexion parece buena, la lista de
79
+ * herramientas llega vacia o con un 400, y desde fuera solo se ve un servidor
80
+ * que "no funciona" sin decir por que.
81
+ */
82
+ private sessionId;
83
+ /** Las mismas cabeceras para peticiones y notificaciones, sesion incluida. */
84
+ private cabecerasHttp;
49
85
  private notify;
86
+ /**
87
+ * Una notificacion por HTTP.
88
+ *
89
+ * Antes esto no existia y `notify()` se iba de vacio en cuanto habia una URL,
90
+ * asi que `notifications/initialized` solo viajaba por stdio. El protocolo la
91
+ * exige despues del `initialize`, y hay servidores que no atienden nada mas
92
+ * hasta recibirla: quedaban colgados en el saludo.
93
+ *
94
+ * No lleva id y no espera resultado -el servidor contesta 202 con el cuerpo
95
+ * vacio-, asi que no se parsea nada. Un fallo aqui tampoco se propaga: si el
96
+ * servidor no la queria, se vera en la peticion siguiente, que si tiene a
97
+ * quien quejarse.
98
+ */
99
+ private httpNotify;
50
100
  private httpRequest;
51
101
  private initialize;
52
102
  private listTools;