chocolatito-code 1.6.7 → 1.6.8

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 (55) hide show
  1. package/README.md +437 -341
  2. package/dist/agent/context.d.ts +6 -0
  3. package/dist/agent/context.js +69 -2
  4. package/dist/agent/loop.js +8 -0
  5. package/dist/agent/toolGate.js +13 -7
  6. package/dist/agent/verifier.js +2 -1
  7. package/dist/config/permissions.d.ts +17 -0
  8. package/dist/config/permissions.js +47 -7
  9. package/dist/config/plataforma.d.ts +62 -0
  10. package/dist/config/plataforma.js +109 -0
  11. package/dist/config/updater.js +4 -1
  12. package/dist/hooks/manager.js +2 -1
  13. package/dist/index.js +99 -6
  14. package/dist/memory/manager.d.ts +15 -5
  15. package/dist/memory/manager.js +146 -29
  16. package/dist/servidor/captura.d.ts +36 -0
  17. package/dist/servidor/captura.js +74 -0
  18. package/dist/servidor/pagina.d.ts +27 -0
  19. package/dist/servidor/pagina.js +268 -0
  20. package/dist/servidor/puente.d.ts +34 -0
  21. package/dist/servidor/puente.js +115 -0
  22. package/dist/servidor/servidor.d.ts +61 -0
  23. package/dist/servidor/servidor.js +249 -0
  24. package/dist/sessions/manager.d.ts +8 -0
  25. package/dist/sessions/manager.js +44 -0
  26. package/dist/sessions/resume.d.ts +12 -0
  27. package/dist/sessions/resume.js +10 -0
  28. package/dist/tools/backgroundTask.d.ts +64 -0
  29. package/dist/tools/backgroundTask.js +264 -0
  30. package/dist/tools/browserExtension.d.ts +2 -1
  31. package/dist/tools/browserExtension.js +48 -0
  32. package/dist/tools/computerUse.js +51 -9
  33. package/dist/tools/definitions.js +93 -1
  34. package/dist/tools/gitAudit.d.ts +41 -0
  35. package/dist/tools/gitAudit.js +282 -0
  36. package/dist/tools/runCommand.js +5 -2
  37. package/dist/tools/runner.js +36 -2
  38. package/dist/tools/safety.d.ts +1 -0
  39. package/dist/tools/safety.js +3 -0
  40. package/dist/tools/todoTool.d.ts +2 -0
  41. package/dist/tools/todoTool.js +29 -0
  42. package/dist/tools/toolDefsComputer.js +9 -0
  43. package/dist/tools/win/hostScript.js +7 -2
  44. package/dist/ui/comandos.js +2 -0
  45. package/dist/ui/marco.d.ts +1 -0
  46. package/dist/ui/marco.js +4 -2
  47. package/dist/ui/permissionPrompt.d.ts +9 -0
  48. package/dist/ui/permissionPrompt.js +30 -0
  49. package/dist/ui/pieFijo.d.ts +1 -1
  50. package/dist/ui/pieFijo.js +1 -1
  51. package/dist/ui/renderer.js +8 -0
  52. package/extension/background.js +80 -0
  53. package/extension/content.js +106 -0
  54. package/extension/manifest.json +61 -60
  55. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -16,6 +16,9 @@ import { showBanner, animateStartupBanner, showHelp, showEffortMenu } from "./ui
16
16
  import { fijarMotor } from "./ui/motorActual.js";
17
17
  import { elegirDeLista } from "./ui/selector.js";
18
18
  import { entrarEnPantallaFija, pantallaFijaPedida, salirDePantallaFija } from "./ui/pantalla.js";
19
+ import { iniciarServidorWeb } from "./servidor/servidor.js";
20
+ import { iniciarCaptura } from "./servidor/captura.js";
21
+ import { entregarEntrada, esperarEntradaWeb, marcarServirActivo } from "./servidor/puente.js";
19
22
  import { montarArbol, desmontarArbol } from "./ui/ink/montarApp.js";
20
23
  import { reafirmarModoCrudo } from "./ui/keyboardGuard.js";
