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.
- package/README.md +437 -341
- package/dist/agent/context.d.ts +6 -0
- package/dist/agent/context.js +69 -2
- package/dist/agent/loop.js +8 -0
- package/dist/agent/toolGate.js +13 -7
- package/dist/agent/verifier.js +2 -1
- package/dist/config/permissions.d.ts +17 -0
- package/dist/config/permissions.js +47 -7
- package/dist/config/plataforma.d.ts +62 -0
- package/dist/config/plataforma.js +109 -0
- package/dist/config/updater.js +4 -1
- package/dist/hooks/manager.js +2 -1
- package/dist/index.js +99 -6
- package/dist/memory/manager.d.ts +15 -5
- package/dist/memory/manager.js +146 -29
- package/dist/servidor/captura.d.ts +36 -0
- package/dist/servidor/captura.js +74 -0
- package/dist/servidor/pagina.d.ts +27 -0
- package/dist/servidor/pagina.js +268 -0
- package/dist/servidor/puente.d.ts +34 -0
- package/dist/servidor/puente.js +115 -0
- package/dist/servidor/servidor.d.ts +61 -0
- package/dist/servidor/servidor.js +249 -0
- package/dist/sessions/manager.d.ts +8 -0
- package/dist/sessions/manager.js +44 -0
- package/dist/sessions/resume.d.ts +12 -0
- package/dist/sessions/resume.js +10 -0
- package/dist/tools/backgroundTask.d.ts +64 -0
- package/dist/tools/backgroundTask.js +264 -0
- package/dist/tools/browserExtension.d.ts +2 -1
- package/dist/tools/browserExtension.js +48 -0
- package/dist/tools/computerUse.js +51 -9
- package/dist/tools/definitions.js +93 -1
- package/dist/tools/gitAudit.d.ts +41 -0
- package/dist/tools/gitAudit.js +282 -0
- package/dist/tools/runCommand.js +5 -2
- package/dist/tools/runner.js +36 -2
- package/dist/tools/safety.d.ts +1 -0
- package/dist/tools/safety.js +3 -0
- package/dist/tools/todoTool.d.ts +2 -0
- package/dist/tools/todoTool.js +29 -0
- package/dist/tools/toolDefsComputer.js +9 -0
- package/dist/tools/win/hostScript.js +7 -2
- package/dist/ui/comandos.js +2 -0
- package/dist/ui/marco.d.ts +1 -0
- package/dist/ui/marco.js +4 -2
- package/dist/ui/permissionPrompt.d.ts +9 -0
- package/dist/ui/permissionPrompt.js +30 -0
- package/dist/ui/pieFijo.d.ts +1 -1
- package/dist/ui/pieFijo.js +1 -1
- package/dist/ui/renderer.js +8 -0
- package/extension/background.js +80 -0
- package/extension/content.js +106 -0
- package/extension/manifest.json +61 -60
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/memory/manager.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
export type MemoryScope = "project" | "global";
|
|
1
2
|
export declare class MemoryManager {
|
|
2
|
-
private
|
|
3
|
-
|
|
4
|
-
|
|
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
|
}
|
package/dist/memory/manager.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
this.
|
|
29
|
+
isProjectCwdValid() {
|
|
30
|
+
if (!this.cwd || typeof this.cwd !== "string")
|
|
31
|
+
return false;
|
|
23
32
|
try {
|
|
24
|
-
|
|
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.
|
|
32
|
-
const
|
|
33
|
-
const
|
|
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
|
-
|
|
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(
|
|
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.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|