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/cli.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/cli.py')
| -rw-r--r-- | claude_sesiones/cli.py | 159 |
1 files changed, 151 insertions, 8 deletions
diff --git a/claude_sesiones/cli.py b/claude_sesiones/cli.py index 90fb51b..4dc5ab2 100644 --- a/claude_sesiones/cli.py +++ b/claude_sesiones/cli.py @@ -14,9 +14,10 @@ from .sessions import ( load_sessions, pick, public_records, session_path, ) from .terminal import ( - Style, clip, fmt_date, fmt_size, plural, print_chat, print_table, - resume_cmd, + Style, clip, fmt_date, fmt_size, plural, print_audit, print_chat, + print_memories, print_memory, print_table, resume_cmd, ) +from . import memory as mem from . import webpage DEFAULT_HTML = "sesiones.html" @@ -46,6 +47,15 @@ borrado (irreversible; pregunta antes, salvo con -y): claude-sesiones --delete-empty borra las vacías claude-sesiones -D 101 -D e0a4300e borra sesiones puntuales claude-sesiones -p /tmp --delete-empty solo las vacías de ese proyecto + +memoria de los proyectos (-m cambia de sesiones a memorias y reusa los mismos +verbos: filtro, -s para leer, -D para borrar): + claude-sesiones -m tabla de memorias + claude-sesiones -m docker busca en nombre, descripción y cuerpo + claude-sesiones -m -s 3 lee la memoria nº 3 + claude-sesiones -m -s deadlock lo mismo, por nombre + claude-sesiones -m --check audita índices, enlaces y orígenes + claude-sesiones -m -D 3 la borra y la saca de MEMORY.md """ @@ -75,6 +85,14 @@ def build_parser(): help="no usa $PAGER para el chat") ap.add_argument("--no-color", action="store_true", help="salida sin color") + recuerdos = ap.add_argument_group("memoria de los proyectos") + recuerdos.add_argument("-m", "--memory", action="store_true", + help="trabaja sobre las memorias en vez de las sesiones") + recuerdos.add_argument("--type", metavar="TIPO", choices=mem.TYPES, + help="filtra por tipo: " + " | ".join(mem.TYPES)) + recuerdos.add_argument("--check", action="store_true", + help="audita índices, enlaces y sesiones de origen") + salida = ap.add_argument_group("exportar") salida.add_argument("--json", action="store_true", help="vuelca todas las sesiones en JSON") @@ -214,19 +232,23 @@ def delete_targets(pool, args): def cmd_json(sessions): import json - json.dump(public_records(sessions), sys.stdout, - ensure_ascii=False, separators=(",", ":")) + payload = webpage.build_payload( + public_records(sessions), + mem.public_records(mem.load_memories(sessions))) + json.dump(payload, sys.stdout, ensure_ascii=False, separators=(",", ":")) sys.stdout.write("\n") return 0 def cmd_html(sessions, args): out = args.html - stats = webpage.write(public_records(sessions), out, + memories = mem.public_records(mem.load_memories(sessions)) + stats = webpage.write(public_records(sessions), out, memories=memories, template=webpage.template_text(args.template)) print(f"{stats['sesiones']} sesiones · {stats['proyectos']} proyectos · " f"{stats['mensajes']} mensajes · {stats['bloques']} bloques " - f"de transcripción → {out}", file=sys.stderr) + f"de transcripción · {stats['memorias']} memorias → {out}", + file=sys.stderr) if args.open: webbrowser.open("file://" + os.path.abspath(out)) return 0 @@ -263,6 +285,124 @@ def cmd_table(sessions, args, st): return 0 +def mem_filtered(memories, args): + return mem.apply_filters(memories, project=args.project, + query=" ".join(args.query) or None, + kind=args.type) + + +def cmd_mem_show(memories, args, st): + m = mem.pick(mem_filtered(memories, args), args.show) + buf = io.StringIO() + print_memory(m, st, buf, path=mem.memory_path(m)) + text = buf.getvalue() + if args.no_pager: + sys.stdout.write(text) + else: + from .terminal import pager + pager(text) + return 0 + + +def cmd_mem_check(memories, sessions, st): + report = mem.audit(memories, sessions) + total = print_audit(report, st, sys.stdout) + if total: + print(f"{st.faint}{plural(total, 'cosa para mirar', 'cosas para mirar')} " + f"en {plural(len(memories), 'memoria', 'memorias')}.{st.reset}") + return 1 + print(f"{st.amber}Todo en orden: " + f"{plural(len(memories), 'memoria', 'memorias')}, " + f"índices y enlaces consistentes.{st.reset}") + return 0 + + +def delete_memories(targets, args, st): + if not targets: + print("No hay memorias que borrar con ese criterio.", file=sys.stderr) + return 0 + + print(f"{st.bold}Se van a borrar " + f"{plural(len(targets), 'memoria', 'memorias')}:{st.reset}\n") + total_kb = 0 + for m in targets: + total_kb += m["k"] + print(f" {st.ink}{clip(m['name'], 38):<38}{st.reset} " + f"{clip(m['ty'], 9):<9} " + f"{st.grey}{clip(m['p'], 34):<34}{st.reset} " + f"{st.faint}{fmt_size(m['k']):>9}{st.reset}") + if m["desc"]: + print(f" {st.faint}{clip(m['desc'], 86)}{st.reset}") + + print(f"\n{st.faint}{fmt_size(total_kb)} en total · " + f"también se quita su línea de MEMORY.md{st.reset}") + + if args.dry_run: + print(f"\n{st.faint}--dry-run: no se tocó nada.{st.reset}") + return 0 + + if not args.yes: + print(f"\n{st.copper}Esto no se puede deshacer.{st.reset}") + if not confirm("¿Confirmás? [s/N] "): + print("Cancelado.", file=sys.stderr) + return 1 + + done = failed = unlisted = 0 + for m in targets: + try: + if mem.delete(m): + unlisted += 1 + done += 1 + except OSError as e: + print(f"error: {m['name']}: {e}", file=sys.stderr) + failed += 1 + + extra = f", {unlisted} sacadas del índice" if unlisted else "" + print(f"\n{plural(done, 'memoria borrada', 'memorias borradas')}{extra}.") + return 1 if failed else 0 + + +def cmd_mem_table(memories, args, st, total): + if not memories: + print("Ninguna memoria coincide con ese filtro.", file=sys.stderr) + return 1 + shown = memories[: args.limit] if args.limit else memories + + print_memories(shown, st, latest_activity(shown), sys.stdout) + + projects = len({m["p"] for m in memories}) + tail = (f"{len(shown)} de {total} memorias" if len(shown) != total + else f"{plural(total, 'memoria', 'memorias')} · " + f"{plural(projects, 'proyecto', 'proyectos')}") + print(f"\n{st.faint}{tail} · -m -s <nº> para leer una{st.reset}") + return 0 + + +def run_memory(sessions, args, st): + memories = mem.load_memories(sessions) + if not memories: + print("Ningún proyecto tiene memorias todavía.", file=sys.stderr) + return 1 + + if args.check: + return cmd_mem_check(memories, sessions, st) + + if args.delete: + pool = mem_filtered(memories, args) + targets, seen = [], set() + for ref in args.delete: + m = mem.pick(pool, ref) + if m["name"] not in seen: + seen.add(m["name"]) + targets.append(m) + return delete_memories(targets, args, st) + + if args.show: + return cmd_mem_show(memories, args, st) + + return cmd_mem_table(mem_filtered(memories, args), args, st, len(memories)) + + def run(args): root = default_root() if not os.path.isdir(root): @@ -275,13 +415,16 @@ def run(args): print("No hay ninguna sesión registrada todavía.", file=sys.stderr) return 1 + st = Style.from_stream(sys.stdout, args.no_color) + + if args.memory: + return run_memory(sessions, args, st) + if args.json: return cmd_json(sessions) if args.html: return cmd_html(sessions, args) - st = Style.from_stream(sys.stdout, args.no_color) - if args.delete or args.delete_empty: targets = delete_targets(filtered(sessions, args), args) return delete_sessions(targets, args, st) |