21
24
  import { askInteractivePrompt, SLASH_COMMANDS } from "./ui/prompt.js";
@@ -27,7 +30,8 @@ import { SkillManager } from "./skills/manager.js";
27
30
  import { MemoryManager } from "./memory/manager.js";
28
31
  import { HookManager } from "./hooks/manager.js";
29
32
  import { SessionManager } from "./sessions/manager.js";
30
- import { prepararRestauracion } from "./sessions/resume.js";
33
+ import { prepararRestauracion, formatearOpcionesDeSesion } from "./sessions/resume.js";
34
+ import { auditGitDiff, formatAuditReport } from "./tools/gitAudit.js";
31
35
  import { globalUndoManager } from "./agent/undoManager.js";
32
36
  import { setAutoApprove } from "./ui/permissionPrompt.js";
33
37
  import { onModeChange, getMode, modeAnnouncement } from "./ui/modes.js";
@@ -76,6 +80,9 @@ function handleFlags(argv) {
76
80
  "",
77
81
  "OPCIONES",
78
82
  " -y, --yes Aprueba las acciones sin preguntar (util en CI).",
83
+ " --servir Abre la sesion en el navegador (solo esta maquina).",
84
+ " --servir-red Igual, pero accesible desde tu red local (movil, tablet).",
85
+ " --puerto N Puerto para --servir (por defecto 4700).",
79
86
  " -v, --version Muestra la version instalada.",
80
87
  " -h, --help Muestra esta ayuda.",
81
88
  "",
@@ -222,7 +229,16 @@ ${nodeViejo}
222
229
  instalarRedDeSeguridad();
223
230
  // --yes / -y aprueba las acciones sin preguntar. Util en modo comando unico y CI.
224
231
  const autoYes = rawArgs.includes("--yes") || rawArgs.includes("-y");
225
- const args = rawArgs.filter((a) => a !== "--yes" && a !== "-y");
232
+ // --servir abre la sesion en el navegador; --servir-red ademas la saca a la
233
+ // red local, que es lo que hace falta para entrar desde el movil.
234
+ const enRed = rawArgs.includes("--servir-red");
235
+ const quiereServir = enRed || rawArgs.includes("--servir");
236
+ const posPuerto = rawArgs.indexOf("--puerto");
237
+ const puertoWeb = posPuerto >= 0 ? Number(rawArgs[posPuerto + 1]) || 0 : 0;
238
+ // Ojo: lo que sobra de argv se convierte en la orden a ejecutar. Una bandera
239
+ // que no se filtre aqui no es una bandera ignorada, es una orden al modelo.
240
+ const banderas = new Set(["--yes", "-y", "--servir", "--servir-red", "--puerto"]);
241
+ const args = rawArgs.filter((a, i) => !banderas.has(a) && !(posPuerto >= 0 && i === posPuerto + 1 && puertoWeb > 0));
226
242
  const singlePrompt = args.join(" ").trim();
227
243
  // Los errores de uso se resuelven ANTES de la licencia y de la API key: no
228
244
  // dependen de estar suscrito, y en CI hay que poder distinguir "lo has
@@ -234,7 +250,7 @@ ${nodeViejo}
234
250
  }
235
251
  // Sin orden y sin terminal no hay a quien preguntar: el REPL moria en el
236
252
  // primer prompt y salia con 0, que en CI se lee como "todo bien".
