aboutsummaryrefslogtreecommitdiffstats
path: root/claude_logbook/webpage.py
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-08-17 15:14:11 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-08-17 15:14:11 -0300
commitffe36e782b064a31b7a695ac1372f1329ac79153 (patch)
tree0a40ea9ade098e8e9ff21028d4a6442c133cba15 /claude_logbook/webpage.py
parent0d1fe286ac6ea30b91c15b44c0872b57a864ea7f (diff)
downloadclaude-logbook-ffe36e782b064a31b7a695ac1372f1329ac79153.tar.gz
claude-logbook-ffe36e782b064a31b7a695ac1372f1329ac79153.zip
Renombrar el proyecto a claude-logbook
El nombre en español no se encontraba, y la zona de "claude + sessions" ya está poblada por cosas distintas con las que se confundía: gapmiss/claude-sessions plugin de Obsidian kyupid/claude-sessions monitor de sesiones en curso (PyPI) claude-session-manager wrapper que preserva contexto (PyPI) LearnedGeek/claude-recall búsqueda con SQLite FTS5 (PyPI) "logbook" cubre las dos mitades — las conversaciones y las memorias — sin caer en "session manager", y encaja con lo que el proyecto produce: una bitácora que se abre en cualquier lado. claude_sesiones/ → claude_logbook/ y todas las referencias ~/.cache/claude-sesiones → ~/.cache/claude-logbook el resumen de PyPI pasa a inglés, como el README principal El comando viejo sigue funcionando: pyproject declara claude-sesiones como segundo entry point del mismo main, para no rehacer la memoria muscular. La interfaz sigue en español; solo cambia cómo se llama e instala el paquete. De paso, memory.py faltaba en la tabla de módulos de los dos READMEs. Claude-Session: https://claude.ai/code/session_01RmtZ9qBemrc9TncwVTG6ED
Diffstat (limited to 'claude_logbook/webpage.py')
-rw-r--r--claude_logbook/webpage.py74
1 files changed, 74 insertions, 0 deletions
diff --git a/claude_logbook/webpage.py b/claude_logbook/webpage.py
new file mode 100644
index 0000000..c8550fe
--- /dev/null
+++ b/claude_logbook/webpage.py
@@ -0,0 +1,74 @@
+"""Genera la página HTML autocontenida a partir de las sesiones.
+
+El resultado no necesita red ni servidor: los datos y las transcripciones van
+embebidos adentro y se abre con doble clic.
+"""
+
+import json
+from importlib import resources
+
+MARKER = "__DATA__"
+
+TEMPLATE_NAME = "template.html"
+
+
+class TemplateError(Exception):
+ """El template no tiene la forma que espera el generador."""
+
+
+def template_text(path=None):
+ if path:
+ with open(path, encoding="utf-8") as f:
+ return f.read()
+ return (resources.files(__package__) / TEMPLATE_NAME).read_text(encoding="utf-8")
+
+
+def build_payload(sessions, memories=None):
+ """Estructura que viaja embebida en la página.
+
+ Un objeto y no una lista porque la página muestra dos cosas distintas:
+ sesiones y memorias de proyecto.
+ """
+ return {"s": list(sessions), "m": list(memories or ())}
+
+
+def encode_payload(payload):
+ """Serializa el payload para meterlo en un <script type=application/json>.
+
+ El parser de HTML corta ese bloque en el primer "</script", y las
+ transcripciones tienen HTML adentro. Escapamos "</" como "<\\/", que es un
+ escape válido de JSON: JSON.parse lo devuelve intacto.
+ """
+ raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
+ return raw.replace("</", "<\\/")
+
+
+def render(records, memories=None, template=None):
+ """Devuelve el HTML completo con los datos ya embebidos."""
+ html = template if template is not None else template_text()
+ # No se puede verificar el resultado buscando el marcador: estas mismas
+ # sesiones incluyen conversaciones sobre este script, así que el payload lo
+ # contiene como texto. Validamos el template antes de sustituir.
+ if html.count(MARKER) != 1:
+ raise TemplateError(
+ f"el template debe tener exactamente un {MARKER} "
+ f"(encontrados: {html.count(MARKER)})")
+ return html.replace(MARKER, encode_payload(build_payload(records, memories)))
+
+
+def write(records, out_path, memories=None, template=None):
+ """Escribe la página y devuelve un resumen de lo que quedó adentro."""
+ html = render(records, memories=memories, template=template)
+ with open(out_path, "w", encoding="utf-8") as f:
+ f.write(html)
+ return summary(records, memories)
+
+
+def summary(records, memories=None):
+ return {
+ "sesiones": len(records),
+ "proyectos": len({r["p"] for r in records}),
+ "mensajes": sum(r["u"] for r in records),
+ "bloques": sum(len(r["c"]) for r in records),
+ "memorias": len(memories or ()),
+ }