diff options
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) |