237
- if (!singlePrompt && !process.stdin.isTTY) {
253
+ if (!singlePrompt && !process.stdin.isTTY && !quiereServir) {
238
254
  console.error(chalk.red("\n✖ La entrada no es una terminal y no se dio ninguna orden.") +
239
255
  chalk.gray('\n En scripts y CI usa: chocolatito --yes "haz X".\n'));
240
256
  process.exit(EXIT_USO);
@@ -254,7 +270,7 @@ ${nodeViejo}
254
270
  : await ensureApiKey();
255
271
  const context = await getProjectContext(process.cwd());
256
272
  const skillManager = new SkillManager(context.cwd);
257
- const memoryManager = new MemoryManager();
273
+ const memoryManager = new MemoryManager(context.cwd);
258
274
  const hookManager = new HookManager(context.cwd);
259
275
  const sessionManager = new SessionManager();
260
276
  const sessionId = `session_${Date.now()}`;
@@ -320,7 +336,9 @@ ${nodeViejo}
320
336
  SLASH_COMMANDS.splice(1, 0, { name: "/resume", description: "Reanudar una sesión anterior guardada", args: "[id]" });
321
337
  }
322
338
  // Single-command mode: chocolatito "crea index.html"
323
- if (singlePrompt) {
339
+ // Con --servir no: ahi la orden es solo la PRIMERA de una sesion que sigue
340
+ // viva esperando al navegador, no una tarea suelta que termina y cierra.
341
+ if (singlePrompt && !quiereServir) {
324
342
  showBanner(context.cwd, currentModel, "idle");
325
343
  renderUserPrompt(singlePrompt);
326
344
  // El modo no interactivo pasa por los mismos hooks que el REPL: un veto que
@@ -382,6 +400,46 @@ ${nodeViejo}
382
400
  // que haya nadie pintandolo deja la consola muda.
383
401
  montarArbol();
384
402
  }
403
+ // EL SERVIDOR WEB, SI SE PIDIO
404
+ //
405
+ // Va DESPUES de montar el arbol, y el orden no es casual: ui/pantalla.ts ya
406
+ // reemplazo los cuatro canales de console para mandarlos a la transcripcion, y
407
+ // la captura tiene que envolver ESA version. Al reves, pantalla envolveria a la
408
+ // captura y al navegador no llegaria nada. Ver servidor/captura.ts.
409
+ if (quiereServir) {
410
+ try {
411
+ iniciarCaptura();
412
+ const web = await iniciarServidorWeb({ puerto: puertoWeb || undefined, enRed });
413
+ marcarServirActivo(true);
414
+ console.log(chalk.hex("#e8833a")("\n 🦊 Sesión abierta en el navegador\n"));
415
+ console.log(` ${chalk.bold("Aquí:")} ${chalk.cyan(web.url)}`);
416
+ for (const dir of web.urlsDeRed) {
417
+ console.log(` ${chalk.bold("Del móvil:")} ${chalk.cyan(dir)}`);
418
+ }
419
+ if (!enRed) {
420
+ console.log(chalk.gray("\n Solo desde esta máquina. Para entrar desde el móvil: --servir-red"));
421
+ }
422
+ else {
423
+ console.log(chalk.yellow("\n ⚠ Abierto a tu red local.") +
424
+ chalk.gray(" Cualquiera que tenga el enlace COMPLETO (con el token)" +
425
+ "\n puede usar esta sesión. No lo pegues en ningún sitio compartido."));
426
+ }
427
+ console.log(chalk.gray("\n Lo que escribas aquí ya no se lee: manda el navegador y este terminal es el monitor.\n"));
428
+ // Una orden en la linea de comandos con --servir es la PRIMERA del turno,
429
+ // no una tarea suelta: entra por la misma puerta que lo que llegue del movil.
430
+ if (singlePrompt)
431
+ entregarEntrada(singlePrompt);
432
+ }
433
+ catch (err) {
434
+ const ocupado = err?.code === "EADDRINUSE";
435
+ const motivo = ocupado ? "el puerto ya está ocupado" : err?.message || String(err);
436
+ console.error(chalk.red("\n✖ No se pudo abrir la sesión web: " + motivo));
437
+ if (ocupado)
438
+ console.error(chalk.gray(" Prueba con otro: chocolatito --servir --puerto 4701"));
439
+ console.error("");
440
+ process.exit(EXIT_USO);
441
+ }
442
+ }
385
443
  // Lo que el usuario escribio mientras el agente trabajaba entra como siguiente
386
444
  // orden, sin tener que volver a teclearlo.
387
445
  let pendiente = null;
@@ -404,6 +462,15 @@ ${nodeViejo}
404
462
  stopYaForzado = false;
405
463
  renderUserPrompt(userInput);
406
464
  }
465
+ else if (quiereServir) {
466
+ // Manda el navegador. No se corre tambien el prompt del teclado a ver cual
467
+ // contesta primero: askInteractivePrompt se queda con el TTY prestado y no
468
+ // sabe cancelarse a medias, y dos entradas peleando por el mismo terminal
469
+ // es exactamente lo que dejo la pantalla en negro dos veces.
470
+ userInput = await esperarEntradaWeb();
471
+ stopYaForzado = false;
472
+ renderUserPrompt(userInput);
473
+ }
407
474
  else {
408
475
  try {
409
476
  userInput = await askInteractivePrompt(isPlanMode ? chalk.cyan("plan ❯") : "❯", agent.currentCwd);
@@ -472,7 +539,27 @@ ${sessionData}
472
539
  continue;
473
540
  }
474
541
  if (trimmed.startsWith("/resume")) {
475
- const targetId = trimmed.slice(7).trim();
542
+ let targetId = trimmed.slice(7).trim();
543
+ if (!targetId) {
544
+ const detailedSessions = sessionManager.listSessionsDetailed();
545
+ if (detailedSessions.length === 0) {
546
+ console.log(chalk.yellow("\n⚠ No hay sesiones guardadas previas.\n"));
547
+ continue;
548
+ }
549
+ if (process.stdin.isTTY) {
550
+ const opciones = formatearOpcionesDeSesion(detailedSessions);
551
+ const elegido = await elegirDeLista("REANUDAR SESIÓN — CHOCOLATITO CODE", opciones);
552
+ if (elegido === null) {
553
+ continue;
554
+ }
555
+ targetId = elegido;
556
+ }
557
+ else {
558
+ const latest = sessionManager.loadLatestSession();
559
+ if (latest)
560
+ targetId = latest.id;
561
+ }
562
+ }
476
563
  const loaded = targetId ? sessionManager.loadSession(targetId) : sessionManager.loadLatestSession();
477
564
  if (loaded) {
478
565
  // Se restaura la conversacion, no el entorno. El array guardado trae en
@@ -586,6 +673,11 @@ Directivas de ejecución:
586
673
  console.log(diffOut ? chalk.gray(diffOut) : chalk.green("✔ Sin cambios pendientes.") + "\n");
587
674
  continue;
588
675
  }
676
+ if (trimmed === "/audit" || trimmed === "/review" || trimmed.startsWith("/audit ") || trimmed.startsWith("/review ")) {
677
+ const report = await auditGitDiff(agent.currentCwd);
678
+ console.log(formatAuditReport(report));
679
+ continue;
680
+ }
589
681
  if (trimmed.startsWith("/commit")) {
590
682
  const msg = trimmed.slice(7).trim();
591
683
  if (!msg) {
@@ -602,6 +694,7 @@ Directivas de ejecución:
602
694
  const res = await changeDir(target, agent.currentCwd);
603
695
  if (res.success) {
604
696
  agent.currentCwd = res.newCwd;
697
+ memoryManager.setCwd(agent.currentCwd);
605
698
  const refreshedContext = await getProjectContext(agent.currentCwd);
606
699
  systemPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
607
700
  agent.clearHistory(systemPrompt);
@@ -1,9 +1,19 @@
1
+ export type MemoryScope = "project" | "global";
1
2
  export declare class MemoryManager {
2
- private memoryDir;
3
- private memoryIndexPath;
4
- constructor();
3
+ private cwd;
4
+ constructor(cwd?: string);
5
+ setCwd(cwd: string): void;
6
+ getCwd(): string;
7
+ private getGlobalDir;
8
+ private getProjectDir;
9
+ private isSamePath;
10
+ private isProjectCwdValid;
11
+ private resolveScope;
12
+ private getDirForScope;
5
13
  private ensureDirectory;
14
+ private sanitizeTopic;
15
+ private readIndexEntries;
6
16
  getMemoryIndex(): string;
7
- saveMemory(topic: string, fact: string): string;
8
- readMemory(topic: string): string;
17
+ saveMemory(topic: string, fact: string, scope?: MemoryScope): string;
18
+ readMemory(topic: string, scope?: MemoryScope): string;
9
19
  }
@@ -2,35 +2,118 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
4
  export class MemoryManager {
5
- memoryDir;
6
- memoryIndexPath;
7
- constructor() {
8
- this.memoryDir = path.join(os.homedir(), ".chocolatito", "memory");
9
- this.memoryIndexPath = path.join(this.memoryDir, "MEMORY.md");
10
- this.ensureDirectory();
5
+ cwd;
6
+ constructor(cwd = process.cwd()) {
7
+ this.cwd = path.resolve(cwd || process.cwd());
11
8
  }
12
- ensureDirectory() {
13
- if (!fs.existsSync(this.memoryDir)) {
14
- fs.mkdirSync(this.memoryDir, { recursive: true });
15
- }
16
- if (!fs.existsSync(this.memoryIndexPath)) {
17
- const defaultIndex = `# 🧠 Memoria Persistente de Chocolatito Code\n\nEste archivo indexa hechos, preferencias del usuario y aprendizajes clave entre proyectos.\n\n- [Preferencias Generales](./preferences.md): Preferencias de desarrollo y estilo.\n`;
18
- fs.writeFileSync(this.memoryIndexPath, defaultIndex, "utf-8");
9
+ setCwd(cwd) {
10
+ this.cwd = path.resolve(cwd || process.cwd());
11
+ }
12
+ getCwd() {
13
+ return this.cwd;
14
+ }
15
+ getGlobalDir() {
16
+ return path.join(os.homedir(), ".chocolatito", "memory");
17
+ }
18
+ getProjectDir() {
19
+ return path.join(this.cwd, ".chocolatito", "memory");
20
+ }
21
+ isSamePath(p1, p2) {
22
+ const r1 = path.resolve(p1);
23
+ const r2 = path.resolve(p2);
24
+ if (process.platform === "win32") {
25
+ return r1.toLowerCase() === r2.toLowerCase();
19
26
  }
27
+ return r1 === r2;
20
28
  }
21
- getMemoryIndex() {
22
- this.ensureDirectory();
29
+ isProjectCwdValid() {
30
+ if (!this.cwd || typeof this.cwd !== "string")
31
+ return false;
23
32
  try {
24
- return fs.readFileSync(this.memoryIndexPath, "utf-8");
33
+ if (this.isSamePath(this.cwd, os.homedir())) {
34
+ return false;
35
+ }
36
+ return true;
25
37
  }
26
38
  catch {
39
+ return false;
40
+ }
41
+ }
42
+ resolveScope(scope) {
43
+ if (scope === "project" || scope === "global") {
44
+ return scope;
45
+ }
46
+ return this.isProjectCwdValid() ? "project" : "global";
47
+ }
48
+ getDirForScope(scope) {
49
+ return scope === "project" ? this.getProjectDir() : this.getGlobalDir();
50
+ }
51
+ ensureDirectory(scope) {
52
+ const dir = this.getDirForScope(scope);
53
+ if (!fs.existsSync(dir)) {
54
+ fs.mkdirSync(dir, { recursive: true });
55
+ }
56
+ return dir;
57
+ }
58
+ sanitizeTopic(topic) {
59
+ return topic.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
60
+ }
61
+ readIndexEntries(dir) {
62
+ if (!fs.existsSync(dir))
63
+ return null;
64
+ const indexPath = path.join(dir, "MEMORY.md");
65
+ if (fs.existsSync(indexPath)) {
66
+ const content = fs.readFileSync(indexPath, "utf-8").trim();
67
+ const lines = content
68
+ .split("\n")
69
+ .map((l) => l.trim())
70
+ .filter((l) => l.length > 0 && (l.startsWith("-") || l.startsWith("*")));
71
+ if (lines.length > 0) {
72
+ return lines.join("\n");
73
+ }
74
+ if (content.length > 0) {
75
+ return content;
76
+ }
77
+ }
78
+ try {
79
+ const files = fs
80
+ .readdirSync(dir)
81
+ .filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
82
+ if (files.length > 0) {
83
+ return files
84
+ .map((f) => {
85
+ const topic = f.replace(/\.md$/, "");
86
+ return `- [${topic}](./${f}): Registro de ${topic}.`;
87
+ })
88
+ .join("\n");
89
+ }
90
+ }
91
+ catch { }
92
+ return null;
93
+ }
94
+ getMemoryIndex() {
95
+ const isProjectValid = this.isProjectCwdValid();
96
+ const projectEntries = isProjectValid ? this.readIndexEntries(this.getProjectDir()) : null;
97
+ const globalEntries = this.readIndexEntries(this.getGlobalDir());
98
+ const hasProject = Boolean(projectEntries);
99
+ const hasGlobal = Boolean(globalEntries);
100
+ if (!hasProject && !hasGlobal) {
27
101
  return "Sin memoria previa.";
28
102
  }
103
+ const projectSection = hasProject
104
+ ? projectEntries
105
+ : "Sin notas locales para este proyecto.";
106
+ const globalSection = hasGlobal
107
+ ? globalEntries
108
+ : "Sin preferencias globales.";
109
+ return `Memoria del Proyecto (.chocolatito/memory/):\n${projectSection}\n\nPreferencias Globales (~/.chocolatito/memory/):\n${globalSection}`;
29
110
  }
30
- saveMemory(topic, fact) {
31
- this.ensureDirectory();
32
- const sanitized = topic.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
33
- const filePath = path.join(this.memoryDir, `${sanitized}.md`);
111
+ saveMemory(topic, fact, scope) {
112
+ const resolvedScope = this.resolveScope(scope);
113
+ const dir = this.ensureDirectory(resolvedScope);
114
+ const sanitized = this.sanitizeTopic(topic);
115
+ const filePath = path.join(dir, `${sanitized}.md`);
116
+ const indexPath = path.join(dir, "MEMORY.md");
34
117
  const timestamp = new Date().toISOString().split("T")[0];
35
118
  const entry = `\n- [${timestamp}] ${fact}`;
36
119
  if (fs.existsSync(filePath)) {
@@ -40,19 +123,53 @@ export class MemoryManager {
40
123
  fs.writeFileSync(filePath, `# Memoria: ${topic}\n${entry}\n`, "utf-8");
41
124
  }
42
125
  // Update MEMORY.md index if not already present
43
- const indexContent = fs.readFileSync(this.memoryIndexPath, "utf-8");
126
+ let indexContent = "";
127
+ if (fs.existsSync(indexPath)) {
128
+ indexContent = fs.readFileSync(indexPath, "utf-8");
129
+ }
44
130
  if (!indexContent.includes(`${sanitized}.md`)) {
45
131
  const link = `- [${topic}](./${sanitized}.md): Registro de ${topic}.\n`;
46
- fs.appendFileSync(this.memoryIndexPath, link, "utf-8");
132
+ fs.appendFileSync(indexPath, link, "utf-8");
133
+ }
134
+ if (resolvedScope === "project") {
135
+ return `✔ Hecho guardado en memoria local de proyecto (.chocolatito/memory/${sanitized}.md).`;
136
+ }
137
+ else {
138
+ return `✔ Hecho guardado en memoria global (~/.chocolatito/memory/${sanitized}.md).`;
47
139
  }
48
- return `✔ Hecho guardado en memoria persistente (~/.chocolatito/memory/${sanitized}.md).`;
49
140
  }
50
- readMemory(topic) {
51
- this.ensureDirectory();
52
- const sanitized = topic.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
53
- const filePath = path.join(this.memoryDir, `${sanitized}.md`);
54
- if (fs.existsSync(filePath)) {
55
- return fs.readFileSync(filePath, "utf-8");
141
+ readMemory(topic, scope) {
142
+ const sanitized = this.sanitizeTopic(topic);
143
+ if (scope === "project") {
144
+ const pPath = path.join(this.getProjectDir(), `${sanitized}.md`);
145
+ if (fs.existsSync(pPath)) {
146
+ return fs.readFileSync(pPath, "utf-8");
147
+ }
148
+ return `No hay memoria registrada para el tema "${topic}" en el proyecto.`;
149
+ }
150
+ if (scope === "global") {
151
+ const gPath = path.join(this.getGlobalDir(), `${sanitized}.md`);
152
+ if (fs.existsSync(gPath)) {
153
+ return fs.readFileSync(gPath, "utf-8");
154
+ }
155
+ return `No hay memoria registrada para el tema "${topic}" en la memoria global.`;
156
+ }
157
+ // Sin scope especificado: buscar primero en proyecto, luego en global
158
+ const sameDir = this.isSamePath(this.getProjectDir(), this.getGlobalDir());
159
+ const pPath = path.join(this.getProjectDir(), `${sanitized}.md`);
160
+ const gPath = path.join(this.getGlobalDir(), `${sanitized}.md`);
161
+ const pExists = this.isProjectCwdValid() && fs.existsSync(pPath);
162
+ const gExists = !sameDir && fs.existsSync(gPath);
163
+ if (pExists && gExists) {
164
+ const pContent = fs.readFileSync(pPath, "utf-8").trim();
165
+ const gContent = fs.readFileSync(gPath, "utf-8").trim();
166
+ return `[Memoria de Proyecto]\n${pContent}\n\n[Memoria Global]\n${gContent}`;
167
+ }
168
+ if (pExists) {
169
+ return fs.readFileSync(pPath, "utf-8");
170
+ }
171
+ if (gExists || (sameDir && fs.existsSync(gPath))) {
172
+ return fs.readFileSync(gPath, "utf-8");
56
173
  }
57
174
  return `No hay memoria registrada para el tema "${topic}".`;
58
175
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * LO QUE SE ESCRIBE, COPIADO PARA EL NAVEGADOR
3
+ *
4
+ * No se redirige nada: se COPIA. El terminal sigue recibiendo exactamente lo
5
+ * mismo que antes, porque quien arranca `--servir` normalmente lo deja abierto
6
+ * en su PC y lo mira desde ahi tambien. Si esto desviara la salida, el que esta
7
+ * delante de la maquina se quedaria sin ver su propia sesion.
8
+ *
9
+ * POR QUE SE ENVUELVE `console` Y NO `process.stdout`
10
+ *
11
+ * Ya esta decidido en ui/salida.ts y ui/pantalla.ts, y por las razones buenas:
12
+ * sustituir process.stdout hace desaparecer pruebas del informe de `node --test`
13
+ * sin que fallen. Aqui se envuelve console, que es otro objeto y no tiene ese
14
+ * problema.
15
+ *
16
+ * EL ORDEN IMPORTA, Y ES LA UNICA TRAMPA DE ESTE ARCHIVO
17
+ *
18
+ * ui/pantalla.ts:144 ya reemplaza los cuatro canales para mandarlos a la
19
+ * transcripcion de Ink. Esto tiene que envolverse DESPUES de eso: asi lo que se
20
+ * llama es la version de pantalla (el terminal sigue pintando) y de paso se
21
+ * copia. Al reves, pantalla envolveria a esto y la copia no se enteraria de
22
+ * nada. Por eso en index.ts la captura arranca despues de entrarEnPantallaFija.
23
+ */
24
+ export interface LineaWeb {
25
+ /** Estable y creciente. El navegador lo usa para no repetir lineas al reconectar. */
26
+ id: number;
27
+ texto: string;
28
+ }
29
+ /** Mete una linea propia (avisos del servidor, eco de lo que llega del movil). */
30
+ export declare function anotarWeb(texto: string): void;
31
+ export declare function iniciarCaptura(): void;
32
+ export declare function detenerCaptura(): void;
33
+ export declare function historialWeb(desde?: number): LineaWeb[];
34
+ export declare function alEscribirWeb(cb: (linea: LineaWeb) => void): () => void;
35
+ /** Solo para pruebas: deja el modulo como recien cargado. */
36
+ export declare function vaciarCaptura(): void;
@@ -0,0 +1,74 @@
1
+ import { format } from "node:util";
2
+ const CANALES = ["log", "error", "warn", "info"];
3
+ /**
4
+ * Tope del historial. Es para el que abre el movil a mitad de sesion y quiere
5
+ * contexto, no un registro completo: eso ya lo tiene el terminal de verdad.
6
+ */
7
+ const MAX_LINEAS = 1_000;
8
+ let lineas = [];
9
+ let siguienteId = 1;
10
+ const oyentes = new Set();
11
+ const previos = new Map();
12
+ /** Reentrada: si un oyente escribe por consola, no se vuelve a capturar. */
13
+ let dentro = false;
14
+ function guardar(texto) {
15
+ for (const trozo of texto.replace(/\r\n/g, "\n").split("\n")) {
16
+ const linea = { id: siguienteId++, texto: trozo };
17
+ lineas.push(linea);
18
+ if (lineas.length > MAX_LINEAS)
19
+ lineas = lineas.slice(-MAX_LINEAS);
20
+ for (const cb of oyentes) {
21
+ try {
22
+ cb(linea);
23
+ }
24
+ catch {
25
+ // Un cliente que se cae no puede tumbar la salida de la sesion.
26
+ }
27
+ }
28
+ }
29
+ }
30
+ /** Mete una linea propia (avisos del servidor, eco de lo que llega del movil). */
31
+ export function anotarWeb(texto) {
32
+ guardar(texto);
33
+ }
34
+ export function iniciarCaptura() {
35
+ if (previos.size)
36
+ return;
37
+ for (const nombre of CANALES) {
38
+ const previo = console[nombre].bind(console);
39
+ previos.set(nombre, previo);
40
+ console[nombre] = (...args) => {
41
+ previo(...args);
42
+ if (dentro)
43
+ return;
44
+ dentro = true;
45
+ try {
46
+ guardar(format(...args));
47
+ }
48
+ finally {
49
+ dentro = false;
50
+ }
51
+ };
52
+ }
53
+ }
54
+ export function detenerCaptura() {
55
+ for (const nombre of CANALES) {
56
+ const previo = previos.get(nombre);
57
+ if (previo)
58
+ console[nombre] = previo;
59
+ }
60
+ previos.clear();
61
+ }
62
+ export function historialWeb(desde = 0) {
63
+ return desde ? lineas.filter((l) => l.id > desde) : lineas.slice();
64
+ }
65
+ export function alEscribirWeb(cb) {
66
+ oyentes.add(cb);
67
+ return () => oyentes.delete(cb);
68
+ }
69
+ /** Solo para pruebas: deja el modulo como recien cargado. */
70
+ export function vaciarCaptura() {
71
+ lineas = [];
72
+ siguienteId = 1;
73
+ oyentes.clear();
74
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * LA PAGINA, ENTERA Y SIN DEPENDENCIAS
3
+ *
4
+ * Nada de CDN: ni una fuente, ni una libreria. Tres razones y las tres pesan.
5
+ * Corre en tu red local, donde puede no haber salida a internet; un CDN veria la
6
+ * direccion desde la que entra cada sesion de cada cliente; y una pagina que
7
+ * depende de un tercero se rompe el dia que el tercero cambia algo.
8
+ *
9
+ * ESTA PENSADA PARA UN MOVIL, Y ESO SON DECISIONES CONCRETAS
10
+ *
11
+ * - La caja de escribir tiene font-size 16px. Por debajo de 16, Safari de iOS
12
+ * hace zoom solo al enfocar el campo y descoloca la pagina entera. Es la
13
+ * razon de ese numero y no otro.
14
+ * - `env(safe-area-inset-bottom)`: sin eso, en un iPhone la barra de escribir
15
+ * queda debajo de la raya del gesto de inicio.
16
+ * - `dvh` y no `vh`: con `vh`, al abrirse el teclado la pagina sigue midiendo
17
+ * la pantalla entera y el campo se va hacia abajo, fuera de la vista.
18
+ * - Se reconecta sola, con espera creciente. Un movil pierde la red a cada rato,
19
+ * y una sesion que hay que recargar a mano no se usa dos veces.
20
+ *
21
+ * EL COLOR SALE DEL ANSI DE VERDAD
22
+ *
23
+ * La transcripcion llega tal cual, con los codigos de chalk dentro. Se traducen
24
+ * aqui a `<span>` en vez de quitarlos: el diff en verde y rojo ES el producto, y
25
+ * un diff en gris no vende nada.
26
+ */
27
+ export declare function paginaHtml(): string;