docs: add 30-day expiry callout + ship 4 auto-save tools
Adds a brief [!IMPORTANT] callout at the top of the README pointing users to the urgent announcement at #1388. Claude Code auto-deletes local JSONL transcripts after 30 days; users without the auto-save hooks wired are losing transcript data off the rolling window. Ships 4 small standalone tools at tools/: - backup_claude_jsonls.sh — rsync ~/.claude/projects/ to a safe folder - render_jsonl.py — convert JSONL transcripts to readable text - find_orphan_claude_jsonls.sh — scan backup locations for orphan Claude Code transcripts (multi-line shape detection + topic preview) - save.md — Claude Code slash command for manual /save into MemPalace Tools verified by independent agent against v3.3.4 source. Read-only on user data. POSIX bash + Python stdlib only.
This commit is contained in:
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""render_jsonl.py — turn one Claude Code JSONL transcript into readable text.
|
||||
|
||||
Claude Code stores conversations at ~/.claude/projects/<proj>/<uuid>.jsonl and
|
||||
Anthropic auto-deletes them after 30 days
|
||||
(https://docs.claude.com/en/docs/claude-code/data-usage). This script renders a
|
||||
JSONL into a clean .txt so you can keep / read / share it without the tooling.
|
||||
|
||||
Usage:
|
||||
python3 render_jsonl.py <input.jsonl> [output.txt]
|
||||
|
||||
Stdlib only. Python 3.9+. Read-only on the input.
|
||||
"""
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
|
||||
def extract_text(content):
|
||||
if isinstance(content, str):
|
||||
return content.strip()
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for blk in content:
|
||||
if isinstance(blk, dict) and blk.get("type") == "text":
|
||||
t = (blk.get("text") or "").strip()
|
||||
if t:
|
||||
parts.append(t)
|
||||
return "\n".join(parts)
|
||||
return ""
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__); sys.exit(1)
|
||||
src = Path(sys.argv[1])
|
||||
if not src.is_file():
|
||||
print(f"ERROR: not a file: {src}"); sys.exit(1)
|
||||
out = open(sys.argv[2], "w", encoding="utf-8") if len(sys.argv) > 2 else sys.stdout
|
||||
|
||||
turns, stamps = [], []
|
||||
for raw in src.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if not raw.strip():
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
role = obj.get("type") or (obj.get("message") or {}).get("role")
|
||||
if role not in ("user", "assistant"):
|
||||
continue
|
||||
msg = obj.get("message") or obj
|
||||
text = extract_text(msg.get("content"))
|
||||
if not text:
|
||||
continue
|
||||
ts = obj.get("timestamp") or ""
|
||||
if ts: stamps.append(ts)
|
||||
turns.append((ts, role, text))
|
||||
|
||||
header = [
|
||||
f"# Claude Code transcript: {src}",
|
||||
f"# Total turns: {len(turns)}",
|
||||
f"# Date range : {min(stamps) if stamps else 'n/a'} -> {max(stamps) if stamps else 'n/a'}",
|
||||
"#" + "-" * 70, "",
|
||||
]
|
||||
out.write("\n".join(header))
|
||||
for ts, role, text in turns:
|
||||
out.write(f"\n[{ts}] {role.upper()}\n{text}\n\n{'-'*72}\n")
|
||||
if out is not sys.stdout:
|
||||
out.close()
|
||||
print(f"Wrote {len(turns)} turns to {sys.argv[2]}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user