diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-08-17 15:12:12 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-08-17 15:12:12 -0300 |
| commit | 0d1fe286ac6ea30b91c15b44c0872b57a864ea7f (patch) | |
| tree | 554d6c8a0bdcf1c30bfa2d752d557b39c1554d6e /claude_sesiones/terminal.py | |
| parent | bd3ca3a04fcd8d882d23deeb3f3a0d6f04b3f93f (diff) | |
| download | claude-logbook-0d1fe286ac6ea30b91c15b44c0872b57a864ea7f.tar.gz claude-logbook-0d1fe286ac6ea30b91c15b44c0872b57a864ea7f.zip | |
Explorar también las memorias de los proyectos
Claude Code deja, además de las sesiones, un directorio memory/ por proyecto:
un .md por recuerdo con frontmatter, y un MEMORY.md que hace de índice. Lo
que no figura en ese índice sigue en disco pero deja de cargarse en contexto,
así que la diferencia entre ambos es justamente lo que vale la pena mirar.
-m cambia de sesiones a memorias y reusa los mismos verbos:
claude-sesiones -m tabla de memorias
claude-sesiones -m docker busca en nombre, descripción y cuerpo
claude-sesiones -m -s 3 lee la nº 3 (o -s <nombre>)
claude-sesiones -m --check audita índices, enlaces y orígenes
claude-sesiones -m -D 3 la borra y la saca de MEMORY.md
claude-sesiones -m --type feedback filtra por tipo declarado
--check es la parte con criterio propio: reporta memorias que no están en
MEMORY.md, entradas del índice que apuntan a archivos que ya no existen,
enlaces [[...]] rotos y sesiones de origen que ya no están.
El registro de una memoria usa claves cortas como el de sesiones, por el
mismo motivo: viaja embebido en el HTML, que ahora tiene su propia pestaña
de memorias.
Claude-Session: https://claude.ai/code/session_01RmtZ9qBemrc9TncwVTG6ED
Diffstat (limited to 'claude_sesiones/terminal.py')
| -rw-r--r-- | claude_sesiones/terminal.py | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/claude_sesiones/terminal.py b/claude_sesiones/terminal.py index 2bf9031..9a16bec 100644 --- a/claude_sesiones/terminal.py +++ b/claude_sesiones/terminal.py @@ -249,3 +249,124 @@ def pager(text): p.communicate(text.encode()) except (OSError, BrokenPipeError): sys.stdout.write(text) + + +# ──────────────────────────────── memorias ──────────────────────────────── + +# Anchos mínimos para las columnas opcionales de la tabla de memorias. +MIN_COLS_MEM_PROJECT = 78 +MIN_COLS_MEM_DESC = 104 + +TYPE_COLOR = {"project": "amber", "user": "blue", + "feedback": "copper", "reference": "grey"} + + +def print_memories(memories, st, now, out, width=None): + width = width or shutil.get_terminal_size((100, 24)).columns + show_project = width >= MIN_COLS_MEM_PROJECT + show_desc = width >= MIN_COLS_MEM_DESC + + # columnas fijas: idx(3) tipo(9) fecha(6) cuándo(9) + separadores + fixed = 3 + 9 + 6 + 9 + gaps = 4 + (1 if show_project else 0) + (1 if show_desc else 0) + flex = max(18, width - fixed - gaps) + if show_desc: + w_name, w_project = int(flex * 0.34), int(flex * 0.28) + elif show_project: + w_name, w_project = int(flex * 0.58), flex - int(flex * 0.58) + else: + w_name, w_project = flex, 0 + w_desc = flex - w_name - w_project if show_desc else 0 + + head = [f"{'#':>3}", f"{st.faint}{'MEMORIA':<{w_name}}{st.reset}", + f"{st.faint}{'TIPO':<9}{st.reset}"] + if show_project: + head.append(f"{st.faint}{'PROYECTO':<{w_project}}{st.reset}") + if show_desc: + head.append(f"{st.faint}{'DESCRIPCIÓN':<{w_desc}}{st.reset}") + head.append(f"{st.faint}{'FECHA':<6} {'CUÁNDO':<9}{st.reset}") + print(" ".join(head), file=out) + + for i, m in enumerate(memories, 1): + # El asterisco marca lo que no está en MEMORY.md: existe pero no se carga. + tag = "" if m["ix"] else f" {st.copper}*{st.reset}" + avail = w_name - visible_len(tag) + cells = [ + f"{st.faint}{i:>3}{st.reset}", + f"{st.ink}{clip(m['name'], avail):<{avail}}{st.reset}{tag}", + f"{getattr(st, TYPE_COLOR.get(m['ty'], 'grey'))}" + f"{clip(m['ty'], 9):<9}{st.reset}", + ] + if show_project: + cells.append(f"{st.grey}{clip(m['p'], w_project):<{w_project}}{st.reset}") + if show_desc: + cells.append(f"{st.grey}{clip(m['desc'], w_desc):<{w_desc}}{st.reset}") + cells.append(f"{fmt_date(m['l']):<6} " + f"{st.faint}{fmt_rel(m['l'], now):<9}{st.reset}") + print(" ".join(cells), file=out) + + +def print_memory(m, st, out, path=None, width=None): + width = min(width or shutil.get_terminal_size((100, 24)).columns, 100) + + print(f"{st.amber}{st.bold}{m['name']}{st.reset}", file=out) + if m["desc"]: + for line in textwrap.wrap(m["desc"], width=width - 2): + print(f"{st.grey}{line}{st.reset}", file=out) + + meta = (f"{m['ty']} · {m['p']} · " + f"{fmt_date(m['l'])} {fmt_time(m['l'])} · {fmt_size(m['k'])}") + print(f"{st.faint}{meta}{st.reset}", file=out) + if path: + print(f"{st.faint}{path}{st.reset}", file=out) + + if not m["ix"]: + aviso = ("este proyecto no tiene MEMORY.md" if not m["hix"] + else "no figura en MEMORY.md, así que no se carga en contexto") + print(f"{st.copper}* {aviso}{st.reset}", file=out) + + print(f"{st.faint}{'─' * min(width, 80)}{st.reset}\n", file=out) + + for line in render_block(m["body"], st, width - 2, st.grey): + print(line, file=out) + + if m["ln"]: + print(f"\n{st.faint}enlaces: {', '.join(m['ln'])}{st.reset}", file=out) + if m["src"]: + print(f"{st.faint}la creó la sesión {m['src'][:8]} " + f"(claude-sesiones -s {m['src'][:8]}){st.reset}", file=out) + + +def print_audit(report, st, out): + """Informe de inconsistencias. Devuelve cuántas se listaron.""" + bloques = ( + ("sin_indice", "Proyectos con memorias pero sin MEMORY.md", + "sin índice no se carga ninguna de sus memorias", + lambda m: f"{clip(m['name'], 38):<38} {st.grey}{m['p']}{st.reset}"), + ("sin_listar", "Memorias que no figuran en su MEMORY.md", + "el índice es lo que se lee al arrancar: si no está, no se recuerda", + lambda m: f"{clip(m['name'], 38):<38} {st.grey}{m['p']}{st.reset}"), + ("indice_fantasma", "Entradas del índice sin archivo", + "apuntan a una memoria que ya no existe", + lambda t: f"{clip(t[1], 38):<38} {st.grey}{t[0]}{st.reset}"), + ("enlaces_rotos", "Enlaces [[...]] sin destino", + "el formato los permite: marcan algo que todavía no se escribió", + lambda t: f"{clip(t[0]['name'], 38):<38} {st.grey}→ [[{t[1]}]]{st.reset}"), + ("origen_perdido", "Memorias cuya sesión de origen ya no existe", + "la memoria sobrevivió a la conversación que la creó", + lambda m: f"{clip(m['name'], 38):<38} {st.grey}{m['src'][:8]}{st.reset}"), + ) + + total = 0 + for key, titulo, nota, fmt in bloques: + items = report.get(key) or [] + if not items: + continue + total += len(items) + print(f"{st.copper}{titulo} ({len(items)}){st.reset}", file=out) + print(f"{st.faint} {nota}{st.reset}", file=out) + for item in items: + print(f" {fmt(item)}", file=out) + print(file=out) + + return total |